01 — What crypto agility actually means
Identifiers in the header, not algorithms in the code path.
Each blob carries a header identifying the algorithms that produced it:
a 2-byte KEM_ID and a 2-byte SIG_ID, integrity-bound
via GCM AAD. Clients choose. The relay validates. Both sides negotiate via
the capabilities endpoint — no hidden defaults, no protocol guessing.
02 — The wire format (v1)
A 10-byte fixed header, length-prefixed variable fields after.
MAGIC 4 bytes PQHB VERSION 1 byte 0x01 KEM_ID 2 bytes uint16 BE SIG_ID 2 bytes uint16 BE (0x0000 = anonymous) FLAGS 1 byte reserved for future features
Length-prefixed variable fields follow: KEM ciphertext, sender public key, optional signature, AES-256-GCM nonce + ciphertext, padding block. Full spec: docs/wire-format-v1.md.
03 — Who produces, who validates
The relay never touches the byte layout.
A v1-capable client produces a blob with its chosen KEM and SIG IDs. The relay validates the header and rejects unsupported combinations with HTTP 415. The relay stores verbatim, serves byte-for-byte. That is what preserves zero knowledge — the relay never touches the byte layout. The recipient client decodes using the algorithms declared in the header.
Not every client surface produces v1 today. Section 06 below lists the exact state of each official client. The two SDKs (sdk-py 3.0.0, sdk-js 3.0.0) emit v1 with real ML-KEM and ML-DSA. The webapp WASM bridge still emits a pre-v1 hybrid layout, and two extensions currently fall back to a server-side path while their client-side crypto is finished. Migration status is tracked openly rather than glossed over.
04 — What is loaded today
3 KEMs plus 18 signatures, covering FIPS 203, 204, 205, and 206.
KEM registry (FIPS 203, ML-KEM)
| ID | Algorithm | NIST category | Status |
|---|---|---|---|
| 0x0001 | ML-KEM-512 | 1 | loaded |
| 0x0002 | ML-KEM-768 | 3 | loaded (default) |
| 0x0003 | ML-KEM-1024 | 5 | loaded |
ML-DSA signatures (FIPS 204)
| ID | Algorithm | NIST category | Status |
|---|---|---|---|
| 0x0000 | none | anonymous | valid (no signature) |
| 0x0001 | ML-DSA-44 | cat 2 | loaded |
| 0x0002 | ML-DSA-65 | cat 3 | loaded (default) |
| 0x0003 | ML-DSA-87 | cat 5 | loaded |
Falcon signatures (FIPS 206)
| ID | Algorithm | NIST category | Status |
|---|---|---|---|
| 0x0100 | Falcon-512 | cat 1 | loaded (compact, fast) |
| 0x0101 | Falcon-1024 | cat 5 | loaded (compact, fast) |
SLH-DSA signatures (FIPS 205, stateless hash-based)
| ID | Algorithm | NIST category | Status |
|---|---|---|---|
| 0x0200 | SLH-DSA-SHA2-128s | cat 1 | loaded · slow signing |
| 0x0201 | SLH-DSA-SHA2-128f | cat 1 | loaded |
| 0x0202 | SLH-DSA-SHA2-192s | cat 3 | loaded · slow signing |
| 0x0203 | SLH-DSA-SHA2-192f | cat 3 | loaded |
| 0x0204 | SLH-DSA-SHA2-256s | cat 5 | loaded · slow signing |
| 0x0205 | SLH-DSA-SHA2-256f | cat 5 | loaded |
| 0x0206 | SLH-DSA-SHAKE-128s | cat 1 | loaded · slow signing |
| 0x0207 | SLH-DSA-SHAKE-128f | cat 1 | loaded |
| 0x0208 | SLH-DSA-SHAKE-192s | cat 3 | loaded · slow signing |
| 0x0209 | SLH-DSA-SHAKE-192f | cat 3 | loaded |
| 0x020A | SLH-DSA-SHAKE-256s | cat 5 | loaded · slow signing |
| 0x020B | SLH-DSA-SHAKE-256f | cat 5 | loaded |
Slow signing variants have smaller signatures (~8 to 30 KB) but multi-second sign operations. Fast variants sign in under a second with larger signatures (17 to 50 KB). Choose based on whether your workload cares more about bandwidth or latency.
Total: 3 KEMs plus 18 signatures loaded in production.
05 — See for yourself
The live capabilities endpoint is the source of truth.
curl https://paramant.app/v2/capabilities
Excerpt (full response lists all 21 entries):
{
"wire_version": 1,
"kem": [
{ "id": 1, "name": "ML-KEM-512", "loaded": true },
{ "id": 2, "name": "ML-KEM-768", "loaded": true },
{ "id": 3, "name": "ML-KEM-1024", "loaded": true }
],
"sig": [
{ "id": 0, "name": "none", "loaded": true },
{ "id": 2, "name": "ML-DSA-65", "loaded": true },
{ "id": 256, "name": "Falcon-512", "loaded": true },
{ "id": 512, "name": "SLH-DSA-SHA2-128s", "loaded": true, "performance_hint": "slow_signing" },
...
]
}
What the relay advertises is what it accepts. Blobs with an unadvertised algorithm ID are rejected with HTTP 415 UnsupportedAlgorithm before any bytes are stored.
06 — Client implementations
Per-client status. Verified from source, not assumed.
The relay accepts wire format v1 from any client that produces it. The table below is the honest state of each official client surface we ship today.
| Client | Default KEM | Default SIG | Wire format | Status |
|---|---|---|---|---|
| sdk-py 3.0.0 | ML-KEM-768 | ML-DSA-65 | v1 | live |
| sdk-js 3.0.0 | ML-KEM-768 | ML-DSA-65 | v1 | live |
| ParaShare (webapp) | ML-KEM-768 + ECDH P-256 | — | pre-v1 hybrid | migrating to v1 |
| ParaDrop (webapp) | ML-KEM-768 + ECDH P-256 | — | pre-v1 hybrid | migrating to v1 |
| Send (browser-encrypted link) | — | — | AES-256-GCM, key in URL fragment | no identity-binding by design |
| Chromium extension | — | — | server-side path (relay encrypts) | client-side crypto in progress |
| Outlook add-in | — | — | server-side path (relay encrypts) | client-side crypto in progress |
Pre-v1 hybrid: the WebApp WASM bridge already runs ML-KEM-768 + ECDH
P-256 + AES-256-GCM client-side, but its blob layout pre-dates the
v1 header above. Migration to v1 lets the same WASM negotiate
KEM / SIG IDs through /v2/capabilities.
Server-side path: the extension authenticates and uploads, and the relay performs encryption with a per-blob key. This is weaker than end-to-end and is being replaced. Until the migration lands, do not treat extension uploads as zero-knowledge.
07 — Adding an algorithm
Three steps. No schema changes, no in-flight blobs affected.
Write the impl
A small file that wraps the library. See relay/crypto/impls/
in the repo for examples.
Register at an ID
One line in relay/crypto/bootstrap.js:
registerSig(0x0300, myImpl).
Deploy
Existing blobs decode unchanged. Clients opt into the new algorithm
via /v2/capabilities.
Full reference: github.com/Apolloccrypt/paramant-relay/blob/main/docs/wire-format-v1.md.
08 — What is not yet built
Honest delta between the header format and what ships.
The FLAGS byte reserves space for options not currently shipping:
- Hybrid KEMs (combining two KEMs in one blob for defence in depth)
- Multi-recipient blobs
- Compressed payloads
- Forward secrecy via ephemeral per-session keys
These are architected-for but not wired up. When they ship, the
FLAGS bits get set and the wire format version stays at 1.
Verify the registry yourself
The capabilities endpoint is live and unauthenticated. It lists every algorithm ID the relay accepts, plus performance hints for the slow-signing SLH-DSA variants. The wire format spec on GitHub documents the header layout in full.
/v2/capabilities Wire format v1 spec