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
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#[repr(u8)]
93enum StartTag {
94 New = 0,
95 Resume = 1,
96 Fork = 2,
97 Noise = 3,
98}
99
100#[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 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 pub fn new(namespace: &[u8]) -> Self {
152 let mut out = Self::start(StartTag::New, None);
153 out.commit(namespace);
154 out
155 }
156
157 pub fn resume(summary: Summary) -> Self {
168 Self::start(StartTag::Resume, Some(summary))
169 }
170
171 pub fn commit(&mut self, data: impl Buf) -> &mut Self {
191 self.append(data);
192 self.flush();
193 self
194 }
195
196 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 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 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 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
268impl Transcript {
270 pub fn sign<S: Signer>(&self, s: &S) -> <S as Signer>::Signature {
276 s.sign(b"", self.summarize().hash.as_bytes())
279 }
280
281 pub fn verify<V: Verifier>(&self, v: &V, sig: &<V as Verifier>::Signature) -> bool {
283 v.verify(b"", self.summarize().hash.as_bytes(), sig)
286 }
287}
288
289#[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 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}