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():
| Provider | Backing store | Rotation supported |
|---|---|---|
mongo | MongoDB-backed envelope-KMS (default) | Yes |
kms | Cloud KMS (AWS KMS / GCP KMS adapter) | Yes |
env | Single key from environment variables | No |
file | Single key from a mounted file | No |
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 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.
| Field | Type | Description |
|---|---|---|
provider | enum | mongo, kms, env, file |
ready | boolean | true when warmup succeeded |
mode | enum | prod-cert, prod-jwk, ephemeral-jwk, legacy-hmac |
kid | string | Active key ID (SHA-256 of SubjectPublicKeyInfo) |
alg | enum | ES256 for ecdsa-p256-jws; future Ed25519 |
certChainDepth | number | Length of the certificate chain (0 for JWK-only modes) |
history | array | null | Last 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 -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:
| HTTP | code | Cause |
|---|---|---|
400 | KMS_ROTATION_UNSUPPORTED | Provider is env or file; rotation requires mongo or kms |
400 | KMS_NOT_CONFIGURED | Backing store unconfigured (e.g. KMS adapter ARN missing) |
400 | KMS_BAD_KEK | Key-encryption-key invalid |
401 | — | Bearer token missing or invalid |
403 | — | Caller is not owner or admin |
500 | KMS_ROTATION_FAILED | Generic 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
owneroradminrole; the per-orgOrgMember.roledoes not apply here. A workspace admin who is an org member cannot rotate keys. - Rotation is provider-dependent.
envandfileproviders cannot rotate — the key lives outside the application's control. Operators wanting in-app rotation must runmongoorkms. - 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.
codeis the machine-readable disambiguator;safeLogis where operators read the full error. - Cache invalidation is automatic.
POST /rotateresets 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 /rotateis 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.rotateevent lands - Sub-Processors — the
hosted-KMS sub-processor disclosure when
kmsprovider is active - Plans & Features —
the
c2paContentCredentialsgate that activates the consumer side - Error Codes — the broader error taxonomy