Skip to main content

C2PA KMS

The C2PA KMS API is the operator-facing surface for the signing-key infrastructure that backs the Provenance API. One status read, one rotation write. The keys themselves never appear on the wire — this resource exposes only the metadata needed to confirm a signer is ready, identify the active kid (key ID), and trigger a generational rotation when the operator's key-rotation policy fires. Tested against API version v1. Mounted at /api/v1/c2pa-kms. JWT auth on every endpoint; both endpoints require the owner or admin role at the system level.

Provider modes

The signer abstracts over four provider modes, resolved at runtime via resolveProviderName():

ProviderBacking storeRotation supported
mongoMongoDB-backed envelope-KMS (default)Yes
kmsCloud KMS (AWS KMS / GCP KMS adapter)Yes
envSingle key from environment variablesNo
fileSingle key from a mounted fileNo

env and file are operator-friendly defaults for development and air-gapped deployments; only mongo and kms support the rotation endpoint.

Endpoints

GET   /api/v1/c2pa-kms/status
POST /api/v1/c2pa-kms/rotate

Get signer status

GET /api/v1/c2pa-kms/status

Returns the active signer's metadata plus the last-10 generation history when the provider is mongo / kms.

curl
curl https://api.adjudon.com/api/v1/c2pa-kms/status \
-H "Authorization: Bearer $ADJUDON_JWT"

Response when the signer is ready (200 OK):

{
"success": true,
"data": {
"provider": "mongo",
"ready": true,
"mode": "prod-jwk",
"kid": "8f7d6c5b4a3...",
"alg": "ES256",
"certChainDepth": 2,
"history": [
{ "kid": "8f7d6c5b...", "generation": 4, "status": "active", "activatedAt": "2026-04-01T00:00:00Z" },
{ "kid": "1b2c3d4e...", "generation": 3, "status": "retired", "activatedAt": "2026-01-01T00:00:00Z", "retiredAt": "2026-04-01T00:00:00Z" }
]
}
}

When the signer is not ready (KMS unwarmed, KEK missing, or backing-store unreachable), the same endpoint returns 200 with ready: false plus a low-cardinality code:

{
"success": true,
"data": {
"provider": "mongo",
"ready": false,
"error": "Signer not ready",
"code": "SIGNER_NOT_READY"
}
}

The handler never echoes the underlying exception message — KMS errors can carry partial key material on some providers, so the controller scrubs the message and surfaces only the code field per Cardinal Rule #4. Operators read the real error from safeLog server-side.

FieldTypeDescription
providerenummongo, kms, env, file
readybooleantrue when warmup succeeded
modeenumprod-cert, prod-jwk, ephemeral-jwk, legacy-hmac
kidstringActive key ID (SHA-256 of SubjectPublicKeyInfo)
algenumES256 for ecdsa-p256-jws; future Ed25519
certChainDepthnumberLength of the certificate chain (0 for JWK-only modes)
historyarray | nullLast 10 generations; null for env / file providers

Rotate the signing key

POST /api/v1/c2pa-kms/rotate

Generates a new generation, marks the previous generation retired, invalidates the in-process signer cache, and warms up the new key. Returns the new kid, generation number, and rotation timestamp.

curl
curl -X POST https://api.adjudon.com/api/v1/c2pa-kms/rotate \
-H "Authorization: Bearer $ADJUDON_JWT"

Response (201 Created):

{
"success": true,
"data": {
"kid": "9a8b7c6d...",
"generation": 5,
"rotatedAt": "2026-05-06T14:02:11.000Z"
}
}

Rotation writes one c2pa.key.rotate entry to the Operations Audit Log with the new kid as resourceId and the generation number in the detail narrative.

Errors:

HTTPcodeCause
400KMS_ROTATION_UNSUPPORTEDProvider is env or file; rotation requires mongo or kms
400KMS_NOT_CONFIGUREDBacking store unconfigured (e.g. KMS adapter ARN missing)
400KMS_BAD_KEKKey-encryption-key invalid
401Bearer token missing or invalid
403Caller is not owner or admin
500KMS_ROTATION_FAILEDGeneric rotation failure (logged via safeLog)

The safeMsg returned to the client is curated — the underlying exception message never flows to the wire.

Common gotchas

  • Admin-only. Both endpoints require the system-level owner or admin role; the per-org OrgMember.role does not apply here. A workspace admin who is an org member cannot rotate keys.
  • Rotation is provider-dependent. env and file providers cannot rotate — the key lives outside the application's control. Operators wanting in-app rotation must run mongo or kms.
  • No keys on the wire. This API never returns key material. Public keys are surfaced via the Provenance settings endpoint (/provenance/settings), which exposes the SubjectPublicKeyInfo bytes for verifiers; private material lives only in the backing store.
  • Cardinal Rule #4 on errors. Both endpoints scrub underlying exception messages from the response. code is the machine-readable disambiguator; safeLog is where operators read the full error.
  • Cache invalidation is automatic. POST /rotate resets the in-process signer cache and warms the new key before responding. Subsequent C2PA manifest issuances use the new key without manual intervention.
  • Idempotency. POST /rotate is not idempotent — every call generates a new generation. Operators with a rotation policy script should debounce client-side or schedule the policy with a fixed cadence to avoid generation inflation.

Rotation cadence in practice

Customers operating under a documented key-rotation policy typically rotate every 90 days, with an emergency rotation path triggered manually after a suspected key compromise. Adjudon does not auto-rotate — the policy decision lives with the operator. The Q3 2026 roadmap includes scheduler- driven auto-rotation gated on a per-org configuration toggle.

See also

  • Provenance API — the consumer of every signing key managed here
  • Audit Log API — where every c2pa.key.rotate event lands
  • Sub-Processors — the hosted-KMS sub-processor disclosure when kms provider is active
  • Plans & Features — the c2paContentCredentials gate that activates the consumer side
  • Error Codes — the broader error taxonomy