Skip to main content

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