Suites (Ciphersuite Registry)
Table of Contents
- Purpose
- The shared ECDH floor
- Registered suites
- Suite descriptor
- Wire binding
- Downgrade resistance
- Peer capability advertisement
- Adding a new suite
Purpose
A plugin in this catalog is the pair (role, suite). The role is the trust-shape — pairwise, single-owner, N-party, one-shot-directed — and the suite is the concrete ciphersuite that pins the AEAD, the nonce length, the KDF, and the in-circuit framing of the ciphertext above the ECDH floor.
Roles and suites are orthogonal. The same pairwise role runs over multiple suites today (enc-xchacha-v1 in native DM and nostr-nip44-v2 in Nostr-bridged DM). The same suite slot can host different roles (a pairwise channel and a one-shot directed envelope can both use enc-xchacha-v1). The registry below is what makes that orthogonality concrete: a plugin spec says "role: pairwise, suite: enc-xchacha-v1", and the wire envelope carries the suite id as an authenticated field.
Before this registry, the plugin catalog welded suite into role — every plugin opened with a fixed §Primitives (no negotiation) table — and the only way to switch a suite was to fork the whole plugin. The mls-lazy plugin's explicit not XChaCha20 rule (ChaCha20-Poly1305 with 12-byte nonce, per RFC 9420) is the first evidence the catalog already needed factoring; NIP-44 is the second.
The shared ECDH floor
Every registered suite operates above the same ECDH primitive:
- Curve:
secp256k1 - Shared output: the 32-byte x-coordinate of the compressed point (
x_only_secret = compressed[1..33]) - x-only public-key inputs (32 bytes) are extended with the
0x02prefix per BIP-340 even-y convention.
This is what makes suites swappable. A pairwise role gets the same x_only_secret whether it is wrapped under enc-xchacha-v1 or nostr-nip44-v2; only the KDF and AEAD above the shared floor differ. New suites that introduce a different curve are out of scope of THIS registry and require a separate enc-curve-vN registry.
Registered suites
| Suite id | AEAD | Nonce | KDF | Wire framing | Source / notes |
|---|---|---|---|---|---|
enc-xchacha-v1 | XChaCha20-Poly1305 (libsodium) | 24 bytes | HKDF-SHA-256, salt = b"", info = ASCII domain separator | base64( nonce(24) ‖ ct+tag ) | ENC native pairwise / single-owner / one-shot. Used by ratchet-pair, ecdh-envelope, identity-aead |
mls-chacha-v1 | ChaCha20-Poly1305 (RFC 8439) | 12 bytes | HKDF-SHA-256, salt = SHA-256 zero block, info = ASCII domain | hex( nonce(12) ) ‖ hex( ct+tag ) | MLS-style N-party. Used by mls-lazy. Matches RFC 9420 conventions (explicitly NOT XChaCha20) |
nostr-nip44-v2 | ChaCha20 + HMAC-SHA-256 (encrypt-then-MAC) | 32 bytes | HKDF-SHA-256, salt = b"nip44-v2", info = b"" | base64( v ‖ nonce(32) ‖ ct ‖ tag ), v = 0x02 | Nostr NIP-44 v2. Externally versioned by Nostr; the suite id pins to v2 explicitly. |
Implementations MUST recognize enc-xchacha-v1 as a registered suite id.
Implementations MUST recognize mls-chacha-v1 as a registered suite id.
Implementations MUST recognize nostr-nip44-v2 as a registered suite id.
Each suite id is a stable string. Future revisions of an existing suite (e.g. a NIP-44 v3) get a NEW suite id (nostr-nip44-v3), not a mutation of the existing one. The id is the unit of versioning.
Suite descriptor
The normative descriptor for a suite is the structured record:
id: nostr-nip44-v2
aead: chacha20-poly1305 # bytes of ct + 16-byte Poly1305 tag (or 32-byte HMAC for v2)
nonce_len: 32 # bytes
kdf: hkdf-sha256
kdf_salt: "nip44-v2" # ASCII; b"" when empty; SHA-256 zero block when "zero"
kdf_info: ""
ecdh_curve: secp256k1
wire_framing: base64(v || nonce || ct || tag)
external_version: { spec: "NIP-44", revision: "v2" } # optionalThe id MUST be lowercase ASCII, MAY use - separators, and MUST NOT use _ or whitespace. Suite-id uniqueness is global; a suite intended for one role can be reused under another role unchanged.
Wire binding
Every encrypted wire envelope MUST carry the suite id as an authenticated field. The suite id is the discriminator the receiver uses to dispatch the decryption path; if it is not authenticated, an attacker who replaces only the suite id can force the receiver into a weaker dispatch (downgrade — see §Downgrade resistance).
Authenticated can mean: (a) signed alongside the ciphertext under a credential the receiver verifies, (b) prefixed to the ciphertext and bound through the AEAD's associated-data input, or (c) tagged through HMAC under the shared KDF output. The choice is suite-local; the requirement is that the suite id and the ciphertext succeed or fail TOGETHER.
The receiver MUST NOT tag-sniff (guess the suite from the ciphertext layout) and MUST NOT accept a suite id that is not in the receiver's locally-validated capability set for the sender.
Downgrade resistance
A downgrade attack is: an active attacker takes a ciphertext encrypted under suite nostr-nip44-v2 and presents it to the receiver with the suite id rewritten to enc-xchacha-v1 (or any weaker suite). If the receiver's dispatch accepts a fresh suite id without authentication, the attacker has substituted a path the sender did not consent to.
Normative requirement:
- Suite-id authentication follows §Wire binding.
- A receiver MUST reject any envelope whose suite id is not present in the sender's advertised capability set (per §Peer capability advertisement).
- Receiver tag-sniffing is forbidden by §Wire binding.
Peer capability advertisement
Senders MUST NOT probe send-side suite selection from client-local heuristics (e.g., browser localStorage). The protocol exposes peer capabilities through the reg_identity registry: an identity's reg_identity content MAY list a suites array of suite ids the identity accepts.
{
"id_pub": "<hex64>",
"suites": ["enc-xchacha-v1", "nostr-nip44-v2"]
}A sender's suite selection process:
- Resolve the recipient's
reg_identity(cached or freshly fetched). - Intersect the recipient's
suiteswith the sender's own capability set. - Pick the highest-preference suite in the intersection (sender-local preference order).
- If the intersection is empty, the sender MUST fail closed — do NOT pick a default.
A sender's preference order is a local policy choice (not protocol). The intersection rule and the fail-closed-on-empty rule are normative.
When a recipient's reg_identity does not declare a suites array, the recipient MUST be assumed to support enc-xchacha-v1 only. Bridges (Nostr in particular) advertise the bridged suite (nostr-nip44-v2) by publishing a reg_identity with that suite listed.
Adding a new suite
A new suite is added by:
- Defining a new entry in §Registered suites with the full descriptor.
- Specifying the wire framing and the authenticated-id binding.
- Bumping any plugin spec that opts into the new suite (the plugin's spec lists which suite ids it accepts; an existing plugin doc remains valid for its current suite set).
- Conformance vectors: a per-suite KAT (known-answer test) directory under
claims/vectors/suites/<suite-id>/*.jsonwith at least one encrypt and one decrypt vector exercising the ECDH floor and the AEAD.
Adding a curve below the ECDH floor is OUT of scope; that requires a separate enc-curve-vN registry coordinated with this catalog.