sealed_channel/transcript.rs
1//! Handshake transcript hashing.
2//!
3//! The transcript binds the *exact wire bytes* of both handshake messages so
4//! that any tampering by a relay (e.g. stripping a capability, downgrading a
5//! version, or substituting a public key) changes the derived keys and is
6//! therefore detected.
7
8use sha2::{Digest, Sha256};
9
10/// Domain-separation tag for the transcript hash.
11const DOMAIN: &[u8] = b"sealed-channel v1 transcript";
12
13/// Computes the handshake transcript hash.
14///
15/// The hash is:
16///
17/// ```text
18/// SHA256( DOMAIN
19/// || le_u64(client_hello.len()) || client_hello
20/// || le_u64(server_challenge.len()) || server_challenge )
21/// ```
22///
23/// where `DOMAIN = b"sealed-channel v1 transcript"` and the lengths are encoded
24/// as little-endian `u64`. Length-prefixing makes the concatenation
25/// unambiguous, so *every* field inside either message — versions,
26/// capabilities, nonces, public keys — is cryptographically bound.
27///
28/// CRITICAL: this hashes the *exact bytes passed in*. Callers must pass the
29/// precise bytes that appeared on the wire and must never re-serialize, since
30/// a re-serialization could differ from what the peer actually saw.
31pub fn transcript_hash(client_hello: &[u8], server_challenge: &[u8]) -> [u8; 32] {
32 let mut hasher = Sha256::new();
33 hasher.update(DOMAIN);
34 hasher.update((client_hello.len() as u64).to_le_bytes());
35 hasher.update(client_hello);
36 hasher.update((server_challenge.len() as u64).to_le_bytes());
37 hasher.update(server_challenge);
38 hasher.finalize().into()
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44
45 #[test]
46 fn hash_is_deterministic() {
47 let a = transcript_hash(b"hello", b"challenge");
48 let b = transcript_hash(b"hello", b"challenge");
49 assert_eq!(a, b);
50 }
51
52 #[test]
53 fn length_prefix_prevents_ambiguity() {
54 // ("ab", "c") vs ("a", "bc") must differ thanks to length prefixing.
55 let a = transcript_hash(b"ab", b"c");
56 let b = transcript_hash(b"a", b"bc");
57 assert_ne!(a, b);
58 }
59
60 #[test]
61 fn any_byte_change_changes_hash() {
62 let base = transcript_hash(b"client", b"server");
63 let flipped = transcript_hash(b"clienX", b"server");
64 assert_ne!(base, flipped);
65 }
66}