Service Info

Status--
Endpointhttp://docker:8092
PQC AlgorithmML-DSA-44/65/87
FIPS StandardFIPS 204 (ML-DSA)

Where PQC is Used

🔑
PQC Key Generation

Generate ML-DSA or SLH-DSA keypairs via Qudo provider. Keys are stored in-memory with alias, usage type, and lifecycle status (ACTIVE, ROTATED, DESTROYED).

ML-DSA-44/65/87, SLH-DSA (FIPS 204/205)
Managed Signing

Sign data using a stored key ID. The private key never leaves the KMS — callers reference keys by ID, not by raw key material. Mirrors AWS KMS Sign API.

ML-DSA (FIPS 204)
Signature Verification

Verify signatures using the stored public key. Audit log records every verification attempt with result.

ML-DSA (FIPS 204)
🔄
Key Rotation

Rotate a key: the old key is marked ROTATED, a new key is generated with the same algorithm and usage. Old key remains for verification of existing signatures.

All PQC algorithms
🗑
Key Destruction

Mark a key as DESTROYED. Destroyed keys cannot be used for signing. Audit trail preserved for compliance.

All PQC algorithms
📋
Audit Logging

Every key operation is logged: CREATE, SIGN, VERIFY, ROTATE, DESTROY. Query by key ID or retrieve the full log. Required for FIPS 140-3 and SOC 2 compliance.

N/A (operational)

Description

This service demonstrates a managed PQC key lifecycle — the same patterns you'll implement using the Qudo JNI provider in your key management system.

Create: provider.generateKeyPair("ML-DSA-65") — store keys with alias and lifecycle status
Sign: provider.sign(data, storedPrivKey, algorithm) — callers reference keys by ID, not raw material
Rotate: Generate new keypair, mark old as ROTATED, keep for verification
Audit: Every CREATE, SIGN, VERIFY, ROTATE, DESTROY operation logged for FIPS 140-3 / SOC 2 compliance

POST /api/kms/create-key Create a new PQC key
🔒 Qudo provider: provider.generateKeyPair(algorithm). Returns keyId for all subsequent operations. Supported: ML-DSA-44/65/87.
POST /api/kms/sign Sign data with a managed key
🔒 Qudo provider: provider.sign(data, storedPrivKey, algorithm). Key must be ACTIVE. Private key never leaves KMS.
POST /api/kms/verify Verify a signature
🔒 Qudo provider: provider.verify(data, sig, storedPubKey, algorithm) -> true/false.
POST /api/kms/rotate-key Rotate an existing key
🔒 Old key marked ROTATED, new key created with same algorithm. Old key remains for signature verification of existing data.
GET /api/kms/keys List all managed keys
🔒 Returns all keys with status (ACTIVE, ROTATED, DESTROYED), algorithm, alias, and creation time.
GET /api/kms/audit Get audit log
🔒 Full audit trail: every CREATE, SIGN, VERIFY, ROTATE, DESTROY operation with timestamps.
DELETE /api/kms/key/{keyId} Destroy a key
🔒 Marks key as DESTROYED. Cannot be used for signing. Audit trail preserved.
GET /api/kms/health Service health + key count
🔒 Returns service status and count of managed PQC keys.

Cloud KMS Migration Reference

Migrate your key management to post-quantum cryptography. Use this playground to try PQC key lifecycle operations, then use the Qudo JNI provider to build managed PQC keys in your own KMS.

1. Introduction 2. Prerequisites 3. Architecture 4. Key Lifecycle 5. Sample Code 6. Migration Guide 7. Audit & Compliance 8. Performance 9. Interactive Demo

1. Introduction

Cloud KMS provides managed post-quantum key operations where private keys never leave the service. Callers reference keys by ID — the same pattern used by AWS KMS, Azure Key Vault, and GCP Cloud KMS.

🔑
Key-ID Based Access

Applications never handle raw private keys. Create a key once, reference it by ID for all sign/verify operations. This is the standard cloud KMS pattern.

🔄
Full Lifecycle Management

