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
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 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}