Key Versioning
This guide explains how key versions and the active key work in Qpher, how the key lifecycle works, and best practices for managing multiple key versions in production.
Prerequisites
- Familiarity with Key Management operations
- Understanding of basic encryption and signing concepts
Key versions and the active key
Encrypt and sign may omit key_version: Qpher uses the active key and returns its version. Decrypt, verify, encapsulate and key wrap always need it.
Store the version each response returns alongside the ciphertext or signature: decrypt and verify need it.
Why versions matter
| Benefit | Explanation |
|---|---|
| Predictable decryption | You always know which key will be used to decrypt data |
| Safe key rotation | Old data continues to decrypt correctly after rotation |
| Audit trail | Every operation is tied to a specific key version for compliance |
| No silent failures | If a key is archived, the operation fails loudly instead of trying another key |
Key Lifecycle Diagram
State Transitions
| Transition | Trigger | What Happens |
|---|---|---|
| Generate | POST /api/v1/kms/keys/generate | Creates the next version (1 for a new algorithm) with active status |
| Rotate | POST /api/v1/kms/keys/rotate | Creates new active version (N+1), old version becomes retired |
| Retire | POST /api/v1/kms/keys/retire | Manually sets a version to retired |
| Archive | Qpher Portal only | Archiving is irreversible: it deletes the private key file (encrypted copies of the file can remain in Qpher's backups), and Qpher no longer uses an archived key. |
Allowed Operations Per Status
Encrypt Decrypt Sign Verify
active Yes Yes Yes Yes
retired No Yes No Yes
archived No No No No
How Rotation Works
When you rotate a key, two things happen atomically:
- A new key pair is generated with version N+1, set to
active - The previous active key (version N) moves to
retired
Before rotation:
v1 [active]
After first rotation:
v1 [retired] v2 [active]
After second rotation:
v1 [retired] v2 [retired] v3 [active]
The new version becomes active and the previous one retired in one step, so there is always exactly one active version. Old data stays decryptable with the retired key.
Decrypting Old Data with Retired Keys
This is the most common key versioning scenario. You have data encrypted with key version 1, but you have since rotated to version 2.
import requests
# Data encrypted with key version 1 (now retired)
old_record = {
"ciphertext": "base64-ciphertext-from-v1...",
"key_version": 1, # This key is now retired
}
# Decryption still works with retired keys
response = requests.post(
"https://api.qpher.ai/api/v1/kem/decrypt",
headers={
"Content-Type": "application/json",
"x-api-key": "qph_your_key_here",
},
json={
"ciphertext": old_record["ciphertext"],
"key_version": old_record["key_version"],
},
)
result = response.json()
print(f"Decrypted successfully with retired key v{old_record['key_version']}")Re-Encryption Strategy
If you want to re-encrypt old data under a new key version (for example, before archiving an old key), follow this pattern:
import requests
API_BASE = "https://api.qpher.ai"
HEADERS = {
"Content-Type": "application/json",
"x-api-key": "qph_your_key_here",
}
def re_encrypt(ciphertext: str, old_version: int, new_version: int) -> dict:
"""Decrypt with old key, re-encrypt with new key."""
# Step 1: Decrypt with the old (retired) key
decrypt_resp = requests.post(
f"{API_BASE}/api/v1/kem/decrypt",
headers=HEADERS,
json={"ciphertext": ciphertext, "key_version": old_version},
)
plaintext = decrypt_resp.json()["data"]["plaintext"]
# Step 2: Encrypt with the new (active) key
encrypt_resp = requests.post(
f"{API_BASE}/api/v1/kem/encrypt",
headers=HEADERS,
json={"plaintext": plaintext, "key_version": new_version},
)
new_data = encrypt_resp.json()["data"]
return {
"ciphertext": new_data["ciphertext"],
"key_version": new_data["key_version"],
}
# Re-encrypt a record from v1 to v2
updated = re_encrypt(
ciphertext="old-ciphertext...",
old_version=1,
new_version=2,
)
print(f"Re-encrypted under key v{updated['key_version']}")Re-encrypt data in batches with error handling and progress tracking. Always verify the re-encryption succeeded before archiving the old key. Consider running re-encryption during off-peak hours to minimize API usage impact.
Best Practices
-
Always store
key_versionalongside ciphertext or signatures — you cannot decrypt or verify without it. -
Let encrypt and sign use the active key — omit
key_versionand store the version the response returns; pin a version only when you need to. -
Rotate keys on a regular schedule — quarterly rotation is a common baseline; adjust based on your compliance requirements.
-
Re-encrypt and re-sign before archiving — archiving is irreversible, so re-encrypt data under the new key, and re-sign anything whose signature must stay verifiable, before archiving the old version.
-
Monitor key usage — track which key versions are still being used for decryption to know when it is safe to archive old versions.
Error Handling
| HTTP Status | Error Code | Cause | Resolution |
|---|---|---|---|
| 404 | ERR_NOT_FOUND_001 | The key version does not exist for this algorithm, or key_version was omitted and the algorithm has no active key | List your keys with GET /api/v1/kms/keys |
| 404 | ERR_CRYPTO_004 | Encrypt, sign, sign-hash, encapsulate or key wrap with a key that is not active | Omit key_version on encrypt, sign and sign-hash to use the active key, or pin the active version |
| 404 | ERR_CRYPTO_004 | Decapsulate or key unwrap with an archived key | Archiving cannot be undone; Qpher no longer uses an archived key |
| 404 | ERR_KMS_003 | Decrypt, verify or verify-hash with an archived key | Archiving cannot be undone; Qpher no longer decrypts or verifies with an archived key |
| 403 | ERR_KMS_020 | Archive requested with an API key | Archive keys in Qpher Portal |
Related Guides
- Key Management — Generate, rotate, and retire keys
- Encrypt Data — Encrypt with your active key
- Decrypt Data — Decrypt with active or retired keys
- Migration Guide — Plan your PQC migration with key versioning in mind