Key Rotation¶
Cosmian KMS supports manual key rotation for all key types through the standard KMIP operations:
| KMIP operation | Applies to | CLI command |
|---|---|---|
Re-Key |
Symmetric keys, secret data | ckms sym keys rekey |
Re-Key Key Pair |
Asymmetric key pairs (RSA, EC, PQC, …) | ckms {rsa,ec,pqc} keys rekey |
ReCertify |
X.509 certificates | ckms certificates certify --certificate-id-to-re-certify |
On every rotation the server:
- Generates a new cryptographic object (or new certificate) under a fresh UID.
- Sets a
ReplacementObjectLinkon the old object pointing to the new UID. - Sets a
ReplacedObjectLinkon the new object pointing back to the old UID. - Transitions the old key to Deactivated (KMIP §4.57, transition 6 — see below).
- Increments
x-rotate-generationand recordsx-rotate-dateon the new object.
Auto-rotation (scheduler-driven, policy-based) is covered separately in Auto-Rotation Policy. HSM key rotation is covered in HSM Key Rotation.
State restrictions¶
Only objects in the following states can be the source of a rotation:
| State | Rotation allowed? | Rationale |
|---|---|---|
| Active | ✅ | Primary valid source state. |
| Deactivated | ✅ | KMIP §6.1.46 permits it; a replacement key should still be issued. |
| Compromised | ✅ | Rotating a compromised key is the recommended incident response. |
| Pre-Active | ❌ | Key material was never activated — rotating unused material is premature. |
| Destroyed | ❌ | Object no longer exists. |
| Destroyed_Compromised | ❌ | Object no longer exists. |
This restriction applies to the source key only. The output of a rotation can enter
Pre-Activeif the request includes anOffset > 0(the new key’s Activation Date is set toInitial Date + Offset).
Old key Deactivated after rotation¶
Why deactivation is enforced¶
A key in the Deactivated state SHALL NOT be used for applying cryptographic protection (encryption, signing, wrapping, MACing, deriving). It MAY still be used to process already-protected data (decryption, signature verification, unwrapping). This asymmetry is the key principle behind post-rotation key handling:
- Limit blast radius. If the old key material is compromised after rotation, no new data was ever encrypted with it — the damage is bounded.
- Enforce the cryptoperiod. NIST SP800-57-1 defines a cryptoperiod as the span during which a key is authorised to protect data. Key rotation ends that period; the Deactivated state makes the boundary enforceable by the server.
- Preserve backward compatibility. Consumers holding ciphertexts produced before rotation must still be able to decrypt. Deactivated keeps the key accessible for decryption without allowing it to encrypt new data.
How KMIP §4.57 mandates it¶
KMIP §4.57 defines the State attribute and lists the only legal state
transitions. Transition 6 governs Active → Deactivated and states that it
SHALL occur in one of three ways:
- The object’s Deactivation Date is reached.
- A client issues a Revoke operation with a non-Compromised reason.
- A client Modify Attribute sets the Deactivation Date to now or the past.
Re-Key / Re-Key Key Pair are not listed as a direct cause of transition 6.
The spec mandates deactivation indirectly through path 1: the KMIP §4.33
Deactivation Date attribute rules list Re-key and Re-key Key Pair under
“When implicitly set”, meaning the server MUST set the Deactivation Date on
the old key as part of the rotation operation. Because that date is set to
now (or already passed), the condition “Deactivation Date is reached”
immediately triggers transition 6.
In short: the spec does not have a direct Re-Key → Deactivated transition, but
it mandates deactivation by requiring the server to set the Deactivation Date
during Re-Key, which then fires transition 6 automatically.
Consequences¶
- The old key can no longer be used for Encrypt or Sign operations
(those require
Activestate). - The old key remains available for Decrypt and Verify (processing
operations accept
Active,Deactivated, andCompromisedstates per KMIP §3.31), so in-flight ciphertexts continue to decrypt. - You can call
Revokeon an already-Deactivated key; the call succeeds as a no-op (the state is already revoked). - You can call
Destroyon a Deactivated key directly — no priorRevokeis required.
Keysets¶
A keyset is a named group of related key generations. Each generation is
a distinct cryptographic key (different material, different UID) produced by
successive Re-Key operations.
Creating a SQL key in a keyset¶
For SQL-backed keys the key’s UID must equal the keyset name from the
start. Supply both UniqueIdentifier and x-rotate-name in the same Create
request:
# Create a key whose UID is the keyset name
ckms sym keys create --key-id my-keyset --algorithm aes --length 256
ckms sym keys set-rotation-policy --key-id my-keyset --name my-keyset
Attempting set-rotation-policy --name X on a SQL key whose UID is not X
is rejected:
Invalid Request: SetAttribute: rotate_name ('X') must equal the key's UID — create the key with the keyset name as its ID
SQL keyset UID scheme¶
UIDs are assigned deterministically at each generation:
my-keyset ← gen 0 (UID equals the keyset name)
my-keyset@1 ← gen 1 (after first Re-Key)
my-keyset@2 ← gen 2 (after second Re-Key)
The Re-Key response always returns the new key’s real UID (e.g. my-keyset@1).
Addressing syntax¶
A keyset can be referenced by name wherever a UniqueIdentifier is accepted
(Encrypt, Decrypt, Get, Re-Key, …):
| Syntax | Resolves to |
|---|---|
my-keyset |
Latest generation (bare name = @latest) |
my-keyset@latest |
Latest generation (explicit alias) |
my-keyset@first |
Generation 0 |
my-keyset@0 |
Generation 0 (numeric alias for @first) |
my-keyset@N |
Generation N |
Encrypt / Sign always resolves to the latest generation.
Decrypt / Verify walks the chain newest-to-oldest until one generation succeeds, allowing ciphertexts encrypted with an older (now Deactivated) key to continue to decrypt after rotation.
@latestis a virtual alias. It is never stored in the database and is never returned in a response.my-keyset(bare name) also resolves to the latest generation even thoughmy-keysetis the literal UID of gen-0. To access gen-0 explicitly, usemy-keyset@0ormy-keyset@first.
Non-latest guard¶
Only the latest generation of a keyset can be rotated via Re-Key.
Attempting to rotate a retired member is rejected:
Invalid Request: ReKey: key '<uid>' is not the latest in its keyset —
only the latest generation can be rotated
Use my-keyset@0 (explicit generation) rather than my-keyset (bare name)
to target an older generation — and expect that call to fail.
Keyset internals (SQL)¶
Keyset state is stored as KMIP vendor attributes in the database:
| Attribute | Type | Meaning |
|---|---|---|
x-rotate-name |
string |
Keyset name; equals the key UID for gen-0. |
x-rotate-generation |
i32 |
0 for gen-0, incremented on each Re-Key. |
x-rotate-latest |
bool |
true on the current key; false on older keys. |
KMIP link attributes mirror the chain for protocol compliance:
ReplacementObjectLink (old → new) and ReplacedObjectLink (new → old).
Keyset chain traversal for Decrypt / Verify uses
x-rotate-generation (sorted descending), not the link back-pointers.
Rotation flows by key type¶
1. Plain symmetric key¶
A plain symmetric key carries only its own policy.
What happens:
- Fresh key material is generated (same algorithm and length).
- The new key gets a new UID (
my-keyset@Nfor keyset keys, fresh UUID otherwise). ReplacedObjectLinkon the new key → old key.ReplacementObjectLinkon the old key → new key.- Old key → Deactivated.
KMIP link chain after two rotations:
flowchart LR
K0["Key₀ (gen 0)
Deactivated"] -->|ReplacementObjectLink| K1["Key₁ (gen 1)
Deactivated"]
K1 -->|ReplacementObjectLink| K2["Key₂ (gen 2)
Active"]
K2 -->|ReplacedObjectLink| K1
K1 -->|ReplacedObjectLink| K0
CLI:
# Manual rotation of a SQL keyset key
ckms sym keys rekey --key-id my-keyset
# Response: new UID = my-keyset@1
# Manual rotation of a plain UUID key
ckms sym keys rekey --key-id <KEY_UID>
2. Wrapping key¶
A wrapping key is referenced by one or more wrapped keys via a
WrappingKeyLink attribute. Rotating a wrapping key re-wraps all its
dependants atomically.
What happens:
- A new wrapping key is created and committed to the database (Phase 1).
- Every key owned by the same user whose
WrappingKeyLinkpoints to the old wrapping key is fetched — regardless of its state (Active, Deactivated, Pre-Active, Compromised, …) — unwrapped in memory (plaintext never stored), and re-wrapped with the new wrapping key (Phase 2). - Each wrapped key’s
WrappingKeyLinkis updated to the new wrapping key UID. - Standard rotation metadata is applied; old wrapping key → Deactivated.
No state filter on dependants. The server queries all keys with a
wrapping_key_idmatching the rotated key, with no restriction on their lifecycle state. Only the owner check applies: dependants owned by a different user are skipped (with a warning logged).
sequenceDiagram
participant Client
participant KMS
participant DB
Client->>KMS: Re-Key(wrapping_key_uid)
KMS->>DB: Phase 1 — create new wrapping key (committed)
loop For each wrapped dependant
KMS->>DB: retrieve wrapped key
KMS->>KMS: unwrap with old wrapping key
KMS->>KMS: re-wrap with new wrapping key
KMS->>DB: update WrappingKeyLink → new wrapping key UID
end
KMS->>DB: Phase 2 — retire old wrapping key (Deactivated, links set)
KMS-->>Client: Re-KeyResponse(new_wrapping_key_uid)
3. Wrapped key¶
A wrapped key stores its key material encrypted under a wrapping key.
What happens:
- The wrapped key is exported and unwrapped in memory using the current wrapping key (plaintext never stored).
- Fresh plaintext key material is generated from the unwrapped attributes.
- The new material is re-wrapped with the same wrapping key.
- The new ciphertext is stored under a new UID with an active
WrappingKeyLink. - Standard rotation metadata is applied; old wrapped key → Deactivated.
sequenceDiagram
participant Client
participant KMS
participant DB
Client->>KMS: Re-Key(wrapped_key_uid)
KMS->>DB: retrieve wrapped key + wrapping key
Note over KMS: unwrap in-memory (plaintext never stored)
KMS->>KMS: generate new key material
KMS->>KMS: re-wrap with same wrapping key
KMS->>DB: store new wrapped key (new UID, same WrappingKeyLink)
KMS->>DB: retire old key (Deactivated, ReplacementObjectLink → new key)
KMS-->>Client: Re-KeyResponse(new_key_uid)
4. Asymmetric key pair¶
Re-Key Key Pair targets the private key UID. The server resolves the
associated public key via the PublicKeyLink attribute.
What happens:
- A new private key + public key pair is generated (same algorithm).
- Both receive new UIDs; the new private key carries a
PublicKeyLinkto the new public key. - Standard
ReplacementObjectLink/ReplacedObjectLinklinks are set on both pairs. - Old private key and old public key → Deactivated.
sequenceDiagram
participant Client
participant KMS
participant DB
Client->>KMS: Re-Key Key Pair(private_key_uid)
KMS->>DB: retrieve private key
KMS->>DB: retrieve linked public key (PublicKeyLink)
KMS->>KMS: generate new key pair (same algorithm)
KMS->>DB: Phase 1 — store new private key + new public key
KMS->>DB: Phase 2 — retire old private key + public key (Deactivated, links)
KMS-->>Client: Re-Key Key PairResponse(new_sk_uid, new_pk_uid)
CLI:
# EC key pair
ckms ec keys rekey --key-id <PRIVATE_KEY_UID>
# RSA key pair
ckms rsa keys rekey --key-id <PRIVATE_KEY_UID>
# Post-quantum (ML-KEM, ML-DSA, SLH-DSA)
ckms pqc keys rekey --key-id <PRIVATE_KEY_UID>
5. Wrapped private key (Covercrypt)¶
A Covercrypt master private key follows the same Re-Key Key Pair flow. The
wrapped key is unwrapped in memory, the Covercrypt partition attributes are
re-keyed, and the new wrapped private key is stored under a fresh UID.
Setting a rotation policy on a wrapped private key always works: the
x-rotate-*attributes are stored in the metadata column (not inside the ciphertext block) and do not require the key to be unwrapped first.
6. Certificate renewal (ReCertify)¶
Certificate renewal creates a new certificate for the same key pair — no new key material is generated.
What happens:
- The existing certificate is retrieved and its issuer / subject are resolved.
- A new certificate is built and signed (same key pair, same issuer).
- The new certificate receives a fresh UID.
ReplacedObjectLinkon the new cert → old cert.ReplacementObjectLinkon the old cert → new cert.- All keys linked to the old certificate have their
CertificateLinkupdated. x-rotate-generationandx-rotate-dateare updated.
sequenceDiagram
participant Client
participant KMS
participant DB
Client->>KMS: ReCertify(old_cert_uid)
KMS->>DB: retrieve old certificate
KMS->>KMS: resolve issuer + subject from old cert
KMS->>KMS: build & sign new certificate (same key pair)
KMS->>DB: Phase 1 — store new cert (fresh UID)
KMS->>DB: Phase 2 — update old cert (ReplacementObjectLink)
KMS->>DB: Phase 2 — relink keys (CertificateLink → new cert)
KMS-->>Client: ReCertifyResponse(new_cert_uid)
Attribute changes (KMIP 2.1 §6.1.45):
| Attribute | New certificate | Old certificate |
|---|---|---|
Unique Identifier |
Fresh UUID | Unchanged |
Initial Date |
Now | Unchanged |
Link[ReplacedObjectLink] |
→ old cert UID | — |
Link[ReplacementObjectLink] |
— | → new cert UID |
Link[PublicKeyLink] |
Copied from old | Unchanged |
Link[PrivateKeyLink] |
Copied from old | Unchanged |
Name |
Inherited from old | Removed (per KMIP spec) |
State |
Active | Active (not Deactivated — certificates are exempt from §4.57) |
x-rotate-generation |
old + 1 | Unchanged |
x-rotate-date |
Now | Unchanged |
CLI:
# Renew a CA-signed certificate (same key pair, new validity period)
ckms certificates certify \
--certificate-id-to-re-certify <OLD_CERT_UID> \
--issuer-private-key-id <ISSUER_SK_UID> \
--days 365
# Self-signed certificate renewal
ckms certificates certify \
--certificate-id-to-re-certify <OLD_CERT_UID> \
--days 3650
Standards:
| Standard | Relevance |
|---|---|
| KMIP 2.1 §6.1.45 | Normative definition of ReCertify |
| RFC 4210 §5.3.5–5.3.6 | CMP Key Update Request / Response (kur/kup) — the wire-protocol equivalent |
| RFC 5280 | X.509v3 certificate structure and validity periods |
7. KEK-protected key (server-wide key-encryption key)¶
When the KMS server is started with --key-encryption-key <KEK_UID>, every
object stored in the database is transparently wrapped by the KEK. Rotation
works identically to the wrapped-key flow above — the server unwraps in memory,
generates fresh material, re-wraps, and stores the result.
# Example server startup with SoftHSM2 KEK
cosmian_kms \
--database-type sqlite \
--hsm-model softhsm2 \
--hsm-slot 0 \
--hsm-password 12345678 \
--key-encryption-key "hsm::softhsm2::0::my-kek"
No special handling is required for rotation policy — SetAttribute on a
KEK-wrapped key writes the x-rotate-* attributes to the metadata column, not
into the ciphertext, so no unwrap is needed.
Rotation dispatch overview¶
flowchart TD
subgraph "Re-Key dispatch"
REQ["Re-Key / Re-Key Key Pair / ReCertify"] --> DISPATCH{"Object type?"}
DISPATCH -->|SymmetricKey, no dependants| PLAIN["Plain rekey
(new material, new UID)"]
DISPATCH -->|SymmetricKey, has WrappingKeyLink dependants| WRAP_K["Wrapping-key rotation
(Phase 1 commit → Phase 2 re-wrap)"]
DISPATCH -->|SymmetricKey, is wrapped| WRAP_D["Wrapped-key rotation
(unwrap → new material → re-wrap)"]
DISPATCH -->|PrivateKey| ASYM["Re-Key Key Pair"]
DISPATCH -->|Certificate| CERT["ReCertify
(same key pair, new cert UID)"]
PLAIN & WRAP_K & WRAP_D & ASYM & CERT --> META["Update metadata
(generation++, date, links,
old key → Deactivated)"]
end
KMIP attribute changes on manual rotation¶
When the user explicitly calls Re-Key (e.g. ckms sym keys rekey), the
following attributes are set on the old and new key:
| Attribute | Old key | New key |
|---|---|---|
Unique Identifier |
unchanged | fresh UUID (or name@N for keyset keys) |
State |
Deactivated (§4.57) | Active |
Link[ReplacementObjectLink] |
→ new key UID | — |
Link[ReplacedObjectLink] |
— | → old key UID |
Link[WrappingKeyLink] |
unchanged | copied from old key |
x-rotate-generation |
unchanged | old value + 1 |
x-rotate-date |
unchanged | timestamp of rotation |
x-rotate-interval |
set to 0 (disabled) |
0 — must be re-armed explicitly on the new key |
x-rotate-name |
unchanged | inherited from old key |
x-rotate-offset |
unchanged | None (not inherited for manual rekey) |
Name |
removed | inherited from old key |
Manual vs auto-rotation difference:
x-rotate-intervalis intentionally set to0(not inherited) on the new key after a manual rotation. This forces the operator to re-evaluate the rotation policy for the new key rather than blindly continuing the old schedule.
Revoking superseded keys¶
After rotation the old key is Deactivated (not Destroyed). Its material persists so that in-flight Decrypt / Verify operations against old ciphertexts continue to work. Once all consumers have migrated, destroy the old key:
# Find the old key UID from the new key's ReplacedObjectLink attribute
ckms objects get-attributes --key-id <NEW_KEY_UID>
# Destroy the old key directly (Deactivated keys do not need a prior Revoke)
ckms sym keys destroy --key-id <OLD_KEY_UID>
If you need to place the old key into Compromised state (e.g. for audit
records), call Revoke first with a compromise reason: