Skip to main content

Module sas

Module sas 

Source
Expand description

Short-Authentication-String (SAS) verification — Phase G.

Two peers OOB-compare a short derived code to confirm they each hold the matching Ed25519 keys (defense against MITM during initial contact, before fingerprint trust is established).

Protocol shape (each step is a signed RoomMessage on the room’s gossipsub topic):

  1. Initiator picks a random 16-byte tx_id + an ephemeral X25519 keypair. Sends SasInit { tx_id, ephemeral_x25519_pubkey, target_fp }.
  2. Responder generates their own ephemeral X25519 keypair, computes ECDH with the initiator’s pubkey, derives the SAS code via derive_sas_code(shared, tx_id), and replies with SasResponse { tx_id, ephemeral_x25519_pubkey }. The responder sees the code locally and shows it.
  3. The initiator computes ECDH the other direction, derives the same code, shows it.
  4. Both users compare codes OOB. Each side presses Match → broadcasts SasConfirm { tx_id, matched: true }.
  5. On receiving the other side’s matched=true, set the partner’s fingerprint as verified=true (per-room + global verified_peers).

The signatures on each envelope bind the ephemeral X25519 pubkeys to the sender’s Ed25519 identity. A MITM who substitutes their own ephemeral key into the exchange ends up with a different SAS code than the legitimate peer would compute, so the OOB comparison fails.

§SAS table — a huddle-internal scheme (NOT Matrix wire-compatible)

huddle uses its own 49-emoji subset of the Matrix MSC 2241 list under a huddle-specific HKDF info string (b"huddle-sas-v1") and maps 6-bit chunks into 0..49 by rejection sampling (see derive_emoji_indices_rejection). This does not interoperate with Matrix SAS: canonical MSC 2241 uses a 64-entry table indexed directly by each 6-bit chunk (no modulus, no rejection sampling) under its own info string, so a Matrix client would derive entirely different emoji. The derivation produces 7 emoji (42 bits / 6 = 7 chunks) and 3 four-digit decimal groups (39 bits / 13 = 3 chunks, each offset +1000 so values land in 1000..=9191); the decimal shape matches the MSC 2241 decimal SAS, but the emoji scheme is huddle↔huddle only. Since both peers run identical deterministic code, MITM detection is unaffected.

§huddle 2.0: optional post-quantum capability binding

derive_sas_code takes both peers’ ML-KEM encapsulation keys (our_mlkem_ek, their_mlkem_ek). The binding is gated on the partner’s key: when we hold a pinned ML-KEM ek for the peer (their_mlkem_ek = Some), the transcript mixes a b"huddle-sas-pqbind-v1" domain tag plus SHA-256 of both eks concatenated in byte-sorted order into the HKDF info — so the pair’s PQ capability becomes part of the out-of-band trust anchor. Sorting makes the binding symmetric: each peer sees the two keys in the opposite (our, their) roles, but sorting yields identical info on both sides, so two honest PQ-capable peers derive the same code. A relay that strips the ML-KEM pubkey from one side’s announce drives that side’s their_mlkem_ek to None (classical transcript) while the other still binds; the two SAS codes then diverge and the OOB comparison catches the silent classical downgrade. The salt (tx_id) and PRF (HKDF-SHA256) are unchanged — only the info domain tag + hash are added. When the partner has no pinned ek (their_mlkem_ek = None — group members, pre-1.3 partners, or the classical fallback) the derivation is byte-for-byte identical to the 1.x b"huddle-sas-v1" transcript regardless of our own key, so old and new peers still agree.

Structs§

SasCode
SAS code information given to both sides for OOB comparison.

Constants§

SAS_EMOJI
huddle’s 49-emoji subset of the Matrix MSC 2241 list, English labels. Indices 0-48; the derivation above rejection-samples 6-bit HKDF chunks into 0..49 (not Matrix’s direct 6-bit indexing — see the module doc).
TX_ID_LEN
Length of the transaction id used as HKDF salt. 16 bytes (128 bits) is plenty of unforgeability; sized to be base64-friendly.

Functions§

derive_sas_code
Derive the 7-emoji + 3-group-decimal SAS code from the X25519 shared secret and the agreed-upon tx_id. Both peers compute this independently and must end up with the same answer for OOB comparison to succeed.
new_session
Fresh X25519 ephemeral keypair + random tx_id. The secret stays on the initiator’s machine until the SAS finishes; the pubkey is transmitted in the signed envelope.
parse_pubkey
Decode a base64-encoded 32-byte X25519 pubkey received over the wire.