In ACME, why is every request signed by the account key rather than by the private key of the certificate being requested?
answer
- the account persists, the certificate key does not
- signed JWS, not a bearer token
- jwk first, kid afterwards
- kid is the account URL returned
- url field pins the request destination
basics
~20 sThe account key authenticates the protocol conversation, not the certificate. It identifies which account placed an order, completed an authorization and may revoke, and it lets the certified key stay out of the automation entirely except for the request submitted at finalize.
solid answer
~40 sEvery ACME request body is a JWS sent as `application/jose+json`, signed by the account key pair. The protected header carries `alg`, the anti-replay `nonce`, the `url` being addressed, and identification of the signer: `jwk` — the full public key — on `newAccount`, where no account exists to point at yet, and `kid` — the account URL returned in that response's `Location` header — on everything afterwards. Signing with the account key is what lets the server attribute an order, an authorization and a revocation request to one account and apply its rate limits and terms agreement. The key being certified is a separate pair with a separate lifetime, and the specification warns against reusing the account key for it.
code
json · 14 lines[
{
"alg": "ES256",
"jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." },
"nonce": "6S8IqOGY7eL2lsGoTZYifg",
"url": "https://acme.ca.example/acme/new-account"
},
{
"alg": "ES256",
"kid": "https://acme.ca.example/acme/acct/1",
"nonce": "sXZ4S4gDHm2iZ0hqJd3Qbw",
"url": "https://acme.ca.example/acme/new-order"
}
]go deeper
Recall that there is an account with its own key pair, separate from the key that goes in the certificate, and that every request is signed rather than carrying a password.
Explain the protected header field by field, when jwk gives way to kid, and where the account URL comes from.
Argue the separation from blast radius: what an attacker gets with the account key versus the certified key, and why the certified key never reaches the automation host.
Decide how many accounts an estate should have at all — one shared account concentrates rate limits and revocation power, many spread it and multiply the credentials to manage.
ACME has no passwords, no bearer tokens and no session. Every request that changes anything is a signed object, and the key that signs it is the **account key** — a pair generated by the client when it first registers, unrelated to the key that will end up inside the certificate. ## What a signed request looks like The body is a JWS in JSON serialization, sent with `Content-Type: application/jose+json`, and it has three parts: a protected header, a payload, and a signature. The protected header carries four things that matter: - **`alg`** — the signature algorithm. - **`nonce`** — a value the server issued, so the request cannot be replayed. - **`url`** — the exact resource URL this request is addressed to. The server rejects a signature whose `url` does not match where the request arrived, so a captured request cannot be aimed at a different endpoint. - **`jwk` or `kid`** — who is signing. ## jwk on the first request, kid afterwards The `jwk` and `kid` fields are alternatives, never both: | Request | Signer field | Why | |---|---|---| | `newAccount` | `jwk` — the full account public key | there is no account URL yet, so the key itself must travel | | every later request | `kid` — the account URL | the server already stores the key for that account and looks it up | | `revokeCert` using the certificate's own key | `jwk` | the requester may not be the account that ordered it | The account URL is not chosen by the client: it is the `Location` header of the `newAccount` response. A client that has the key but not the URL re-sends `newAccount` with `onlyReturnExisting` set, which asks the server to return the existing account for that key and to fail rather than create a new one. ## Why not sign with the key being certified Three reasons, and they are all about separation of concerns: 1. **Identity across orders.** A certificate key belongs to one certificate. The account is the thing that persists: it holds the terms agreement, it accumulates rate-limit history, it owns the authorizations that make a later order cheap. There has to be a stable identity across orders, and the certificate key cannot be it. 2. **Authorization state.** When a challenge is validated, the key authorization binds the proof to the account key. The server therefore knows which account proved control of which name, and can refuse an order from a different account that never proved anything. 3. **Blast radius.** The two keys have different exposure and different lifetimes. Compromise of the account key lets an attacker place orders and revoke; compromise of the certificate key lets them impersonate the service. Keeping them distinct means one incident does not automatically become the other, which is why the specification warns against reusing an account key as a certificate key. The practical effect is that the certified private key never has to be online where the protocol runs. The only point at which it is involved is the certification request posted to `finalize`. ## Account-level controls the signature makes possible Because the server can attribute every request to an account, it can attach policy to one: - **`externalAccountRequired`** — advertised in the directory's `meta` object by an authority that will not create an account without a binding to an existing customer record, so a bare `newAccount` is refused. - **`onlyReturnExisting`** — the client's way of saying "look this key up, do not enrol it". - **`keyChange`** — the resource that rolls the account key. The request is doubly signed: an inner JWS signed by the **new** key, naming the account and the old key, wrapped in an outer JWS signed by the **old** key. Both signatures are needed, so neither key alone can move the account. - **rate limits**, which are counted against the account and surface as an error in the `urn:ietf:params:acme:error:` namespace. ## The failure this prevents For a chain of pop-up stores, the automation that places orders runs in a deploy pipeline while the certified keys are generated on the machines that serve traffic. If the protocol were signed by the certified key, every one of those keys would have to reach the pipeline. Signing with an account key means the pipeline holds one credential that can order and revoke, and none that can impersonate a running storefront.
- What does the `url` field in the protected header defend against?Redirection of a valid signature to a different resource. Without it, a signed body captured on its way to one endpoint could be submitted to another that accepts the same shape — a challenge-ready POST aimed at a revocation resource, for example. The server compares the `url` in the signed header against the resource the request actually reached and rejects a mismatch, so the signature covers its own destination.
- How does an account change its key without losing its orders and authorizations?Through the `keyChange` resource, with a doubly signed request: an inner JWS signed by the new key that names the account URL and the old key, wrapped in an outer JWS signed by the old key. The server verifies both, proving that whoever holds the old key intends the change and that the new key exists and consents. The account URL is unchanged, so its history follows it.
- A client has the account key but lost the account URL. What does it send?A `newAccount` request with `onlyReturnExisting` set to true. The server looks the key up and returns the existing account, with its URL in the `Location` header, rather than enrolling a second account for the same key. Without that field, a server would happily create a new account, and the client would silently lose the rate-limit history and cached authorizations attached to the first one.
saying these in an interview costs you the question
- Thinks the account key is the key inside the certificate
- Says both jwk and kid appear in the same protected header
- Believes the client picks its own account identifier
- Treats the signature as a bearer token that can be reused
- Assumes rate limits are counted per name rather than per account