Skip to content

Security Model

Executive Overview

CCM2 adopts a defense‑in‑depth security architecture that separates concerns between the web SPA, the local Agent, the Backend, and the hardware wallet. All human and administrative actions are authenticated via a trusted Identity Provider. Sensitive device operations are authorized by short‑lived, scope‑bound operation tokens issued by the Backend and are executed over an end‑to‑end (E2E) encrypted channel between the wallet and the Backend. The Agent never sees plaintext; it only brokers transport and user prompts.

Trust anchors originate from the manufacturer and are transferred to the enterprise through a one‑time ownership process, aligning with the FIDO Alliance FDO (FIDO Device Onboard) principles. After ownership, enterprise (ESA) and user (UK) keys establish mutually authenticated E2E channels with forward secrecy (ECDHE) and authenticated encryption (AEAD). All actions are auditable and enforced by policy.

Entra Abstraction and Protection

  • The Browser SPA and the local Agent never handle Microsoft Graph secrets or long-lived Entra credentials. Only the Backend communicates with Entra/Graph, and it does so with tenant-scoped application credentials that are stored in a secure vault or secret store. Front-end components receive policy decisions and signed authorizations rather than raw tokens, which prevents lateral movement into Entra from a compromised client.
  • Before any sensitive directory or device action is performed—such as creating or deleting a passkey, issuing a legacy password, or changing policy—the Backend issues a short-lived, signed operation token. This token clearly states the administrator identity, the intended target user or device, the permitted scopes, and the expiry. The local Agent will refuse to execute if that token is absent, expired, or altered. This functions as a digitally signed transaction approval for each operation.
  • When delegated Graph access is necessary (for example, to confirm that a user truly holds an administrator role in tenants that do not emit wids or app roles), the Backend uses the signed-in user’s own Entra token. The Backend cannot expand those user-granted permissions; it can only narrow them with its RBAC policies and operation-token checks.
  • Even though the Backend holds Graph application permissions, it deliberately inserts an additional authorization layer—role checks, operation tokens, and audit enforcement—between Entra and any local device or browser. A compromised Agent or SPA session therefore cannot directly leverage the Backend’s broader permissions to make uncontrolled directory changes.
  • Every Entra/Graph-backed change and every Password Manager action is written to an immutable audit log with the initiating administrator, the target, the approved scope, timestamps, and correlation identifiers. Operation tokens are short-lived and single-purpose and are validated against pinned public keys to prevent replay. This combination limits blast radius and provides traceability suitable for CISO oversight.

Latest Security Updates

  • Password Manager module is integrated but disabled by default; it is feature‑flagged, uses mTLS to the broker, and emits unified audit records with per‑credential capability flags.
  • Agent hardened mode (JWKS validation + fail‑closed) is now the default, reducing the risk of token tampering in the field.
  • Admin role discovery can fall back to a delegated Graph Directory.Read.All call when wids/app roles are unavailable; the call runs under the signed‑in user and is cached for minutes, not stored server‑side.

Threat Model Snapshot

  • Assets: Entra tenant permissions (Graph app creds + delegated user tokens), Backend JWT keys, ESA/device keys, operation tokens, audit trail, Password Manager broker secrets.
  • Trust boundaries: Browser ↔ Backend (IdP OIDC/SAML/Kerberos), Backend ↔ Graph/LDAP, Backend ↔ Agent (localhost TLS), Agent ↔ Wallet (USB/BLE), Backend ↔ Broker (mTLS).
  • Threats and mitigations:
  • Compromised admin session → Backend reissues only short‑lived session + operation tokens; destructive actions require step‑up consent and are bound to device/user scope.
  • Abuse of Graph app permissions → Credentials live only in Backend secret store (KV/Secrets/env), never leave the service; every Graph call is policy‑gated, signed/audited, and tied to a specific admin identity.
  • Agent compromise → Wallet‑Backend channel is E2E encrypted; Agent only forwards opaque frames and validates signed operation tokens (jti/scp/aud enforced) before invoking protected ops.
  • Replay/token forgery → Backend RSA keys are pinned via JWKS; operation tokens carry jti/exp/nonce and are rejected if reused or JWKS fetch fails.
  • Entra hijack via CCM → Directory operations execute with the signed‑in admin’s delegated scope or tightly scoped Backend app permissions; no pathway exists for Agent/SPA to mint Graph tokens or alter directory objects without Backend‑issued tokens and audit.

