diff --git a/ed25519-dalek/Cargo.toml b/ed25519-dalek/Cargo.toml index 0c10ed446..9975aa6e0 100644 --- a/ed25519-dalek/Cargo.toml +++ b/ed25519-dalek/Cargo.toml @@ -33,7 +33,7 @@ sha2 = { version = "0.10", default-features = false } subtle = { version = "2.3.0", default-features = false } # optional features -merlin = { version = "3", default-features = false, optional = true } +keccak = { version = "0.1", default-features = false, optional = true } rand_core = { version = "0.6.4", default-features = false, optional = true } serde = { version = "1.0", default-features = false, optional = true } zeroize = { version = "1.5", default-features = false, optional = true } @@ -50,7 +50,9 @@ criterion = { version = "0.5", features = ["html_reports"] } hex-literal = "0.4" rand = "0.8" rand_core = { version = "0.6.4", default-features = false } +rand_chacha = "0.3.1" serde = { version = "1.0", features = ["derive"] } +strobe-rs = "0.5" toml = { version = "0.7" } [[bench]] @@ -64,7 +66,7 @@ alloc = ["curve25519-dalek/alloc", "ed25519/alloc", "serde?/alloc", "zeroize/all std = ["alloc", "ed25519/std", "serde?/std", "sha2/std"] asm = ["sha2/asm"] -batch = ["alloc", "merlin", "rand_core"] +batch = ["alloc", "dep:keccak", "rand_core"] fast = ["curve25519-dalek/precomputed-tables"] digest = ["signature/digest"] # Exposes the hazmat module diff --git a/ed25519-dalek/src/batch.rs b/ed25519-dalek/src/batch.rs index fa79677d5..2c65c5c22 100644 --- a/ed25519-dalek/src/batch.rs +++ b/ed25519-dalek/src/batch.rs @@ -9,6 +9,9 @@ //! Batch signature verification. +mod strobe; +mod transcript; + use alloc::vec::Vec; use core::iter::once; @@ -21,7 +24,7 @@ use curve25519_dalek::traits::VartimeMultiscalarMul; pub use curve25519_dalek::digest::Digest; -use merlin::Transcript; +use transcript::Transcript; use rand_core::RngCore; @@ -32,6 +35,14 @@ use crate::errors::SignatureError; use crate::signature::InternalSignature; use crate::VerifyingKey; +/// Domain separation label to initialize the STROBE context. +/// +/// This is not to be confused with the crate's semver string: +/// the latter applies to the API, while this label defines the protocol. +/// E.g. it is possible that crate 2.0 will have an incompatible API, +/// but implement the same 1.0 protocol. +const MERLIN_PROTOCOL_LABEL: &[u8] = b"Merlin v1.0"; + /// An implementation of `rand_core::RngCore` which does nothing. This is necessary because merlin /// demands an `Rng` as input to `TranscriptRngBuilder::finalize()`. Using this with `finalize()` /// yields a PRG whose input is the hashed transcript. diff --git a/ed25519-dalek/src/batch/strobe.rs b/ed25519-dalek/src/batch/strobe.rs new file mode 100644 index 000000000..6aa730286 --- /dev/null +++ b/ed25519-dalek/src/batch/strobe.rs @@ -0,0 +1,239 @@ +//! Minimal implementation of (parts of) Strobe. + +use core::ops::{Deref, DerefMut}; + +#[cfg(feature = "zeroize")] +use zeroize::Zeroize; + +/// Strobe R value; security level 128 is hardcoded +const STROBE_R: u8 = 166; + +const FLAG_I: u8 = 1; +const FLAG_A: u8 = 1 << 1; +const FLAG_C: u8 = 1 << 2; +const FLAG_T: u8 = 1 << 3; +const FLAG_M: u8 = 1 << 4; +const FLAG_K: u8 = 1 << 5; + +fn transmute_state(st: &mut AlignedKeccakState) -> &mut [u64; 25] { + unsafe { &mut *(st as *mut AlignedKeccakState as *mut [u64; 25]) } +} + +/// This is a wrapper around 200-byte buffer that's always 8-byte aligned +/// to make pointers to it safely convertible to pointers to [u64; 25] +/// (since u64 words must be 8-byte aligned) +#[derive(Clone)] +#[repr(align(8))] +struct AlignedKeccakState([u8; 200]); + +#[cfg(feature = "zeroize")] +impl Drop for AlignedKeccakState { + fn drop(&mut self) { + self.0.zeroize(); + } +} + +/// A Strobe context for the 128-bit security level. +/// +/// Only `meta-AD`, `AD`, `KEY`, and `PRF` operations are supported. +#[derive(Clone)] +pub struct Strobe128 { + state: AlignedKeccakState, + pos: u8, + pos_begin: u8, + cur_flags: u8, +} + +impl ::core::fmt::Debug for Strobe128 { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + // Ensure that the Strobe state isn't accidentally logged + write!(f, "Strobe128: STATE OMITTED") + } +} + +impl Strobe128 { + pub fn new(protocol_label: &[u8]) -> Strobe128 { + let initial_state = { + let mut st = AlignedKeccakState([0u8; 200]); + st[0..6].copy_from_slice(&[1, STROBE_R + 2, 1, 0, 1, 96]); + st[6..18].copy_from_slice(b"STROBEv1.0.2"); + keccak::f1600(transmute_state(&mut st)); + + st + }; + + let mut strobe = Strobe128 { + state: initial_state, + pos: 0, + pos_begin: 0, + cur_flags: 0, + }; + + strobe.meta_ad(protocol_label, false); + + strobe + } + + pub fn meta_ad(&mut self, data: &[u8], more: bool) { + self.begin_op(FLAG_M | FLAG_A, more); + self.absorb(data); + } + + pub fn ad(&mut self, data: &[u8], more: bool) { + self.begin_op(FLAG_A, more); + self.absorb(data); + } + + pub fn prf(&mut self, data: &mut [u8], more: bool) { + self.begin_op(FLAG_I | FLAG_A | FLAG_C, more); + self.squeeze(data); + } + + pub fn key(&mut self, data: &[u8], more: bool) { + self.begin_op(FLAG_A | FLAG_C, more); + self.overwrite(data); + } +} + +impl Strobe128 { + fn run_f(&mut self) { + self.state[self.pos as usize] ^= self.pos_begin; + self.state[(self.pos + 1) as usize] ^= 0x04; + self.state[(STROBE_R + 1) as usize] ^= 0x80; + keccak::f1600(transmute_state(&mut self.state)); + self.pos = 0; + self.pos_begin = 0; + } + + fn absorb(&mut self, data: &[u8]) { + for byte in data { + self.state[self.pos as usize] ^= byte; + self.pos += 1; + if self.pos == STROBE_R { + self.run_f(); + } + } + } + + fn overwrite(&mut self, data: &[u8]) { + for byte in data { + self.state[self.pos as usize] = *byte; + self.pos += 1; + if self.pos == STROBE_R { + self.run_f(); + } + } + } + + fn squeeze(&mut self, data: &mut [u8]) { + for byte in data { + *byte = self.state[self.pos as usize]; + self.state[self.pos as usize] = 0; + self.pos += 1; + if self.pos == STROBE_R { + self.run_f(); + } + } + } + + fn begin_op(&mut self, flags: u8, more: bool) { + // Check if we're continuing an operation + if more { + assert_eq!( + self.cur_flags, flags, + "You tried to continue op {:#b} but changed flags to {:#b}", + self.cur_flags, flags, + ); + return; + } + + // Skip adjusting direction information (we just use AD, PRF) + assert_eq!( + flags & FLAG_T, + 0u8, + "You used the T flag, which this implementation doesn't support" + ); + + let old_begin = self.pos_begin; + self.pos_begin = self.pos + 1; + self.cur_flags = flags; + + self.absorb(&[old_begin, flags]); + + // Force running F if C or K is set + let force_f = 0 != (flags & (FLAG_C | FLAG_K)); + + if force_f && self.pos != 0 { + self.run_f(); + } + } +} + +impl Deref for AlignedKeccakState { + type Target = [u8; 200]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for AlignedKeccakState { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[cfg(test)] +mod tests { + use strobe_rs::{self, SecParam}; + + #[test] + fn test_conformance() { + let mut s1 = super::Strobe128::new(b"Conformance Test Protocol"); + let mut s2 = strobe_rs::Strobe::new(b"Conformance Test Protocol", SecParam::B128); + + // meta-AD(b"msg"); AD(msg) + + let msg = [99u8; 1024]; + + s1.meta_ad(b"ms", false); + s1.meta_ad(b"g", true); + s1.ad(&msg, false); + + s2.meta_ad(b"ms", false); + s2.meta_ad(b"g", true); + s2.ad(&msg, false); + + // meta-AD(b"prf"); PRF() + + let mut prf1 = [0u8; 32]; + s1.meta_ad(b"prf", false); + s1.prf(&mut prf1, false); + + let mut prf2 = [0u8; 32]; + s2.meta_ad(b"prf", false); + s2.prf(&mut prf2, false); + + assert_eq!(prf1, prf2); + + // meta-AD(b"key"); KEY(prf output) + + s1.meta_ad(b"key", false); + s1.key(&prf1, false); + + s2.meta_ad(b"key", false); + s2.key(&prf2, false); + + // meta-AD(b"prf"); PRF() + + let mut prf1 = [0u8; 32]; + s1.meta_ad(b"prf", false); + s1.prf(&mut prf1, false); + + let mut prf2 = [0u8; 32]; + s2.meta_ad(b"prf", false); + s2.prf(&mut prf2, false); + + assert_eq!(prf1, prf2); + } +} diff --git a/ed25519-dalek/src/batch/transcript.rs b/ed25519-dalek/src/batch/transcript.rs new file mode 100644 index 000000000..6af752c2d --- /dev/null +++ b/ed25519-dalek/src/batch/transcript.rs @@ -0,0 +1,380 @@ +use super::strobe::Strobe128; +use super::MERLIN_PROTOCOL_LABEL; + +fn encode_usize_as_u32(x: usize) -> [u8; 4] { + u32::try_from(x).expect("usize too large").to_le_bytes() +} + +/// A transcript of a public-coin argument. +/// +/// The prover's messages are added to the transcript using +/// [`append_message`](Transcript::append_message), and the verifier's +/// challenges can be computed using +/// [`challenge_bytes`](Transcript::challenge_bytes). +/// +/// # Creating and using a Merlin transcript +/// +/// To create a Merlin transcript, use [`Transcript::new()`]. This +/// function takes a domain separation label which should be unique to +/// the application. +/// +/// To use the transcript with a Merlin-based proof implementation, +/// the prover's side creates a Merlin transcript with an +/// application-specific domain separation label, and passes a `&mut` +/// reference to the transcript to the proving function(s). +/// +/// To verify the resulting proof, the verifier creates their own +/// Merlin transcript using the same domain separation label, then +/// passes a `&mut` reference to the verifier's transcript to the +/// verification function. +/// +/// # Implementing proofs using Merlin +/// +/// For information on the design of Merlin and how to use it to +/// implement a proof system, see the documentation at +/// [merlin.cool](https://merlin.cool), particularly the [Using +/// Merlin](https://merlin.cool/use/index.html) section. +#[derive(Clone)] +pub struct Transcript { + strobe: Strobe128, +} + +impl Transcript { + /// Initialize a new transcript with the supplied `label`, which + /// is used as a domain separator. + /// + /// # Note + /// + /// This function should be called by a proof library's API + /// consumer (i.e., the application using the proof library), and + /// **not by the proof implementation**. See the [Passing + /// Transcripts](https://merlin.cool/use/passing.html) section of + /// the Merlin website for more details on why. + pub fn new(label: &'static [u8]) -> Transcript { + let mut transcript = Transcript { + strobe: Strobe128::new(MERLIN_PROTOCOL_LABEL), + }; + transcript.append_message(b"dom-sep", label); + + transcript + } + + /// Append a prover's `message` to the transcript. + /// + /// The `label` parameter is metadata about the message, and is + /// also appended to the transcript. See the [Transcript + /// Protocols](https://merlin.cool/use/protocol.html) section of + /// the Merlin website for details on labels. + pub fn append_message(&mut self, label: &'static [u8], message: &[u8]) { + let data_len = encode_usize_as_u32(message.len()); + self.strobe.meta_ad(label, false); + self.strobe.meta_ad(&data_len, true); + self.strobe.ad(message, false); + } + + /// Fill the supplied buffer with the verifier's challenge bytes. + /// + /// The `label` parameter is metadata about the challenge, and is + /// also appended to the transcript. See the [Transcript + /// Protocols](https://merlin.cool/use/protocol.html) section of + /// the Merlin website for details on labels. + #[cfg(test)] + pub fn challenge_bytes(&mut self, label: &'static [u8], dest: &mut [u8]) { + let data_len = encode_usize_as_u32(dest.len()); + self.strobe.meta_ad(label, false); + self.strobe.meta_ad(&data_len, true); + self.strobe.prf(dest, false); + } + + /// Fork the current [`Transcript`] to construct an RNG whose output is bound + /// to the current transcript state as well as prover's secrets. + /// + /// See the [`TranscriptRngBuilder`] documentation for more details. + pub fn build_rng(&self) -> TranscriptRngBuilder { + TranscriptRngBuilder { + strobe: self.strobe.clone(), + } + } +} + +/// Constructs a [`TranscriptRng`] by rekeying the [`Transcript`] with +/// prover secrets and an external RNG. +/// +/// The prover uses a [`TranscriptRngBuilder`] to rekey with its +/// witness data, before using an external RNG to finalize to a +/// [`TranscriptRng`]. The resulting [`TranscriptRng`] will be a PRF +/// of all of the entire public transcript, the prover's secret +/// witness data, and randomness from the external RNG. +/// +/// # Note +/// +/// Protocols that require randomness in multiple places (e.g., to +/// choose blinding factors for a multi-round protocol) should create +/// a fresh [`TranscriptRng`] **each time they need randomness**, +/// rather than reusing a single instance. This ensures that the +/// randomness in each round is bound to the latest transcript state, +/// rather than just the state of the transcript when randomness was +/// first required. +/// +/// # Typed Witness Data +/// +/// Like the [`Transcript`], the [`TranscriptRngBuilder`] provides a +/// minimal, byte-oriented API, and like the [`Transcript`], this API +/// can be extended to allow rekeying with protocol-specific types +/// using an extension trait. See the [Transcript +/// Protocols](https://merlin.cool/use/protocol.html) section of the +/// Merlin website for more details. +/// +/// [rekey_with_witness_bytes]: TranscriptRngBuilder::rekey_with_witness_bytes +/// [finalize]: TranscriptRngBuilder::finalize +pub struct TranscriptRngBuilder { + strobe: Strobe128, +} + +impl TranscriptRngBuilder { + /// Rekey the transcript using the provided witness data. + /// + /// The `label` parameter is metadata about `witness`. + #[cfg(test)] + pub fn rekey_with_witness_bytes( + mut self, + label: &'static [u8], + witness: &[u8], + ) -> TranscriptRngBuilder { + let witness_len = encode_usize_as_u32(witness.len()); + self.strobe.meta_ad(label, false); + self.strobe.meta_ad(&witness_len, true); + self.strobe.key(witness, false); + + self + } + + /// Use the supplied external `rng` to rekey the transcript, so + /// that the finalized [`TranscriptRng`] is a PRF bound to + /// randomness from the external RNG, as well as all other + /// transcript data. + pub fn finalize(mut self, rng: &mut R) -> TranscriptRng + where + R: rand_core::RngCore + rand_core::CryptoRng, + { + let random_bytes = { + let mut bytes = [0u8; 32]; + rng.fill_bytes(&mut bytes); + bytes + }; + + self.strobe.meta_ad(b"rng", false); + self.strobe.key(&random_bytes, false); + + TranscriptRng { + strobe: self.strobe, + } + } +} + +/// An RNG providing synthetic randomness to the prover. +/// +/// A [`TranscriptRng`] is constructed from a [`Transcript`] using a +/// [`TranscriptRngBuilder`]; see its documentation for details on +/// how to construct one. +/// +/// The transcript RNG construction is described in the [Generating +/// Randomness](https://merlin.cool/transcript/rng.html) section of +/// the Merlin website. +pub struct TranscriptRng { + strobe: Strobe128, +} + +impl rand_core::RngCore for TranscriptRng { + fn next_u32(&mut self) -> u32 { + rand_core::impls::next_u32_via_fill(self) + } + + fn next_u64(&mut self) -> u64 { + rand_core::impls::next_u64_via_fill(self) + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + let dest_len = encode_usize_as_u32(dest.len()); + self.strobe.meta_ad(&dest_len, false); + self.strobe.prf(dest, false); + } + + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { + self.fill_bytes(dest); + Ok(()) + } +} + +impl rand_core::CryptoRng for TranscriptRng {} + +#[cfg(test)] +mod tests { + use alloc::vec::Vec; + use strobe_rs::SecParam; + use strobe_rs::Strobe; + + use super::*; + + /// Test against a full strobe implementation to ensure we match the few + /// operations we're interested in. + struct TestTranscript { + state: Strobe, + } + + impl TestTranscript { + /// Strobe init; meta-AD(label) + pub fn new(label: &[u8]) -> TestTranscript { + let mut tt = TestTranscript { + state: Strobe::new(MERLIN_PROTOCOL_LABEL, SecParam::B128), + }; + tt.append_message(b"dom-sep", label); + + tt + } + + /// Strobe op: meta-AD(label || len(message)); AD(message) + pub fn append_message(&mut self, label: &[u8], message: &[u8]) { + // metadata = label || len(message); + let mut metadata: Vec = Vec::with_capacity(label.len() + 4); + metadata.extend_from_slice(label); + metadata.extend_from_slice(&encode_usize_as_u32(message.len())); + + self.state.meta_ad(&metadata, false); + self.state.ad(&message, false); + } + + /// Strobe op: meta-AD(label || len(dest)); PRF into challenge_bytes + #[cfg(test)] + pub fn challenge_bytes(&mut self, label: &[u8], dest: &mut [u8]) { + let prf_len = dest.len(); + + // metadata = label || len(challenge_bytes); + let mut metadata: Vec = Vec::with_capacity(label.len() + 4); + metadata.extend_from_slice(label); + metadata.extend_from_slice(&encode_usize_as_u32(prf_len)); + + self.state.meta_ad(&metadata, false); + self.state.prf(dest, false); + } + } + + /// Test a simple protocol with one message and one challenge + #[test] + fn equivalence_simple() { + let mut real_transcript = Transcript::new(b"test protocol"); + let mut test_transcript = TestTranscript::new(b"test protocol"); + + real_transcript.append_message(b"some label", b"some data"); + test_transcript.append_message(b"some label", b"some data"); + + let mut real_challenge = [0u8; 32]; + let mut test_challenge = [0u8; 32]; + + real_transcript.challenge_bytes(b"challenge", &mut real_challenge); + test_transcript.challenge_bytes(b"challenge", &mut test_challenge); + + assert_eq!(real_challenge, test_challenge); + } + + /// Test a complex protocol with multiple messages and challenges, + /// with messages long enough to wrap around the sponge state, and + /// with multiple rounds of messages and challenges. + #[test] + fn equivalence_complex() { + let mut real_transcript = Transcript::new(b"test protocol"); + let mut test_transcript = TestTranscript::new(b"test protocol"); + + let data = vec![99; 1024]; + + real_transcript.append_message(b"step1", b"some data"); + test_transcript.append_message(b"step1", b"some data"); + + let mut real_challenge = [0u8; 32]; + let mut test_challenge = [0u8; 32]; + + for _ in 0..32 { + real_transcript.challenge_bytes(b"challenge", &mut real_challenge); + test_transcript.challenge_bytes(b"challenge", &mut test_challenge); + + assert_eq!(real_challenge, test_challenge); + + real_transcript.append_message(b"bigdata", &data); + test_transcript.append_message(b"bigdata", &data); + + real_transcript.append_message(b"challengedata", &real_challenge); + test_transcript.append_message(b"challengedata", &test_challenge); + } + } + + #[test] + fn transcript_rng_is_bound_to_transcript_and_witnesses() { + use curve25519_dalek::scalar::Scalar; + use rand_chacha::ChaChaRng; + use rand_core::SeedableRng; + + // Check that the TranscriptRng is bound to the transcript and + // the witnesses. This is done by producing a sequence of + // transcripts that diverge at different points and checking + // that they produce different challenges. + + let protocol_label = b"test TranscriptRng collisions"; + let commitment1 = b"commitment data 1"; + let commitment2 = b"commitment data 2"; + let witness1 = b"witness data 1"; + let witness2 = b"witness data 2"; + + let mut t1 = Transcript::new(protocol_label); + let mut t2 = Transcript::new(protocol_label); + let mut t3 = Transcript::new(protocol_label); + let mut t4 = Transcript::new(protocol_label); + + t1.append_message(b"com", commitment1); + t2.append_message(b"com", commitment2); + t3.append_message(b"com", commitment2); + t4.append_message(b"com", commitment2); + + let mut r1 = t1 + .build_rng() + .rekey_with_witness_bytes(b"witness", witness1) + .finalize(&mut ChaChaRng::from_seed([0; 32])); + + let mut r2 = t2 + .build_rng() + .rekey_with_witness_bytes(b"witness", witness1) + .finalize(&mut ChaChaRng::from_seed([0; 32])); + + let mut r3 = t3 + .build_rng() + .rekey_with_witness_bytes(b"witness", witness2) + .finalize(&mut ChaChaRng::from_seed([0; 32])); + + let mut r4 = t4 + .build_rng() + .rekey_with_witness_bytes(b"witness", witness2) + .finalize(&mut ChaChaRng::from_seed([0; 32])); + + let s1 = Scalar::random(&mut r1); + let s2 = Scalar::random(&mut r2); + let s3 = Scalar::random(&mut r3); + let s4 = Scalar::random(&mut r4); + + // Transcript t1 has different commitments than t2, t3, t4, so + // it should produce distinct challenges from all of them. + assert_ne!(s1, s2); + assert_ne!(s1, s3); + assert_ne!(s1, s4); + + // Transcript t2 has different witness variables from t3, t4, + // so it should produce distinct challenges from all of them. + assert_ne!(s2, s3); + assert_ne!(s2, s4); + + // Transcripts t3 and t4 have the same commitments and + // witnesses, so they should give different challenges only + // based on the RNG. Checking that they're equal in the + // presence of a bad RNG checks that the different challenges + // above aren't because the RNG is accidentally different. + assert_eq!(s3, s4); + } +} diff --git a/ed25519-dalek/src/lib.rs b/ed25519-dalek/src/lib.rs index 21d8737ba..e44d7535c 100644 --- a/ed25519-dalek/src/lib.rs +++ b/ed25519-dalek/src/lib.rs @@ -242,7 +242,7 @@ #![warn(future_incompatible, rust_2018_idioms)] #![deny(missing_docs)] // refuse to compile if documentation is missing #![deny(clippy::unwrap_used)] // don't allow unwrap -#![cfg_attr(not(test), forbid(unsafe_code))] +#![cfg_attr(not(any(test, feature = "batch")), forbid(unsafe_code))] #![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg, doc_cfg_hide))] #![cfg_attr(docsrs, doc(cfg_hide(docsrs)))]