diff --git a/ed25519-dalek/CHANGELOG.md b/ed25519-dalek/CHANGELOG.md index c3fc94a5..ee94ea0b 100644 --- a/ed25519-dalek/CHANGELOG.md +++ b/ed25519-dalek/CHANGELOG.md @@ -34,6 +34,7 @@ Entries are listed in reverse chronological order per undeprecated major series. * Add `pkcs` feature to support PKCS #8 (de)serialization of `SigningKey` and `VerifyingKey` * Add `fast` feature to include basepoint tables * Add tests for validation criteria +* Add `SigningKey::verify_stream()`, and `VerifyingKey::verify_stream()` * Impl `DigestSigner`/`DigestVerifier` for `SigningKey`/`VerifyingKey`, respectively * Impl `Hash` for `VerifyingKey` * Impl `Clone`, `Drop`, and `ZeroizeOnDrop` for `SigningKey` diff --git a/ed25519-dalek/src/hazmat.rs b/ed25519-dalek/src/hazmat.rs index 78496130..7b627e5b 100644 --- a/ed25519-dalek/src/hazmat.rs +++ b/ed25519-dalek/src/hazmat.rs @@ -169,6 +169,30 @@ where esk.raw_sign_prehashed::(prehashed_message, verifying_key, context) } +/// Compute an ordinary Ed25519 signature over the given message. `CtxDigest` is the digest used to +/// calculate the pseudorandomness needed for signing. According to the Ed25519 spec, `CtxDigest = +/// Sha512`. +/// +/// The `msg_update` closure provides the message content, updating a hash argument. +/// It will be called twice. +/// +/// # ⚠️ Unsafe +/// +/// Do NOT use this function unless you absolutely must. Using the wrong values in +/// `ExpandedSecretKey` can leak your signing key. See +/// [here](https://github.com/MystenLabs/ed25519-unsafe-libs) for more details on this attack. +pub fn raw_sign_byupdate( + esk: &ExpandedSecretKey, + msg_update: F, + verifying_key: &VerifyingKey, +) -> Result +where + CtxDigest: Digest, + F: Fn(&mut CtxDigest) -> Result<(), SignatureError>, +{ + esk.raw_sign_byupdate::(msg_update, verifying_key) +} + /// The ordinary non-batched Ed25519 verification check, rejecting non-canonical R /// values.`CtxDigest` is the digest used to calculate the pseudorandomness needed for signing. /// According to the Ed25519 spec, `CtxDigest = Sha512`. @@ -202,6 +226,25 @@ where vk.raw_verify_prehashed::(prehashed_message, context, signature) } +/// The ordinary non-batched Ed25519 verification check, rejecting non-canonical R +/// values.`CtxDigest` is the digest used to calculate the pseudorandomness needed for signing. +/// According to the Ed25519 spec, `CtxDigest = Sha512`. +/// Instead of passing the message directly (`sign()`), the caller +/// provides a `msg_update` closure that will be called to feed the +/// hash of the message being signed. + +pub fn raw_verify_byupdate( + vk: &VerifyingKey, + msg_update: F, + signature: &ed25519::Signature, +) -> Result<(), SignatureError> +where + CtxDigest: Digest, + F: Fn(&mut CtxDigest) -> Result<(), SignatureError>, +{ + vk.raw_verify_byupdate::(msg_update, signature) +} + #[cfg(test)] mod test { #![allow(clippy::unwrap_used)] @@ -263,4 +306,111 @@ mod test { .unwrap(); raw_verify_prehashed::(&vk, h, Some(ctx_str), &sig).unwrap(); } + + #[test] + fn sign_byupdate() { + // Generate the keypair + let mut rng = OsRng; + let esk = ExpandedSecretKey::random(&mut rng); + let vk = VerifyingKey::from(&esk); + + let msg = b"realistic"; + // signatures are deterministic so we can compare with a good one + let good_sig = raw_sign::(&esk, msg, &vk); + + let sig = raw_sign_byupdate::( + &esk, + |h| { + h.update(msg); + Ok(()) + }, + &vk, + ); + assert!(sig.unwrap() == good_sig, "sign byupdate matches"); + + let sig = raw_sign_byupdate::( + &esk, + |h| { + h.update(msg); + Err(SignatureError::new()) + }, + &vk, + ); + assert!(sig.is_err(), "sign byupdate failure propagates"); + + let sig = raw_sign_byupdate::( + &esk, + |h| { + h.update(&msg[..1]); + h.update(&msg[1..]); + Ok(()) + }, + &vk, + ); + assert!(sig.unwrap() == good_sig, "sign byupdate two part"); + } + + #[test] + fn verify_byupdate() { + // Generate the keypair + let mut rng = OsRng; + let esk = ExpandedSecretKey::random(&mut rng); + let vk = VerifyingKey::from(&esk); + + let msg = b"Torrens title"; + let sig = raw_sign::(&esk, msg, &vk); + let wrong_sig = raw_sign::(&esk, b"nope", &vk); + + let r = raw_verify_byupdate::( + &vk, + |h| { + h.update(msg); + Ok(()) + }, + &sig, + ); + assert!(r.is_ok(), "verify byupdate success"); + + let r = raw_verify_byupdate::( + &vk, + |h| { + h.update(msg); + Ok(()) + }, + &wrong_sig, + ); + assert!(r.is_err(), "verify byupdate wrong fails"); + + let r = raw_verify_byupdate::( + &vk, + |h| { + h.update(&msg[..5]); + h.update(&msg[5..]); + Ok(()) + }, + &sig, + ); + assert!(r.is_ok(), "verify byupdate two-part"); + + let r = raw_verify_byupdate::( + &vk, + |h| { + h.update(msg); + h.update(b"X"); + Ok(()) + }, + &sig, + ); + assert!(r.is_err(), "verify byupdate extra fails"); + + let r = raw_verify_byupdate::( + &vk, + |h| { + h.update(msg); + Err(SignatureError::new()) + }, + &sig, + ); + assert!(r.is_err(), "verify byupdate error propagates"); + } } diff --git a/ed25519-dalek/src/signing.rs b/ed25519-dalek/src/signing.rs index fad45f70..3fceb65f 100644 --- a/ed25519-dalek/src/signing.rs +++ b/ed25519-dalek/src/signing.rs @@ -42,7 +42,7 @@ use crate::{ errors::{InternalError, SignatureError}, hazmat::ExpandedSecretKey, signature::InternalSignature, - verifying::VerifyingKey, + verifying::{StreamVerifier, VerifyingKey}, Signature, }; @@ -481,6 +481,16 @@ impl SigningKey { self.verifying_key.verify_strict(message, signature) } + /// Constructs stream verifier with candidate `signature`. + /// + /// See [`VerifyingKey::verify_stream()`] for more details. + pub fn verify_stream( + &self, + signature: &ed25519::Signature, + ) -> Result { + self.verifying_key.verify_stream(signature) + } + /// Convert this signing key into a byte representation of a(n) (unreduced) Curve25519 scalar. /// /// This can be used for performing X25519 Diffie-Hellman using Ed25519 keys. The bytes output @@ -766,6 +776,7 @@ impl ExpandedSecretKey { /// This definition is loose in its parameters so that end-users of the `hazmat` module can /// change how the `ExpandedSecretKey` is calculated and which hash function to use. #[allow(non_snake_case)] + #[allow(clippy::unwrap_used)] #[inline(always)] pub(crate) fn raw_sign( &self, @@ -774,11 +785,35 @@ impl ExpandedSecretKey { ) -> Signature where CtxDigest: Digest, + { + // OK unwrap, update can't fail. + self.raw_sign_byupdate( + |h: &mut CtxDigest| { + h.update(message); + Ok(()) + }, + verifying_key, + ) + .unwrap() + } + + /// Sign a message provided in parts. The `msg_update` closure + /// will be called twice to hash the message parts. + #[allow(non_snake_case)] + #[inline(always)] + pub(crate) fn raw_sign_byupdate( + &self, + msg_update: F, + verifying_key: &VerifyingKey, + ) -> Result + where + CtxDigest: Digest, + F: Fn(&mut CtxDigest) -> Result<(), SignatureError>, { let mut h = CtxDigest::new(); h.update(self.hash_prefix); - h.update(message); + msg_update(&mut h)?; let r = Scalar::from_hash(h); let R: CompressedEdwardsY = EdwardsPoint::mul_base(&r).compress(); @@ -786,12 +821,12 @@ impl ExpandedSecretKey { h = CtxDigest::new(); h.update(R.as_bytes()); h.update(verifying_key.as_bytes()); - h.update(message); + msg_update(&mut h)?; let k = Scalar::from_hash(h); let s: Scalar = (k * self.scalar) + r; - InternalSignature { R, s }.into() + Ok(InternalSignature { R, s }.into()) } /// The prehashed signing function for Ed25519 (i.e., Ed25519ph). `CtxDigest` is the digest diff --git a/ed25519-dalek/src/verifying.rs b/ed25519-dalek/src/verifying.rs index 29f8a4d1..9c319b55 100644 --- a/ed25519-dalek/src/verifying.rs +++ b/ed25519-dalek/src/verifying.rs @@ -43,6 +43,9 @@ use crate::{ signing::SigningKey, }; +mod stream; +pub use self::stream::StreamVerifier; + /// An ed25519 public key. /// /// # Note @@ -187,58 +190,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 @@ -254,7 +207,30 @@ 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 { + Err(InternalError::Verify.into()) + } + } + + #[allow(non_snake_case)] + pub(crate) fn raw_verify_byupdate( + &self, + msg_update: F, + signature: &ed25519::Signature, + ) -> Result<(), SignatureError> + where + CtxDigest: Digest, + F: Fn(&mut CtxDigest) -> Result<(), SignatureError>, + { + let signature = InternalSignature::try_from(signature)?; + + let mut c = RCompute::::new(self, signature, None); + msg_update(&mut c.h)?; + let expected_R = c.finish(); + if expected_R == signature.R { Ok(()) } else { @@ -290,7 +266,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(()) @@ -416,7 +393,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 { @@ -424,8 +401,21 @@ 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. + 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 /// @@ -478,7 +468,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(()) @@ -507,6 +497,74 @@ impl VerifyingKey { } } +// Helper for verification. Computes the _expected_ R component of the signature. The +// caller compares this to the real R component. +// For prehashed 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, +{ + pub 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 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 fn update(&mut self, m: &[u8]) { + self.h.update(m) + } + + pub 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 00000000..771efe01 --- /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 c05efa3c..745211d1 100644 --- a/ed25519-dalek/tests/ed25519.rs +++ b/ed25519-dalek/tests/ed25519.rs @@ -335,6 +335,45 @@ mod integrations { ); } + #[cfg(feature = "digest")] + #[test] + fn sign_verify_digest_equivalence() { + // TestSignVerify + let keypair: SigningKey; + let good_sig: Signature; + let bad_sig: Signature; + + let good: &[u8] = "test message".as_bytes(); + let bad: &[u8] = "wrong message".as_bytes(); + + let mut csprng = OsRng {}; + + keypair = SigningKey::generate(&mut csprng); + good_sig = keypair.sign(&good); + bad_sig = 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(&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() {