Transport

  • HTTPS on 127.0.0.1:<port>; default 17620 with fallback to random free port.
  • Self-signed cert generated on first run; SHA-256 fingerprint stored locally and shown to the user during pairing.

Backend transport: - Backend uses TLS with modern ciphers; all secrets and tokens transmitted via HTTPS only. - SPA authentication via IdP (OIDC/SAML/Kerberos) terminates at Backend; SPA receives a Backend session token. SPA does not call Entra/Graph or LDAP directly except for optional delegated admin-role discovery (Graph Directory.Read.All) when app roles/wids are unavailable; those calls run under the user’s token and are cached client-side.

MVP vs Hardened Modes

  • Agent token validation modes:
  • Hardened (DEFAULT): validates RS256 signatures against Backend JWKS and enforces issuer/audience. This is now the default mode (AGENT_VALIDATE_TOKENS=true).
  • Dev mode: decodes operation tokens without signature validation (INSECURE - for local development only). Explicitly set AGENT_VALIDATE_TOKENS=false to enable.
  • IMPORTANT: Token validation is now enabled by default. The agent will fail closed if JWKS cannot be fetched or tokens are invalid.
  • TLS: The Agent uses a self-signed localhost certificate; in production, distribute a per-tenant trusted certificate or install a local root and issue per-host certs.
  • Scopes: Operation tokens encode scopes in the standard scp claim (space-delimited). The Agent also accepts scopes (string or array) for compatibility.

