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::{BatchVerifier, 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        self.summarize().sign(s)
277    }
278
279    /// Verify a signature produced by [Transcript::sign].
280    pub fn verify<V: Verifier>(&self, v: &V, sig: &<V as Verifier>::Signature) -> bool {
281        self.summarize().verify(v, sig)
282    }
283
284    /// Append a signature produced by [Transcript::sign] to a batch verifier.
285    pub fn add_to_batch<B: BatchVerifier>(
286        &self,
287        batch: &mut B,
288        public_key: &B::PublicKey,
289        signature: &<B::PublicKey as Verifier>::Signature,
290    ) -> bool {
291        self.summarize().add_to_batch(batch, public_key, signature)
292    }
293}
294
295impl Summary {
296    /// Use a signer to create a signature over this summary.
297    pub fn sign<S: Signer>(&self, s: &S) -> <S as Signer>::Signature {
298        // Note: We pass an empty namespace here, since the namespace may be included
299        // within the transcript summary already via `Transcript::new`.
300        s.sign(b"", self.as_ref())
301    }
302
303    /// Verify a signature produced by [Summary::sign].
304    pub fn verify<V: Verifier>(&self, v: &V, sig: &<V as Verifier>::Signature) -> bool {
305        // Note: We pass an empty namespace here, since the namespace may be included
306        // within the transcript summary already via `Transcript::new`.
307        v.verify(b"", self.as_ref(), sig)
308    }
309
310    /// Append a signature produced by [Summary::sign] to a batch verifier.
311    pub fn add_to_batch<B: BatchVerifier>(
312        &self,
313        batch: &mut B,
314        public_key: &B::PublicKey,
315        signature: &<B::PublicKey as Verifier>::Signature,
316    ) -> bool {
317        // Note: We pass an empty namespace here, since the namespace may be included
318        // within the transcript summary already via `Transcript::new`.
319        batch.add(b"", self.as_ref(), public_key, signature)
320    }
321}
322
323/// Represents a summary of a transcript.
324///
325/// This is the primary way to compare two transcripts for equality.
326/// You can think of this as a hash over the transcript, providing a commitment
327/// to the data it recorded.
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
329pub struct Summary {
330    hash: blake3::Hash,
331}
332
333impl FixedSize for Summary {
334    const SIZE: usize = blake3::OUT_LEN;
335}
336
337impl Write for Summary {
338    fn write(&self, buf: &mut impl bytes::BufMut) {
339        self.hash.as_bytes().write(buf)
340    }
341}
342
343impl Read for Summary {
344    type Cfg = ();
345
346    fn read_cfg(buf: &mut impl Buf, _cfg: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
347        Ok(Self {
348            hash: blake3::Hash::from_bytes(ReadExt::read(buf)?),
349        })
350    }
351}
352
353impl AsRef<[u8]> for Summary {
354    fn as_ref(&self) -> &[u8] {
355        self.hash.as_bytes().as_slice()
356    }
357}
358
359impl Deref for Summary {
360    type Target = [u8];
361
362    fn deref(&self) -> &Self::Target {
363        self.as_ref()
364    }
365}
366
367impl PartialOrd for Summary {
368    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
369        Some(self.cmp(other))
370    }
371}
372
373impl Ord for Summary {
374    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
375        self.as_ref().cmp(other.as_ref())
376    }
377}
378
379impl Display for Summary {
380    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
381        write!(f, "{}", commonware_utils::hex(self.as_ref()))
382    }
383}
384
385impl Span for Summary {}
386impl Array for Summary {}
387
388impl crate::Digest for Summary {
389    const EMPTY: Self = Self {
390        hash: blake3::Hash::from_bytes([0u8; blake3::OUT_LEN]),
391    };
392}
393
394impl Random for Summary {
395    fn random(mut rng: impl CryptoRngCore) -> Self {
396        let mut bytes = [0u8; blake3::OUT_LEN];
397        rng.fill_bytes(&mut bytes[..]);
398        Self {
399            hash: blake3::Hash::from_bytes(bytes),
400        }
401    }
402}
403
404#[cfg(feature = "arbitrary")]
405impl arbitrary::Arbitrary<'_> for Summary {
406    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
407        let bytes: [u8; blake3::OUT_LEN] = u.arbitrary()?;
408        Ok(Self {
409            hash: blake3::Hash::from_bytes(bytes),
410        })
411    }
412}
413
414#[cfg(test)]
415mod test {
416    use super::*;
417    use crate::ed25519;
418    use commonware_codec::{DecodeExt as _, Encode};
419    use commonware_utils::test_rng;
420
421    #[test]
422    fn test_namespace_affects_summary() {
423        let s1 = Transcript::new(b"Test-A").summarize();
424        let s2 = Transcript::new(b"Test-B").summarize();
425        assert_ne!(s1, s2);
426    }
427
428    #[test]
429    fn test_namespace_doesnt_leak_into_data() {
430        let s1 = Transcript::new(b"Test-A").summarize();
431        let s2 = Transcript::new(b"Test-").commit(b"".as_slice()).summarize();
432        assert_ne!(s1, s2);
433    }
434
435    #[test]
436    fn test_commit_separates_data() {
437        let s1 = Transcript::new(b"").commit(b"AB".as_slice()).summarize();
438        let s2 = Transcript::new(b"")
439            .commit(b"A".as_slice())
440            .commit(b"B".as_slice())
441            .summarize();
442        assert_ne!(s1, s2);
443    }
444
445    #[test]
446    fn test_append_commit_works() {
447        let s1 = Transcript::new(b"")
448            .append(b"A".as_slice())
449            .commit(b"B".as_slice())
450            .summarize();
451        let s2 = Transcript::new(b"").commit(b"AB".as_slice()).summarize();
452        assert_eq!(s1, s2);
453    }
454
455    #[test]
456    fn test_fork_returns_different_result() {
457        let t1 = Transcript::new(b"");
458        let t2 = t1.fork(b"");
459        assert_ne!(t1.summarize(), t2.summarize());
460    }
461
462    #[test]
463    fn test_fork_label_matters() {
464        let t1 = Transcript::new(b"");
465        let t2 = t1.fork(b"A");
466        let t3 = t2.fork(b"B");
467        assert_ne!(t2.summarize(), t3.summarize());
468    }
469
470    #[test]
471    fn test_noise_and_summarize_are_different() {
472        let t1 = Transcript::new(b"");
473        let mut s1_bytes = [0u8; 32];
474        t1.noise(b"foo").fill_bytes(&mut s1_bytes[..]);
475        let s1 = Summary {
476            hash: blake3::Hash::from_bytes(s1_bytes),
477        };
478        let s2 = t1.summarize();
479        assert_ne!(s1, s2);
480    }
481
482    #[test]
483    fn test_noise_stream_chunking_doesnt_matter() {
484        let mut s = [0u8; 2 * BLOCK_LEN];
485        Transcript::new(b"test")
486            .noise(b"NOISE")
487            .fill_bytes(&mut s[..]);
488        // Split up the bytes into two chunks
489        for i in 0..s.len() {
490            let mut s_prime = [0u8; 2 * BLOCK_LEN];
491            let mut noise = Transcript::new(b"test").noise(b"NOISE");
492            noise.fill_bytes(&mut s_prime[..i]);
493            noise.fill_bytes(&mut s_prime[i..]);
494            assert_eq!(s, s_prime);
495        }
496    }
497
498    #[test]
499    fn test_noise_label_matters() {
500        let mut s1 = [0u8; 32];
501        let mut s2 = [0u8; 32];
502        let t1 = Transcript::new(b"test");
503        t1.noise(b"A").fill_bytes(&mut s1);
504        t1.noise(b"B").fill_bytes(&mut s2);
505        assert_ne!(s1, s2);
506    }
507
508    #[test]
509    fn test_summarize_resume_is_different_than_new() {
510        let s = Transcript::new(b"test").summarize();
511        let s1 = Transcript::new(s.hash.as_bytes()).summarize();
512        let s2 = Transcript::resume(s).summarize();
513        assert_ne!(s1, s2);
514    }
515
516    #[test]
517    fn test_summary_encode_roundtrip() {
518        let s = Transcript::new(b"test").summarize();
519        assert_eq!(&s, &Summary::decode(s.encode()).unwrap());
520    }
521
522    #[test]
523    fn test_summary_sign_verify_matches_transcript() {
524        let sk = ed25519::PrivateKey::from_seed(7);
525        let pk = sk.public_key();
526        let mut transcript = Transcript::new(b"test");
527        transcript.commit(b"DATA".as_slice());
528        let summary = transcript.summarize();
529
530        let sig = summary.sign(&sk);
531        assert_eq!(sig, transcript.sign(&sk));
532        assert!(summary.verify(&pk, &sig));
533        assert!(transcript.verify(&pk, &sig));
534    }
535
536    #[test]
537    fn test_summary_add_to_batch_matches_transcript() {
538        let sk = ed25519::PrivateKey::from_seed(7);
539        let pk = sk.public_key();
540        let mut transcript = Transcript::new(b"test");
541        transcript.commit(b"DATA".as_slice());
542        let summary = transcript.summarize();
543        let sig = transcript.sign(&sk);
544
545        let mut summary_batch = ed25519::Batch::new();
546        assert!(summary.add_to_batch(&mut summary_batch, &pk, &sig));
547        let mut transcript_batch = ed25519::Batch::new();
548        assert!(transcript.add_to_batch(&mut transcript_batch, &pk, &sig));
549
550        assert!(summary_batch.verify(&mut test_rng()));
551        assert!(transcript_batch.verify(&mut test_rng()));
552    }
553
554    #[test]
555    fn test_missing_append() {
556        let s1 = Transcript::new(b"foo").append(b"AB".as_slice()).summarize();
557        let s2 = Transcript::new(b"foo")
558            .append(b"A".as_slice())
559            .commit(b"B".as_slice())
560            .summarize();
561        assert_eq!(s1, s2)
562    }
563
564    #[cfg(feature = "arbitrary")]
565    mod conformance {
566        use super::*;
567        use commonware_codec::conformance::CodecConformance;
568
569        commonware_conformance::conformance_tests! {
570            CodecConformance<Summary>,
571        }
572    }
573}