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:
JoseSignersigns client and session assertions with ES256 or RS256;JoseDpopProofProvidercreates a fresh RFC 9449 proof for each request; andverify_dpop_proof/verifyDpopProofverify 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.