Backend JWT Key Management & JWKS

  • Persistent keys are mandatory. The backend signs operation tokens with an RSA key (Jwt.PrivateKeyPem or Jwt.PrivateKeyPath). In dev, store the PEM under version control only if it is a throwaway key.
  • JWKS endpoint: /.well-known/jwks.json exposes the active key. The Agent fetches the JWKS for every validation, so any restart that rotates the key instantly breaks outstanding tokens.
  • Configuration precedence: Database configuration (configuration_documents table) overrides environment variables and appsettings.*. If a stored value leaves KeyId or PrivateKeyPath blank, the backend will reuse those blanks; clear the row or update it via the Admin UI to keep JWKS in sync.
  • Dev mixed stack: When running backend in Docker and agent locally, mount the same private key into the container and set JWT_KEY_ID/JWT_PRIVATE_KEY_PATH in ops/.env. The SPA/Agent JWKS URL must point at the Docker backend (http://localhost:8080/.well-known/jwks.json).
  • Rotation plan: To rotate keys safely, load the new key (with a new KeyId) alongside the old one, serve a JWKS containing both during overlap, then retire the old key after agents have refreshed.

Backend Authorization Policies

  • AdminOnly, UserAdminOnly, and AuthAdminOnly ensure only the correct Entra roles can reach sensitive endpoints. The policies inspect both roles (custom app roles) and wids (directory roles) claims.
  • OperationsController rejects token requests if scopes such as webauthn:create, passkey:delete, or certificate:import are requested by a user without an Authentication Administrator-equivalent role.
  • SPA helpers mirror these policies so the UI hides controls that would ultimately be rejected by the API, but the backend is the enforcement point.

Entra Permissions & Abstraction Layer

  • Graph connector lives only in Backend; SPA and Agent cannot call Entra/Graph or LDAP directly. Graph client credentials (application permissions needed for passkey/user lifecycle such as UserAuthenticationMethod.ReadWrite.All) are stored in HSM/Vault/Secrets and are never sent to clients.
  • When tenants lack wids/app roles, the SPA may perform a delegated Graph Directory.Read.All call purely to confirm that the signed‑in user is an admin; this uses the user’s token, not a stored secret, and is short‑lived.
  • Backend issues operation tokens that bind the admin identity, device_id/user_id, scopes, and expiry. Even though the Backend can act on Graph with application permissions, it will only do so when a valid operation token exists and policies pass, adding a layer of authorization between Entra and the local agent/device.
  • Delegated flows inherit only the signed‑in admin’s Entra permissions; the Backend overlay (RBAC + operation tokens) narrows them further to a specific action/target so CCM cannot amplify a user beyond their directory role.
  • All directory actions (Graph/LDAP) are audited with the initiating admin, scope, target principal, and correlation ids. JWKS pinning plus jti replay prevention makes it difficult to reuse Graph access even if an access token is leaked.
  • CCM does not expose a path for Agent or SPA compromise to hijack Entra. The Backend is the only holder of Graph app credentials, requires an authenticated admin session + signed operation token for each call, and records immutable audit events for every Graph‑backed change.

Password Manager Integration (Security Posture)

  • Feature-flagged per tenant and off by default; enabling requires admin session + policy scopes. Agent module is loaded only when the flag is set.
  • Broker communication uses mTLS on the service mesh; broker trusts the Backend JWKS and requires signed operation tokens for password issuance or legacy-app automation.
  • Unified credential store tags capabilities (["entra_passkey","legacy_otp"]) so Entra-bound credentials and legacy OTP uses stay isolated. TTLs enforced server-side via Redis; secrets expire automatically.
  • Audit is shared: broker and agent emit events (session created/issued/invalidated, shadow account changes, connector health) to the Backend audit pipeline with correlation ids.
  • No Entra escalation path: the Password Manager broker never talks to Graph or Entra; it only consumes Backend-issued scoped tokens, so the wide Graph permissions stay confined to the Backend layer.

Security FAQ

  • Does CCM introduce extra attack surface for Entra? CCM does not expose Graph secrets to the SPA or Agent; only the Backend holds app credentials. It adds an authorization layer (RBAC + short-lived operation tokens) between Entra and devices, so client compromise cannot directly use wide permissions.
  • Does the app hold a registered secret with higher permissions than the user? Yes, the Backend holds a Graph application credential to perform admin functions. It is stored in vault/secret store, never returned to clients, and its use is gated by RBAC and per-operation signed tokens.
  • Can CCM elevate a user beyond their Entra role via delegated calls? No. Delegated calls (e.g., admin-role discovery) use the user’s own token and cannot expand privileges; CCM can only further restrict actions through its policies.
  • How are signing keys and JWKS protected? Backend signing keys are persistent and stored securely; Agents pin JWKS and fail closed if validation cannot occur. Operation tokens carry jti/exp to prevent replay.
  • What is the worst case if the Backend is compromised? An attacker could abuse Graph app permissions. Mitigations include least-privilege scopes, vault isolation, key rotation, short-lived scoped tokens, and immutable audit to support detection and response.
  • What if the Agent is compromised? The Agent cannot mint Graph tokens or decrypt wallet traffic. Wallet ↔ Backend messages are end-to-end encrypted; the Agent only forwards opaque frames and validates operation tokens before invoking protected operations.
  • How is auditing handled? Every Entra/Graph-backed change and Password Manager action logs who, what, when, target, scope, and correlation id. Logs are immutable and can be exported to SIEM for monitoring.
  • What is the Password Manager impact on Entra? The Password Manager module is off by default, uses mTLS to its broker, and does not call Entra/Graph. It relies on Backend-issued scoped tokens, so Entra permissions remain confined to the Backend.
  • How are secrets rotated and monitored? Graph app credentials and Backend signing keys are rotated regularly (see rotation plan); JWKS is published for Agents, and failures trigger fail-closed behavior. Access to secrets is monitored via vault access logs.
  • How are consent and step-up enforced? Session tokens are bound to origins, and step-up actions require explicit user presence/PIN and fresh short-lived codes. Operation tokens are single-purpose and expire quickly to limit blast radius.

Auditing

  • Each request carries X-Agent-Request-Id for correlation.
  • Local logs record operations and device IDs, avoiding sensitive payload logging.
  • Backend stores immutable audit logs for SPA actions and Agent-reported results; export streams to SIEM.

Hardening

  • Rate-limit session and device calls.
  • Optional user PIN/biometric verification at Agent layer for sensitive operations.
  • Origin pinning cache with explicit reset.
  • Disallow mixed‑content calls; require HTTPS origins for SPA.
  • Per‑operation consent prompts and clear UX for destructive actions (backup, reset).
  • KMS/HSM for tenant keys, with periodic key rotation; rotate Backend signing keys and publish JWKS to Agents.
  • Envelope encryption for passwords; optional double-wrapping using wallet device public keys so only device can decrypt.

Secure Channel Establishment

FDO (FIDO Device Onboard) Primer

FDO is a standard by the FIDO Alliance designed to simplify and secure the process of moving a device from factory trust (manufacturer) to operational trust (owner). Its lifecycle stages include: - DI (Device Initialization): Device is provisioned at the factory with a device identity (keypair + certificate) under the Manufacturer Root (MR). - TO0/TO1 (Rendezvous): Mechanisms for devices to discover the owner or rendezvous server (optional in CCM2 since the Agent mediates locally). - TO2 (Owner Transfer): Cryptographic protocol that transfers ownership by provisioning owner credentials/policies to the device, authenticated under the manufacturer trust.

Why it matters here: CCM2 mirrors these steps to provision an Enterprise Security Admin (ESA) key to the wallet under manufacturer trust, then uses per‑user keys and ephemeral key exchange to operate securely thereafter. We do not require internet rendezvous; the SPA/Agent provides the local ‘handoff’. The protocol uses modern primitives (ECDH, HKDF, AEAD) and can carry claims in compact COSE/CBOR formats.

Wallet ↔ Backend End-to-End Secure Channel (FDO-inspired)

Goal: Establish an end-to-end, cryptographically authenticated and confidential channel between each Crayonic Wallet and the Backend, so the local Agent is an untrusted relay that cannot decrypt or forge messages. The design borrows from FIDO Alliance FDO (Device Onboard) phases while fitting our SPA/Agent topology.

Key Hierarchy and Lifecycle

  • Manufacturer Root (MR): Manufacturer CA root(s) shipped with Backend as trust anchors.
  • Device Identity Keypair (DIK): Unique per device at manufacture; public key certified by MR in a Device Certificate (DevCert).
  • Enterprise Security Admin (ESA) Keypair: Per-tenant owner/admin key stored in HSM/Vault; ESA public key is provisioned to devices during ownership transfer and authorizes administrative commands/policies.
  • User Keypair (UK): Per-user key on device for user-bound channels and operations; Backend stores only the public key and metadata.
  • Ephemeral Session Keys (ESK): ECDHE keys per session for forward secrecy.

Algorithms - Identity: ECDSA P-256 or Ed25519 (X.509/COSE) - Key agreement: X25519 or P-256 ECDHE - AEAD: AES-256-GCM or ChaCha20-Poly1305; KDF: HKDF-SHA256

Ownership Transfer (FDO-aligned)

Analogous to FDO DI/TO2: 1) DI (factory): Device holds DIK + DevCert signed by MR. 2) Owner assignment (TO2-equivalent): Backend proves ownership and injects ESA public key and initial policy under MR trust. Performed locally via Agent (encrypted end-to-end): - Device → Backend: { DevCert, nonce_d, Sig_DIK(nonce_d) } - Backend validates MR chain; returns { ESA_pub, policy_seed, nonce_b, Sig_ESA(nonce_d|nonce_b|ESA_pub|policy_seed) } encrypted/authenticated to DIK. - Device pins ESA_pub and persists policy.

