From 6dc7a1c7c5e81bafa485580b20fa1f0e4bf0b01c Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 2 Jun 2025 23:30:57 +0100 Subject: [PATCH] Verify by digest update + StreamVerifier (#735) * Replace recompute_R with a separate RCompute This struct can be use to implement verifiers with incremental updates * Add raw_sign_byupdate and raw_verify_byupdate These allow signing/verifying a non-prehashed message but don't require the whole message to be provided at once. * Tests for raw_sign_byupdate, raw_verify_byupdate * Add StreamVerifier * Make StreamVerifier use RCompute This allows it to use the same implementation as non-stream signature verification. * Guard StreamVerifier behind hazmat feature * docs: disambiguate unsafety Co-authored-by: Tony Arcieri * chore: relax F bounds on raw_verify_byupdate * chore: remove raw_sign_byupdate and raw_verify_byupdate * chore: address clippy lints within new code * docs: fixup changelog * test: invert new chunked test * chore: revert raw_sign --------- Co-authored-by: Matt Johnston Co-authored-by: Tony Arcieri --- ed25519-dalek/CHANGELOG.md | 2 + ed25519-dalek/src/signing.rs | 13 +++ ed25519-dalek/src/verifying.rs | 155 ++++++++++++++++---------- ed25519-dalek/src/verifying/stream.rs | 45 ++++++++ ed25519-dalek/tests/ed25519.rs | 44 ++++++++ 5 files changed, 203 insertions(+), 56 deletions(-) create mode 100644 ed25519-dalek/src/verifying/stream.rs diff --git a/ed25519-dalek/CHANGELOG.md b/ed25519-dalek/CHANGELOG.md index 9d1b65e..c2e6179 100644 --- a/ed25519-dalek/CHANGELOG.md +++ b/ed25519-dalek/CHANGELOG.md @@ -8,6 +8,8 @@ Entries are listed in reverse chronological order per undeprecated major series. # Unreleased +* Add `SigningKey::verify_stream()`, and `VerifyingKey::verify_stream()` + # 2.x series ## 2.1.1 diff --git a/ed25519-dalek/src/signing.rs b/ed25519-dalek/src/signing.rs index f3c1053..144569e 100644 --- a/ed25519-dalek/src/signing.rs +++ b/ed25519-dalek/src/signing.rs @@ -39,6 +39,8 @@ use signature::DigestSigner; #[cfg(feature = "zeroize")] use zeroize::{Zeroize, ZeroizeOnDrop}; +#[cfg(feature = "hazmat")] +use crate::verifying::StreamVerifier; use crate::{ constants::{KEYPAIR_LENGTH, SECRET_KEY_LENGTH}, errors::{InternalError, SignatureError}, @@ -483,6 +485,17 @@ impl SigningKey { self.verifying_key.verify_strict(message, signature) } + /// Constructs stream verifier with candidate `signature`. + /// + /// See [`VerifyingKey::verify_stream()`] for more details. + #[cfg(feature = "hazmat")] + pub fn verify_stream( + &self, + signature: &ed25519::Signature, + ) -> Result { + self.verifying_key.verify_stream(signature) + } + /// Convert this signing key into a byte representation of an unreduced, unclamped Curve25519 /// scalar. This is NOT the same thing as `self.to_scalar().to_bytes()`, since `to_scalar()` /// performs a clamping step, which changes the value of the resulting scalar. diff --git a/ed25519-dalek/src/verifying.rs b/ed25519-dalek/src/verifying.rs index 2bb40eb..48e6c16 100644 --- a/ed25519-dalek/src/verifying.rs +++ b/ed25519-dalek/src/verifying.rs @@ -42,6 +42,11 @@ use crate::{ signing::SigningKey, }; +#[cfg(feature = "hazmat")] +mod stream; +#[cfg(feature = "hazmat")] +pub use self::stream::StreamVerifier; + /// An ed25519 public key. /// /// # Note @@ -186,58 +191,8 @@ impl VerifyingKey { self.point.is_small_order() } - // A helper function that computes `H(R || A || M)` where `H` is the 512-bit hash function - // given by `CtxDigest` (this is SHA-512 in spec-compliant Ed25519). If `context.is_some()`, - // this does the prehashed variant of the computation using its contents. - #[allow(non_snake_case)] - fn compute_challenge( - context: Option<&[u8]>, - R: &CompressedEdwardsY, - A: &CompressedEdwardsY, - M: &[u8], - ) -> Scalar - where - CtxDigest: Digest, - { - let mut h = CtxDigest::new(); - if let Some(c) = context { - h.update(b"SigEd25519 no Ed25519 collisions"); - h.update([1]); // Ed25519ph - h.update([c.len() as u8]); - h.update(c); - } - h.update(R.as_bytes()); - h.update(A.as_bytes()); - h.update(M); - - Scalar::from_hash(h) - } - - // Helper function for verification. Computes the _expected_ R component of the signature. The - // caller compares this to the real R component. If `context.is_some()`, this does the - // prehashed variant of the computation using its contents. - // Note that this returns the compressed form of R and the caller does a byte comparison. This - // means that all our verification functions do not accept non-canonically encoded R values. - // See the validation criteria blog post for more details: - // https://hdevalence.ca/blog/2020-10-04-its-25519am - #[allow(non_snake_case)] - fn recompute_R( - &self, - context: Option<&[u8]>, - signature: &InternalSignature, - M: &[u8], - ) -> CompressedEdwardsY - where - CtxDigest: Digest, - { - let k = Self::compute_challenge::(context, &signature.R, &self.compressed, M); - let minus_A: EdwardsPoint = -self.point; - // Recall the (non-batched) verification equation: -[k]A + [s]B = R - EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &(minus_A), &signature.s).compress() - } - /// The ordinary non-batched Ed25519 verification check, rejecting non-canonical R values. (see - /// [`Self::recompute_R`]). `CtxDigest` is the digest used to calculate the pseudorandomness + /// [`Self::RCompute`]). `CtxDigest` is the digest used to calculate the pseudorandomness /// needed for signing. According to the spec, `CtxDigest = Sha512`. /// /// This definition is loose in its parameters so that end-users of the `hazmat` module can @@ -253,7 +208,7 @@ impl VerifyingKey { { let signature = InternalSignature::try_from(signature)?; - let expected_R = self.recompute_R::(None, &signature, message); + let expected_R = RCompute::::compute(self, signature, None, message); if expected_R == signature.R { Ok(()) } else { @@ -289,7 +244,8 @@ impl VerifyingKey { ); let message = prehashed_message.finalize(); - let expected_R = self.recompute_R::(Some(ctx), &signature, &message); + + let expected_R = RCompute::::compute(self, signature, Some(ctx), &message); if expected_R == signature.R { Ok(()) @@ -415,7 +371,7 @@ impl VerifyingKey { return Err(InternalError::Verify.into()); } - let expected_R = self.recompute_R::(None, &signature, message); + let expected_R = RCompute::::compute(self, signature, None, message); if expected_R == signature.R { Ok(()) } else { @@ -423,8 +379,22 @@ impl VerifyingKey { } } + /// Constructs stream verifier with candidate `signature`. + /// + /// Useful for cases where the whole message is not available all at once, allowing the + /// internal signature state to be updated incrementally and verified at the end. In some cases, + /// this will reduce the need for additional allocations. + #[cfg(feature = "hazmat")] + pub fn verify_stream( + &self, + signature: &ed25519::Signature, + ) -> Result { + let signature = InternalSignature::try_from(signature)?; + Ok(StreamVerifier::new(*self, signature)) + } + /// Verify a `signature` on a `prehashed_message` using the Ed25519ph algorithm, - /// using strict signture checking as defined by [`Self::verify_strict`]. + /// using strict signature checking as defined by [`Self::verify_strict`]. /// /// # Inputs /// @@ -477,7 +447,7 @@ impl VerifyingKey { } let message = prehashed_message.finalize(); - let expected_R = self.recompute_R::(Some(ctx), &signature, &message); + let expected_R = RCompute::::compute(self, signature, Some(ctx), &message); if expected_R == signature.R { Ok(()) @@ -511,6 +481,79 @@ impl VerifyingKey { } } +/// Helper for verification. Computes the _expected_ R component of the signature. The +/// caller compares this to the real R component. +/// This computes `H(R || A || M)` where `H` is the 512-bit hash function +/// given by `CtxDigest` (this is SHA-512 in spec-compliant Ed25519). +/// +/// For pre-hashed variants a `h` with the context already included can be provided. +/// Note that this returns the compressed form of R and the caller does a byte comparison. This +/// means that all our verification functions do not accept non-canonically encoded R values. +/// See the validation criteria blog post for more details: +/// https://hdevalence.ca/blog/2020-10-04-its-25519am +pub(crate) struct RCompute { + key: VerifyingKey, + signature: InternalSignature, + h: CtxDigest, +} + +#[allow(non_snake_case)] +impl RCompute +where + CtxDigest: Digest, +{ + /// If `prehash_ctx.is_some()`, this does the prehashed variant of the computation using its + /// contents. + pub(crate) fn compute( + key: &VerifyingKey, + signature: InternalSignature, + prehash_ctx: Option<&[u8]>, + message: &[u8], + ) -> CompressedEdwardsY { + let mut c = Self::new(key, signature, prehash_ctx); + c.update(message); + c.finish() + } + + pub(crate) fn new( + key: &VerifyingKey, + signature: InternalSignature, + prehash_ctx: Option<&[u8]>, + ) -> Self { + let R = &signature.R; + let A = &key.compressed; + + let mut h = CtxDigest::new(); + if let Some(c) = prehash_ctx { + h.update(b"SigEd25519 no Ed25519 collisions"); + h.update([1]); // Ed25519ph + h.update([c.len() as u8]); + h.update(c); + } + + h.update(R.as_bytes()); + h.update(A.as_bytes()); + Self { + key: *key, + signature, + h, + } + } + + pub(crate) fn update(&mut self, m: &[u8]) { + self.h.update(m) + } + + pub(crate) fn finish(self) -> CompressedEdwardsY { + let k = Scalar::from_hash(self.h); + + let minus_A: EdwardsPoint = -self.key.point; + // Recall the (non-batched) verification equation: -[k]A + [s]B = R + EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &(minus_A), &self.signature.s) + .compress() + } +} + impl Verifier for VerifyingKey { /// Verify a signature on a message with this keypair's public key. /// diff --git a/ed25519-dalek/src/verifying/stream.rs b/ed25519-dalek/src/verifying/stream.rs new file mode 100644 index 0000000..771efe0 --- /dev/null +++ b/ed25519-dalek/src/verifying/stream.rs @@ -0,0 +1,45 @@ +use curve25519_dalek::edwards::CompressedEdwardsY; +use sha2::Sha512; + +use crate::verifying::RCompute; +use crate::{signature::InternalSignature, InternalError, SignatureError, VerifyingKey}; + +/// An IUF verifier for ed25519. +/// +/// Created with [`VerifyingKey::verify_stream()`] or [`SigningKey::verify_stream()`]. +/// +/// [`SigningKey::verify_stream()`]: super::SigningKey::verify_stream() +#[allow(non_snake_case)] +pub struct StreamVerifier { + cr: RCompute, + sig_R: CompressedEdwardsY, +} + +impl StreamVerifier { + /// Constructs new stream verifier. + /// + /// Seeds hash state with public key and signature components. + pub(crate) fn new(public_key: VerifyingKey, signature: InternalSignature) -> Self { + Self { + cr: RCompute::new(&public_key, signature, None), + sig_R: signature.R, + } + } + + /// Digest message chunk. + pub fn update(&mut self, chunk: impl AsRef<[u8]>) { + self.cr.update(chunk.as_ref()); + } + + /// Finalize verifier and check against candidate signature. + #[allow(non_snake_case)] + pub fn finalize_and_verify(self) -> Result<(), SignatureError> { + let expected_R = self.cr.finish(); + + if expected_R == self.sig_R { + Ok(()) + } else { + Err(InternalError::Verify.into()) + } + } +} diff --git a/ed25519-dalek/tests/ed25519.rs b/ed25519-dalek/tests/ed25519.rs index edab8a8..dd49c6e 100644 --- a/ed25519-dalek/tests/ed25519.rs +++ b/ed25519-dalek/tests/ed25519.rs @@ -334,6 +334,50 @@ mod integrations { ); } + #[cfg(feature = "digest")] + #[test] + fn sign_verify_digest_equivalence() { + // TestSignVerify + + let mut csprng = OsRng {}; + + let good: &[u8] = "test message".as_bytes(); + let bad: &[u8] = "wrong message".as_bytes(); + + let keypair: SigningKey = SigningKey::generate(&mut csprng); + let good_sig: Signature = keypair.sign(good); + let bad_sig: Signature = keypair.sign(bad); + + let mut verifier = keypair.verify_stream(&good_sig).unwrap(); + verifier.update(good); + assert!( + verifier.finalize_and_verify().is_ok(), + "Verification of a valid signature failed!" + ); + + let mut verifier = keypair.verify_stream(&bad_sig).unwrap(); + verifier.update(good); + assert!( + verifier.finalize_and_verify().is_err(), + "Verification of a signature on a different message passed!" + ); + + let mut verifier = keypair.verify_stream(&good_sig).unwrap(); + verifier.update("test "); + verifier.update("message"); + assert!( + verifier.finalize_and_verify().is_ok(), + "Verification of a valid signature failed!" + ); + + let mut verifier = keypair.verify_stream(&good_sig).unwrap(); + verifier.update(bad); + assert!( + verifier.finalize_and_verify().is_err(), + "Verification of a signature on a different message passed!" + ); + } + #[cfg(feature = "digest")] #[test] fn ed25519ph_sign_verify() {