http://docker:8092Generate 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)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)Verify signatures using the stored public key. Audit log records every verification attempt with result.
ML-DSA (FIPS 204)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 algorithmsMark a key as DESTROYED. Destroyed keys cannot be used for signing. Audit trail preserved for compliance.
All PQC algorithmsEvery 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)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
/api/kms/create-key
Create a new PQC key
/api/kms/sign
Sign data with a managed key
/api/kms/verify
Verify a signature
/api/kms/rotate-key
Rotate an existing key
/api/kms/keys
List all managed keys
/api/kms/audit
Get audit log
/api/kms/key/{keyId}
Destroy a key
/api/kms/health
Service health + key count
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.
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.
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.
CREATE → SIGN/VERIFY → ROTATE → DESTROY. Each transition is audited. Rotated keys remain available for verification of existing signatures.
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.
| Component | Requirement | Purpose |
|---|---|---|
| OpenSSL | 3.6.1+ with Qudo provider | PQC key generation, signing, verification |
| Qudo Provider | qudo-fips.dylib loaded in openssl.cnf | FIPS 203/204/205 algorithms |
| Java | 17+ | Spring Boot 3.2.3 runtime |
| Network | Port 8092 accessible | KMS API endpoint |
openssl list -providers should show qudoprovider.
All KMS crypto operations go through this provider.
The KMS follows the envelope pattern: applications interact with key IDs, never raw key material.
| Algorithm | FIPS | Security Level | Recommended Use |
|---|---|---|---|
ML-DSA-44 | FIPS 204 | Level 2 | High-throughput signing (API tokens, sessions) |
ML-DSA-65 | FIPS 204 | Level 3 | General-purpose (default). CNSA 2.0 compliant |
ML-DSA-87 | FIPS 204 | Level 5 | Long-lived artifacts (certs, legal, firmware) |
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
# 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"]
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}")
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
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
End-to-end workflow: create key, sign document, verify, rotate, and verify with old key.
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[]'
Migrating from classical key management to PQC. The KMS API surface stays the same — only the algorithm changes.
| Aspect | Classical (Before) | PQC (After) |
|---|---|---|
| Signing Algorithm | RSA-2048, ECDSA P-256 | ML-DSA-65 (FIPS 204) |
| Key Size (Public) | RSA: 256 bytes, ECDSA: 65 bytes | ML-DSA-65: ~2,726 bytes (PEM) |
| Signature Size | RSA: 256 bytes, ECDSA: 72 bytes | ML-DSA-65: 3,309 bytes |
| Signing Latency | ~1ms | ~5-15ms |
| Crypto Provider | Default OpenSSL / BouncyCastle | Qudo FIPS Provider |
| Aspect | Details |
|---|---|
| API Pattern | Create → Sign → Verify → Rotate → Destroy (same lifecycle) |
| Key Referencing | Still by key ID, not raw key material |
| Data Format | Still base64-encoded input/output |
| Audit Trail | Same logging structure |
| Access Control | Same IAM policies apply |
| Rotation Policy | Same rotation workflow |
| Cloud KMS Operation | This API | Classical Algorithm | PQC Replacement |
|---|---|---|---|
AWS KMS CreateKey | POST /api/kms/create-key | RSA_2048, ECC_NIST_P256 | ML-DSA-65 |
AWS KMS Sign | POST /api/kms/sign | RSASSA_PKCS1_V1_5_SHA_256 | ML-DSA-65 |
AWS KMS Verify | POST /api/kms/verify | Same | ML-DSA-65 |
Azure create-key | POST /api/kms/create-key | RSA, EC | ML-DSA-65 |
GCP CreateCryptoKey | POST /api/kms/create-key | RSA_SIGN_PKCS1_2048_SHA256 | ML-DSA-65 |
Any KMS RotateKey | POST /api/kms/rotate-key | Same workflow | Same workflow |
Every key operation is logged. This is required for:
Audit of all cryptographic module operations. Key lifecycle events must be traceable.
Evidence of key management controls: creation, usage, rotation, destruction.
Key management procedures must be documented and auditable. Rotation evidence required.
{
"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"}
]
}
Measured on this platform using the Qudo OpenSSL provider. All times include key lookup + crypto operation.
| Operation | Algorithm | Latency | Output Size |
|---|---|---|---|
| Key Generation | ML-DSA-44 | ~5-10ms | Private: 2,560 bytes, Public: 1,312 bytes |
| Key Generation | ML-DSA-65 | ~8-15ms | Private: 4,032 bytes, Public: 1,952 bytes |
| Key Generation | ML-DSA-87 | ~10-20ms | Private: 4,896 bytes, Public: 2,592 bytes |
| Sign | ML-DSA-65 | ~5-15ms | Signature: 3,309 bytes |
| Verify | ML-DSA-65 | ~3-10ms | Boolean result |
| Rotate | Any | ~15-30ms | New key (same as Key Gen) |
Walk through the full key lifecycle: Create → Sign → Verify → Rotate → Audit.
Verify the original data, then try tampered data to see ML-DSA detect the change.
Rotate the key. Old key becomes ROTATED (can still verify). New key becomes ACTIVE.
See every operation recorded for this key.
| Pitfall | Impact | Fix |
|---|---|---|
| Storing private keys unencrypted | Key compromise if storage is breached | Always 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 inaccessible | Keep the old KEK available during transition. Re-wrap existing keys with the new KEK before discarding the old one. |
| Not zeroising key material after use | Keys linger in heap memory | Arrays.fill(privKey, (byte) 0) immediately after signing. JVM doesn't guarantee immediate GC. |
| Missing audit trail | No visibility into key usage, hard to investigate incidents | Log 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 algorithms | Cross-protocol attacks | Generate separate keys per algorithm and purpose. ML-DSA-65 keys should not be reused as ML-DSA-44 keys. |
KMS_KEK) — in production, source it from an HSM or cloud secret manager (AWS KMS, GCP Secret Manager, HashiCorp Vault).POST /api/kms/keys.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.