Rotation: ESA keys rotated periodically; new ESA_pub signed by previous ESA or MR CA, enforced by device policy.

User Enrollment and Channel Establishment

1) UK creation/attestation: Device generates UK; attests UK_pub with DIK or ESA-bound signing key (EAT/COSE or X.509 ext). Backend verifies and records UK_pub. 2) Session channel (user-bound): - ECDHE: Device generates edh_d, Backend generates edh_b and exchange pubs via Agent. - Shared secret: ss = ECDH(edh_d.priv, edh_b.pub) = ECDH(edh_b.priv, edh_d.pub). - Derive keys: k_enc, k_mac = HKDF(ss, info = device_id | user_id | jti | nonces | transcript_hash). - Endpoint auth: Device signs its ephemeral with UK (or DIK if pre-user), includes DevCert/attestation; Backend signs with ESA key, includes ESA chain. - Result: All subsequent payloads are AEAD-protected end-to-end; Agent cannot inspect or modify.

3) Bind Authorization: Include Backend operation_token (JWT) ID (jti) in HKDF info and in the first frame to cryptographically tie the session to the authorization grant.

4) Presence/Verification: Device requests user presence/PIN/Bio signals; Agent surfaces prompts via /events.

Message Framing

  • Use compact CBOR/COSE or protobuf messages.
  • Each frame contains { seq, nonce, ciphertext, tag }; nonces derived or randomized per frame; replay rejected.
  • Large payloads are chunked; Agent handles transport-only concerns.

