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_math::algebra::Random;
11use commonware_utils::{Array, Span};
12use core::{fmt::Display, ops::Deref};
13use rand_core::{
14    impls::{next_u32_via_fill, next_u64_via_fill},
15    CryptoRng, CryptoRngCore, RngCore,
16};
17use zeroize::ZeroizeOnDrop;
18
19/// Provides an implementation of [CryptoRngCore].
20///
21/// We intentionally don't expose this struct, to make the impl returned by
22/// [Transcript::noise] completely opaque.
23#[derive(ZeroizeOnDrop)]
24struct Rng {
25    inner: blake3::OutputReader,
26    buf: [u8; BLOCK_LEN],
27    start: usize,
28}
29
30impl Rng {
31    const fn new(inner: blake3::OutputReader) -> Self {
32        Self {
33            inner,
34            buf: [0u8; BLOCK_LEN],
35            start: BLOCK_LEN,
36        }
37    }
38}
39
40impl RngCore for Rng {
41    fn next_u32(&mut self) -> u32 {
42        next_u32_via_fill(self)
43    }
44
45    fn next_u64(&mut self) -> u64 {
46        next_u64_via_fill(self)
47    }
48
49    fn fill_bytes(&mut self, dest: &mut [u8]) {
50        let dest_len = dest.len();
51        let remaining = &self.buf[self.start..];
52        if remaining.len() >= dest_len {
53            dest.copy_from_slice(&remaining[..dest_len]);
54            self.start += dest_len;
55            return;
56        }
57
58        let (start, mut dest) = dest.split_at_mut(remaining.len());
59        start.copy_from_slice(remaining);
60        self.start = BLOCK_LEN;
61
62        while dest.len() >= BLOCK_LEN {
63            let (block, rest) = dest.split_at_mut(BLOCK_LEN);
64            self.inner.fill(block);
65            dest = rest;
66        }
67
68        let dest_len = dest.len();
69        if dest_len > 0 {
70            self.inner.fill(&mut self.buf[..]);
71            dest.copy_from_slice(&self.buf[..dest_len]);
72            self.start = dest_len;
73        }
74    }
75
76    fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand::Error> {
77        self.fill_bytes(dest);
78        Ok(())
79    }
80}
81
82impl CryptoRng for Rng {}
83
84/// Ensures different [Transcript] initializations are unique.
85#[repr(u8)]
86enum StartTag {
87    New = 0,
88    Resume = 1,
89    Fork = 2,
90    Noise = 3,
91}
92
93/// Provides a convenient abstraction over hashing data and deriving randomness.
94///
95/// It automatically takes care of details like:
96/// - correctly segmenting packets of data,
97/// - domain separating different uses of tags and randomness,
98/// - making sure that secret state is zeroized as necessary.
99#[derive(ZeroizeOnDrop)]
100pub struct Transcript {
101    hasher: blake3::Hasher,
102    pending: u64,
103}
104
105impl Transcript {
106    fn start(tag: StartTag, summary: Option<Summary>) -> Self {
107        // By starting with an optional key, we basically get to hash in 32 bytes
108        // for free, since they won't affect the number of bytes we can process without
109        // a call to the compression function. So, in many cases where we want to
110        // link a new transcript to a previous history, we take an optional summary.
111        let mut hasher = summary.map_or_else(blake3::Hasher::new, |s| {
112            blake3::Hasher::new_keyed(s.hash.as_bytes())
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        // Note: We pass an empty namespace here, since the namespace may be included
269        // within the transcript summary already via `Self::new`.
270        s.sign(b"", self.summarize().hash.as_bytes())
271    }
272
273    /// Verify a signature produced by [Transcript::sign].
274    pub fn verify<V: Verifier>(&self, v: &V, sig: &<V as Verifier>::Signature) -> bool {
275        // Note: We pass an empty namespace here, since the namespace may be included
276        // within the transcript summary already via `Self::new`.
277        v.verify(b"", self.summarize().hash.as_bytes(), sig)
278    }
279}
280
281/// Represents a summary of a transcript.
282///
283/// This is the primary way to compare two transcripts for equality.
284/// You can think of this as a hash over the transcript, providing a commitment
285/// to the data it recorded.
286#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
287pub struct Summary {
288    hash: blake3::Hash,
289}
290
291impl FixedSize for Summary {
292    const SIZE: usize = blake3::OUT_LEN;
293}
294
295impl Write for Summary {
296    fn write(&self, buf: &mut impl bytes::BufMut) {
297        self.hash.as_bytes().write(buf)
298    }
299}
300
301impl Read for Summary {
302    type Cfg = ();
303
304    fn read_cfg(buf: &mut impl Buf, _cfg: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
305        Ok(Self {
306            hash: blake3::Hash::from_bytes(ReadExt::read(buf)?),
307        })
308    }
309}
310
311impl AsRef<[u8]> for Summary {
312    fn as_ref(&self) -> &[u8] {
313        self.hash.as_bytes().as_slice()
314    }
315}
316
317impl Deref for Summary {
318    type Target = [u8];
319
320    fn deref(&self) -> &Self::Target {
321        self.as_ref()
322    }
323}
324
325impl PartialOrd for Summary {
326    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
327        Some(self.cmp(other))
328    }
329}
330
331impl Ord for Summary {
332    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
333        self.as_ref().cmp(other.as_ref())
334    }
335}
336
337impl Display for Summary {
338    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
339        write!(f, "{}", commonware_utils::hex(self.as_ref()))
340    }
341}
342
343impl Span for Summary {}
344impl Array for Summary {}
345
346impl crate::Digest for Summary {}
347
348impl Random for Summary {
349    fn random(mut rng: impl CryptoRngCore) -> Self {
350        let mut bytes = [0u8; blake3::OUT_LEN];
351        rng.fill_bytes(&mut bytes[..]);
352        Self {
353            hash: blake3::Hash::from_bytes(bytes),
354        }
355    }
356}
357
358#[cfg(feature = "arbitrary")]
359impl arbitrary::Arbitrary<'_> for Summary {
360    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
361        let bytes: [u8; blake3::OUT_LEN] = u.arbitrary()?;
362        Ok(Self {
363            hash: blake3::Hash::from_bytes(bytes),
364        })
365    }
366}
367
368#[cfg(test)]
369mod test {
370    use super::*;
371    use commonware_codec::{DecodeExt as _, Encode};
372
373    #[test]
374    fn test_namespace_affects_summary() {
375        let s1 = Transcript::new(b"Test-A").summarize();
376        let s2 = Transcript::new(b"Test-B").summarize();
377        assert_ne!(s1, s2);
378    }
379
380    #[test]
381    fn test_namespace_doesnt_leak_into_data() {
382        let s1 = Transcript::new(b"Test-A").summarize();
383        let s2 = Transcript::new(b"Test-").commit(b"".as_slice()).summarize();
384        assert_ne!(s1, s2);
385    }
386
387    #[test]
388    fn test_commit_separates_data() {
389        let s1 = Transcript::new(b"").commit(b"AB".as_slice()).summarize();
390        let s2 = Transcript::new(b"")
391            .commit(b"A".as_slice())
392            .commit(b"B".as_slice())
393            .summarize();
394        assert_ne!(s1, s2);
395    }
396
397    #[test]
398    fn test_append_commit_works() {
399        let s1 = Transcript::new(b"")
400            .append(b"A".as_slice())
401            .commit(b"B".as_slice())
402            .summarize();
403        let s2 = Transcript::new(b"").commit(b"AB".as_slice()).summarize();
404        assert_eq!(s1, s2);
405    }
406
407    #[test]
408    fn test_fork_returns_different_result() {
409        let t1 = Transcript::new(b"");
410        let t2 = t1.fork(b"");
411        assert_ne!(t1.summarize(), t2.summarize());
412    }
413
414    #[test]
415    fn test_fork_label_matters() {
416        let t1 = Transcript::new(b"");
417        let t2 = t1.fork(b"A");
418        let t3 = t2.fork(b"B");
419        assert_ne!(t2.summarize(), t3.summarize());
420    }
421
422    #[test]
423    fn test_noise_and_summarize_are_different() {
424        let t1 = Transcript::new(b"");
425        let mut s1_bytes = [0u8; 32];
426        t1.noise(b"foo").fill_bytes(&mut s1_bytes[..]);
427        let s1 = Summary {
428            hash: blake3::Hash::from_bytes(s1_bytes),
429        };
430        let s2 = t1.summarize();
431        assert_ne!(s1, s2);
432    }
433
434    #[test]
435    fn test_noise_stream_chunking_doesnt_matter() {
436        let mut s = [0u8; 2 * BLOCK_LEN];
437        Transcript::new(b"test")
438            .noise(b"NOISE")
439            .fill_bytes(&mut s[..]);
440        // Split up the bytes into two chunks
441        for i in 0..s.len() {
442            let mut s_prime = [0u8; 2 * BLOCK_LEN];
443            let mut noise = Transcript::new(b"test").noise(b"NOISE");
444            noise.fill_bytes(&mut s_prime[..i]);
445            noise.fill_bytes(&mut s_prime[i..]);
446            assert_eq!(s, s_prime);
447        }
448    }
449
450    #[test]
451    fn test_noise_label_matters() {
452        let mut s1 = [0u8; 32];
453        let mut s2 = [0u8; 32];
454        let t1 = Transcript::new(b"test");
455        t1.noise(b"A").fill_bytes(&mut s1);
456        t1.noise(b"B").fill_bytes(&mut s2);
457        assert_ne!(s1, s2);
458    }
459
460    #[test]
461    fn test_summarize_resume_is_different_than_new() {
462        let s = Transcript::new(b"test").summarize();
463        let s1 = Transcript::new(s.hash.as_bytes()).summarize();
464        let s2 = Transcript::resume(s).summarize();
465        assert_ne!(s1, s2);
466    }
467
468    #[test]
469    fn test_summary_encode_roundtrip() {
470        let s = Transcript::new(b"test").summarize();
471        assert_eq!(&s, &Summary::decode(s.encode()).unwrap());
472    }
473
474    #[cfg(feature = "arbitrary")]
475    mod conformance {
476        use super::*;
477        use commonware_codec::conformance::CodecConformance;
478
479        commonware_conformance::conformance_tests! {
480            CodecConformance<Summary>,
481        }
482    }
483}