From 9d96265a48080d9c2e81ed7312e681a8a0de6de1 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sun, 11 Jun 2023 22:41:49 +0800 Subject: [PATCH 1/5] Replace recompute_R with a separate ComputeR This struct can be use to implement verifiers with incremental updates --- src/verifying.rs | 129 +++++++++++++++++++++++++++-------------------- 1 file changed, 74 insertions(+), 55 deletions(-) diff --git a/src/verifying.rs b/src/verifying.rs index 1d25f38..adc36ee 100644 --- a/src/verifying.rs +++ b/src/verifying.rs @@ -197,58 +197,8 @@ impl VerifyingKey { VerifyingKey { compressed, point } } - // 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::ComputeR`]). `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 @@ -264,7 +214,7 @@ impl VerifyingKey { { let signature = InternalSignature::try_from(signature)?; - let expected_R = self.recompute_R::(None, &signature, message); + let expected_R = ComputeR::::compute(self, signature, None, message); if expected_R == signature.R { Ok(()) } else { @@ -300,7 +250,8 @@ impl VerifyingKey { ); let message = prehashed_message.finalize(); - let expected_R = self.recompute_R::(Some(ctx), &signature, &message); + + let expected_R = ComputeR::::compute(self, signature, Some(ctx), &message); if expected_R == signature.R { Ok(()) @@ -426,7 +377,7 @@ impl VerifyingKey { return Err(InternalError::Verify.into()); } - let expected_R = self.recompute_R::(None, &signature, message); + let expected_R = ComputeR::::compute(self, signature, None, message); if expected_R == signature.R { Ok(()) } else { @@ -488,7 +439,7 @@ impl VerifyingKey { } let message = prehashed_message.finalize(); - let expected_R = self.recompute_R::(Some(ctx), &signature, &message); + let expected_R = ComputeR::::compute(self, signature, Some(ctx), &message); if expected_R == signature.R { Ok(()) @@ -517,6 +468,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 ComputeR { + key: VerifyingKey, + signature: InternalSignature, + h: CtxDigest, +} + +#[allow(non_snake_case)] +impl ComputeR +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. /// From a447cf69c784d411fcd5fec117c2c3e8ad9ccfb9 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sun, 11 Jun 2023 22:42:56 +0800 Subject: [PATCH 2/5] 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. --- src/hazmat.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ src/signing.rs | 31 ++++++++++++++++++++++++++++--- src/verifying.rs | 23 +++++++++++++++++++++++ 3 files changed, 94 insertions(+), 3 deletions(-) diff --git a/src/hazmat.rs b/src/hazmat.rs index 4414a84..2e94832 100644 --- a/src/hazmat.rs +++ b/src/hazmat.rs @@ -183,6 +183,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`. @@ -216,6 +240,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 { use super::*; diff --git a/src/signing.rs b/src/signing.rs index b0f0b49..313d872 100644 --- a/src/signing.rs +++ b/src/signing.rs @@ -743,6 +743,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, @@ -751,11 +752,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(); @@ -763,12 +788,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/src/verifying.rs b/src/verifying.rs index adc36ee..c2368db 100644 --- a/src/verifying.rs +++ b/src/verifying.rs @@ -222,6 +222,29 @@ impl VerifyingKey { } } + #[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 = ComputeR::::new(self, signature, None); + msg_update(&mut c.h)?; + let expected_R = c.finish(); + + if expected_R == signature.R { + Ok(()) + } else { + Err(InternalError::Verify.into()) + } + } + /// The prehashed non-batched Ed25519 verification check, rejecting non-canonical R values. /// (see [`Self::recompute_R`]). `CtxDigest` is the digest used to calculate the /// pseudorandomness needed for signing. `MsgDigest` is the digest used to hash the signed From f42d32552e2774b8073fed8e8be1f7bb302be880 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 12 Jun 2023 22:19:36 +0800 Subject: [PATCH 3/5] Tests for raw_sign_byupdate, raw_verify_byupdate --- src/hazmat.rs | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/src/hazmat.rs b/src/hazmat.rs index 2e94832..567809a 100644 --- a/src/hazmat.rs +++ b/src/hazmat.rs @@ -318,4 +318,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"); + } } From 915fb7d9035f59da051e97d9a4a5481fc4e16d93 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 17 May 2023 00:03:58 +0100 Subject: [PATCH 4/5] Add StreamVerifier --- src/signing.rs | 12 ++++++++- src/verifying.rs | 18 ++++++++++++- src/verifying/stream.rs | 58 +++++++++++++++++++++++++++++++++++++++++ tests/ed25519.rs | 39 +++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 src/verifying/stream.rs diff --git a/src/signing.rs b/src/signing.rs index 313d872..16944ed 100644 --- a/src/signing.rs +++ b/src/signing.rs @@ -41,7 +41,7 @@ use crate::{ errors::{InternalError, SignatureError}, hazmat::ExpandedSecretKey, signature::InternalSignature, - verifying::VerifyingKey, + verifying::{StreamVerifier, VerifyingKey}, Signature, }; @@ -473,6 +473,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 diff --git a/src/verifying.rs b/src/verifying.rs index c2368db..3b49370 100644 --- a/src/verifying.rs +++ b/src/verifying.rs @@ -43,6 +43,9 @@ use crate::{ signing::SigningKey, }; +mod stream; +pub use self::stream::StreamVerifier; + /// An ed25519 public key. /// /// # Note @@ -408,8 +411,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 /// diff --git a/src/verifying/stream.rs b/src/verifying/stream.rs new file mode 100644 index 0000000..76f7d3f --- /dev/null +++ b/src/verifying/stream.rs @@ -0,0 +1,58 @@ +use curve25519_dalek::{edwards::EdwardsPoint, scalar::Scalar}; +use sha2::{Digest, Sha512}; + +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() +#[derive(Debug)] +pub struct StreamVerifier { + /// Public key to verify with. + pub(crate) public_key: VerifyingKey, + + /// Candidate signature to verify against. + pub(crate) signature: InternalSignature, + + /// Hash state. + pub(crate) hasher: Sha512, +} + +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 { + let mut hasher = Sha512::new(); + hasher.update(signature.R.as_bytes()); + hasher.update(public_key.as_bytes()); + + Self { + public_key, + hasher, + signature, + } + } + + /// Digest message chunk. + pub fn update(&mut self, chunk: impl AsRef<[u8]>) { + self.hasher.update(&chunk); + } + + /// Finalize verifier and check against candidate signature. + #[allow(non_snake_case)] + pub fn finalize_and_verify(self) -> Result<(), SignatureError> { + let minus_A: EdwardsPoint = -self.public_key.point; + let k = Scalar::from_hash(self.hasher); + let R = + EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &(minus_A), &self.signature.s); + + if R.compress() == self.signature.R { + Ok(()) + } else { + Err(InternalError::Verify.into()) + } + } +} diff --git a/tests/ed25519.rs b/tests/ed25519.rs index 6632f01..60729cb 100644 --- a/tests/ed25519.rs +++ b/tests/ed25519.rs @@ -338,6 +338,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() { From f1b73030afd0fc2d4cdd0a74087f0eb0ba2e81b7 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 12 Jun 2023 21:40:21 +0800 Subject: [PATCH 5/5] Make StreamVerifier use ComputeR This allows it to use the same implementation as non-stream signature verification. --- src/verifying/stream.rs | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/src/verifying/stream.rs b/src/verifying/stream.rs index 76f7d3f..1ee278e 100644 --- a/src/verifying/stream.rs +++ b/src/verifying/stream.rs @@ -1,6 +1,7 @@ -use curve25519_dalek::{edwards::EdwardsPoint, scalar::Scalar}; -use sha2::{Digest, Sha512}; +use curve25519_dalek::edwards::CompressedEdwardsY; +use sha2::Sha512; +use crate::verifying::ComputeR; use crate::{signature::InternalSignature, InternalError, SignatureError, VerifyingKey}; /// An IUF verifier for ed25519. @@ -8,16 +9,10 @@ use crate::{signature::InternalSignature, InternalError, SignatureError, Verifyi /// Created with [`VerifyingKey::verify_stream()`] or [`SigningKey::verify_stream()`]. /// /// [`SigningKey::verify_stream()`]: super::SigningKey::verify_stream() -#[derive(Debug)] +#[allow(non_snake_case)] pub struct StreamVerifier { - /// Public key to verify with. - pub(crate) public_key: VerifyingKey, - - /// Candidate signature to verify against. - pub(crate) signature: InternalSignature, - - /// Hash state. - pub(crate) hasher: Sha512, + cr: ComputeR, + sig_R: CompressedEdwardsY, } impl StreamVerifier { @@ -25,31 +20,23 @@ impl StreamVerifier { /// /// Seeds hash state with public key and signature components. pub(crate) fn new(public_key: VerifyingKey, signature: InternalSignature) -> Self { - let mut hasher = Sha512::new(); - hasher.update(signature.R.as_bytes()); - hasher.update(public_key.as_bytes()); - Self { - public_key, - hasher, - signature, + cr: ComputeR::new(&public_key, signature, None), + sig_R: signature.R, } } /// Digest message chunk. pub fn update(&mut self, chunk: impl AsRef<[u8]>) { - self.hasher.update(&chunk); + 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 minus_A: EdwardsPoint = -self.public_key.point; - let k = Scalar::from_hash(self.hasher); - let R = - EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &(minus_A), &self.signature.s); + let expected_R = self.cr.finish(); - if R.compress() == self.signature.R { + if expected_R == self.sig_R { Ok(()) } else { Err(InternalError::Verify.into())