Revocation and Recovery

  • DRL: Backend maintains a device revocation list and propagates during sessions.
  • ESA rotation: Devices accept new ESA_pub signed by prior ESA or MR root.
  • User key revocation: Backend marks UK revoked; device enforces on next sync; channel establishment using revoked UK is refused.
  • Lost device: Backend flags device; future sessions are rejected or result in wipe per policy.

FDO Alignment

  • DevCert/DIK ←→ FDO Device Credential; ESA provisioning ←→ TO2; Rendezvous phases (TO0/TO1) are replaced by SPA/Agent-assisted local flow.
  • Recommend COSE/EAT for attestation claims and compact messaging.

References

  • FIDO Alliance — FDO (FIDO Device Onboard) Specification Overview: https://fidoalliance.org/fido-device-onboard-fdo/
  • FDO Architecture & Protocols (GitHub): https://github.com/fido-device-onboard/pri-fidoiot
  • IETF RFC 5869 — HKDF: https://www.rfc-editor.org/rfc/rfc5869
  • IETF RFC 5116 — AEAD: https://www.rfc-editor.org/rfc/rfc5116
  • IETF RFC 8152 — COSE: https://www.rfc-editor.org/rfc/rfc8152
  • IETF Draft — Entity Attestation Token (EAT): https://datatracker.ietf.org/doc/draft-ietf-rats-eat/

Backend Endpoints (supporting the channel)

  • POST /v1/provision/device/claim — verify DevCert/DIK possession; return ESA provisioning payload.
  • POST /v1/provision/device/user-attest — accept UK attestation; store UK_pub.
  • POST /v1/channel/start — negotiate ECDHE, provide Backend ephemeral and policy salt.
  • POST /v1/channel/message — relay opaque AEAD frames (Agent acts as pass-through).

Terminology and Abbreviations (Clear Definitions)

  • MR: Manufacturer Root certificate (X.509) used to sign device certificates.
  • DIK: Device Identity Keypair. Long-term asymmetric key unique to each wallet. Public key is in DevCert.
  • DevCert: X.509 certificate for DIK.pub, signed by MR.
  • ESA: Enterprise Security Admin keypair. Identifies the enterprise owner; private key in HSM/Vault.
  • UK: User Keypair created on device for a specific user.
  • ECDH/ECDHE: Elliptic Curve Diffie-Hellman (Ephemeral) key agreement to derive a shared secret.
  • HKDF: HMAC-based Key Derivation Function (RFC 5869) to derive encryption keys from shared secret.
  • AEAD: Authenticated Encryption with Associated Data (e.g., AES-GCM). Provides confidentiality + integrity.
  • COSE/CBOR: Compact object signing/encryption (RFC 8152) and binary JSON-like encoding used for messages.
  • EAT: Entity Attestation Token (IETF draft), CBOR/COSE based attestation format.
  • JWT/JWKS: JSON Web Token / JSON Web Key Set; Backend signs operation tokens; Agents validate using JWKS.
  • jti: JWT ID, a unique identifier for a specific token instance.

Cryptographic Choices (Practical Guidance)

  • Curves: Prefer X25519 for ECDH and Ed25519 for signatures (fast, safe). If platform constraints require NIST, use P-256.
  • Libraries:
  • .NET: System.Security.Cryptography (ECDsa, ECDiffieHellman), Microsoft.IdentityModel.Tokens (JWT), NSec optional.
  • Python: cryptography (hazmat ECDH/Ed25519/AESGCM), python-jose/jwcrypto for JWT.
  • C/Embedded: libsodium (crypto_kx, crypto_aead), mbedTLS (EC, GCM).
  • Randomness: Use OS CSPRNG (RandomNumberGenerator in .NET, secrets/os.urandom in Python). Never roll your own.
  • Nonces: Unique per AEAD frame. For AES-GCM 96-bit nonces, derive as nonce = H(base || seq) or use a counter with a random prefix.

