01What 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.
02The 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.
03Who 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 the blob as it is and serves it back 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.
04What is supported, and what is loaded
3 KEMs plus 17 signatures in the code, covering FIPS 203, 204, 205, and 206. Our relays run the default core mode, which loads two of them: ML-KEM-768 and ML-DSA-65.
KEM registry (FIPS 203, ML-KEM)
| ID | Algorithm | NIST category | Status |
|---|---|---|---|
| 0x0001 | ML-KEM-512 | 1 | extended mode only |
| 0x0002 | ML-KEM-768 | 3 | loaded (default, core) |
| 0x0003 | ML-KEM-1024 | 5 | extended mode only |
ML-DSA signatures (FIPS 204)
| ID | Algorithm | NIST category | Status |
|---|---|---|---|
| 0x0000 | none | anonymous | valid (no signature) |
| 0x0001 | ML-DSA-44 | cat 2 | extended mode only |
| 0x0002 | ML-DSA-65 | cat 3 | loaded (default, core) |
| 0x0003 | ML-DSA-87 | cat 5 | extended mode only |
Falcon signatures (FIPS 206)
| ID | Algorithm | NIST category | Status |
|---|---|---|---|
| 0x0100 | Falcon-512 | cat 1 | extended mode only (compact, fast) |
| 0x0101 | Falcon-1024 | cat 5 | extended mode only (compact, fast) |
SLH-DSA signatures (FIPS 205, stateless hash-based)
| ID | Algorithm | NIST category | Status |
|---|---|---|---|
| 0x0200 | SLH-DSA-SHA2-128s | cat 1 | extended mode only · slow signing |
| 0x0201 | SLH-DSA-SHA2-128f | cat 1 | extended mode only |
| 0x0202 | SLH-DSA-SHA2-192s | cat 3 | extended mode only · slow signing |
| 0x0203 | SLH-DSA-SHA2-192f | cat 3 | extended mode only |
| 0x0204 | SLH-DSA-SHA2-256s | cat 5 | extended mode only · slow signing |
| 0x0205 | SLH-DSA-SHA2-256f | cat 5 | extended mode only |
| 0x0206 | SLH-DSA-SHAKE-128s | cat 1 | extended mode only · slow signing |
| 0x0207 | SLH-DSA-SHAKE-128f | cat 1 | extended mode only |
| 0x0208 | SLH-DSA-SHAKE-192s | cat 3 | extended mode only · slow signing |
| 0x0209 | SLH-DSA-SHAKE-192f | cat 3 | extended mode only |
| 0x020A | SLH-DSA-SHAKE-256s | cat 5 | extended mode only · slow signing |
| 0x020B | SLH-DSA-SHAKE-256f | cat 5 | extended mode only |
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 17 signatures in the code. The default core mode loads ML-KEM-768 and ML-DSA-65, and that is what our relays run; a self-hosted relay with CRYPTO_MODE=extended loads all of them.
05See for yourself
The live capabilities endpoint is the source of truth.
curl https://paramant.app/v2/capabilities
What our relays answer today (core mode). An extended relay lists 21 entries:
{
"wire_version": 1,
"kem": [
{ "id": 2, "name": "ML-KEM-768", "loaded": true }
],
"sig": [
{ "id": 0, "name": "none", "loaded": true },
{ "id": 2, "name": "ML-DSA-65", "loaded": true }
]
}
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.
06Client implementations
Per-client status. Verified from source, not assumed.
The relay accepts wire format v1 from any client that produces it. The table below shows where each official client surface we ship stands 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 | n/a | pre-v1 hybrid | migrating to v1 |
| Send (browser-encrypted link) | n/a | n/a | AES-256-GCM, key in URL fragment | no identity-binding by design |
| Chromium extension | n/a | n/a | AES-256-GCM, key in URL fragment | client-side, no identity-binding |
| Outlook add-in | n/a | n/a | AES-256-GCM, key in URL fragment | client-side, no identity-binding |
Pre-v1 hybrid: the web app 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.
The extensions do encrypt on your own machine, but not with the hybrid
above: extensions/shared/paramant-core.js generates an
AES-256-GCM key with Web Crypto and puts it in the URL fragment, exactly
like the browser-encrypted link. There is no key exchange and therefore
no binding to a recipient, so whoever the link reaches can open the file.
Treat an extension upload as a sealed link, not as an end-to-end transfer
to a named person.
07Adding an algorithm
Three steps. No schema changes, no in-flight blobs affected.
Write the implementation
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.
08What is not yet built
The gap between what the header format allows 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
The format has room for these, but they are not wired up yet. 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