commonware_cryptography/
transcript.rs1use 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#[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#[repr(u8)]
86enum StartTag {
87 New = 0,
88 Resume = 1,
89 Fork = 2,
90 Noise = 3,
91}
92
93#[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 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 pub fn new(namespace: &[u8]) -> Self {
148 let mut out = Self::start(StartTag::New, None);
149 out.commit(namespace);
150 out
151 }
152
153 pub fn resume(summary: Summary) -> Self {
164 Self::start(StartTag::Resume, Some(summary))
165 }
166
167 pub fn commit(&mut self, data: impl Buf) -> &mut Self {
187 self.append(data);
188 self.flush();
189 self
190 }
191
192 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 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 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 pub fn summarize(&self) -> Summary {
253 self.assert_committed();
254 Summary {
255 hash: self.hasher.finalize(),
256 }
257 }
258}
259
260impl Transcript {
262 pub fn sign<S: Signer>(&self, s: &S) -> <S as Signer>::Signature {
268 s.sign(b"", self.summarize().hash.as_bytes())
271 }
272
273 pub fn verify<V: Verifier>(&self, v: &V, sig: &<V as Verifier>::Signature) -> bool {
275 v.verify(b"", self.summarize().hash.as_bytes(), sig)
278 }
279}
280
281#[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 const EMPTY: Self = Self {
348 hash: blake3::Hash::from_bytes([0u8; blake3::OUT_LEN]),
349 };
350}
351
352impl Random for Summary {
353 fn random(mut rng: impl CryptoRngCore) -> Self {
354 let mut bytes = [0u8; blake3::OUT_LEN];
355 rng.fill_bytes(&mut bytes[..]);
356 Self {
357 hash: blake3::Hash::from_bytes(bytes),
358 }
359 }
360}
361
362#[cfg(feature = "arbitrary")]
363impl arbitrary::Arbitrary<'_> for Summary {
364 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
365 let bytes: [u8; blake3::OUT_LEN] = u.arbitrary()?;
366 Ok(Self {
367 hash: blake3::Hash::from_bytes(bytes),
368 })
369 }
370}
371
372#[cfg(test)]
373mod test {
374 use super::*;
375 use commonware_codec::{DecodeExt as _, Encode};
376
377 #[test]
378 fn test_namespace_affects_summary() {
379 let s1 = Transcript::new(b"Test-A").summarize();
380 let s2 = Transcript::new(b"Test-B").summarize();
381 assert_ne!(s1, s2);
382 }
383
384 #[test]
385 fn test_namespace_doesnt_leak_into_data() {
386 let s1 = Transcript::new(b"Test-A").summarize();
387 let s2 = Transcript::new(b"Test-").commit(b"".as_slice()).summarize();
388 assert_ne!(s1, s2);
389 }
390
391 #[test]
392 fn test_commit_separates_data() {
393 let s1 = Transcript::new(b"").commit(b"AB".as_slice()).summarize();
394 let s2 = Transcript::new(b"")
395 .commit(b"A".as_slice())
396 .commit(b"B".as_slice())
397 .summarize();
398 assert_ne!(s1, s2);
399 }
400
401 #[test]
402 fn test_append_commit_works() {
403 let s1 = Transcript::new(b"")
404 .append(b"A".as_slice())
405 .commit(b"B".as_slice())
406 .summarize();
407 let s2 = Transcript::new(b"").commit(b"AB".as_slice()).summarize();
408 assert_eq!(s1, s2);
409 }
410
411 #[test]
412 fn test_fork_returns_different_result() {
413 let t1 = Transcript::new(b"");
414 let t2 = t1.fork(b"");
415 assert_ne!(t1.summarize(), t2.summarize());
416 }
417
418 #[test]
419 fn test_fork_label_matters() {
420 let t1 = Transcript::new(b"");
421 let t2 = t1.fork(b"A");
422 let t3 = t2.fork(b"B");
423 assert_ne!(t2.summarize(), t3.summarize());
424 }
425
426 #[test]
427 fn test_noise_and_summarize_are_different() {
428 let t1 = Transcript::new(b"");
429 let mut s1_bytes = [0u8; 32];
430 t1.noise(b"foo").fill_bytes(&mut s1_bytes[..]);
431 let s1 = Summary {
432 hash: blake3::Hash::from_bytes(s1_bytes),
433 };
434 let s2 = t1.summarize();
435 assert_ne!(s1, s2);
436 }
437
438 #[test]
439 fn test_noise_stream_chunking_doesnt_matter() {
440 let mut s = [0u8; 2 * BLOCK_LEN];
441 Transcript::new(b"test")
442 .noise(b"NOISE")
443 .fill_bytes(&mut s[..]);
444 for i in 0..s.len() {
446 let mut s_prime = [0u8; 2 * BLOCK_LEN];
447 let mut noise = Transcript::new(b"test").noise(b"NOISE");
448 noise.fill_bytes(&mut s_prime[..i]);
449 noise.fill_bytes(&mut s_prime[i..]);
450 assert_eq!(s, s_prime);
451 }
452 }
453
454 #[test]
455 fn test_noise_label_matters() {
456 let mut s1 = [0u8; 32];
457 let mut s2 = [0u8; 32];
458 let t1 = Transcript::new(b"test");
459 t1.noise(b"A").fill_bytes(&mut s1);
460 t1.noise(b"B").fill_bytes(&mut s2);
461 assert_ne!(s1, s2);
462 }
463
464 #[test]
465 fn test_summarize_resume_is_different_than_new() {
466 let s = Transcript::new(b"test").summarize();
467 let s1 = Transcript::new(s.hash.as_bytes()).summarize();
468 let s2 = Transcript::resume(s).summarize();
469 assert_ne!(s1, s2);
470 }
471
472 #[test]
473 fn test_summary_encode_roundtrip() {
474 let s = Transcript::new(b"test").summarize();
475 assert_eq!(&s, &Summary::decode(s.encode()).unwrap());
476 }
477
478 #[cfg(feature = "arbitrary")]
479 mod conformance {
480 use super::*;
481 use commonware_codec::conformance::CodecConformance;
482
483 commonware_conformance::conformance_tests! {
484 CodecConformance<Summary>,
485 }
486 }
487}