← Back to Research

Review date: 2026-06-12; status reconciled 2026-07-31. Scope: internal/crypto/{crypto.go,hybrid.go} plus a strategic read of the ledger / proof-of-transit design. Companion to CODE_GRAPH.md.

This is a focused first-pass review of the cryptographic core and the architectural premise. It is not a full audit — see Recommended next steps.

What's solid

The ECIES construction in crypto.go is correct, which is rarer than it should be:

  • AAD is bound into the GCM tag, so ciphertext can't be transplanted onto a different message envelope.
  • Fresh ephemeral X25519 key per message, with HKDF re-derivation using the ephemeral public key as context — every message gets a unique AES-256 key.
  • Built on crypto/ecdh, so all-zero / low-order-point shared secrets are rejected by the standard library rather than hand-rolled.
  • With a unique key per message, the random 96-bit GCM nonce is belt-and-suspenders rather than a nonce-reuse landmine.

hybrid.go uses the real crypto/mlkem (Go 1.24 stdlib FIPS-203) for ML-KEM-768 + X25519 instead of a vendored Kyber — the right call.

Findings

Ordered by importance.

1. Hybrid KEM combiner is the "okay" version, not the gold standard (design)

EncapsulateHybrid/DecapsulateHybrid derive the session key as:

HKDF-SHA256( x25519_ss || mlkem_ss , info = "aftersmtp-hybrid-kem-v1" )

The KEM ciphertext / X25519 ephemeral public key is not fed into the combiner. X25519 is famously non-binding (the shared secret does not commit to the ephemeral), so the modern hybrid designs — X-Wing and draft-ietf-tls-hybrid-design — additionally hash the KEM ciphertext and the recipient public key into the KDF transcript to get IND-CCA robustness for the combiner as a whole.

It is defensible as-is because ML-KEM's Fujisaki-Okamoto transform makes mlkem_ss commit to its ciphertext. But for a protocol meant to outlive RSA, match X-Wing exactly: include the full transcript in the combiner. Cheap now, expensive to retrofit once clients exist.

Action: bind ciphertext || recipient_pubkey into the HKDF info/IKM, or adopt X-Wing's SHA3-based combiner directly.

2. Hybrid wire-format comment still contradicts the code (correctness / interop)

The header comment in hybrid.go states:

[1184 bytes ML-KEM-768 ciphertext]Total: 1216 bytes

The code is correct (hybridCTLen = 32 + 1088 = 1120); the comment is wrong. ML-KEM-768 ciphertext is 1088 bytes — 1184 is the encapsulation key size, which the comment conflates with the ciphertext.

This matters because it is a wire-format spec: anyone writing an interop client (Rust/Node/Python per the /library goal) from the comment ships a broken framer.

Status: still open as of 2026-07-31. Fix the header comment to 1088 / 1120 and assert the sizes in a unit test so doc and code cannot drift again.

3. sharedSecretCache weakens the forward-secrecy story (security)

Decrypt caches raw ECDH shared secrets in a process-wide 10k-entry LRU keyed by SHA-256(myPub || ephemeralPub). The stated reasoning is sound — distinct ephemeral ⇒ distinct key, so it only hits on literal retries/duplicates — but the security cost is under-stated:

  • The entire premise of ephemeral DH is that the secret is destroyed after use. Holding thousands of raw secrets in RAM past the message lifetime means a memory-disclosure bug (Heartbleed-class) harvests live decryption keys for many messages at once.
  • For a product positioned around "ephemeral / time-expiring email," a long-lived in-memory pool of decryption secrets is an awkward asterisk.

Action: cache the parsed peer key (the validation/parse is the cheap win to keep) but not the derived secret; or, if the secret cache stays, give it an aggressive TTL and zero the bytes on eviction. The ECDH scalar-mult it saves is only realized on duplicate ciphertexts, which the queue/dedupe layer should be handling anyway.

4. PQ path is not yet on the message path (status, not a bug)

hybrid.go exists as a primitive but Encrypt/Decrypt still use classical X25519-only ECIES. The "quantum-resistant" capability is built but not wired into the data plane. Fine as roadmap — just don't let the README imply messages are PQ-protected today. Track the cutover explicitly.

Architecture / strategic

The cryptography and the QUIC transport stand on their own merits. The part that warrants the hardest scrutiny is proof-of-transit: anchoring every message hash on-chain. Three questions it must answer before the ledger earns its place:

  1. Throughput & cost. Real email volume vs. Substrate block space and extrinsic cost. What is the per-message anchoring budget, and what is the batching strategy (Merkle-root-per-epoch rather than per-message)?
  2. Metadata privacy. A public, immutable log of (hash, timestamp, sender DID, recipient DID) is a permanent traffic-analysis goldmine even though message bodies are sealed. This is arguably a regression vs. today's email for metadata-sensitive senders. What is the unlinkability story?
  3. Key lifecycle on an append-only log. Revocation and rotation against an immutable ledger is the hard problem MLS spent years on. What happens when a DID's Ed25519/X25519 key is compromised?

DKIM + DANE + MTA-STS + ARC already deliver non-repudiation and anti-spoofing without a chain. The sharpest framing: what does the ledger buy that a DNSSEC-anchored Certificate-Transparency-style log of public keys would not, at a fraction of the cost and metadata exposure? If there's a crisp answer, this is a real protocol. If the answer is "decentralization," that premise deserves its own validation against what email operators actually want.

(The project now has an optional identity.PoWManager, but cmd/aftersmtp does not attach it to the client API. Public registration is therefore still not protected by the main running gateway.)

Recommended next steps

  • Full line-level pass over internal/protocol/legacy (the SMTP on/off-ramp — most attacker-exposed surface: parsing, STARTTLS enforcement, header injection, rewrite/alias logic).
  • Review internal/ledger extrinsic submission and the SQLite fallback for consistency/replay properties.
  • Fuzz crypto.Decrypt and DecapsulateHybrid against malformed ciphertexts.
  • Confirm QUIC server has the bounded worker pool / anti-amplification controls noted in ARCHITECTURE_REVIEW.md §1 before any exposure.