CREATE → SIGN/VERIFY → ROTATE → DESTROY. Each transition is audited. Rotated keys remain available for verification of existing signatures.

📋
Compliance-Ready Audit

Every operation is logged with key ID, operation type, timestamp, and details. Required for FIPS 140-3 Level 2+, SOC 2, and PCI-DSS compliance.

Qudo Provider: All cryptographic operations (key generation, signing, verification) are performed by the Qudo OpenSSL FIPS provider. The KMS layer manages lifecycle and access control; Qudo handles the math.

2. Prerequisites

ComponentRequirementPurpose
OpenSSL3.6.1+ with Qudo providerPQC key generation, signing, verification
Qudo Providerqudo-fips.dylib loaded in openssl.cnfFIPS 203/204/205 algorithms
Java17+Spring Boot 3.2.3 runtime
NetworkPort 8092 accessibleKMS API endpoint
Verify Qudo: openssl list -providers should show qudoprovider. All KMS crypto operations go through this provider.

3. Architecture

The KMS follows the envelope pattern: applications interact with key IDs, never raw key material.

Application
KMS API
Key ID + data
KMS Service
Key lookup + lifecycle
Qudo Provider
openssl pkeyutl -sign -rawin

Key States

ACTIVE
Can sign + verify
ROTATED
Verify only
DESTROYED
No operations

Supported Algorithms

AlgorithmFIPSSecurity LevelRecommended Use
ML-DSA-44FIPS 204Level 2High-throughput signing (API tokens, sessions)
ML-DSA-65FIPS 204Level 3General-purpose (default). CNSA 2.0 compliant
ML-DSA-87FIPS 204Level 5Long-lived artifacts (certs, legal, firmware)

4. Key Lifecycle

Create a Key

cURL
Python
Java
Go
curl -X POST http://localhost:8092/api/kms/create-key \
  -H "Content-Type: application/json" \
  -d '{
    "alias": "signing-key-prod",
    "algorithm": "ML-DSA-65",
    "usage": "SIGN"
  }'
# Response: {"status":"success","result":{"keyId":"key-a1b2c3d4","alias":"signing-key-prod","algorithm":"ML-DSA-65","usage":"SIGN","status":"ACTIVE"}}
import requests

resp = requests.post("http://localhost:8092/api/kms/create-key", json={
    "alias": "signing-key-prod",
    "algorithm": "ML-DSA-65",
    "usage": "SIGN"
})
key = resp.json()["result"]
key_id = key["keyId"]  # Use this for all subsequent operations
print(f"Created key: {key_id}, algorithm: {key['algorithm']}")
HttpClient client = HttpClient.newHttpClient();
String body = """
    {"alias":"signing-key-prod","algorithm":"ML-DSA-65","usage":"SIGN"}""";
HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("http://localhost:8092/api/kms/create-key"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
// Parse keyId from response for subsequent operations
payload := map[string]string{
    "alias":     "signing-key-prod",
    "algorithm": "ML-DSA-65",
    "usage":     "SIGN",
}
body, _ := json.Marshal(payload)
resp, _ := http.Post("http://localhost:8092/api/kms/create-key",
    "application/json", bytes.NewReader(body))
// Parse keyId from response

Sign Data

cURL
Python
# Data must be base64-encoded
DATA=$(echo -n "document to sign" | base64)
curl -X POST http://localhost:8092/api/kms/sign \
  -H "Content-Type: application/json" \
  -d "{\"keyId\":\"key-a1b2c3d4\",\"data\":\"$DATA\"}"
# Response: {"status":"success","result":{"signature":"","keyId":"key-a1b2c3d4","algorithm":"ML-DSA-65"}}
import base64

data_b64 = base64.b64encode(b"document to sign").decode()
resp = requests.post("http://localhost:8092/api/kms/sign", json={
    "keyId": key_id,
    "data": data_b64
})
signature = resp.json()["result"]["signature"]

Verify Signature

cURL
Python
curl -X POST http://localhost:8092/api/kms/verify \
  -H "Content-Type: application/json" \
  -d '{
    "keyId": "key-a1b2c3d4",
    "data": "",
    "signature": ""
  }'
