commonware_cryptography/
transcript.rs

1//! This module provides a [Transcript] abstraction.
2//!
3//! This is useful for hashing data, committing to it, and extracting secure
4//! randomness from it. The API evades common footguns when doing these things
5//! in an ad hoc way.
6
7use blake3::BLOCK_LEN;
8use bytes::Buf;
9use commonware_codec::{varint::UInt, EncodeSize, Write};
10use rand_core::{
11    impls::{next_u32_via_fill, next_u64_via_fill},
12    CryptoRng, CryptoRngCore, RngCore,
13};
14use zeroize::ZeroizeOnDrop;
15
16/// Provides an implementation of [CryptoRngCore].
17///
18/// We intentionally don't expose this struct, to make the impl returned by
19/// [Transcript::noise] completely opaque.
20#[derive(ZeroizeOnDrop)]
21struct Rng {
22    inner: blake3::OutputReader,
23    buf: [u8; BLOCK_LEN],
24    start: usize,
25}
26
27impl Rng {
28    fn new(inner: blake3::OutputReader) -> Self {
29        Self {
30            inner,
31            buf: [0u8; BLOCK_LEN],
32            start: BLOCK_LEN,
33        }
34    }
35}
36
37impl RngCore for Rng {
38    fn next_u32(&mut self) -> u32 {
39        next_u32_via_fill(self)
40    }
41
42    fn next_u64(&mut self) -> u64 {
43        next_u64_via_fill(self)
44    }
45
46    fn fill_bytes(&mut self, dest: &mut [u8]) {
47        let dest_len = dest.len();
48        let remaining = &self.buf[self.start..];
49        if remaining.len() >= dest_len {
50            dest.copy_from_slice(&remaining[..dest_len]);
51            self.start += dest_len;
52            return;
53        }
54
55        let (start, mut dest) = dest.split_at_mut(remaining.len());
56        start.copy_from_slice(remaining);
57        self.start = BLOCK_LEN;
58
59        while dest.len() >= BLOCK_LEN {
60            let (block, rest) = dest.split_at_mut(BLOCK_LEN);
61            self.inner.fill(block);
62            dest = rest;
63        }
64
65        let dest_len = dest.len();
66        if dest_len > 0 {
67            self.inner.fill(&mut self.buf[..]);
68            dest.copy_from_slice(&self.buf[..dest_len]);
69            self.start = dest_len;
70        }
71    }
72
73    fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand::Error> {
74        self.fill_bytes(dest);
75        Ok(())
76    }
77}
78
79impl CryptoRng for Rng {}
80
81/// Ensures different [Transcript] initializations are unique.
82#[repr(u8)]
83enum StartTag {
84    New = 0,
85    Resume = 1,
86    Fork = 2,
87    Noise = 3,
88}
89
90/// Provides a convenient abstraction over hashing data and deriving randomness.
91///
92/// It automatically takes care of details like:
93/// - correctly segmenting packets of data,
94/// - domain separating different uses of tags and randomness,
95/// - making sure that secret state is zeroized as necessary.
96#[derive(ZeroizeOnDrop)]
97pub struct Transcript {
98    hasher: blake3::Hasher,
99    pending: u64,
100}
101
102impl Transcript {
103    fn start(tag: StartTag, summary: Option<Summary>) -> Self {
104        // By starting with an optional key, we basically get to hash in 32 bytes
105        // for free, since they won't affect the number of bytes we can process without
106        // a call to the compression function. So, in many cases where we want to
107        // link a new transcript to a previous history, we take an optional summary.
108        let mut hasher = match summary {
109            Some(s) => blake3::Hasher::new_keyed(s.hash.as_bytes()),
110            None => blake3::Hasher::new(),
111        };
112        hasher.update(&[tag as u8]);
113        Self { hasher, pending: 0 }
114    }
115
116    fn flush(&mut self) {
117        let mut pending_bytes = [0u8; 9];
118        let pending = UInt(self.pending);
119        pending.write(&mut &mut pending_bytes[..]);
120        self.hasher.update(&pending_bytes[..pending.encode_size()]);
121        self.pending = 0;
122    }
123
124    fn do_append(&mut self, data: &[u8]) {
125        self.hasher.update(data);
126        self.pending += data.len() as u64;
127    }
128
129    fn assert_committed(&self) {
130        assert!(self.pending == 0, "transcript had uncommitted data");
131    }
132}
133
134impl Transcript {
135    /// Create a new transcript.
136    ///
137    /// The namespace serves to disambiguate two transcripts, so that even if they record
138    /// the same information, the results will be different:
139    /// ```
140    /// # use commonware_cryptography::transcript::Transcript;
141    /// let s1 = Transcript::new(b"n1").commit(b"A".as_slice()).summarize();
142    /// let s2 = Transcript::new(b"n2").commit(b"A".as_slice()).summarize();
143    /// assert_ne!(s1, s2);
144    /// ```
145    pub fn new(namespace: &[u8]) -> Self {
146        let mut out = Self::start(StartTag::New, None);
147        out.commit(namespace);
148        out
149    }
150
151    /// Start a transcript from a summary.
152    ///
153    /// Note that this will not produce the same result as if the transcript
154    /// were never summarized to begin with.
155    /// ```
156    /// # use commonware_cryptography::transcript::Transcript;
157    /// let s1 = Transcript::new(b"test").commit(b"A".as_slice()).summarize();
158    /// let s2 = Transcript::resume(s1.clone()).summarize();
159    /// assert_ne!(s1, s2);
160    /// ```
161    pub fn resume(summary: Summary) -> Self {
162        Self::start(StartTag::Resume, Some(summary))
163    }
164
165    /// Record data in this transcript.
166    ///
167    /// Calls to record automatically separate out data:
168    /// ```
169    /// # use commonware_cryptography::transcript::Transcript;
170    /// let s1 = Transcript::new(b"test").commit(b"A".as_slice()).commit(b"B".as_slice()).summarize();
171    /// let s2 = Transcript::new(b"test").commit(b"AB".as_slice()).summarize();
172    /// assert_ne!(s1, s2);
173    /// ```
174    ///
175    /// In particular, even a call with an empty string matters:
176    /// ```
177    /// # use commonware_cryptography::transcript::Transcript;
178    /// let s1 = Transcript::new(b"test").summarize();
179    /// let s2 = Transcript::new(b"testt").commit(b"".as_slice()).summarize();
180    /// assert_ne!(s1, s2);
181    /// ```
182    ///
183    /// If you want to provide data incrementally, use [Self::append].
184    pub fn commit(&mut self, data: impl Buf) -> &mut Self {
185        self.append(data);
186        self.flush();
187        self
188    }
189
190    /// Like [Self::commit], except that subsequent calls to [Self::append] or [Self::commit] are
191    /// considered part of the same message.
192    ///
193    /// [Self::commit] needs to be called before calling any other method, besides [Self::append],
194    /// in order to avoid having uncommitted data.
195    ///
196    /// ```
197    /// # use commonware_cryptography::transcript::Transcript;
198    /// let s1 = Transcript::new(b"test").append(b"A".as_slice()).commit(b"B".as_slice()).summarize();
199    /// let s2 = Transcript::new(b"test").commit(b"AB".as_slice()).summarize();
200    /// assert_eq!(s1, s2);
201    /// ```
202    pub fn append(&mut self, mut data: impl Buf) -> &mut Self {
203        while data.has_remaining() {
204            let chunk = data.chunk();
205            self.do_append(chunk);
206            data.advance(chunk.len());
207        }
208        self
209    }
210
211    /// Create a new instance sharing the same history.
212    ///
213    /// This instance will commit to the same data, but it will produce a different
214    /// summary and noise:
215    /// ```
216    /// # use commonware_cryptography::transcript::Transcript;
217    /// let t = Transcript::new(b"test");
218    /// assert_ne!(t.summarize(), t.fork(b"A").summarize());
219    /// assert_ne!(t.fork(b"A").summarize(), t.fork(b"B").summarize());
220    /// ```
221    pub fn fork(&self, label: &'static [u8]) -> Self {
222        let mut out = Self::start(StartTag::Fork, Some(self.summarize()));
223        out.commit(label);
224        out
225    }
226
227    /// Pull out some noise from this transript.
228    ///
229    /// This noise will depend on all of the messages committed to the transcript
230    /// so far, and can be used as a secure source of randomness, for generating
231    /// keys, and other things.
232    ///
233    /// The label will also affect the noise. Changing the label will change
234    /// the stream of bytes generated.
235    pub fn noise(&self, label: &'static [u8]) -> impl CryptoRngCore {
236        let mut out = Self::start(StartTag::Noise, Some(self.summarize()));
237        out.commit(label);
238        Rng::new(out.hasher.finalize_xof())
239    }
240
241    /// Extract a compact summary from this transcript.
242    ///
243    /// This can be used to compare transcripts for equality:
244    /// ```
245    /// # use commonware_cryptography::transcript::Transcript;
246    /// let s1 = Transcript::new(b"test").commit(b"DATA".as_slice()).summarize();
247    /// let s2 = Transcript::new(b"test").commit(b"DATA".as_slice()).summarize();
248    /// assert_eq!(s1, s2);
249    /// ```
250    pub fn summarize(&self) -> Summary {
251        self.assert_committed();
252        Summary {
253            hash: self.hasher.finalize(),
254        }
255    }
256}
257
258/// Represents a summary of a transcript.
259///
260/// This is the primary way to compare two transcripts for equality.
261/// You can think of this as a hash over the transcript, providing a commitment
262/// to the data it recorded.
263#[derive(Debug, Clone, PartialEq, Eq, ZeroizeOnDrop)]
264pub struct Summary {
265    hash: blake3::Hash,
266}
267
268#[cfg(test)]
269mod test {
270    use super::*;
271
272    #[test]
273    fn test_namespace_affects_summary() {
274        let s1 = Transcript::new(b"Test-A").summarize();
275        let s2 = Transcript::new(b"Test-B").summarize();
276        assert_ne!(s1, s2);
277    }
278
279    #[test]
280    fn test_namespace_doesnt_leak_into_data() {
281        let s1 = Transcript::new(b"Test-A").summarize();
282        let s2 = Transcript::new(b"Test-").commit(b"".as_slice()).summarize();
283        assert_ne!(s1, s2);
284    }
285
286    #[test]
287    fn test_commit_separates_data() {
288        let s1 = Transcript::new(b"").commit(b"AB".as_slice()).summarize();
289        let s2 = Transcript::new(b"")
290            .commit(b"A".as_slice())
291            .commit(b"B".as_slice())
292            .summarize();
293        assert_ne!(s1, s2);
294    }
295
296    #[test]
297    fn test_append_commit_works() {
298        let s1 = Transcript::new(b"")
299            .append(b"A".as_slice())
300            .commit(b"B".as_slice())
301            .summarize();
302        let s2 = Transcript::new(b"").commit(b"AB".as_slice()).summarize();
303        assert_eq!(s1, s2);
304    }
305
306    #[test]
307    fn test_fork_returns_different_result() {
308        let t1 = Transcript::new(b"");
309        let t2 = t1.fork(b"");
310        assert_ne!(t1.summarize(), t2.summarize());
311    }
312
313    #[test]
314    fn test_fork_label_matters() {
315        let t1 = Transcript::new(b"");
316        let t2 = t1.fork(b"A");
317        let t3 = t2.fork(b"B");
318        assert_ne!(t2.summarize(), t3.summarize());
319    }
320
321    #[test]
322    fn test_noise_and_summarize_are_different() {
323        let t1 = Transcript::new(b"");
324        let mut s1_bytes = [0u8; 32];
325        t1.noise(b"foo").fill_bytes(&mut s1_bytes[..]);
326        let s1 = Summary {
327            hash: blake3::Hash::from_bytes(s1_bytes),
328        };
329        let s2 = t1.summarize();
330        assert_ne!(s1, s2);
331    }
332
333    #[test]
334    fn test_noise_stream_chunking_doesnt_matter() {
335        let mut s = [0u8; 2 * BLOCK_LEN];
336        Transcript::new(b"test")
337            .noise(b"NOISE")
338            .fill_bytes(&mut s[..]);
339        // Split up the bytes into two chunks
340        for i in 0..s.len() {
341            let mut s_prime = [0u8; 2 * BLOCK_LEN];
342            let mut noise = Transcript::new(b"test").noise(b"NOISE");
343            noise.fill_bytes(&mut s_prime[..i]);
344            noise.fill_bytes(&mut s_prime[i..]);
345            dbg!(i);
346            assert_eq!(s, s_prime);
347        }
348    }
349
350    #[test]
351    fn test_noise_label_matters() {
352        let mut s1 = [0u8; 32];
353        let mut s2 = [0u8; 32];
354        let t1 = Transcript::new(b"test");
355        t1.noise(b"A").fill_bytes(&mut s1);
356        t1.noise(b"B").fill_bytes(&mut s2);
357        assert_ne!(s1, s2);
358    }
359
360    #[test]
361    fn test_summarize_resume_is_different_than_new() {
362        let s = Transcript::new(b"test").summarize();
363        let s1 = Transcript::new(s.hash.as_bytes()).summarize();
364        let s2 = Transcript::resume(s).summarize();
365        assert_ne!(s1, s2);
366    }
367}