Protocols and APIs (Developer-Focused)

Below are the detailed message structures and API contracts. All binary values are Base64URL encoded when represented in JSON. Example JSON blocks may contain inline comments for explanation; actual requests must use valid JSON without comments.

Actors and Modules (Who Sends What)

  • Wallet (Device): Generates DIK/UK, performs ECDH, signs device/user proofs, encrypts/decrypts E2E frames.
  • Agent (Local): Untrusted relay and UX surface. Initiates local consent, collects device I/O, forwards opaque frames. Verifies operation_token signature before allowing protected ops.
  • SPA (Browser): Authenticates admin, calls Backend for policy/tokens, orchestrates flows. Talks to Agent on localhost for device prompts.
  • Backend (Service): Root of authorization/policy, directory connectors, audit, ESA key holder. Participates in E2E channel with Wallet.

Tip: SPA decides and authorizes; Agent touches hardware; Backend enforces and decrypts; Wallet secures secrets.

1) Device Claim / Ownership Transfer

Request: POST /v1/provision/device/claim - Purpose: Prove device identity (DIK possession) and establish a one-time key agreement for encrypting the ESA provisioning payload. - Body:

{
  "device_cert": "<base64 DER DevCert>",
  "nonce_d": "<base64url 16-32 bytes>",
  "sig_dik": "<base64url signature over nonce_d>",
  "ka_pub": "<base64url device ECDH public key (X25519 or P-256)>"
}

Response 200:

{
  "nonce_b": "<base64url>",
  "esa_pub": "<base64 DER SubjectPublicKeyInfo or COSE_Key>",
  "policy_seed": "<base64url>",
  "sig_esa": "<base64url signature over (nonce_d || nonce_b || esa_pub || policy_seed)>",
  "enc": {
    "alg": "X25519-HKDF-SHA256-CHACHA20POLY1305",
    "ephemeral_pub": "<base64url backend ephemeral>",
    "ciphertext": "<base64url>",
    "tag": "<base64url>",
    "aad": "<base64url device_id | version>"
  }
}
Notes: - Backend verifies device_cert against MR and sig_dik with DIK.pub. - Backend performs ECDH with ka_pub to encrypt any sensitive fields in enc (e.g., secrets, initial keys). For minimal design, esa_pub and policy_seed can be outside enc but are also signed.

Pseudocode (Backend):

verify_cert_chain(device_cert, MR)
assert verify_signature(DIK_pub, nonce_d, sig_dik)
eph_b = ECDH.generate_keypair()
ss = ECDH(eph_b.priv, ka_pub)
k = HKDF(ss, info = "claim" || device_id || nonce_d)
nonce = random(12)
ciphertext, tag = AEAD_Encrypt(k, nonce, plaintext=provision_blob, aad=device_id)
sig_esa = SIG(ESA_priv, nonce_d || nonce_b || esa_pub || policy_seed)
return {...}

Pseudocode (Device):

assert verify_signature(ESA_pub, (nonce_d || nonce_b || esa_pub || policy_seed), sig_esa)
ss = ECDH(ka_priv, eph_b_pub)
k = HKDF(ss, info = "claim" || device_id || nonce_d)
provision_blob = AEAD_Decrypt(k, nonce, ciphertext, aad=device_id, tag)
persist(ESA_pub, policy_seed)

Sequence (who sends what):

sequenceDiagram
  autonumber
  participant W as Wallet (Device)
  participant A as Agent (Local)
  participant S as SPA (Browser)
  participant B as Backend
  S->>B: Authenticated session (IdP)
  S->>A: Start local session (consent)
  A-->>S: X-Agent-Session token
  A->>W: Open device
  A->>W: Request DevCert, nonce, signature
  W-->>A: device_cert, nonce_d, sig_dik, ka_pub
  A->>S: Forward claim payload (opaque)
  S->>B: POST /v1/provision/device/claim (payload)
  B-->>S: nonce_b, esa_pub, policy_seed, sig_esa, enc(...)
  S->>A: Forward response
  A->>W: Apply provisioning (opaque to A)