# Response: {"status":"success","result":{"valid":true,"keyId":"key-a1b2c3d4","algorithm":"ML-DSA-65"}}
resp = requests.post("http://localhost:8092/api/kms/verify", json={
    "keyId": key_id,
    "data": data_b64,
    "signature": signature
})
is_valid = resp.json()["result"]["valid"]
print(f"Signature valid: {is_valid}")

Rotate Key

cURL
Python
curl -X POST http://localhost:8092/api/kms/rotate-key \
  -H "Content-Type: application/json" \
  -d '{"keyId":"key-a1b2c3d4"}'
# Old key: ROTATED (verify only). New key: ACTIVE (sign + verify)
# Response: {"status":"success","newKey":{"keyId":"key-e5f6g7h8","alias":"signing-key-prod-rotated",...}}
resp = requests.post("http://localhost:8092/api/kms/rotate-key", json={
    "keyId": key_id
})
new_key = resp.json()["newKey"]
new_key_id = new_key["keyId"]
# Old key can still verify existing signatures
# New key is used for all new signing operations

Destroy Key

cURL
curl -X DELETE http://localhost:8092/api/kms/key/key-a1b2c3d4
# Response: {"status":"success","keyId":"key-a1b2c3d4","newStatus":"DESTROYED"}
# Key cannot be used for any operations after destruction

5. Complete Migration Example

End-to-end workflow: create key, sign document, verify, rotate, and verify with old key.

Python
Java
Bash
import requests, base64

KMS = "http://localhost:8092/api/kms"

# 1. Create a signing key
key = requests.post(f"{KMS}/create-key", json={
    "alias": "invoice-signer",
    "algorithm": "ML-DSA-65",
    "usage": "SIGN"
}).json()["result"]
key_id = key["keyId"]
print(f"Created: {key_id} ({key['algorithm']})")

# 2. Sign a document
doc = base64.b64encode(b"Invoice #1234: $50,000").decode()
sig_resp = requests.post(f"{KMS}/sign", json={
    "keyId": key_id, "data": doc
}).json()["result"]
print(f"Signature: {sig_resp['signature'][:40]}...")

# 3. Verify the signature
verify = requests.post(f"{KMS}/verify", json={
    "keyId": key_id,
    "data": doc,
    "signature": sig_resp["signature"]
}).json()["result"]
print(f"Valid: {verify['valid']}")

# 4. Rotate the key
new_key = requests.post(f"{KMS}/rotate-key", json={
    "keyId": key_id
}).json()["newKey"]
print(f"Rotated. New key: {new_key['keyId']}")

# 5. Old key can still verify existing signatures
verify_old = requests.post(f"{KMS}/verify", json={
    "keyId": key_id,  # old key (ROTATED)
    "data": doc,
    "signature": sig_resp["signature"]
}).json()["result"]
print(f"Old key still verifies: {verify_old['valid']}")

# 6. Check audit trail
audit = requests.get(f"{KMS}/audit?keyId={key_id}").json()
for entry in audit["auditLog"]:
    print(f"  {entry['operation']}: {entry['details']}")
import java.net.http.*;
import java.net.URI;
import java.util.Base64;

var client = HttpClient.newHttpClient();
var KMS = "http://localhost:8092/api/kms";

