notes: research/cipher-suites
This data as json
| path | title | type | status | classification | words | body |
|---|---|---|---|---|---|---|
| research/cipher-suites | KERI Suite cipher suites: what the code tables carry | reference | v0.1 — 2026-10-15: drafted by Claude from the CESR specification and keripy's code tables; unread by Mark; marked partner on Mark's word 2026-10-21, paraphrase only | partner | 3125 | # KERI Suite cipher suites: what the code tables carry This note lists the cryptographic primitives that the Composable Event Streaming Representation (CESR) code tables carry for the Key Event Receipt Infrastructure (KERI) and Authentic Chained Data Container (ACDC) protocol stack, genus/version `-_AAACAA` (version 2.00), and checks each code against keripy, the Python reference implementation. It is part 3, "Cryptography and cipher suites", of the KERI Suite knowledge system (KSKB). Every code below was copied from the files read on 2026-10-15, not from memory. Sources: the CESR specification in `trustoverip/kswg-cesr-specification` at the `v1.0` tag (commit `037129608b9e6960858b752019ac273d40d7386c`, 2026-03-05), at `main` (commit `bad6edd84f928fe1e77a722edbdd829e0878377f`, 2026-08-28), and at the `v1.1` branch (commit `65fd7518b6fa318ef3fc700a824b18112b8392e2`, 2026-08-26), file `spec/spec-body.md`, Annex A "Master code table" and "Indexed code table"; keripy in `WebOfTrust/keripy` at `main` (commit `c102367ea0b8416ffb3c01403d18b3fd4a6a38bf`, 2026-10-14), files `src/keri/core/coring.py`, `src/keri/core/indexing.py`, `src/keri/core/signing.py` and `src/keri/app/keeping.py`; and the KERI specification in `trustoverip/kswg-keri-specification` at `main` (commit `fbdd4a6155e48248873cccd5cf6f79b6b631a021`), file `spec/spec-body.md`, compared against its `v1.0.1` tag (commit `71cb54ebb445dd9d8cb33cd29a5f50894fafc569`), which differs only in two typo fixes. On CESR `main`, `spec/spec-body.md` is byte-identical to the `v1.0` tag (same Git blob, `66d6e0b2`), so in this note "v1.0" covers both the release and the editor's draft on `main`. The `v1.1` branch is the working draft for the next version and is the only place the post-quantum codes appear. Line numbers below refer to the files at those commits. **Post-quantum pull requests, state as of 2026-10-15 (GitHub API).** CESR PR #165, "Add post-quantum signature primitives (FN-DSA, ML-DSA, SLH-DSA)", opened 2026-03-18 by dhh1128, was merged on 2026-08-25 into the `v1.1` branch, not `main`. CESR PR #178, "Revert 'Add post-quantum signature primitives'", opened 2026-09-22 from branch `revert-165-pq`, also targets `v1.1` and is open and unmerged. The post-quantum codes are therefore present on `v1.1` and absent from `v1.0` and `main`. A "first cut of Falcon support" commit pushed to CESR `main` on 2026-03-18 was reverted the same day (commit `7a6adca678`). Column key for Table 1: "v1.0" means the `v1.0` tag and `main`; "v1.1" means the `v1.1` branch. Every code marked "v1.0" is also on `v1.1`. ## Table 1: primitives by function | Primitive | Algorithm | CESR code(s) in the spec | keripy codex name | Spec version | Notes | |---|---|---|---|---|---| | **Signing keys** | | | | | | | Public verification key, transferable | Ed25519 | `D` | `MatterCodex.Ed25519` | v1.0, v1.1 | 44 characters. Also in `PreCodex` and `PreNonDigCodex` | | Public verification key, non-transferable | Ed25519 | `B` | `MatterCodex.Ed25519N` | v1.0, v1.1 | 44 characters. Also in `NonTransCodex` | | Private key seed | Ed25519 | `A` | `MatterCodex.Ed25519_Seed` | v1.0, v1.1 | 32-byte seed, 44 characters | | Public verification key, transferable | ECDSA secp256k1 | `1AAB` | `MatterCodex.ECDSA_256k1` | v1.0, v1.1 | 33-byte compressed point, 48 characters. Spec: "verification or encryption key" | | Public verification key, non-transferable | ECDSA secp256k1 | `1AAA` | `MatterCodex.ECDSA_256k1N` | v1.0, v1.1 | 48 characters | | Private key seed | ECDSA secp256k1 | `J` | `MatterCodex.ECDSA_256k1_Seed` | v1.0, v1.1 | 44 characters | | Public verification key, transferable | ECDSA secp256r1 | `1AAJ` | `MatterCodex.ECDSA_256r1` | v1.0, v1.1 | 48 characters. Spec: "verification or encryption key" | | Public verification key, non-transferable | ECDSA secp256r1 | `1AAI` | `MatterCodex.ECDSA_256r1N` | v1.0, v1.1 | 48 characters | | Private key seed | ECDSA secp256r1 | `Q` | `MatterCodex.ECDSA_256r1_Seed` | v1.0, v1.1 | 44 characters | | Public verification key, transferable | Ed448 | `1AAD` | `MatterCodex.Ed448` | v1.0, v1.1 | 80 characters. Code only in keripy: no signer or verifier (see Table 2) | | Public verification key, non-transferable | Ed448 | `1AAC` | `MatterCodex.Ed448N` | v1.0, v1.1 | 80 characters. Code only in keripy | | Private key seed | Ed448 | `K` | `MatterCodex.Ed448_Seed` | v1.0, v1.1 | 76 characters. Code only in keripy | | **Signatures** | | | | | | | Signature, non-indexed | Ed25519 | `0B` | `MatterCodex.Ed25519_Sig` | v1.0, v1.1 | 64 bytes, 88 characters | | Signature, non-indexed | ECDSA secp256k1 | `0C` | `MatterCodex.ECDSA_256k1_Sig` | v1.0, v1.1 | 64 bytes (r and s, 32 bytes each, in keripy), 88 characters | | Signature, non-indexed | ECDSA secp256r1 | `0I` | `MatterCodex.ECDSA_256r1_Sig` | v1.0, v1.1 | 64 bytes, 88 characters | | Signature, non-indexed | Ed448 | `1AAE` | `MatterCodex.Ed448_Sig` | v1.0, v1.1 | 114 bytes, 156 characters. keripy also lists it in `PreCodex` as "self-signing derivation" | | Indexed signature | Ed25519 | `A#` (both lists, same index), `B#` (current only), `2A####` (big, dual), `2B####` (big, current only) | `IndexerCodex.Ed25519_Sig`, `Ed25519_Crt_Sig`, `Ed25519_Big_Sig`, `Ed25519_Big_Crt_Sig` | v1.0, v1.1 | 88 or 92 characters | | Indexed signature | ECDSA secp256k1 | `C#`, `D#`, `2C####`, `2D####` | `IndexerCodex.ECDSA_256k1_Sig`, `ECDSA_256k1_Crt_Sig`, `ECDSA_256k1_Big_Sig`, `ECDSA_256k1_Big_Crt_Sig` | v1.0, v1.1 | 88 or 92 characters | | Indexed signature | ECDSA secp256r1 | not found | `IndexerCodex.ECDSA_256r1_Sig` (`E`), `ECDSA_256r1_Crt_Sig` (`F`), `ECDSA_256r1_Big_Sig` (`2E`), `ECDSA_256r1_Big_Crt_Sig` (`2F`) | neither | keripy only (Table 2) | | Indexed signature | Ed448 | `0A##` (dual), `0B##` (current only), `3A######` (big, dual), `3B######` (big, current only) | `IndexerCodex.Ed448_Sig`, `Ed448_Crt_Sig`, `Ed448_Big_Sig`, `Ed448_Big_Crt_Sig` | v1.0, v1.1 | 156 or 160 characters | | **Digests** | | | | | | | Digest, 256-bit | Blake3-256 | `E` | `MatterCodex.Blake3_256` | v1.0, v1.1 | Also in `DigCodex`, `PreCodex`, `NonceCodex`. keripy default digest (`Diger` default code) | | Digest, 512-bit | Blake3-512 | `0D` | `MatterCodex.Blake3_512` | v1.0, v1.1 | keripy computes it with `blake3.blake3`, length 64 (coring.py 3558) | | Digest, 256-bit | Blake2b-256 | `F` | `MatterCodex.Blake2b_256` | v1.0, v1.1 | keripy: `hashlib.blake2b` with digest size 32 | | Digest, 512-bit | Blake2b-512 | `0E` | `MatterCodex.Blake2b_512` | v1.0, v1.1 | | | Digest, 256-bit | Blake2s-256 | `G` | `MatterCodex.Blake2s_256` | v1.0, v1.1 | | | Digest, 512-bit | Blake2s-512 | not found | not found | neither | Neither source defines it. Blake2s has a maximum output of 256 bits by design (algorithm property, not from these sources) | | Digest, 256-bit | SHA3-256 | `H` | `MatterCodex.SHA3_256` | v1.0, v1.1 | | | Digest, 512-bit | SHA3-512 | `0F` | `MatterCodex.SHA3_512` | v1.0, v1.1 | | | Digest, 256-bit | SHA2-256 | `I` | `MatterCodex.SHA2_256` | v1.0, v1.1 | | | Digest, 512-bit | SHA2-512 | `0G` | `MatterCodex.SHA2_512` | v1.0, v1.1 | | | **Encryption keys and ciphertexts** | | | | | | | Public encryption key | X25519 | `C` | `MatterCodex.X25519` | v1.0, v1.1 | "may be converted from Ed25519 public key" | | Private decryption key or seed | X25519 | `O` | `MatterCodex.X25519_Private` | v1.0, v1.1 | "may be converted from Ed25519 key/seed" | | Public encryption key | X448 | `L` | `MatterCodex.X448` | v1.0, v1.1 | 76 characters. Code only in keripy | | Sealed-box ciphertext of a 44-character seed | X25519 | `P` | `MatterCodex.X25519_Cipher_Seed` | v1.0, v1.1 | 124 characters | | Sealed-box ciphertext of a 24-character salt | X25519 | `1AAH` | `MatterCodex.X25519_Cipher_Salt` | v1.0, v1.1 | 100 characters | | Sealed-box ciphertext, sniffable plaintext, variable length | X25519 | `4C`, `5C`, `6C`, `7AAC`, `8AAC`, `9AAC` | `X25519_Cipher_L0` to `_L2`, `X25519_Cipher_Big_L0` to `_Big_L2` | v1.0, v1.1 | Lead sizes 0, 1, 2; small and big | | Sealed-box ciphertext, QB64 plaintext, variable length | X25519 | `4D`, `5D`, `6D`, `7AAD`, `8AAD`, `9AAD` | `X25519_Cipher_QB64_L0` to `_Big_L2` | v1.0, v1.1 | QB64 is the qualified Base64 text domain | | Sealed-box ciphertext, QB2 plaintext, variable length | X25519 | `4E`, `5E`, `6E`, `7AAE`, `8AAE`, `9AAE` | `X25519_Cipher_QB2_L0` to `_Big_L2` | v1.0, v1.1 | QB2 is the qualified binary domain | | Hybrid Public Key Encryption (HPKE) Base mode ciphertext, variable length | HPKE Base | `4F`, `5F`, `6F`, `7AAF`, `8AAF`, `9AAF` | `HPKEBase_Cipher_L0` to `_Big_L2` | v1.0, v1.1 | Spec: "of QB2 plaintext"; keripy comment: "of sniffable stream plaintext" (Table 2). The HPKE cipher suite (KEM, KDF, AEAD) is not named in the code | | **Salts and seeds** | | | | | | | Random salt, seed, nonce, private key or number, 128-bit | none (random bytes) | `0A` | `MatterCodex.Salt_128` | v1.0, v1.1 | 24 characters. The only code keripy's `Salter` accepts (signing.py 417-418) | | 256-bit value | none (random or derived) | `a` | `MatterCodex.Salt_256` | v1.0, v1.1 | Spec: "Blinding factor 256 bits"; keripy: "Salt/seed/nonce/blind" (Table 2) | | **Post-quantum (v1.1 branch only)** | | | | | | | FN-DSA-512 public key, signature, private key seed | FN-DSA (Falcon), FIPS 206 | `1AAQ` key, `1AAR` signature, `2AAC` seed | not found | v1.1 | Signature is the FIPS 206 padded format, 666 bytes, 892 characters | | FN-DSA-1024 public key, signature, seed | FN-DSA, FIPS 206 | `2AAA` key, `2AAB` signature, `2AAD` seed | not found | v1.1 | Padded signature 1280 bytes, 1712 characters | | ML-DSA-44 public key, signature, seed | ML-DSA, FIPS 204 | `3AAA` key, `2AAF` signature, `2AAG` seed | not found | v1.1 | | | ML-DSA-65 public key, signature, seed | ML-DSA, FIPS 204 | `2AAE` key, `1AAT` signature, `2AAH` seed | not found | v1.1 | | | ML-DSA-87 public key, signature, seed | ML-DSA, FIPS 204 | `1AAS` key, `3AAB` signature, `2AAI` seed | not found | v1.1 | | | SLH-DSA-SHA2-128s public key, signature, seed | SLH-DSA, FIPS 205 | `2AAJ` key, `2AAL` signature, `1AAY` seed | not found | v1.1 | Only the six small-signature (`s`) parameter sets are admitted | | SLH-DSA-SHAKE-128s public key, signature, seed | SLH-DSA, FIPS 205 | `2AAK` key, `2AAM` signature, `1AAZ` seed | not found | v1.1 | | | SLH-DSA-SHA2-192s public key, signature, seed | SLH-DSA, FIPS 205 | `1AAU` key, `1AAW` signature, `1AAa` seed | not found | v1.1 | | | SLH-DSA-SHAKE-192s public key, signature, seed | SLH-DSA, FIPS 205 | `1AAV` key, `1AAX` signature, `1AAb` seed | not found | v1.1 | | | SLH-DSA-SHA2-256s public key, signature, seed | SLH-DSA, FIPS 205 | `3AAC` key, `2AAN` signature, `1AAc` seed | not found | v1.1 | | | SLH-DSA-SHAKE-256s public key, signature, seed | SLH-DSA, FIPS 205 | `3AAD` key, `2AAO` signature, `1AAd` seed | not found | v1.1 | | | **Other** | | | | | | | Post-quantum codes in v1.0 | none | none | none | v1.0 has none | v1.0 Annex A "Code table entry policy" says codes "will be added" when NIST approves post-quantum operations, naming Falcon as a candidate | | ML-KEM key encapsulation | ML-KEM, FIPS 203 | not found | not found | neither | v1.1 says ML-KEM codes "are not defined here and are left to a later version" | | Post-quantum indexed signatures | FN-DSA, ML-DSA, SLH-DSA | not found | not found | neither | v1.1 leaves this open as CESR issue #171 | Lengths: for all 104 primitive codes in the v1.0 master table, keripy's code also exists in `Matter.Sizes` with the same full length in characters (checked by script). The v1.1 post-quantum lengths were checked by hand against the stated raw sizes and the 24-bit alignment rule; all 33 are consistent. ## Table 2: where the spec and keripy differ | Item | CESR specification | keripy (`c102367`) | Kind of difference | |---|---|---|---| | ECDSA secp256r1 indexed signatures | Not in the v1.0 or v1.1 indexed code table; the prose (Annex "Indexed codes", spec-body.md 688) names only Ed25519 and secp256k1 for 64-byte signatures | `E`, `F`, `2E`, `2F` in `IndexerCodex` (indexing.py 45-60); used by `Signer._secp256r1` (signing.py 270-285) | Code in keripy only | | Datagram ("gram") header codes | `0P` Gram Head Neck, `0Q` Gram Head, `0R` Gram Head AID Neck, `0S` Gram Head AID | None of the four; instead `b` `GramHead` (coring.py 263, size hs=1, ss=3, fs=8) | Different code for the same purpose; `b` is unassigned in the spec | | Code `a` | "Blinding factor 256 bits, Cryptographic strength deterministically generated from random salt" | `Salt_256`, "Salt/seed/nonce/blind 256 bits", and a member of `NonceCodex` | Same code, broader meaning in keripy | | HPKE Base ciphertext `4F` to `9AAF` | "HPKE Base cipher bytes of QB2 plaintext" | Comments say "of sniffable stream plaintext" (coring.py 331-336) | Same codes, different description | | Ed448 signature `1AAE` as identifier prefix | Listed as "Ed448 signature" only | Also in `PreCodex`, commented "Self-signing derivation" (coring.py 607) | Same code, keripy allows an extra role | | Ed448 and X448 support | Codes `K`, `L`, `1AAC`, `1AAD`, `1AAE`, `0A##`, `0B##`, `3A######`, `3B######` defined | Codes defined, but `Signer` supports only Ed25519, secp256r1 and secp256k1 seeds (signing.py 112-124, raising "Unsupported signer code" otherwise) and `Verfer` only Ed25519, secp256r1, secp256k1 (coring.py 3391-3398) | Defined in both, implemented in neither path of keripy | | Test codes | None | `1__-`, `1___`, `2__-`, `2___`, `3__-`, `3___` in `MatterCodex`; `0z`, `1z`, `4z` in `IndexerCodex`, all marked "Testing purposes only" or "Test" | keripy only, test use | | Post-quantum codes | 33 codes on the `v1.1` branch (PR #165) | None on `main` (no match for Falcon, ML-DSA, SLH-DSA, FN-DSA or quantum in the four files read) | Spec draft only | No code has a different full length in the two sources. No code was found that means a different algorithm in the two sources. ## Mechanisms ### Weighted and fractional thresholds The KERI specification defines the `kt` (current) and `nt` (next) thresholds as either a hex-encoded integer, read as M of N, or a list of clauses of rational fractional weights (KERI spec, "Key and key digest threshold fields", spec-body.md 325-331). Clauses are ANDed, and each clause is satisfied when the weights of keys with verified signatures sum to at least one; one level of nesting is allowed, where a single-entry map gives a weight to a nested list of weights ("Fractionally weighted threshold", 1417-1466). keripy's `Tholder._satisfy_weighted` implements this with Python `Fraction` values (coring.py 4788-4830), and caps a threshold at 1000 weights as a local denial-of-service guard that its own comment says is "NOT a protocol maximum" (coring.py 4324-4334). The specification's nine-key example key list ends with A<sup>0</sup> where A<sup>8</sup> is meant (spec-body.md 1435). ### Digest-based pre-rotation Each establishment event carries the current public keys and the digests of the next public keys; a rotation must reveal a subset of the prior next keys that satisfies the prior next threshold, and its signatures must satisfy both the new current threshold and the prior next threshold (KERI spec, "Pre-rotation" and "Rotation using pre-rotation", 1362-1416). An empty next list makes an identifier non-transferable at inception, or abandoned at rotation (340). Partial and augmented rotation permit holding some pre-rotated keys back and adding keys that were not pre-rotated (1468-1472). The next-key digests use the ordinary digest codes in Table 1; v1.1 states that no separate "digest of a public key" code is defined (v1.1 spec-body.md 1559). ### Argon2id salty key derivation in keripy `Salter.stretch` derives each seed with `pysodium.crypto_pwhash` and `alg=crypto_pwhash_ALG_ARGON2ID13`, using the 16-byte `0A` salt as the Argon2 salt and a derivation path string as the password (signing.py 454-460). The tiers are low: opslimit 2, memlimit 67,108,864 bytes (64 MiB); med: 3 and 268,435,456 (256 MiB); high: 4 and 1,073,741,824 (1 GiB); and a test-only `temp` mode of 1 and 8,192 bytes (signing.py 438-452). The default tier is low (signing.py 395; keeping.py 757). `SaltyCreator` builds the path as stem (or the hex prefix index) followed by the hex rotation index and hex key index (keeping.py 545-547). The `stretch` docstring says "argon2d" while the code uses Argon2id (signing.py 427 against 460). The KERI specification's informative example uses the same call with the test parameters (KERI spec, "Working Examples Setup", 1914-1919). ### Signature randomness keripy signs Ed25519 with libsodium's `crypto_sign_detached` (signing.py 214); Ed25519 as defined in RFC 8032 derives its nonce from the private key and message, so its signatures are deterministic (a property of the algorithm, not checked in libsodium source here). keripy signs ECDSA on both curves with pyca/cryptography `sign(ser, ec.ECDSA(hashes.SHA256()))` and passes no deterministic-signing option (signing.py 263-264 and 317-318), then stores r and s as two 32-byte big-endian integers (signing.py 265-268). Neither the CESR nor the KERI specification read here states the hash used with ECDSA or whether ECDSA nonces must be deterministic. The ECDSA private scalar is the 32-byte seed read as a big-endian integer (signing.py 133-135, 142-144). ### Post-quantum position CESR v1.0 and `main` define no post-quantum codes; Annex A says codes "will be added" once NIST approves post-quantum operations (spec-body.md 783), and the "Post-Quantum Security" section relies on hiding pre-rotated public keys behind digests, citing Bernstein's analysis that quantum computation gives no advantage in hash collision search (1501-1506). The KERI specification repeats that argument ("Post-Quantum Security", 1710-1713) and defines Surprise Quantum Attack Recovery (SQAR): a rotation that gives the exposed pre-rotated keys zero signing weight and moves signing authority to post-quantum keys (1578-1606), though no post-quantum key codes exist in the v1.0 tables it builds on. The CESR `v1.1` branch adds 33 codes for FN-DSA, ML-DSA and the six SLH-DSA `s` parameter sets, all in the four-character tables, with FN-DSA signatures in the fixed padded format, and leaves ML-KEM and post-quantum indexed signatures open (v1.1 spec-body.md 783-792, 1021-1055, 1551-1568). Those codes are the subject of open revert PR #178. One v1.1 sentence says a four-character code is added "to a Primitive that is already at least 892 characters long", which holds for the signatures but not for the 48-character seeds or the 68-character SLH-DSA public keys in the same table (v1.1 spec-body.md 785). ## Not verified - The KSWG (Trust over IP KERI Suite Working Group) minutes of September and October 2026: not read. As reported, they describe the post-quantum revert as a "KERI specification" pull request, while PR #165 and PR #178 are in the CESR specification repository; and the revert they describe has not been merged as of 2026-10-15. - Whether `v1.1` or `main` is the branch the KSWG treats as the editor's draft. `spec-head.md` on both the `v1.0` tag and `main` already reads "Specification Status: v1.1". - Blake2s-512, ECDSA secp256r1 indexed signature codes in the spec, post-quantum codes in keripy, ML-KEM codes, and post-quantum indexed codes: not found in any source read. - Whether pyca/cryptography uses a random or deterministic ECDSA nonce by default, and how it handles a seed at or above the curve order: library source not read. - libsodium's Ed25519 implementation and its `crypto_pwhash` constants: the tier values are taken from keripy's literals and comments, not checked against libsodium. - Whether a keripy branch other than `main` carries post-quantum codes: only `main` was read. - The keripy `Cipher`, `Encrypter` and `Decrypter` classes (signing.py 495-1148) were not read in detail, so which variable-length cipher codes keripy can actually produce is not checked. - The v1.1 post-quantum raw sizes were checked for internal consistency only, not against the FIPS documents. |
Links from other tables
- 0 rows from summary in resources