#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc = include_str!("../README.md")] #![no_std] use zeroize::Zeroize; use digest::{block_api::BlockSizeUser, Digest, Output, HashMarker}; #[cfg(feature = "merlin")] mod merlin; #[cfg(feature = "merlin")] pub use crate::merlin::MerlinTranscript; /// Tests for a transcript. #[cfg(any(test, feature = "tests"))] pub mod tests; /// A transcript trait valid over a variety of transcript formats. pub trait Transcript: Send + Clone { type Challenge: Send + Sync + Clone + AsRef<[u8]>; /// Create a new transcript with the specified name. fn new(name: &'static [u8]) -> Self; /// Apply a domain separator to the transcript. fn domain_separate(&mut self, label: &'static [u8]); /// Append a message to the transcript. fn append_message>(&mut self, label: &'static [u8], message: M); /// Produce a challenge. /// /// Implementors MUST update the transcript as it does so, preventing the same challenge from /// being generated multiple times. fn challenge(&mut self, label: &'static [u8]) -> Self::Challenge; /// Produce a RNG seed. /// /// Helper function for parties needing to generate random data from an agreed upon state. /// /// Implementors MAY internally call the challenge function for the needed bytes, and accordingly /// produce a transcript conflict between two transcripts, one which called challenge(label) and /// one which called rng_seed(label) at the same point. fn rng_seed(&mut self, label: &'static [u8]) -> [u8; 32]; } #[derive(Clone, Copy)] enum DigestTranscriptMember { Name, Domain, Label, Value, Challenge, Continued, Challenged, } impl DigestTranscriptMember { fn as_u8(&self) -> u8 { match self { DigestTranscriptMember::Name => 0, DigestTranscriptMember::Domain => 1, DigestTranscriptMember::Label => 2, DigestTranscriptMember::Value => 3, DigestTranscriptMember::Challenge => 4, DigestTranscriptMember::Continued => 5, DigestTranscriptMember::Challenged => 6, } } } /// A simple transcript format constructed around the specified hash algorithm. #[derive(Clone, Debug)] pub struct DigestTranscript(D); impl DigestTranscript { fn append(&mut self, kind: DigestTranscriptMember, value: &[u8]) { self.0.update([kind.as_u8()]); // Assumes messages don't exceed 16 exabytes self.0.update(u64::try_from(value.len()).unwrap().to_le_bytes()); self.0.update(value); } } impl Transcript for DigestTranscript { type Challenge = Output; fn new(name: &'static [u8]) -> Self { let mut res = DigestTranscript(D::new()); res.append(DigestTranscriptMember::Name, name); res } fn domain_separate(&mut self, label: &'static [u8]) { self.append(DigestTranscriptMember::Domain, label); } fn append_message>(&mut self, label: &'static [u8], message: M) { self.append(DigestTranscriptMember::Label, label); self.append(DigestTranscriptMember::Value, message.as_ref()); } fn challenge(&mut self, label: &'static [u8]) -> Self::Challenge { self.append(DigestTranscriptMember::Challenge, label); let mut cloned = self.0.clone(); // Explicitly fork these transcripts to prevent length extension attacks from being possible // (at least, without the additional ability to remove a byte from a finalized hash) self.0.update([DigestTranscriptMember::Continued.as_u8()]); cloned.update([DigestTranscriptMember::Challenged.as_u8()]); cloned.finalize() } fn rng_seed(&mut self, label: &'static [u8]) -> [u8; 32] { let mut seed = [0; 32]; seed.copy_from_slice(&self.challenge(label)[.. 32]); seed } } // Digest doesn't implement Zeroize // Implement Zeroize for DigestTranscript by writing twice the block size to the digest in an // attempt to overwrite the internal hash state/any leftover bytes impl Zeroize for DigestTranscript where D: BlockSizeUser, { fn zeroize(&mut self) { // Update in 4-byte chunks to reduce call quantity and enable word-level update optimizations const WORD_SIZE: usize = 4; // block_size returns the block_size in bytes // Use a ceil div in case the block size isn't evenly divisible by our word size let words = D::block_size().div_ceil(WORD_SIZE); for _ in 0 .. (2 * words) { self.0.update([255; WORD_SIZE]); } // Hopefully, the hash state is now overwritten to the point no data is recoverable // These writes may be optimized out if they're never read // Attempt to get them marked as read fn mark_read(transcript: &DigestTranscript) { // Just get a challenge from the state let mut challenge = core::hint::black_box(transcript.0.clone().finalize()); challenge.as_mut().zeroize(); } mark_read(self) } } /// The recommended transcript, guaranteed to be secure against length-extension attacks. #[cfg(feature = "recommended")] pub type RecommendedTranscript = DigestTranscript;