Annotated fields (Device → Backend): - device_cert: DevCert (X.509 DER) — proves DIK public key is signed by MR. - nonce_d: Device random nonce to prevent replay. - sig_dik: DIK signature over nonce_d proving DIK private key possession. - ka_pub: Device one-time ECDH public key for encrypting ESA payload.

Annotated fields (Backend → Device): - nonce_b: Backend random nonce to bind response. - esa_pub: ESA public key (DER/COSE) to be pinned by device. - policy_seed: Salt to anchor initial policy versioning. - sig_esa: ESA signature over nonce_d || nonce_b || esa_pub || policy_seed. - enc: AEAD envelope encrypted to ka_pub (backend sends its ephemeral public key too).

2) User Attestation

Request: POST /v1/provision/device/user-attest

{
  "device_id": "<string>",
  "user_id": "<string>",
  "uk_pub": "<base64 DER SPKI or COSE_Key>",
  "attestation": {
    "fmt": "EAT-COSE" | "X509-CHAIN",
    "evidence": "<base64url>"
  }
}
Response 204: stored.

Backend verifies attestation (DIK/ESA-linked) and records uk_pub for the user/device.

Sequence (who sends what):

sequenceDiagram
  autonumber
  participant W as Wallet
  participant A as Agent
  participant S as SPA
  participant B as Backend
  S->>A: Ask device to create a user key (UK)
  A->>W: Create UK
  W-->>A: uk_pub and attestation fmt+evidence
  A->>S: Forward
  S->>B: POST /v1/provision/device/user-attest (payload)
  B-->>S: 204 No Content

Annotated fields: - uk_pub: New user public key (DER/COSE). - attestation: Evidence linking uk_pub to device trust (DIK/ESA or platform attestation).

3) Channel Start (ECDHE + Binding)

Request: POST /v1/channel/start

{
  "device_id": "<string>",
  "user_id": "<string>",
  "edh_d_pub": "<base64url device ephemeral>",
  "sig_uk": "<base64url signature over edh_d_pub>",
  "operation_token": "<JWT>"
}

Response 200:

{
  "edh_b_pub": "<base64url>",
  "sig_esa": "<base64url signature over edh_b_pub>",
  "policy_salt": "<base64url>",
  "channel_id": "<uuid>"
}

Key Derivation (both sides):

ss = ECDH(edh_priv_local, edh_pub_remote)
info = "chan" || device_id || user_id || channel_id || jti(operation_token) || policy_salt || transcript_hash
{k_enc, k_mac} = HKDF_Expand(HKDF_Extract(ss, salt = 0), info)

Sequence (who sends what):

sequenceDiagram
  autonumber
  participant W as Wallet
  participant A as Agent
  participant S as SPA
  participant B as Backend
  S->>B: POST /v1/ops/token (scope, device_id, user_id)
  B-->>S: operation_token (JWT)
  S->>A: Request channel start
  A->>W: Generate device ephemeral edh_d
  W-->>A: edh_d_pub
  W-->>A: sig_uk on edh_d_pub
  A->>S: Forward
  S->>B: POST /v1/channel/start {device_id, user_id, edh_d_pub, sig_uk, operation_token}
  B-->>S: {edh_b_pub, sig_esa, policy_salt, channel_id}
  S->>A: Forward
  A->>W: Apply and derive keys

Annotated fields (request): - edh_d_pub: Device ECDHE public key. - sig_uk: UK signature over edh_d_pub (binds to user). - operation_token: Backend JWT authorizing the session/operation.

Annotated fields (response): - edh_b_pub: Backend ECDHE public key. - sig_esa: ESA signature over edh_b_pub (binds to enterprise). - policy_salt: Policy state salt for HKDF context. - channel_id: Identifier for subsequent frames.

### 4) Channel Message Relay

