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.
6use crate::{Signer, Verifier};
7use blake3::BLOCK_LEN;
8use bytes::Buf;
9use commonware_codec::{varint::UInt, EncodeSize, FixedSize, Read, ReadExt, Write};
10use commonware_utils::{Array, Span};
11use core::{fmt::Display, ops::Deref};
12use rand_core::{
13    impls::{next_u32_via_fill, next_u64_via_fill},
14    CryptoRng, CryptoRngCore, RngCore,
15};
16use zeroize::ZeroizeOnDrop;
17
18/// Provides an implementation of [CryptoRngCore].
19///
20/// We intentionally don't expose this struct, to make the impl returned by
21/// [Transcript::noise] completely opaque.
22#[derive(ZeroizeOnDrop)]
23struct Rng {
24    inner: blake3::OutputReader,
25    buf: [u8; BLOCK_LEN],
26    start: usize,
27}
28
29impl Rng {
30    fn new(inner: blake3::OutputReader) -> Self {
31        Self {
32            inner,
33            buf: [0u8; BLOCK_LEN],
34            start: BLOCK_LEN,
35        }
36    }
37}
38
39impl RngCore for Rng {
40    fn next_u32(&mut self) -> u32 {
41        next_u32_via_fill(self)
42    }
43
44    fn next_u64(&mut self) -> u64 {
45        next_u64_via_fill(self)
46    }
47
48    fn fill_bytes(&mut self, dest: &mut [u8]) {
49        let dest_len = dest.len();
50        let remaining = &self.buf[self.start..];
51        if remaining.len() >= dest_len {
52            dest.copy_from_slice(&remaining[..dest_len]);
53            self.start += dest_len;
54            return;
55        }
56
57        let (start, mut dest) = dest.split_at_mut(remaining.len());
58        start.copy_from_slice(remaining);
59        self.start = BLOCK_LEN;
60
61        while dest.len() >= BLOCK_LEN {
62            let (block, rest) = dest.split_at_mut(BLOCK_LEN);
63            self.inner.fill(block);
64            dest = rest;
65        }
66
67        let dest_len = dest.len();
68        if dest_len > 0 {
69            self.inner.fill(&mut self.buf[..]);
70            dest.copy_from_slice(&self.buf[..dest_len]);
71            self.start = dest_len;
72        }
73    }
74
75    fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand::Error> {
76        self.fill_bytes(dest);
77        Ok(())
78    }
79}
80
81impl CryptoRng for Rng {}
82
83/// Ensures different [Transcript] initializations are unique.
84#[repr(u8)]
85enum StartTag {
86    New = 0,
87    Resume = 1,
88    Fork = 2,
89    Noise = 3,
90}
91
92/// Provides a convenient abstraction over hashing data and deriving randomness.
93///
94/// It automatically takes care of details like:
95/// - correctly segmenting packets of data,
96/// - domain separating different uses of tags and randomness,
97/// - making sure that secret state is zeroized as necessary.
98#[derive(ZeroizeOnDrop)]
99pub struct Transcript {
100    hasher: blake3::Hasher,
101    pending: u64,
102}
103
104impl Transcript {
105    fn start(tag: StartTag, summary: Option<Summary>) -> Self {
106        // By starting with an optional key, we basically get to hash in 32 bytes
107        // for free, since they won't affect the number of bytes we can process without
108        // a call to the compression function. So, in many cases where we want to
109        // link a new transcript to a previous history, we take an optional summary.
110        let mut hasher = match summary {
111            Some(s) => blake3::Hasher::new_keyed(s.hash.as_bytes()),
112            None => blake3::Hasher::new(),
113        };
114        hasher.update(&[tag as u8]);
115        Self { hasher, pending: 0 }
116    }
117
118    fn flush(&mut self) {
119        let mut pending_bytes = [0u8; 9];
120        let pending = UInt(self.pending);
121        pending.write(&mut &mut pending_bytes[..]);
122        self.hasher.update(&pending_bytes[..pending.encode_size()]);
123        self.pending = 0;
124    }
125
126    fn do_append(&mut self, data: &[u8]) {
127        self.hasher.update(data);
128        self.pending += data.len() as u64;
129    }
130
131    fn assert_committed(&self) {
132        assert!(self.pending == 0, "transcript had uncommitted data");
133    }
134}
135
136impl Transcript {
137    /// Create a new transcript.
138    ///
139    /// The namespace serves to disambiguate two transcripts, so that even if they record
140    /// the same information, the results will be different:
141    /// ```
142    /// # use commonware_cryptography::transcript::Transcript;
143    /// let s1 = Transcript::new(b"n1").commit(b"A".as_slice()).summarize();
144    /// let s2 = Transcript::new(b"n2").commit(b"A".as_slice()).summarize();
145    /// assert_ne!(s1, s2);
146    /// ```
147    pub fn new(namespace: &[u8]) -> Self {
148        let mut out = Self::start(StartTag::New, None);
149        out.commit(namespace);
150        out
151    }
152
153    /// Start a transcript from a summary.
154    ///
155    /// Note that this will not produce the same result as if the transcript
156    /// were never summarized to begin with.
157    /// ```
158    /// # use commonware_cryptography::transcript::Transcript;
159    /// let s1 = Transcript::new(b"test").commit(b"A".as_slice()).summarize();
160    /// let s2 = Transcript::resume(s1.clone()).summarize();
161    /// assert_ne!(s1, s2);
162    /// ```
163    pub fn resume(summary: Summary) -> Self {
164        Self::start(StartTag::Resume, Some(summary))
165    }
166
167    /// Record data in this transcript.
168    ///
169    /// Calls to record automatically separate out data:
170    /// ```
171    /// # use commonware_cryptography::transcript::Transcript;
172    /// let s1 = Transcript::new(b"test").commit(b"A".as_slice()).commit(b"B".as_slice()).summarize();
173    /// let s2 = Transcript::new(b"test").commit(b"AB".as_slice()).summarize();
174    /// assert_ne!(s1, s2);
175    /// ```
176    ///
177    /// In particular, even a call with an empty string matters:
178    /// ```
179    /// # use commonware_cryptography::transcript::Transcript;
180    /// let s1 = Transcript::new(b"test").summarize();
181    /// let s2 = Transcript::new(b"testt").commit(b"".as_slice()).summarize();
182    /// assert_ne!(s1, s2);
183    /// ```
184    ///
185    /// If you want to provide data incrementally, use [Self::append].
186    pub fn commit(&mut self, data: impl Buf) -> &mut Self {
187        self.append(data);
188        self.flush();
189        self
190    }
191
192    /// Like [Self::commit], except that subsequent calls to [Self::append] or [Self::commit] are
193    /// considered part of the same message.
194    ///
195    /// [Self::commit] needs to be called before calling any other method, besides [Self::append],
196    /// in order to avoid having uncommitted data.
197    ///
198    /// ```
199    /// # use commonware_cryptography::transcript::Transcript;
200    /// let s1 = Transcript::new(b"test").append(b"A".as_slice()).commit(b"B".as_slice()).summarize();
201    /// let s2 = Transcript::new(b"test").commit(b"AB".as_slice()).summarize();
202    /// assert_eq!(s1, s2);
203    /// ```
204    pub fn append(&mut self, mut data: impl Buf) -> &mut Self {
205        while data.has_remaining() {
206            let chunk = data.chunk();
207            self.do_append(chunk);
208            data.advance(chunk.len());
209        }
210        self
211    }
212
213    /// Create a new instance sharing the same history.
214    ///
215    /// This instance will commit to the same data, but it will produce a different
216    /// summary and noise:
217    /// ```
218    /// # use commonware_cryptography::transcript::Transcript;
219    /// let t = Transcript::new(b"test");
220    /// assert_ne!(t.summarize(), t.fork(b"A").summarize());
221    /// assert_ne!(t.fork(b"A").summarize(), t.fork(b"B").summarize());
222    /// ```
223    pub fn fork(&self, label: &'static [u8]) -> Self {
224        let mut out = Self::start(StartTag::Fork, Some(self.summarize()));
225        out.commit(label);
226        out
227    }
228
229    /// Pull out some noise from this transript.
230    ///
231    /// This noise will depend on all of the messages committed to the transcript
232    /// so far, and can be used as a secure source of randomness, for generating
233    /// keys, and other things.
234    ///
235    /// The label will also affect the noise. Changing the label will change
236    /// the stream of bytes generated.
237    pub fn noise(&self, label: &'static [u8]) -> impl CryptoRngCore {
238        let mut out = Self::start(StartTag::Noise, Some(self.summarize()));
239        out.commit(label);
240        Rng::new(out.hasher.finalize_xof())
241    }
242
243    /// Extract a compact summary from this transcript.
244    ///
245    /// This can be used to compare transcripts for equality:
246    /// ```
247    /// # use commonware_cryptography::transcript::Transcript;
248    /// let s1 = Transcript::new(b"test").commit(b"DATA".as_slice()).summarize();
249    /// let s2 = Transcript::new(b"test").commit(b"DATA".as_slice()).summarize();
250    /// assert_eq!(s1, s2);
251    /// ```
252    pub fn summarize(&self) -> Summary {
253        self.assert_committed();
254        Summary {
255            hash: self.hasher.finalize(),
256        }
257    }
258}
259
260// Utility methods which can be created using the other methods.
261impl Transcript {
262    /// Use a signer to create a signature over this transcript.
263    ///
264    /// Conceptually, this is the same as:
265    /// - signing the operations that have been performed on the transcript,
266    /// - or, equivalently, signing randomness or a summary extracted from the transcript.
267    pub fn sign<S: Signer>(&self, s: &S) -> <S as Signer>::Signature {
268        s.sign(None, self.summarize().hash.as_bytes())
269    }
270
271    /// Verify a signature produced by [Transcript::sign].
272    pub fn verify<V: Verifier>(&self, v: &V, sig: &<V as Verifier>::Signature) -> bool {
273        v.verify(None, self.summarize().hash.as_bytes(), sig)
274    }
275}
276
277/// Represents a summary of a transcript.
278///
279/// This is the primary way to compare two transcripts for equality.
280/// You can think of this as a hash over the transcript, providing a commitment
281/// to the data it recorded.
282#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
283pub struct Summary {
284    hash: blake3::Hash,
285}
286
287impl FixedSize for Summary {
288    const SIZE: usize = blake3::OUT_LEN;
289}
290
291impl Write for Summary {
292    fn write(&self, buf: &mut impl bytes::BufMut) {
293        self.hash.as_bytes().write(buf)
294    }
295}
296
297impl Read for Summary {
298    type Cfg = ();
299
300    fn read_cfg(buf: &mut impl Buf, _cfg: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
301        Ok(Self {
302            hash: blake3::Hash::from_bytes(ReadExt::read(buf)?),
303        })
304    }
305}
306
307impl AsRef<[u8]> for Summary {
308    fn as_ref(&self) -> &[u8] {
309        self.hash.as_bytes().as_slice()
310    }
311}
312
313impl Deref for Summary {
314    type Target = [u8];
315
316    fn deref(&self) -> &Self::Target {
317        self.as_ref()
318    }
319}
320
321impl PartialOrd for Summary {
322    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
323        Some(self.cmp(other))
324    }
325}
326
327impl Ord for Summary {
328    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
329        self.as_ref().cmp(other.as_ref())
330    }
331}
332
333impl Display for Summary {
334    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
335        write!(f, "{}", commonware_utils::hex(self.as_ref()))
336    }
337}
338
339impl Span for Summary {}
340impl Array for Summary {}
341
342impl crate::Digest for Summary {
343    fn random<R: CryptoRngCore>(rng: &mut R) -> Self {
344        let mut bytes = [0u8; blake3::OUT_LEN];
345        rng.fill_bytes(&mut bytes[..]);
346        Self {
347            hash: blake3::Hash::from_bytes(bytes),
348        }
349    }
350}
351
352#[cfg(test)]
353mod test {
354    use super::*;
355    use commonware_codec::{DecodeExt as _, Encode};
356
357    #[test]
358    fn test_namespace_affects_summary() {
359        let s1 = Transcript::new(b"Test-A").summarize();
360        let s2 = Transcript::new(b"Test-B").summarize();
361        assert_ne!(s1, s2);
362    }
363
364    #[test]
365    fn test_namespace_doesnt_leak_into_data() {
366        let s1 = Transcript::new(b"Test-A").summarize();
367        let s2 = Transcript::new(b"Test-").commit(b"".as_slice()).summarize();
368        assert_ne!(s1, s2);
369    }
370
371    #[test]
372    fn test_commit_separates_data() {
373        let s1 = Transcript::new(b"").commit(b"AB".as_slice()).summarize();
374        let s2 = Transcript::new(b"")
375            .commit(b"A".as_slice())
376            .commit(b"B".as_slice())
377            .summarize();
378        assert_ne!(s1, s2);
379    }
380
381    #[test]
382    fn test_append_commit_works() {
383        let s1 = Transcript::new(b"")
384            .append(b"A".as_slice())
385            .commit(b"B".as_slice())
386            .summarize();
387        let s2 = Transcript::new(b"").commit(b"AB".as_slice()).summarize();
388        assert_eq!(s1, s2);
389    }
390
391    #[test]
392    fn test_fork_returns_different_result() {
393        let t1 = Transcript::new(b"");
394        let t2 = t1.fork(b"");
395        assert_ne!(t1.summarize(), t2.summarize());
396    }
397
398    #[test]
399    fn test_fork_label_matters() {
400        let t1 = Transcript::new(b"");
401        let t2 = t1.fork(b"A");
402        let t3 = t2.fork(b"B");
403        assert_ne!(t2.summarize(), t3.summarize());
404    }
405
406    #[test]
407    fn test_noise_and_summarize_are_different() {
408        let t1 = Transcript::new(b"");
409        let mut s1_bytes = [0u8; 32];
410        t1.noise(b"foo").fill_bytes(&mut s1_bytes[..]);
411        let s1 = Summary {
412            hash: blake3::Hash::from_bytes(s1_bytes),
413        };
414        let s2 = t1.summarize();
415        assert_ne!(s1, s2);
416    }
417
418    #[test]
419    fn test_noise_stream_chunking_doesnt_matter() {
420        let mut s = [0u8; 2 * BLOCK_LEN];
421        Transcript::new(b"test")
422            .noise(b"NOISE")
423            .fill_bytes(&mut s[..]);
424        // Split up the bytes into two chunks
425        for i in 0..s.len() {
426            let mut s_prime = [0u8; 2 * BLOCK_LEN];
427            let mut noise = Transcript::new(b"test").noise(b"NOISE");
428            noise.fill_bytes(&mut s_prime[..i]);
429            noise.fill_bytes(&mut s_prime[i..]);
430            assert_eq!(s, s_prime);
431        }
432    }
433
434    #[test]
435    fn test_noise_label_matters() {
436        let mut s1 = [0u8; 32];
437        let mut s2 = [0u8; 32];
438        let t1 = Transcript::new(b"test");
439        t1.noise(b"A").fill_bytes(&mut s1);
440        t1.noise(b"B").fill_bytes(&mut s2);
441        assert_ne!(s1, s2);
442    }
443
444    #[test]
445    fn test_summarize_resume_is_different_than_new() {
446        let s = Transcript::new(b"test").summarize();
447        let s1 = Transcript::new(s.hash.as_bytes()).summarize();
448        let s2 = Transcript::resume(s).summarize();
449        assert_ne!(s1, s2);
450    }
451
452    #[test]
453    fn test_summary_encode_roundtrip() {
454        let s = Transcript::new(b"test").summarize();
455        assert_eq!(&s, &Summary::decode(s.encode()).unwrap());
456    }
457}