// 1. Create key
var createReq = HttpRequest.newBuilder()
    .uri(URI.create(KMS + "/create-key"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(
        "{\"alias\":\"invoice-signer\",\"algorithm\":\"ML-DSA-65\",\"usage\":\"SIGN\"}"))
    .build();
var createResp = client.send(createReq, HttpResponse.BodyHandlers.ofString());
// Parse keyId from JSON response

// 2. Sign
var data = Base64.getEncoder().encodeToString("Invoice #1234".getBytes());
var signReq = HttpRequest.newBuilder()
    .uri(URI.create(KMS + "/sign"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(
        "{\"keyId\":\"" + keyId + "\",\"data\":\"" + data + "\"}"))
    .build();
var signResp = client.send(signReq, HttpResponse.BodyHandlers.ofString());

// 3. Verify, Rotate, Destroy follow the same pattern
#!/bin/bash
KMS="http://localhost:8092/api/kms"

# Create key
KEY_ID=$(curl -s -X POST $KMS/create-key \
  -H "Content-Type: application/json" \
  -d '{"alias":"test","algorithm":"ML-DSA-65","usage":"SIGN"}' \
  | jq -r '.result.keyId')
echo "Created: $KEY_ID"

# Sign
DATA=$(echo -n "test data" | base64)
SIG=$(curl -s -X POST $KMS/sign \
  -H "Content-Type: application/json" \
  -d "{\"keyId\":\"$KEY_ID\",\"data\":\"$DATA\"}" \
  | jq -r '.result.signature')
echo "Signature: ${SIG:0:40}..."

# Verify
VALID=$(curl -s -X POST $KMS/verify \
  -H "Content-Type: application/json" \
  -d "{\"keyId\":\"$KEY_ID\",\"data\":\"$DATA\",\"signature\":\"$SIG\"}" \
  | jq -r '.result.valid')
echo "Valid: $VALID"

# Audit
curl -s "$KMS/audit?keyId=$KEY_ID" | jq '.auditLog[]'

6. Migration Guide

Migrating from classical key management to PQC. The KMS API surface stays the same — only the algorithm changes.

What Changes

AspectClassical (Before)PQC (After)
Signing AlgorithmRSA-2048, ECDSA P-256ML-DSA-65 (FIPS 204)
Key Size (Public)RSA: 256 bytes, ECDSA: 65 bytesML-DSA-65: ~2,726 bytes (PEM)
Signature SizeRSA: 256 bytes, ECDSA: 72 bytesML-DSA-65: 3,309 bytes
Signing Latency~1ms~5-15ms
Crypto ProviderDefault OpenSSL / BouncyCastleQudo FIPS Provider

What Does NOT Change

AspectDetails
API PatternCreate → Sign → Verify → Rotate → Destroy (same lifecycle)
Key ReferencingStill by key ID, not raw key material
Data FormatStill base64-encoded input/output
Audit TrailSame logging structure
Access ControlSame IAM policies apply
Rotation PolicySame rotation workflow

Cloud Provider Mapping

Cloud KMS OperationThis APIClassical AlgorithmPQC Replacement
AWS KMS CreateKeyPOST /api/kms/create-keyRSA_2048, ECC_NIST_P256ML-DSA-65
AWS KMS SignPOST /api/kms/signRSASSA_PKCS1_V1_5_SHA_256ML-DSA-65
AWS KMS VerifyPOST /api/kms/verifySameML-DSA-65
Azure create-keyPOST /api/kms/create-keyRSA, ECML-DSA-65
GCP CreateCryptoKeyPOST /api/kms/create-keyRSA_SIGN_PKCS1_2048_SHA256ML-DSA-65
Any KMS RotateKeyPOST /api/kms/rotate-keySame workflowSame workflow
Migration strategy: Start by creating PQC keys alongside classical keys (dual-key period). Sign with both. Once all verifiers support PQC, deprecate classical keys via rotation.

7. Audit & Compliance

Every key operation is logged. This is required for:

📜
FIPS 140-3 Level 2+

Audit of all cryptographic module operations. Key lifecycle events must be traceable.

🛡
SOC 2 Type II

Evidence of key management controls: creation, usage, rotation, destruction.

💳
PCI-DSS

Key management procedures must be documented and auditable. Rotation evidence required.

Audit Log Format

{
  "auditLog": [
    {"keyId": "key-a1b2c3d4", "operation": "CREATE", "user": "system", "timestamp": "2026-04-09T...", "details": "Created ML-DSA-65 key, alias: signing-key-prod"},
    {"keyId": "key-a1b2c3d4", "operation": "SIGN",   "user": "system", "timestamp": "2026-04-09T...", "details": "Signed 16 bytes with ML-DSA-65"},
    {"keyId": "key-a1b2c3d4", "operation": "VERIFY", "user": "system", "timestamp": "2026-04-09T...", "details": "Verified signature, result: true"},
    {"keyId": "key-a1b2c3d4", "operation": "ROTATE", "user": "system", "timestamp": "2026-04-09T...", "details": "Key rotated, old key marked ROTATED"},
    {"keyId": "key-a1b2c3d4", "operation": "DESTROY","user": "system", "timestamp": "2026-04-09T...", "details": "Key destroyed"}
  ]
}

8. Performance

Measured on this platform using the Qudo OpenSSL provider. All times include key lookup + crypto operation.

OperationAlgorithmLatencyOutput Size
Key GenerationML-DSA-44~5-10msPrivate: 2,560 bytes, Public: 1,312 bytes
Key GenerationML-DSA-65~8-15msPrivate: 4,032 bytes, Public: 1,952 bytes
Key GenerationML-DSA-87~10-20msPrivate: 4,896 bytes, Public: 2,592 bytes
SignML-DSA-65~5-15msSignature: 3,309 bytes
VerifyML-DSA-65~3-10msBoolean result
RotateAny~15-30msNew key (same as Key Gen)
Comparison: RSA-2048 key generation takes ~50-100ms. ML-DSA-65 keygen is faster. Signing is slightly slower (~5-15ms vs ~1ms) but still well within API latency budgets.

9. Interactive Demo

Walk through the full key lifecycle: Create → Sign → Verify → Rotate → Audit.

Step 1: Create a PQC Key

Step 2: Sign Data

Step 3: Verify Signature

Verify the original data, then try tampered data to see ML-DSA detect the change.

Step 4: Rotate Key

Rotate the key. Old key becomes ROTATED (can still verify). New key becomes ACTIVE.

Step 5: View Audit Trail

See every operation recorded for this key.

Common Pitfalls

PitfallImpactFix
Storing private keys unencryptedKey compromise if storage is breachedAlways wrap private keys with AES-256-GCM using a KEK derived from an HSM or secret manager. Never store raw PEM on disk.
Wrong KEK after rotation"Tag mismatch!" error — all keys become inaccessibleKeep the old KEK available during transition. Re-wrap existing keys with the new KEK before discarding the old one.
Not zeroising key material after useKeys linger in heap memoryArrays.fill(privKey, (byte) 0) immediately after signing. JVM doesn't guarantee immediate GC.
Missing audit trailNo visibility into key usage, hard to investigate incidentsLog every sign/verify/create/rotate operation with timestamp, keyId, algorithm, and caller identity. The playground's /api/kms/keys/{id} shows this pattern.
Using the same key for multiple algorithmsCross-protocol attacksGenerate separate keys per algorithm and purpose. ML-DSA-65 keys should not be reused as ML-DSA-44 keys.

FAQ

Q: Can I export private keys from the KMS?
By design, a production KMS should not export raw private keys. The playground allows it for learning, but in production, keys should be used only through the KMS API (sign/verify) and never leave the HSM boundary.
Q: What happens to rotated keys?
The old key remains in the KMS for verification of previously-signed data. It is marked as "rotated" in the audit log. Only the new key is used for new signing operations. Delete old keys only after all signatures referencing them have expired.
Q: How does the KEK (key-encryption key) work?
The KEK wraps each private key with AES-256-GCM before storing it in the database. On retrieval, the KMS unwraps the private key, uses it for the requested operation, then zeroises it in memory. The KEK itself comes from an environment variable (KMS_KEK) — in production, source it from an HSM or cloud secret manager (AWS KMS, GCP Secret Manager, HashiCorp Vault).
Q: Which PQC algorithms does the KMS support?
All algorithms available from the Qudo provider: ML-DSA-44/65/87, ML-KEM-512/768/1024, and SLH-DSA variants. Specify the algorithm when creating a key via POST /api/kms/keys.

Crypto Inventory Scanner

Analyze any endpoint's cryptographic configuration. Enter a host:port to scan its TLS setup and identify what's quantum-safe vs what needs migration.

Scan Endpoint