Crypto Engineering3 min

DPoP, HMAC, One-Time Nonces, AES-GCM: Anatomy of a Zero-Trust Certificate Issuance API

DPoP, HMAC, One-Time Nonces, AES-GCM: Anatomy of a Zero-Trust Certificate Issuance API

Our SDK lets any browser obtain a signing certificate from AWS Private CA. That makes the issuance endpoint the most security-critical surface we operate: whoever tricks it gets a trusted identity. So the endpoint is defended in depth, with four independent mechanisms per request. This post walks the full lifecycle, from challenge to issued certificate, and explains what each layer actually stops.

The threat model in one paragraph

An API key alone authenticates an account, not a request. Keys embedded in client-side code leak by definition. What we actually need to prove before issuing a certificate: the caller holds the private key matching the CSR, the request is fresh and unmodified, and the payload is confidential in transit. Every layer below exists because one of those statements would otherwise be false.

Layer one: challenge first, one-time nonces

The flow starts at a challenge endpoint that issues a nonce and stores it in DynamoDB. The nonce joins the subsequent certificate request, and the server consumes it exactly once, deleting it only after every other verification has passed. Order matters here: consume too early and a failed verification burns legitimate retries; consume too late and replays slip through. The response from the challenge endpoint is itself encrypted, which brings us to layer four in a moment.

Layer two: DPoP binds the request to the key

DPoP (Demonstrating Proof-of-Possession, defined in RFC 9449) has the client sign a JWT per request, bound to the HTTP method and URL in its claims. The server imports the JWK from the proof header and verifies both signature and claims. A stolen request is useless without the key, and a proof minted for one endpoint fails at another.

Layer three: the DPoP key must be the CSR key

This is the check most designs skip. Proving possession of a key is not enough; the key must be the one being certified. The server compares the DPoP public key with the public key inside the CSR. Without this, an attacker could present a victim's CSR with their own proof, or vice versa. One comparison closes a full substitution class.

Layer four: canonical HMAC and encrypted payloads

A canonical HMAC-SHA256 signature covers method, URI, selected headers, and the payload hash, derived from the shared API secret. On top of that, request and response bodies are AES-GCM encrypted with a PBKDF2-derived key. These are application-layer controls complementing TLS, not an argument that TLS is inadequate; they keep the payload meaningful only to parties holding the secret.

One detail I appreciate in hindsight: a middleware factory on the client fixes the ordering, so a consumer cannot accidentally sign before encrypting or encrypt twice. Security middleware that can be assembled wrong will be.

Then, and only then: issuance

Four independent checks guard issuance. The nonce is consumed only after all of them pass.

With all four checks green, the Lambda asks ACM Private CA to issue the certificate, with usage metering tied to the same request. Repeat clients get their existing certificate where possible instead of minting duplicates. The private key never crosses the wire at any point; only the CSR does (as shown in our client key lifecycle and signer bridge post).

Frequently asked questions

Is this OAuth? No. DPoP here proves possession of the signing key per request, not delegated user authorization. The API key still identifies the customer.

Why both DPoP and HMAC? They bind different things. DPoP binds the request to the client's private key; HMAC binds it to the shared secret and covers the exact canonical request. Each catches tampering the other cannot.

What happens on clock skew or retries? Nonce consumption happens once, after all verifications, so a failed attempt can be retried with the same nonce within its validity window.

None of these mechanisms is exotic. The value is in the composition: each layer assumes the others might fail, which is what zero trust means when you apply it to your own API.

Latest articles

How to find us

Want to learn more?