Skip to main content

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.

No Implicit Latest 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​

BenefitExplanation
Predictable decryptionYou always know which key will be used to decrypt data
Safe key rotationOld data continues to decrypt correctly after rotation
Audit trailEvery operation is tied to a specific key version for compliance
No silent failuresIf a key is archived, the operation fails loudly instead of trying another key
Multi-region safetyNo race conditions between key creation and propagation

Key Lifecycle Diagram​

State Transitions​

TransitionTriggerWhat Happens
GeneratePOST /api/v1/kms/keys/generateCreates version 1 with active status
RotatePOST /api/v1/kms/keys/rotateCreates new active version (N+1), old version becomes retired
RetirePOST /api/v1/kms/keys/retireManually sets a version to retired
ArchiveAutomatic or manualMoves 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:

  1. A new key pair is generated with version N+1, set to active
  2. 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]
Brief Concurrent Window

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.

Decrypt with a Retired Key
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:

Re-Encrypt Data Under New Key
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-Encryption Best Practices

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​

  1. Always store key_version alongside ciphertext or signatures — you cannot decrypt or verify without it.

  2. Query the active key before new operations — use GET /api/v1/kms/keys/active to find the current version, then pass it explicitly.

  3. Rotate keys on a regular schedule — quarterly rotation is a common baseline; adjust based on your compliance requirements.

  4. Re-encrypt before archiving — ensure all data is re-encrypted under the new key before moving old keys to archived status.

  5. 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 StatusError CodeCauseResolution
404ERR_KMS_003Key version does not existCheck your tenant's key list with GET /api/v1/kms/keys
409ERR_KEM_004Attempted encrypt/sign with a retired or archived keyUse the current active key version for new operations
409ERR_KEM_005Attempted decrypt/verify with an archived keyContact support to restore the key to retired status