The burn decision now lives solely in the Session dispatch: chab3's
WireError (unauthentic or truncated box) proves no key knowledge and
burns nothing; any other ValueError/struct.error means a peer who
opened the box misbehaved, so the credential burns. handshake.py drops
its try/except wrappers and the _check helper; inner-box WireErrors
are reclassified to plain ValueError since they occur inside an
already valid outer box.
The packed 1216-byte hybrid public had no consumer that wanted it
whole -- everything unpacked immediately. Message layouts and the
credential record now carry peer_m | peer_x as explicit fields; _PUB
and the unpack-at-use idiom are gone. credential_id values change with
the record layout. Step comments in the exchange methods explain what
each solve/challenge proves.
Each exchange party ships a single x25519 ephemeral public (e_alice in
msg1, e_bob in msg2) and every x25519 leg is a plain DH against those
shipped keys; ML-KEM legs ship as bare ciphertexts. Encapsulation is
split into explicit ML-KEM + DH steps at the call sites, key objects
are kept as members (statics derived once from the persisted idroot,
ephemerals generated directly), and all fixed-width wire layouts are
full-message structs. Also: role-suffixed key naming throughout, dead
struct/layout cleanup.