Skip to main content

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}