πŸ”
Keys Held
…
⚑
Largest-Key Retrieval
…
πŸ—„οΈ
Sealed Storage
…
πŸ”’
Engines
…
πŸ›‘οΈ
NIST SP 800-88 Rev. 2
Cryptographic erase
πŸ“œ
Audit Log
…

PQ CRYPTA PLATFORM

🏠 Main

πŸ“° News

πŸ‘€ Account

⟨ QUANTUM ERROR PORTAL ⟩

Navigate the Error Dimensions

Key Pairs Held for Their Owners

Live from the API: … keys across both stores, … of key tables. Loading live figures…

…
Engines
…
Largest Key
…
Largest-Key Retrieval (p50)
…
zstd Before Sealing

System Overview

How Keys Get There

  • πŸ”‘ POST /keys/generate with "store": true
  • πŸ“¦ Or automatically, for a signed-in caller whose inline answer would exceed 1 MiB (a Classic McEliece pair is 3.75 MB of base64), if that caller may list and revoke vault keys: the vault keeps nothing its owner could not find and remove. Other callers get the keys inline
  • πŸ”— The answer is a key_id; /encrypt and /decrypt take it
  • 🚫 Private keys never leave the vault; only the public half can be read
  • πŸ‘€ Callers with no account get the pair inline, as before

Two Stores, One key_id

key_vault Generated pairs; per-key data keys, policies, rotation, audit chain
managed_keys Pairs brought with /keys/import; sealed under the at-rest key
Resolution key_id opens the caller's key from either store, checked for owner and algorithm

What Runs

Per-key data keys
Cryptographic erase
Access policies
Rotation & retention
Hash-chained audit
Guarded memory

Technical Architecture

Request

key_id on /encrypt, /decrypt
Account, address, credential

Vault (Rust, pqcrypta-api)

Owner, state, policy checks
Usage counted in the read
Unwrap data key β†’ AES-256-GCM open in guarded memory
Audit row before use

Storage (PostgreSQL)

sealed_keypair BYTEA (EXTERNAL)
wrapped_dek BYTEA
Append-only hash-chained log
MAC'd Merkle roots
sealed_keypair = "PQVR" | 0x02 | nonce (12) | AES-256-GCM(data key, nonce, aad, zstd(key pair JSON)) | tag (16)
aad            = "PQVR" | 0x02 | key id | owner | algorithm          (each length-prefixed)
wrapped_dek    = data key sealed with AES-256-GCM under HKDF-SHA256(vault key, salt, "pqcrypta/keys/vault/dek/v2"),
                 bound to key id and owner
data key, nonce, salts = SP 800-90A CTR_DRBG (AES-256); the data key drawn with prediction resistance

Identity

key_vault key_id UUID PRIMARY KEY owner_id VARCHAR(255) algorithm VARCHAR(255) lineage_id UUID

Sealed Material

key_vault sealed_keypair BYTEA wrapped_dek BYTEA plain_bytes BIGINT record_version SMALLINT

Lifecycle

key_vault expires_at / revoked_at TIMESTAMPTZ rotation_policy, retention_policy rotated_from / rotated_to / retired_at shredded_at, shred_reason

Use

key_vault usage_count, max_usage_count BIGINT last_used_at TIMESTAMPTZ access_policy JSONB keygen_parameters JSONB

Chain

key_vault_access_log seq BIGINT UNIQUE prev_hash, entry_hash BYTEA success, error_code

Context

key_vault_access_log accessed_by, owner_id ip_address INET, api_key_prefix duration_us, bytes_processed

Performance, Measured

Storage (live)

Key pairs' JSON, vault (… keys)
…
As sealed and stored
…
Saved by zstd before sealing
…
PostgreSQL storage for sealed_keypair
…
Managed keys (…), TOAST …
…

Retrieval, Last 7 Days (live)

Every use of a vault key records how long fetching, unwrapping, decrypting, decompressing and parsing it took.

EngineUsesp50p95Pair
Loading…

On-Host Benchmark (live)

The API measures the vault on its own host weekly; loading the latest run…

EnginePairStore /sRetrieve /sUse /sRetrieve, 8 tasks /sRetrieve p50

Reference Measurement

Measured 2026-10-02 on Intel(R) Xeon(R) Processor @ 2.10GHz, 4 cores available with PostgreSQL 16 on the same host, 5 iterations per operation, release build (key_vault_bench). Operations per second are one stream's (1000 / mean ms) except the 8-task column. Store seals and inserts a pair with its audit row; retrieve opens it for decryption with its audit row; use is what /encrypt then /decrypt by key_id cost without HTTP: two retrievals plus the engine's own encryption and decryption, which is most of it for the engines with heavy stacks (post-zk-homomorphic, the multi-layer stacks), not the vault.

