Split hybrid public keys into separate wire fields

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.
This commit is contained in:
LeoVasanko committed 2026-10-07 17:59:13 +00:00
1 parent 168287ca98
commit f035859fd2
5 files changed
+92 -73

No files matched your search

+13 -14
View File
@@ -81,12 +81,12 @@ def test_registration_credential(psk):
run_handshake(a, b)
blob_a, blob_b = a.credential, b.credential
assert len(blob_a) == len(blob_b) == _CREDENTIAL.size
id_a, shared_a, pub_b, sas_a, vf_a = _CREDENTIAL.unpack(blob_a)
id_b, shared_b, pub_a, sas_b, vf_b = _CREDENTIAL.unpack(blob_b)
id_a, shared_a, pub_b_m, pub_b_x, sas_a, vf_a = _CREDENTIAL.unpack(blob_a)
id_b, shared_b, pub_a_m, pub_a_x, sas_b, vf_b = _CREDENTIAL.unpack(blob_b)
assert id_a != id_b # fresh random identity roots per party
assert shared_a == shared_b # same rotated shared key
assert pub_a == _public_from_idroot(id_a) # peer pubs match peer idroots
assert pub_b == _public_from_idroot(id_b)
assert (pub_a_m, pub_a_x) == _public_from_idroot(id_a) # pubs match idroots
assert (pub_b_m, pub_b_x) == _public_from_idroot(id_b)
assert sas_a == sas_b # the stored comparison digest
assert vf_a and vf_b # confirmed by run_handshake's answers
@@ -317,7 +317,7 @@ def test_verification_not_required(psk):
assert not a.verified and not b.verified
assert est_a.sas == est_b.sas # the SAS is still exposed
assert a.state == b.state == "established"
assert _CREDENTIAL.unpack(a._store["x"])[4] is False # stored unverified
assert _CREDENTIAL.unpack(a._store["x"])[5] is False # stored unverified
def test_wrong_calls_rejected(psk):
@@ -478,7 +478,7 @@ def test_unverified_resumption_is_silent(psk):
a, b = make_sessions(psk)
run_handshake(a, b, answer=None)
cred_a, cred_b = a.credential, b.credential
assert _CREDENTIAL.unpack(cred_a)[4] is False
assert _CREDENTIAL.unpack(cred_a)[5] is False
a2, b2 = make_sessions(cred_a, cred_b)
(m1,) = a2()
@@ -492,7 +492,7 @@ def test_unverified_resumption_is_silent(psk):
a2.confirm(True) # compared out of band: flag it verified
b2.confirm(True)
assert a2.verified and b2.verified
assert _CREDENTIAL.unpack(a2._store["x"])[4] is True # flag flipped
assert _CREDENTIAL.unpack(a2._store["x"])[5] is True # flag flipped
a3, b3 = make_sessions(a2._store["x"], b2._store["x"])
run_handshake(a3, b3) # verified: silent as before
@@ -639,7 +639,7 @@ def test_flag_flip_tolerates_parallel_confirm(psk):
r1.confirm(True) # flips the slot to verified
r2.confirm(True) # slot changed under it, but same credential
assert r1.verified and r2.verified
assert _CREDENTIAL.unpack(cli["s"])[4] is True
assert _CREDENTIAL.unpack(cli["s"])[5] is True
def test_commitment_reveal_checked(psk):
@@ -695,8 +695,8 @@ def test_mitm_reused_reveal_detected(psk):
# leg beta: the MITM recommits the stolen reveal toward bob
hs = m_out._exchange
hs._reveal = r_a
hs._eph_m, hs._ephemeral, e_pub = _fresh_ephemeral()
body = _MSG1_REGISTER.pack(e_pub, kdf("commit", e_pub, r_a))
hs._eph_m, hs._ephemeral, e_m, e_x = _fresh_ephemeral()
body = _MSG1_REGISTER.pack(e_m, e_x, kdf("commit", e_m, e_x, r_a))
msg1m = bytes(encrypt(body, key=kdf("reg/msg1", psk), ad=b""))
hs._chain.advance(msg1m) # startA was bypassed: advance the chain
(m2b,) = b(msg1m)
@@ -734,9 +734,8 @@ def test_foreign_mlkem_static_rejected_at_msg4(psk):
# mlkem half, her own x25519 half): she cannot decapsulate bob's ct_se
# challenge, so bob's confirm tag does not match her root and she burns
a, b = make_sessions(psk)
real = a._exchange._static_pub
foreign = _public_from_idroot(secrets.token_bytes(32))
a._exchange._static_pub = foreign[:1184] + real[1184:]
# alice presents a static whose ML-KEM half she does not hold
a._exchange._static_m = _public_from_idroot(secrets.token_bytes(32))[0]
(m1,) = a()
(m2,) = b(m1)
(m3,) = a(m2)
@@ -770,7 +769,7 @@ def test_credential_id(psk):
run_handshake(a, b, answer=None)
blob = a.credential
# stable across the verified flag (metadata may change)
flagged = _CREDENTIAL.pack(*_CREDENTIAL.unpack(blob)[:4], True)
flagged = _CREDENTIAL.pack(*_CREDENTIAL.unpack(blob)[:5], True)
assert credential_id(blob) == credential_id(flagged)
# tied to the credential: a different record hashes differently
assert credential_id(blob) != credential_id(psk) # rotation rotates it
+1 -1
View File
@@ -186,7 +186,7 @@ async def test_verification_callback():
assert await srv.receive() == b"resumed"
assert seen == [] # no comparison asked
assert not srv.session.verified and not cli.session.verified
assert cli.session.verification.sas == _CREDENTIAL.unpack(cred_cli)[3]
assert cli.session.verification.sas == _CREDENTIAL.unpack(cred_cli)[4]
cli.session.confirm(True) # compared out of band: mark verified
srv.session.confirm(True)
assert cli.session.verified and srv.session.verified