Account Management
ACME accounts are persistent identities that tie a public key to one or more email addresses. Every order, authorization, and certificate is associated with an account.
Account lifecycle
stateDiagram-v2
direction LR
[*] --> valid : create account\nPOST /acme/new-account
valid --> deactivated : POST status=deactivated
deactivated --> [*]
Accounts start in valid status. A valid account can:
- Create new orders.
- Manage existing orders and authorizations.
- Download certificates.
- Revoke certificates it owns.
- Update its contact list.
- Rotate its key.
- Deactivate itself.
A deactivated account is permanently disabled. All subsequent requests using a deactivated account’s key are rejected.
Creating an account
Send a POST request to /acme/new-account with a JWS signed by the account’s public key in jwk form. The payload must contain:
| Field | Type | Required | Description |
|---|---|---|---|
contact | array of strings | No | mailto: URIs for contact addresses |
onlyReturnExisting | boolean | No | If true, return the existing account or error |
Only mailto: URIs are accepted as contact values. Any other scheme causes a unsupportedContact error.
Example payload:
{
"contact": ["mailto:admin@example.com"],
"onlyReturnExisting": false
}
Response on creation (201 Created):
{
"status": "valid",
"contact": ["mailto:admin@example.com"],
"orders": "https://acme.example.com/acme/orders/<account-id>"
}
The Location header contains the account URL: https://acme.example.com/acme/account/<account-id>.
If an account already exists for the submitted key and onlyReturnExisting is false, the server returns the existing account with HTTP 200 rather than creating a duplicate.
Reading account details
POST to the account URL (/acme/account/<id>) with an empty payload (POST-as-GET). The kid header must reference the account being queried, and it must match the account ID in the URL.
Updating contact information
POST to /acme/account/<id> with a payload containing the new contact array:
{
"contact": ["mailto:new-admin@example.com"]
}
Contact addresses are replaced entirely; partial updates are not supported.
Deactivating an account
POST to /acme/account/<id> with:
{
"status": "deactivated"
}
The account is immediately marked deactivated. This action is irreversible.
Key rollover
To replace an account’s signing key without losing the account:
- Construct an outer JWS signed with the old key addressed to
/acme/key-change. - Embed an inner JWS signed with the new key as the payload. The inner JWS payload must contain
{ "account": "<account-url>" }.
POST /acme/key-change
Content-Type: application/jose+json
{
"protected": "<outer-header-signed-with-old-key>",
"payload": "<inner-jws-signed-with-new-key>",
"signature": "<outer-signature>"
}
sequenceDiagram
participant C as ACME Client
participant S as Akāmu Server
Note over C: Currently holds old private key
C->>C: Generate new key pair
C->>C: Build inner JWS signed by NEW key
Note right of C: payload = {"account": "https://…/acme/account/ID"}
C->>C: Wrap in outer JWS signed by OLD key
Note right of C: url = /acme/key-change, kid = account URL
C->>S: POST /acme/key-change
S->>S: Verify outer JWS signature (old key from DB)
S->>S: Verify inner JWS signature (new key from inner jwk)
S->>S: Check inner payload account == outer kid account URL
S->>S: Check new key not registered to another account
S->>S: Replace stored public_key + jwk_thumbprint
S-->>C: 200 OK (account object)
Note over C: All future requests must be signed with the new key
The server verifies:
- The outer JWS is signed by the current account key.
- The inner JWS is signed by the new key (
jwkmust be present in the inner header). - The inner payload’s
accountfield matches the account URL derived from the outerkid. - The new key is not already registered to another account.
On success, the account’s stored key and thumbprint are replaced with the new key. Subsequent requests must be signed with the new key.
Security considerations
- Each account is identified by the SHA-256 thumbprint of its JWK public key. The thumbprint is stored in the database to enable fast lookup without storing or parsing the full public key on every request.
- Key rollover is the only mechanism to change the signing key. There is no password or other credential; possession of the private key is the sole proof of identity.
- Contact email addresses are stored as plain text JSON in the database and are not validated against any mail server.