EnginePair JSONStoredStore /sRetrieve /sUse /sRetrieve, 8 tasks /sRetrieve p50
ai-synthesized-crypto-agile 17.35 KiB 11.39 KiB 307 405 195.0 768 2.28 ms
classic-mceliece 1.35 MiB 1.01 MiB 61 67 40.7 317 13.63 ms
classical 1.83 KiB 1002 B 581 389 172.5 1,045 2.57 ms
entropy-orchestrated 12.47 KiB 7.79 KiB 358 334 165.5 1,103 2.91 ms
fn-dsa-1024-security 13.67 KiB 8.25 KiB 478 393 166.3 1,142 2.52 ms
fn-dsa-512-compact 11.18 KiB 6.29 KiB 541 421 186.6 967 2.30 ms
fn-dsa-dual-signature 13.32 KiB 8.26 KiB 493 344 1.6 1,055 2.93 ms
fn-dsa-fp-hardened 12.94 KiB 8.04 KiB 500 382 161.7 1,072 2.54 ms
fn-dsa-transition-stack 16.70 KiB 9.96 KiB 432 283 124.2 989 3.22 ms
fn-dsa-zk-stack 134.80 KiB 84.73 KiB 223 225 11.7 780 4.45 ms
frodokem-1344 84.96 KiB 50.32 KiB 369 291 26.0 1,032 3.57 ms
frodokem-976 61.93 KiB 22.61 KiB 377 318 52.9 1,127 3.09 ms
hqc-128 6.97 KiB 5.17 KiB 539 339 63.5 1,131 2.72 ms
hqc-192 12.90 KiB 9.64 KiB 405 310 28.7 1,107 3.17 ms
hqc-256 20.00 KiB 7.93 KiB 479 289 18.2 1,105 2.76 ms
hybrid 17.95 KiB 11.61 KiB 526 363 133.2 941 2.59 ms
lattice-code-hybrid 357.08 KiB 266.26 KiB 161 157 1.5 503 6.33 ms
max-secure-crypto-agile 13.44 KiB 8.21 KiB 527 393 164.3 1,082 2.54 ms
max-secure-hybrid-transition 26.42 KiB 17.96 KiB 537 412 167.0 969 2.38 ms
max-secure-lightweight 8.32 KiB 4.18 KiB 478 211 114.6 888 4.47 ms
max-secure-pqc-zk 8.61 KiB 4.59 KiB 519 326 1.0 1,165 2.93 ms
max-secure-pure-pq 7.07 KiB 3.77 KiB 574 316 149.7 1,230 3.20 ms
max-secure-stateless 8.80 KiB 4.55 KiB 477 343 1.6 979 2.93 ms
ml-kem-1024 6.99 KiB 3.72 KiB 514 363 147.4 1,215 2.68 ms
multi-algorithm 17.55 KiB 11.48 KiB 447 352 1.5 1,068 2.88 ms
multi-kem 7.78 KiB 4.13 KiB 486 358 1.5 1,127 2.77 ms
multi-kem-triple 13.97 KiB 8.80 KiB 524 371 1.5 1,200 2.71 ms
post-quantum 17.34 KiB 11.26 KiB 539 325 148.5 1,021 2.76 ms
post-zk-homomorphic 15.74 KiB 9.67 KiB 345 300 0.7 966 3.44 ms
pq3-stack 17.51 KiB 11.39 KiB 479 359 156.7 1,045 2.65 ms
quad-layer 24.88 KiB 14.97 KiB 407 302 1.6 842 3.26 ms
quantum-lattice-fusion 97.42 KiB 58.07 KiB 378 284 28.2 792 3.47 ms
quantum-resistant-consensus 29.15 KiB 14.92 KiB 502 367 159.4 825 2.66 ms
x25519-mlkem768 5.59 KiB 3.03 KiB 553 367 155.1 1,489 2.56 ms

Security

Access Control

  • Owner isolation: another account's key reads as "no such key", and the attempt is logged where its owner sees it
  • JSONB access policy: operations, networks (CIDR), API keys (id or prefix) or sessions, time window
  • Expiry and usage limits; the use is counted in the same UPDATE that reads the key
  • Managing a key (policy, rotation, revocation) stays its owner's, whatever its policy

Tamper-Evident Audit

  • Every access and refusal is a row; the log refuses UPDATE, DELETE and TRUNCATE
  • The API's database role can only read and append: the log, its roots and its trigger functions belong to another role, so the API cannot disable or replace them, and only a superuser can drop them
  • SHA-256 hash chain: each row's hash covers the previous one and all its fields
  • Hourly RFC 6962 Merkle roots, HMAC-SHA256'd with a key the database does not hold
  • Verification walks it all; owners get inclusion proofs
  • Live: …

