Key Versioning
This guide explains why Qpher requires explicit key versions on every PQC operation, 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
Why Explicit Key Versions?â
Qpher requires a key_version on every encrypt, decrypt, sign, and verify operation. There is no implicit "latest" or "default" key.
Qpher deliberately does not provide a "use the latest key" API. Implicit key selection is a common source of cryptographic bugs: data encrypted with one key version could silently fail to decrypt when the active key changes. Explicit versioning eliminates this entire class of errors.
Benefits of Explicit Versioningâ
| 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 |
| Multi-region safety | No race conditions between key creation and propagation |
Key Lifecycle Diagramâ
State Transitionsâ
| Transition | Trigger | What Happens |
|---|---|---|
| Generate | POST /api/v1/kms/keys/generate | Creates version 1 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 | Automatic or manual | Moves retired key to archived (all operations disabled) |
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]
During rotation, there is a brief window where both the old and new keys are available. New encrypt/sign operations use the new active key, while old data remains decryptable with the retired key. No data is lost.
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. -
Query the active key before new operations â use
GET /api/v1/kms/keys/activeto find the current version, then pass it explicitly. -
Rotate keys on a regular schedule â quarterly rotation is a common baseline; adjust based on your compliance requirements.
-
Re-encrypt before archiving â ensure all data is re-encrypted under the new key before moving old keys to archived status.
-
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_KMS_003 | Key version does not exist | Check your tenant's key list with GET /api/v1/kms/keys |
| 409 | ERR_KEM_004 | Attempted encrypt/sign with a retired or archived key | Use the current active key version for new operations |
| 409 | ERR_KEM_005 | Attempted decrypt/verify with an archived key | Contact support to restore the key to retired status |
Related Guidesâ
- Key Management â Generate, rotate, and retire keys
- Encrypt Data â Use explicit key versions for encryption
- Decrypt Data â Decrypt with active or retired keys
- Migration Guide â Plan your PQC migration with key versioning in mind