Imported from barum/tessera (
.claude/skills/tessera-seal-impl/SKILL.md). Install upstream withnpx skills add barum/tessera --skill tessera-seal-impl. Copyright stays with the author.
tessera-seal-impl — Working on the TesseraSeal reference implementations
TesseraSeal is a pure post-quantum encryption envelope carried in the same monorepo as TesseraID and TesseraHash, owning variant byte 0x04. This skill encodes the rules for changing it correctly. Umbrella discipline lives in ../../../CLAUDE.md; sibling skills tessera-id-impl and tessera-hash-impl own the analogous concerns for their projects.
Frozen wire labels (do NOT rename)
TesseraSeal's KDF / combiner domain-separation labels are byte strings hashed into the envelope. They are frozen v1 wire tags — not rebrandable names. The literal text pqcrypt v1 ... MUST stay verbatim in every implementation or every conformance vector breaks. These are the only place the token pqcrypt legitimately remains in code:
"pqcrypt v1 header""pqcrypt v1 aead k""pqcrypt v1 aead n""pqcrypt v1 commit""pqcrypt v1 timing pad""pqcrypt v1 hybrid combiner"
When you see one of these in code, leave it exactly as-is and add a comment: frozen v1 wire tag — not a rebrandable name.
Authority order
- TesseraSeal whitepaper LaTeX at tessera-seal/latex/, built to
TesseraSeal_Whitepaper.pdf. Normative. - tessera-seal/python/tessera_seal.py — readable Python reference (TSEAL-00003).
- tessera-seal/c/tessera_seal.h — canonical C ABI (TSEAL-00005).
- Go, Rust follow in v1 alpha. C++, TypeScript, Java, CPython ext follow in v1.1+.
Never resolve a cross-language disagreement by changing the spec to match an outlier.
The hard invariants (do not silently break)
- 16-byte fixed header:
TS(6) || VAR(1)=0x04 || SUITE(1) || CTR(2) || KEM_CT_LEN(2) || AD_LEN(4), big-endian. - Variant byte =
0x04. Decoders MUST reject every other byte. TesseraID owns0x01/0x02, TesseraHash owns0x03, both MUST reject0x04. - Suite byte allocation (v1.x):
0x01mandatory: ML-KEM-1024 + AES-256-GCM + UtC commit (32 B) + HKDF-SHA3-256. Pure post-quantum.0x02optional: ML-KEM-1024 + ChaCha20-Poly1305 + UtC commit + HKDF-SHA3-256. Pure post-quantum.0x06hybrid (v1.2, TSEAL-00021): X25519 + ML-KEM-1024 + AES-256-GCM + UtC + HKDF-SHA3-256. Opt-in defense-in-depth. X-Wing-style combiner:K_session = SHAKE-256("pqcrypt v1 hybrid combiner" ‖ ss_mlkem ‖ x_ss ‖ ml_kem_ct ‖ x_eph_pk, 32)(the label is a frozen v1 wire tag — not a rebrandable name). Hybrid sizes: pk=1600 B (1568+32), sk=3200 B (3168+32), KEM_CT=1600 B (1568+32), seed=96 B (64+32).0x00,0x03–0x05,0x07–0xFF: reject. (AEGIS-128L on0x03was evaluated and deferred in TSEAL-00020.)
- Core promise is PQ-only. Suites
0x01/0x02carry the pure-PQ brand promise; the residual-risk acknowledgment in the whitepaper appendix applies to them. Suite0x06is the opt-in hybrid alternative, allocated as a separate suite byte. Adding any further hybrid or PQ-only suite is another new suite byte, never an in-place change. - Key-committing: every envelope (including hybrid
0x06) has a 32-byte UtC commitment tag (HMAC-SHA3-256 over header ‖ KEM_CT ‖ AD ‖ AEAD_CT). - Single
DecryptionError/TESSERA_SEAL_E_DECRYPT_FAILEDfor every authentication-class failure. No distinguishing oracle. Structural errors (truncation, unknown variant/suite, suite/key mismatch) DO get distinct codes — they don't touch secret-correlated bytes. - Constant-time decrypt path including synthetic-key AEAD verify on structural failures.
- Deterministic AEAD nonce derived from
K_sessionvia HKDF-Expand. Same rule for hybrid; the SHAKE-256 combiner produces a freshK_sessionper envelope because each envelope has a fresh ML-KEM Encaps + fresh X25519 ephemeral. - Suite/key consistency.
encrypt()MUST reject whenpub.suite != suite;decrypt()MUST reject whenpriv.suite != header.suite. Mismatch isBadKey-class, not auth-class. - OS CSPRNG only for ML-KEM internal randomness AND for the X25519 ephemeral in hybrid. No userspace PRNG.
- No "Kyber" naming in public APIs. Use
ML-KEM-1024. Hybrid surface usesX25519andML-KEM-1024only; never "Curve25519" or "ECDH" in user-facing names. - Length limits:
KEM_CT_LEN ≤ 4096(1568 for non-hybrid, 1600 for hybrid),AD_LEN ≤ 65536, plaintext ≤ 4 GiB. - Polyglot hybrid coverage gate. Hybrid
0x06is a polyglot KAT contract: as of v1.3 the contract covers Python ↔ Rust ↔ Go. Languages without hybrid MUST reject0x06asUNKNOWN_SUITEand skiphybrid_*.jsonKAT files per tools/conformance/run.sh:hybrid_supported().
A change that affects any of these is a spec change. See "Spec change workflow" below.
How to make an implementation-only change
Optimization or bug-fix work within one language that keeps observable behavior identical:
- Identify files in
tessera-seal/<lang>/. Stay inside that directory. - Capture baseline by running tests + conformance harness.
- Make the change.
- Tests must still pass unmodified. If you had to "update" a test, you've altered observable behavior — promote to spec-change workflow.
- Cross-language harness must stay green:
bash tools/conformance/run.sh. - Refresh
tessera-seal/benchmarks/<lang>.txtif performance is the point.
How to make a spec change (cross-language)
- Update the spec first: edit
tessera-seal/latex/secNN_*.tex, rebuild viacd tessera-seal/latex && make. Commit the LaTeX edits + the rebuilt PDF. - Update the Python reference (tessera-seal/python/tessera_seal.py) to match.
- Update C (tessera-seal/c/tessera_seal.h first if offsets/constants/error-codes change, then
tessera_seal.c). - Port to remaining v1 alpha languages — Go, Rust — in lockstep. Each must produce byte-identical output for the same input as Python.
- Regenerate conformance vectors in
tessera-seal/conformance/vectors/from the Python reference; commit verbatim. - Mirror tests to every alpha language.
- Refresh benchmarks if performance-relevant.
- Write release notes at
tessera-seal/docs/release-notes/YYYY-MM-DD-<slug>.mdwith sections: Summary, Wire/API changes, Migration, Test plan, Rollback, Security considerations (if applicable). Append totessera-seal/docs/release-notes/CHANGELOG.md. - Update READMEs: root
README.md,tessera-seal/README.md, per-language READMEs.
If the change is security-relevant (touches variant byte, suite byte, AEAD/commit construction, ML-KEM library choice, nonce derivation, decrypt error path, side-channel posture), tag the PR sec(TSEAL-NNNNN) and add a "Security considerations" section to the release-notes entry.
Prefer parallel agent execution for the per-language port — each port is independent once Python + spec are settled.
How to port hybrid suite 0x06 to a new language (v1.4 backlog)
Hybrid lands in Python (v1.2, TSEAL-00021), Rust (v1.3, TSEAL-00028), and Go (v1.3, TSEAL-00029). Remaining: C (TSEAL-00030), C++ (TSEAL-00031), TypeScript (TSEAL-00032), Java (TSEAL-00033), CPython ext (TSEAL-00034). The pattern from Rust/Go ports:
- Pin the X25519 source. Prefer stdlib (Go's
crypto/ecdh); otherwise a constant-time, audited library that does not bring in a userspace PRNG. For C: OpenSSL 3.5+EVP_PKEY_X25519(matches the existing ML-KEM source). For TS:@noble/curves/ed25519X25519 (matches the@noblefamily). For Java: SunJCEXDH(JEP 339, no BouncyCastle). - Add the suite constant (
SUITE_HYBRID_X25519_MLKEM1024_AESGCM = 0x06) and the hybrid size constants (X25519: 32 B all sizes; hybrid pk=1600, sk=3200, KEM_CT=1600, seed=96). - Extend the key types to optionally carry the X25519 half. Keep the non-hybrid happy path byte-identical — Rust and Go both used optional fields (
Option<[u8; 32]>/[]byte). For C with its fixed-size structs, the chosen path is separatetessera_seal_pub_hybrid_t/tessera_seal_priv_hybrid_tstructs + dedicated entry points (preserves the v1.0/v1.1 C ABI byte-for-byte). - Add
suite_encaps/suite_decaps/hybrid_combinehelpers routing per suite. Combiner is exactly:K_session = SHAKE-256("pqcrypt v1 hybrid combiner" ‖ ss_mlkem ‖ x_ss ‖ ml_kem_ct ‖ x_eph_pk, 32)(frozen v1 wire tag — not a rebrandable name). Order is fixed; flipping it breaks the polyglot KAT contract. - Update encrypt/decrypt to call the new helpers and add the suite/key consistency checks (
pub.suite == suite;priv.suite == header.suite). - Route hybrid through AES-256-GCM in the AEAD dispatch (suite
0x06shares the AES-GCM AEAD with0x01). - Add conformance runner support for 96-byte seeds (the Python/Rust/Go runners had a hard
len(seed) != 64check that needed widening to64 or 96). - Flip the language's entry in tools/conformance/run.sh:hybrid_supported().
- Add the per-language tests matching the Rust set (~6 tests): roundtrip, envelope size, wire
KEM_CT_LEN == 1600, AD-binding negative, X25519-half tamper →DecryptionError, KAT consumption ofhybrid_decrypt.json+hybrid_ad_binding.json. - Refresh the language's bench file under tessera-seal/benchmarks/ with hybrid keygen / encrypt_1KB / decrypt_1KB lines.
- Verify byte-identity:
bash tools/conformance/run.shreports the language's PASS count rising by 9 (the three hybrid_*.json files).
Reference release notes for the shape: tseal-00028-hybrid-rust.md, tseal-00029-hybrid-go.md.
Commands
Test & bench, by language (all from repo root). v1.x alpha languages first:
# Python
cd tessera-seal/python && python3 -m pytest test_tessera_seal.py -v
cd tessera-seal/python && python bench.py
cd tessera-seal/python && python3 cli.py {keygen,pubkey,encrypt,decrypt,info} # TSEAL-00022 CLI (tessera_seal is a flat module, not a package)
# C
cd tessera-seal/c && make test
cd tessera-seal/c && make bench
# Go
cd tessera-seal/go && go test ./...
cd tessera-seal/go && go test -bench=. -benchmem
# Rust
cd tessera-seal/rust && cargo test --release
cd tessera-seal/rust && cargo run --release --bin tessera-seal-bench
# Whitepaper
cd tessera-seal/latex && make
# Cross-language conformance (all 3 projects)
bash tools/conformance/run.sh
# v1.x alpha quick loop (skip languages not ported yet):
TESSERA_CONFORMANCE_SKIP="cpython java cpp typescript" bash tools/conformance/run.sh
What lives where
- tessera-seal/python/tessera_seal.py — Python reference (TSEAL-00003 + hybrid TSEAL-00021).
tessera-seal/python/tessera_seal_fast.py— planned v1.1 optimized variant, not yet written. Until it lands, the CPython extension wraps the C reference directly (see below), not atessera_seal_fast.pymirror.- tessera-seal/python/cli.py — CLI binary (TSEAL-00022).
- tessera-seal/c/tessera_seal.{h,c} — canonical ABI. Uses OpenSSL 3.5+
EVP_PKEYML-KEM (encapsulate/decapsulate) for ML-KEM-1024 (TSEAL-00005). Bench atbench_tessera_seal.c. - tessera-seal/go/tessera_seal.go — uses stdlib
crypto/mlkem(Go 1.26+) and stdlibcrypto/ecdhfor hybrid (TSEAL-00029). - tessera-seal/rust/src/lib.rs — uses RustCrypto
ml-kem0.3 andx25519-dalek2 for hybrid (TSEAL-00028). Bench bin atbenches/bench_bin.rs. - tessera-seal/cpython/tessera_seal_ext.c — CPython extension;
setup.pycompiles it against../c/tessera_seal.c(+../../common/c/hkdf_sha3.c), so it wraps the C reference directly. Notessera_seal_fast.pymirror exists yet. - tessera-seal/conformance/vectors/ — KAT vectors generated from Python. Hybrid vectors:
hybrid_decrypt.json,hybrid_ad_binding.json,hybrid_peek_header.json(TSEAL-00021). - tessera-seal/plugin/tessera-seal-plugin-protocol.md — plugin wire protocol. Reference plugin: tessera-seal/plugin/tessera_seal_plugin_file.py; keyfile format
.tseal-key-v1.1. - tessera-seal/docs/side-channel-disclaimer.md — normative side-channel posture.
- tessera-seal/docs/security-audit-v1.2.md — internal threat-model + known-attacks walk-through (TSEAL-00025).
- tessera-seal/benchmarks/ — one
.txtper language;c.txt,python.txt,go.txt,rust.txtall refreshed at v1.3 (2026-05-21). - tessera-seal/latex/ — whitepaper LaTeX. Built artifact:
TesseraSeal_Whitepaper.pdf. - .github/workflows/tessera-seal.yml — TesseraSeal-only matrix CI (TSEAL-00023).
Cross-language consistency checks
After any wire/encoding/KDF/AEAD/commit change:
- Generate N envelopes in Python from a fixed RNG seed; emit hex.
- In each other v1 alpha language, decrypt each envelope with the corresponding private key; assert the recovered plaintext matches.
- Generate N envelopes in each language; cross-decrypt them in Python. All must succeed.
- Run
bash tools/conformance/run.shto automate (1)–(3) over the full KAT set.
Python is right by authority order. If a language disagrees, fix the language.
Anti-patterns specific to this project
- Don't add X25519 to suite
0x01or0x02— those carry the pure-PQ brand promise. Hybrid is its own suite byte0x06(TSEAL-00021); any further hybrid construction needs its own new suite byte. - Don't widen the suite set to include AEGIS/Ascon without verified library support across all v1 alpha languages. AEGIS-128L on
0x03was evaluated and deferred in TSEAL-00020. - Don't replace the deterministic-nonce derivation with a random-per-envelope nonce. The fresh-per-envelope K_session is what makes deterministic safe. Hybrid is the same — every envelope gets a fresh ML-KEM Encaps and a fresh X25519 ephemeral, so
K_sessionis fresh per envelope. - Don't split
DecryptionErrorinto "bad AEAD tag" vs "bad commit tag" vs "bad KEM ciphertext" vs "AD mismatch". One error, one code path. Suite/key mismatch IS distinguishable (BadKey-class, structural). - Don't skip the synthetic-key AEAD verify on structural failures. The timing must stay flat.
- Don't change the combiner order or label for hybrid
0x06. It is fixed:SHAKE-256("pqcrypt v1 hybrid combiner" ‖ ss_mlkem ‖ x_ss ‖ ml_kem_ct ‖ x_eph_pk, 32)(frozen v1 wire tag — not a rebrandable name). Flipping the order is a polyglot KAT break, not a design choice. - Don't expose the token "Kyber" in any Python class name, Rust type, Go identifier, C macro, or doc string. Rename at the binding layer. Same rule for "Curve25519" / "ECDH" in hybrid surfaces — use
X25519andML-KEM-1024. - Don't replace
os.urandom/getrandom/crypto/rand.Read/OsRng/SecureRandom/crypto.randomByteswith a userspace PRNG, even a fast one. The IND-CCA2 claim depends on it. Same rule for the X25519 ephemeral in hybrid. - Don't add hybrid
0x06to a new language without flipping its entry intools/conformance/run.sh:hybrid_supported()AND verifying byte-identical KAT consumption against Python/Rust/Go. - Don't relax the side-channel disclaimer (docs/side-channel-disclaimer.md). v1 explicitly does not defend against power/EM/DVFS/prefetcher attacks; saying otherwise is a security claim TesseraSeal v1 cannot make.
- Don't bake a key-on-disk format into the core wire spec. That belongs in plugins (modeled on age C2SP #5). The reference format is
.tseal-key-v1.1(Argon2id + AES-256-GCM key wrap, embeds public key).
When you're done
In the PR / commit summary, name explicitly:
- Which language(s) changed.
- Whether this was a spec change (and if so, that the LaTeX + Python + C + all v1 alpha langs were updated).
- Whether the whitepaper was rebuilt (
TesseraSeal_Whitepaper.pdf). - Whether benchmark files were refreshed.
- Test command(s) you ran and that they passed.
- Whether
bash tools/conformance/run.shstayed green. - Whether any security invariant was touched — if yes, the PR is
sec(TSEAL-NNNNN)and the release-notes entry has a "Security considerations" section.
If the change is purely internal with no operator impact: state release-notes: minimal — code-only refactor, identical wire behavior.