Lifecycle

  • Rotation policies: manual, 1, 3, 6 or 12 months, enforced hourly
  • A rotated key's id keeps working: encryption uses the lineage's newest key, and ciphertexts name the key that sealed them
  • Retention for rotated-out keys: 90 days, 1 year, 3 years or permanent; they decrypt only
  • Revocation is final

Cryptographic Erase

  • NIST SP 800-88 Rev. 2's method for encrypted storage: the record's data key is destroyed
  • Revoked, expired and retention-ended keys erased hourly, 100 per batch
  • The row stays (its audit trail keeps a subject); its sealed material is gone
  • Limits: old row versions linger until VACUUM; backups taken earlier keep the sealed data key while the vault key exists

🧠 Guarded Memory

  • Opened records and private keys live in their own mappings: pages locked in RAM (mlock), PROT_NONE guard pages either side, MADV_DONTDUMP
  • Read-only (or unreadable) while not being written; a write faults
  • A canary before the data, checked on free: an out-of-bounds write aborts the process
  • Wiped 0x00, 0xFF, 0x00 (volatile writes, compiler fence), then unlocked and unmapped
  • Live: …

🎲 SP 800-90A DRBG

  • CTR_DRBG with AES-256, no derivation function: AWS-LC's implementation (the DRBG of its FIPS 140-3 module, linked outside the validated boundary)
  • Seeded with 384 bits from getrandom(2); reseeded every 65,536 requests and before every data key (prediction resistance)
  • Known-answer test of instantiate, generate and reseed before first use; a failure is a permanent error state
  • Draws the vault's data keys, nonces, salts and key ids; key pairs come from each engine's generator over getrandom
  • Live: …
πŸ›‘οΈ
SP 800-88 Rev. 2 erase
🎲
SP 800-90A Rev. 1 DRBG
🌳
RFC 6962 Merkle roots
πŸ”’
AES-256-GCM (SP 800-38D)

API Integration

1

Key Generation

POST /keys/generate
{
  "algorithm": "classic-mceliece",
  "rotation_policy": "12-month",
  "store": true
}
key_id β†’ the stored pair's id
β†’
2

Encryption

POST /encrypt
{
  "algorithm": "classic-mceliece",
  "data": "SGVsbG8...",
  "key_id": "…"
}
data β†’ ciphertext naming its key
β†’
3

Decryption

POST /decrypt
{
  "algorithm": "classic-mceliece",
  "data": "…",
  "key_id": "…"
}
data β†’ the plaintext

Endpoints

Keys

GET /key-vault/keysThe caller's keys: state, dates, policies, usage, sizes
GET /key-vault/keys/{key_id}One key and its latest audit entries
GET …/{key_id}/publicThe public half (not a counted use)
PUT …/{key_id}/policyAccess policy, rotation, retention, usage limit, expiry
POST …/{key_id}/rotateNew pair now; the old one decrypts only
POST …/{key_id}/revokeFinal; erased now (erase_now) or within the hour

Audit and Figures

GET /key-vault/auditThe caller's entries, with chain links and inclusion proofs
GET /key-vault/audit/verifyThe log walked: hashes, links, roots, MACs (?scope=recent from the latest root)
GET /key-vault/statsEverything live on this page (public)

Infrastructure

DatabasePostgreSQL 14 or later (exact microsecond time in the chain)
Drivertokio-postgres through deadpool-postgres
Connections…
Inline limit…

FHE Keys: the Vault's Largest Pairs

A post-zk-homomorphic pair made with its evaluation key carries a seeded TFHE server key of 106 MB (tfhe-rs 1.8 integer, KS32-PBS with ciphertext compression; 281 MB expanded), so /keys/generate/post-zk-homomorphic always keeps it here and answers a key_id. The client key stays sealed in its record for the owner (only /fhe/decrypt and /decrypt read it). Public keys of 8 MiB or more are sealed beside the record under its data key (key_vault_parts; the record's authenticated data names each one's place and length, so it opens only with exactly its parts), and a use reads them only if it needs them: decrypting opens a 61 KB record, not 142 MB. Evaluation needs only public material: /fhe/evaluate asks the vault to authorize each use (owner, state, access policy, usage limit, audit row, the same checks as opening the record) and keeps the expanded server key of the most recently used pair in memory (more with PQCRYPTA_FHE_KEY_CACHE_BYTES), so the server key is read once per key, not per evaluation. Access policies can allow or refuse evaluate separately from decrypt. Before 2026-10-04 the engine made ClientKey-only pairs (16 KB) that could not be evaluated on, and this page described FHE keys that did not exist.