Skip to content

Key Operations Narrowing Forbidden

URI: https://docs.ankatech.co/errors/key-ops-narrowing-forbidden
HTTP Status: 422 Unprocessable Entity

When you see this

A key rotation supplied an explicit key_ops set that drops an operation the current PRIMARY material permits. Within a key's fixed purpose family a rotation may only preserve or widen its operation set — never narrow it.

The gate is mandatory and not configurable. It runs after the purpose gate and the structural gate, and before any new key material is generated, so a refused rotation leaves the key exactly as it was.

Reported as a 422, not a 500

The refusal reaches the handler that owns it, so a rotation that would narrow keyOps receives this 422, never a generic 500.

Common Causes

  • Rotating a key that permits encrypt and decrypt while supplying key_ops of ["decrypt"] only.
  • Rotating a signing key that permits sign and verify while supplying ["verify"].
  • Copying a key_ops array from a different key, or from an older revision of a request, that happens to be narrower than the current material's.

Response Example

{
  "type": "https://docs.ankatech.co/errors/key-ops-narrowing-forbidden",
  "title": "Key Operations Narrowing Forbidden",
  "status": 422,
  "detail": "Rotation may not narrow key_ops. Dropped operations: decrypt.",
  "instance": "/api/v3/admin/tenants/7c9e6679-7425-40de-944b-e07fc1f90ae7/keys/my-mlkem-key/rotations",
  "correlationId": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": 1730000000,
  "extensions": {
    "errorCode": "KEY_OPS_NARROWING_FORBIDDEN"
  }
}

Content-Type: application/problem+json — the response follows RFC 9457 Problem Details.

The detail names only the dropped operations. It never carries key material, the kid, a UUID or a tenant identifier; the kid you supplied is echoed in instance.

The example above shows the Core API shape — the Core API is where this refusal originates, and it carries the correlation identifier as the top-level correlationId. The Admin API relays the same refusal on its tenant-scoped rotation route (POST /api/v3/admin/tenants/{tenantId}/keys/{kid}/rotate), so on that path instance is the Admin API route and the correlation identifier is extensions.requestId — see Where extension members live.

Branch on type, not on errorCode

extensions.errorCode is emitted by the Core API and is convenient for a direct integration, but it does not survive the Admin API relay: the relay's problem view is a closed four-member record (type, title, status, detail), so extension members are dropped end to end.

type is the identifier to dispatch on. RFC 9457 §3.1.1 makes it the primary identifier of a problem, this type is a dedicated slug rather than a shared one, and it is relayed unchanged — so a consumer branching on type gets the same discrimination on both the direct and the relayed path, and never has to parse detail.

if ("https://docs.ankatech.co/errors/key-ops-narrowing-forbidden"
        .equals(problemDetails.getType().toString())) {
    // widen or omit key_ops, then resubmit
}

How to Resolve

  1. Read the current PRIMARY material's key_ops (GET /api/v3/key-management/keys/{kid}/metadata).
  2. Resubmit the rotation with key_ops that preserve or widen that set.
  3. Or simply omit key_ops. A rotation that omits it keeps the current operations within the key's purpose, which can never narrow them. It never takes the new algorithm's full allowed set: an RSA or EC key created for signing stays a signing key.

Narrowing an operation set is a separate action

Rotation exists to change key material, not to reduce a key's capability. If you genuinely need a narrower operation set, create a key with that set rather than trying to reach it through a rotation.

For full schema definitions, examples, and interactive testing, see the Developer Hub Reference.