Skip to main content

JOSE signing and DPoP proofs

The runnable dpop-proof Pydantic AI example creates and verifies a proof, rejects its replay, and exposes only boolean policy evidence to the model. It is the reference example for the host/model boundary described below.

Both SDKs implement the asymmetric cryptographic operations needed by the current session slice:

  • JoseSigner signs client and session assertions with ES256 or RS256;
  • JoseDpopProofProvider creates a fresh RFC 9449 proof for each request; and
  • verify_dpop_proof / verifyDpopProof verify the signature, method, URL, issuance window, nonce, access-token hash, and replay state.

ES256 is the default. RS256 is accepted for interoperability. Symmetric algorithms and none are rejected even if the underlying JOSE library supports them.

In TypeScript, import device-side JoseDpopProofProvider from the portable package entry. Import JoseSigner, verifyDpopProof, and the replay-cache contracts from @datalayer/personal-agent-protocol/server. This enforced entry split keeps company assertion signing and proof verification out of browser bundles; it does not make an in-process Node private key equivalent to KMS/HSM custody.

Key custody​

The local signers receive an asymmetric private key object. DPoP providers receive key material from an application-owned resolver keyed by the request's tenant, subject, and company issuer. The protected header contains only the public JWK; a JWK with any private member is rejected.

For local development, an in-process private key is sufficient. TypeScript can also use a non-exportable Web Crypto key in trusted device code. Production hosts using an HSM, KMS, or another remote custody service should contribute their own Reactor signer and DPoP provider. Never place private JWKs, PEM values, proofs, assertions, or access tokens in configuration returned to a browser, logs, analytics, tool arguments, or model context.

Replay cache contract​

Verification requires an atomic replay-cache implementation. Its mark_once / markOnce operation receives a key composed from the public-key thumbprint and proof jti, plus an expiry. It must return true exactly once across every process that can accept proofs for that company. An ordinary per-process map is appropriate only for tests.

The expiry is the last moment the verifier would still accept the identifier: a DPoP proof's iat plus the acceptance window, a client or Session assertion's exp plus the 30-second clock skew, a browser assertion's exp. A cache must refuse the identifier until then and may forget it after: the default MemoryReplayCache sweeps expired identifiers whenever it has doubled, so it holds about twice those still alive. check_replay_cache / checkReplayCache (personal_agent_protocol.company.testing, the ./server entry) checks a cache: of many callers marking one identifier at once, exactly one wins. See Load tests.

The verifier checks replay state last, after signature and binding validation, so invalid traffic cannot fill the replay cache with attacker-chosen IDs.

URL and token binding​

The proof's htu excludes query and fragment, as required by DPoP, while htm uses the uppercase HTTP method. An API proof includes ath, the base64url SHA-256 hash of the exact access token. A token-endpoint proof must omit ath. When the company requests a nonce, the verifier requires an exact match.

The current helper returns the verified public JWK and its RFC 7638 thumbprint so a company session service can bind an issued token to that key. Token issuance and durable session storage remain the next company-side slice.