Request: `POST /v1/channel/message`
{ "channel_id": "", "seq": 1, "nonce": "", "aad": "", "ciphertext": "", "tag": "" }
Response 200:
{ "seq": 1, "nonce": "", "ciphertext": "", "tag": "" }
Notes:
- Agent just forwards request/response between device and Backend; content is opaque.
- Sequence numbers must be strictly increasing; Backend and device maintain independent rx/tx counters.

Sequence (who sends what):
```mermaid
sequenceDiagram
  autonumber
  participant W as Wallet
  participant A as Agent
  participant B as Backend
  W-->>A: seq, nonce, aad, ciphertext, tag
  A->>B: POST /v1/channel/message (channel_id, seq, nonce, aad, ciphertext, tag)
  B-->>A: seq, nonce, ciphertext, tag
  A->>W: Deliver backend frame (opaque)

Annotated fields: - seq: Monotonic per direction. - nonce: Unique per frame (see Nonce Strategy). - aad: Associated data (authenticated, not encrypted). - ciphertext/tag: AEAD outputs using derived keys.

Reference Pseudocode (Primitives)

HKDF (using library):

PRK = HKDF_Extract(salt, IKM = ss)
OKM = HKDF_Expand(PRK, info, L = 64)
k_enc = OKM[0:32]
k_mac = OKM[32:64]

AEAD Encrypt/Decrypt (AES-GCM):

Encrypt(k_enc, nonce, aad, plaintext):
  return AES_GCM_Encrypt(k_enc, nonce, aad, plaintext) -> (ciphertext, tag)

Decrypt(k_enc, nonce, aad, ciphertext, tag):
  return AES_GCM_Decrypt(k_enc, nonce, aad, ciphertext, tag) -> plaintext or error

Signature Verification:

verify_signature(pub, data, sig):
  return ECDSA_Verify(pub, SHA256(data), sig)

Nonce Strategy (counter-based):

nonce_prefix = random(4 bytes)
seq = 0
next_nonce():
  seq += 1
  return nonce_prefix || uint64_be(seq)

Implementation Notes & Pitfalls

  • Always validate certificate chains (DevCert → MR) and maintain a trusted MR store.
  • Check token audience, issuer, expiry, and jti uniqueness (no replay).
  • Zeroize secrets after use where possible; protect ESA private keys in HSM/Vault.
  • Enforce strict input validation; reject unknown fields; limit request sizes; rate limit endpoints.
  • Clock skew: allow small leeway (±2–5 minutes) for token validity; prefer monotonic counters for channel seq.
  • Use constant-time comparisons for MAC/tag validation; never leak error details that help oracle attacks.
  • SPA initiates /session/start { origin, nonce } and receives a handshake_id.
  • User approves in Agent consent UI or deep link, producing a code.
  • SPA finalizes via /session/finish { handshake_id, code } and receives a short-lived token (5–10 minutes).
  • Token is bound to allowed_origins and must be sent as X-Agent-Session on all protected endpoints.
  • Level 1 (Standard): device listing, CSR generation, passkey creation, certificate import, signing.
  • Level 2 (Step‑up): policy changes, backup/export/import, PIN change, wallet reset. Requires explicit re‑confirmation with on‑device/user‑presence and a fresh one‑time code. Tokens for step‑up actions have shorter TTL (e.g., 2–3 minutes) and single‑use.

Operation Tokens (Backend → Agent)

  • Backend issues JWTs authorizing sensitive Agent operations. Claims include: admin subject, tenant id, scopes, device_id/user_id, expiry, nonce, and audit correlation id.
  • Agent pins the Backend public key (per tenant) and validates signature, audience, and TTL.
  • SPA must supply both X-Agent-Session and operation_token in the request body for protected endpoints.

CORS and CSRF

  • Agent enforces strict CORS allowlist for the bound origin; Origin header is required.
  • No cookie authentication; all state changes require POST with token header.
  • Reject non-XHR/Fetch requests (no form posts or preflight-less changes).

Least Privilege

  • Keep Crayonic provider off unless a device is present.
  • Windows WebAuthn provider only exposes make-credential and never exports keys.
  • Backend RBAC: separate roles for user management, passkey ops, certificate ops, wallet policy, and password vault operations.