From 4f218d8e6794c429a1acd4faadf12b9168370afe Mon Sep 17 00:00:00 2001 From: andrew lyon Date: Sat, 7 Jan 2023 08:21:54 -0800 Subject: [PATCH] Adding verify_prehashed_strict() (#212) Combines `verify_prehashed` and `verify_strict` to allow strict verification with prehashed values. --- src/verifying.rs | 127 +++++++++++++++++++------- tests/ed25519.rs | 225 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 268 insertions(+), 84 deletions(-) diff --git a/src/verifying.rs b/src/verifying.rs index bb58aa924..b95543676 100644 --- a/src/verifying.rs +++ b/src/verifying.rs @@ -141,6 +141,28 @@ impl VerifyingKey { VerifyingKey(compressed, point) } + // A helper function that computes H(R || A || M) as well as its prehashed version + #[allow(non_snake_case)] + fn compute_challenge( + context: Option<&[u8]>, + R: &CompressedEdwardsY, + A: &CompressedEdwardsY, + M: &[u8], + ) -> Scalar { + let mut h = Sha512::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) + } + /// Verify a `signature` on a `prehashed_message` using the Ed25519ph algorithm. /// /// # Inputs @@ -171,8 +193,6 @@ impl VerifyingKey { { let signature = InternalSignature::try_from(signature)?; - let mut h: Sha512 = Sha512::default(); - let ctx: &[u8] = context.unwrap_or(b""); debug_assert!( ctx.len() <= 255, @@ -180,16 +200,12 @@ impl VerifyingKey { ); let minus_A: EdwardsPoint = -self.1; - - h.update(b"SigEd25519 no Ed25519 collisions"); - h.update([1]); // Ed25519ph - h.update([ctx.len() as u8]); - h.update(ctx); - h.update(signature.R.as_bytes()); - h.update(self.as_bytes()); - h.update(prehashed_message.finalize().as_slice()); - - let k = Scalar::from_hash(h); + let k = Self::compute_challenge( + Some(ctx), + &signature.R, + &self.0, + prehashed_message.finalize().as_slice(), + ); let R: EdwardsPoint = EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &(minus_A), &signature.s); @@ -270,24 +286,18 @@ impl VerifyingKey { ) -> Result<(), SignatureError> { let signature = InternalSignature::try_from(signature)?; - let mut h: Sha512 = Sha512::new(); - let minus_A: EdwardsPoint = -self.1; - - let signature_R: EdwardsPoint = match signature.R.decompress() { - None => return Err(InternalError::Verify.into()), - Some(x) => x, - }; + let signature_R = signature + .R + .decompress() + .ok_or_else(|| SignatureError::from(InternalError::Verify))?; // Logical OR is fine here as we're not trying to be constant time. if signature_R.is_small_order() || self.1.is_small_order() { return Err(InternalError::Verify.into()); } - h.update(signature.R.as_bytes()); - h.update(self.as_bytes()); - h.update(message); - - let k = Scalar::from_hash(h); + let minus_A: EdwardsPoint = -self.1; + let k = Self::compute_challenge(None, &signature.R, &self.0, message); let R: EdwardsPoint = EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &(minus_A), &signature.s); @@ -297,6 +307,67 @@ impl VerifyingKey { Err(InternalError::Verify.into()) } } + + /// Verify a `signature` on a `prehashed_message` using the Ed25519ph algorithm, + /// using strict signture checking as defined by [`Self::verify_strict`]. + /// + /// # Inputs + /// + /// * `prehashed_message` is an instantiated hash digest with 512-bits of + /// output which has had the message to be signed previously fed into its + /// state. + /// * `context` is an optional context string, up to 255 bytes inclusive, + /// which may be used to provide additional domain separation. If not + /// set, this will default to an empty string. + /// * `signature` is a purported Ed25519ph [`Signature`] on the `prehashed_message`. + /// + /// # Returns + /// + /// Returns `true` if the `signature` was a valid signature created by this + /// `Keypair` on the `prehashed_message`. + #[allow(non_snake_case)] + pub fn verify_prehashed_strict( + &self, + prehashed_message: D, + context: Option<&[u8]>, + signature: &ed25519::Signature, + ) -> Result<(), SignatureError> + where + D: Digest, + { + let signature = InternalSignature::try_from(signature)?; + + let ctx: &[u8] = context.unwrap_or(b""); + debug_assert!( + ctx.len() <= 255, + "The context must not be longer than 255 octets." + ); + + let signature_R = signature + .R + .decompress() + .ok_or_else(|| SignatureError::from(InternalError::Verify))?; + + // Logical OR is fine here as we're not trying to be constant time. + if signature_R.is_small_order() || self.1.is_small_order() { + return Err(InternalError::Verify.into()); + } + + let minus_A: EdwardsPoint = -self.1; + let k = Self::compute_challenge( + Some(ctx), + &signature.R, + &self.0, + prehashed_message.finalize().as_slice(), + ); + let R = EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &(minus_A), &signature.s); + + if R == signature_R { + Ok(()) + } else { + Err(InternalError::Verify.into()) + } + } } impl Verifier for VerifyingKey { @@ -309,14 +380,8 @@ impl Verifier for VerifyingKey { fn verify(&self, message: &[u8], signature: &ed25519::Signature) -> Result<(), SignatureError> { let signature = InternalSignature::try_from(signature)?; - let mut h: Sha512 = Sha512::new(); let minus_A: EdwardsPoint = -self.1; - - h.update(signature.R.as_bytes()); - h.update(self.as_bytes()); - h.update(message); - - let k = Scalar::from_hash(h); + let k = Self::compute_challenge(None, &signature.R, &self.0, message); let R: EdwardsPoint = EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &(minus_A), &signature.s); diff --git a/tests/ed25519.rs b/tests/ed25519.rs index 408148509..1a65d90cc 100644 --- a/tests/ed25519.rs +++ b/tests/ed25519.rs @@ -21,15 +21,22 @@ use sha2::Sha512; #[cfg(test)] mod vectors { - use curve25519_dalek::{edwards::EdwardsPoint, scalar::Scalar}; - use sha2::{digest::Digest, Sha512}; - use std::convert::TryFrom; + use super::*; - use std::fs::File; - use std::io::BufRead; - use std::io::BufReader; + use curve25519_dalek::{ + constants::ED25519_BASEPOINT_POINT, + edwards::{CompressedEdwardsY, EdwardsPoint}, + scalar::Scalar, + traits::IsIdentity, + }; + use sha2::{digest::Digest, Sha512}; - use super::*; + use std::{ + convert::TryFrom, + fs::File, + io::{BufRead, BufReader}, + ops::Neg, + }; // TESTVECTORS is taken from sign.input.gz in agl's ed25519 Golang // package. It is a selection of test cases from @@ -81,6 +88,13 @@ mod vectors { "Signature verification failed on line {}", lineno ); + assert!( + expected_verifying_key + .verify_strict(&msg_bytes, &sig2) + .is_ok(), + "Signature strict verification failed on line {}", + lineno + ); } } @@ -116,81 +130,154 @@ mod vectors { ); assert!( signing_key - .verify_prehashed(prehash_for_verifying, None, &sig2) + .verify_prehashed(prehash_for_verifying.clone(), None, &sig2) .is_ok(), "Could not verify ed25519ph signature!" ); + assert!( + expected_verifying_key + .verify_prehashed_strict(prehash_for_verifying, None, &sig2) + .is_ok(), + "Could not strict-verify ed25519ph signature!" + ); } + // + // The remaining items in this mod are for the repudiation tests + // + // Taken from curve25519_dalek::constants::EIGHT_TORSION[4] const EIGHT_TORSION_4: [u8; 32] = [ 236, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, ]; - fn compute_hram(message: &[u8], pub_key: &EdwardsPoint, signature_r: &EdwardsPoint) -> Scalar { - let k_bytes = Sha512::default() - .chain_update(&signature_r.compress().as_bytes()) - .chain_update(&pub_key.compress().as_bytes()[..]) - .chain_update(&message); - let mut k_output = [0u8; 64]; - k_output.copy_from_slice(k_bytes.finalize().as_slice()); - Scalar::from_bytes_mod_order_wide(&k_output) + // Computes the prehashed or non-prehashed challenge, depending on whether context is given + fn compute_challenge( + message: &[u8], + pub_key: &EdwardsPoint, + signature_r: &EdwardsPoint, + context: Option<&[u8]>, + ) -> Scalar { + let mut h = Sha512::default(); + if let Some(c) = context { + h.update(b"SigEd25519 no Ed25519 collisions"); + h.update(&[1]); + h.update(&[c.len() as u8]); + h.update(c); + } + h.update(&signature_r.compress().as_bytes()); + h.update(&pub_key.compress().as_bytes()[..]); + h.update(&message); + Scalar::from_hash(h) } fn serialize_signature(r: &EdwardsPoint, s: &Scalar) -> Vec { [&r.compress().as_bytes()[..], &s.as_bytes()[..]].concat() } + const WEAK_PUBKEY: CompressedEdwardsY = CompressedEdwardsY(EIGHT_TORSION_4); + + // Pick a random Scalar + fn non_null_scalar() -> Scalar { + let mut rng = rand::rngs::OsRng; + let mut s_candidate = Scalar::random(&mut rng); + while s_candidate == Scalar::ZERO { + s_candidate = Scalar::random(&mut rng); + } + s_candidate + } + + fn pick_r(s: Scalar) -> EdwardsPoint { + let r0 = s * ED25519_BASEPOINT_POINT; + // Pick a torsion point of order 2 + r0 + WEAK_PUBKEY.decompress().unwrap().neg() + } + + // Tests that verify_strict() rejects small-order pubkeys. We test this by explicitly + // constructing a pubkey-signature pair that verifies with respect to two distinct messages. + // This should be accepted by verify(), but rejected by verify_strict(). #[test] fn repudiation() { - use curve25519_dalek::traits::IsIdentity; - use std::ops::Neg; - let message1 = b"Send 100 USD to Alice"; let message2 = b"Send 100000 USD to Alice"; - // Pick a random Scalar - fn non_null_scalar() -> Scalar { - let mut rng = rand::rngs::OsRng; - let mut s_candidate = Scalar::random(&mut rng); - while s_candidate == Scalar::ZERO { - s_candidate = Scalar::random(&mut rng); - } - s_candidate - } let mut s: Scalar = non_null_scalar(); - - fn pick_r_and_pubkey(s: Scalar) -> (EdwardsPoint, EdwardsPoint) { - let r0 = s * curve25519_dalek::constants::ED25519_BASEPOINT_POINT; - // Pick a torsion point of order 2 - let pub_key = curve25519_dalek::edwards::CompressedEdwardsY(EIGHT_TORSION_4) - .decompress() - .unwrap(); - let r = r0 + pub_key.neg(); - (r, pub_key) + let pubkey = WEAK_PUBKEY.decompress().unwrap(); + let mut r = pick_r(s); + + // Find an R such that + // H(R || A || M₁) · A == A == H(R || A || M₂) · A + // This happens with high probability when A is low order. + while !(pubkey.neg() + compute_challenge(message1, &pubkey, &r, None) * pubkey) + .is_identity() + || !(pubkey.neg() + compute_challenge(message2, &pubkey, &r, None) * pubkey) + .is_identity() + { + // We pick an s and let R = sB - A where B is the basepoint + s = non_null_scalar(); + r = pick_r(s); } - let (mut r, mut pub_key) = pick_r_and_pubkey(s); + // At this point, both verification equations hold: + // sB = R + H(R || A || M₁) · A + // = R + H(R || A || M₂) · A + // Check that this is true + let signature = serialize_signature(&r, &s); + let vk = VerifyingKey::from_bytes(&pubkey.compress().as_bytes()).unwrap(); + let sig = Signature::try_from(&signature[..]).unwrap(); + assert!(vk.verify(message1, &sig).is_ok()); + assert!(vk.verify(message2, &sig).is_ok()); + + // Now check that the sigs fail under verify_strict. This is because verify_strict rejects + // small order pubkeys. + assert!(vk.verify_strict(message1, &sig).is_err()); + assert!(vk.verify_strict(message2, &sig).is_err()); + } - while !(pub_key.neg() + compute_hram(message1, &pub_key, &r) * pub_key).is_identity() - || !(pub_key.neg() + compute_hram(message2, &pub_key, &r) * pub_key).is_identity() + // Identical to repudiation() above, but testing verify_prehashed against + // verify_prehashed_strict. See comments above for a description of what's happening. + #[test] + fn repudiation_prehash() { + let message1 = Sha512::new().chain_update(b"Send 100 USD to Alice"); + let message2 = Sha512::new().chain_update(b"Send 100000 USD to Alice"); + let message1_bytes = message1.clone().finalize(); + let message2_bytes = message2.clone().finalize(); + + let mut s: Scalar = non_null_scalar(); + let pubkey = WEAK_PUBKEY.decompress().unwrap(); + let mut r = pick_r(s); + let context_str = Some(&b"edtest"[..]); + + while !(pubkey.neg() + + compute_challenge(&message1_bytes, &pubkey, &r, context_str) * pubkey) + .is_identity() + || !(pubkey.neg() + + compute_challenge(&message2_bytes, &pubkey, &r, context_str) * pubkey) + .is_identity() { s = non_null_scalar(); - let key = pick_r_and_pubkey(s); - r = key.0; - pub_key = key.1; + r = pick_r(s); } + // Check that verify_prehashed succeeds on both sigs let signature = serialize_signature(&r, &s); - let pk = VerifyingKey::from_bytes(&pub_key.compress().as_bytes()).unwrap(); + let vk = VerifyingKey::from_bytes(&pubkey.compress().as_bytes()).unwrap(); let sig = Signature::try_from(&signature[..]).unwrap(); - // The same signature verifies for both messages - assert!(pk.verify(message1, &sig).is_ok() && pk.verify(message2, &sig).is_ok()); - // But not with a strict signature: verify_strict refuses small order keys - assert!( - pk.verify_strict(message1, &sig).is_err() || pk.verify_strict(message2, &sig).is_err() - ); + assert!(vk + .verify_prehashed(message1.clone(), context_str, &sig) + .is_ok()); + assert!(vk + .verify_prehashed(message2.clone(), context_str, &sig) + .is_ok()); + + // Check that verify_prehashed_strict fails on both sigs + assert!(vk + .verify_prehashed_strict(message1.clone(), context_str, &sig) + .is_err()); + assert!(vk + .verify_prehashed_strict(message2.clone(), context_str, &sig) + .is_err()); } } @@ -212,6 +299,7 @@ mod integrations { let mut csprng = OsRng; signing_key = SigningKey::generate(&mut csprng); + let verifying_key = signing_key.verifying_key(); good_sig = signing_key.sign(&good); bad_sig = signing_key.sign(&bad); @@ -219,14 +307,26 @@ mod integrations { signing_key.verify(&good, &good_sig).is_ok(), "Verification of a valid signature failed!" ); + assert!( + verifying_key.verify_strict(&good, &good_sig).is_ok(), + "Strict verification of a valid signature failed!" + ); assert!( signing_key.verify(&good, &bad_sig).is_err(), "Verification of a signature on a different message passed!" ); + assert!( + verifying_key.verify_strict(&good, &bad_sig).is_err(), + "Strict verification of a signature on a different message passed!" + ); assert!( signing_key.verify(&bad, &good_sig).is_err(), "Verification of a signature on a different message passed!" ); + assert!( + verifying_key.verify_strict(&bad, &good_sig).is_err(), + "Strict verification of a signature on a different message passed!" + ); } #[test] @@ -256,6 +356,7 @@ mod integrations { let context: &[u8] = b"testing testing 1 2 3"; signing_key = SigningKey::generate(&mut csprng); + let verifying_key = signing_key.verifying_key(); good_sig = signing_key .sign_prehashed(prehashed_good1, Some(context)) .unwrap(); @@ -265,22 +366,40 @@ mod integrations { assert!( signing_key - .verify_prehashed(prehashed_good2, Some(context), &good_sig) + .verify_prehashed(prehashed_good2.clone(), Some(context), &good_sig) .is_ok(), "Verification of a valid signature failed!" ); + assert!( + verifying_key + .verify_prehashed_strict(prehashed_good2, Some(context), &good_sig) + .is_ok(), + "Strict verification of a valid signature failed!" + ); assert!( signing_key - .verify_prehashed(prehashed_good3, Some(context), &bad_sig) + .verify_prehashed(prehashed_good3.clone(), Some(context), &bad_sig) .is_err(), "Verification of a signature on a different message passed!" ); + assert!( + verifying_key + .verify_prehashed_strict(prehashed_good3, Some(context), &bad_sig) + .is_err(), + "Strict verification of a signature on a different message passed!" + ); assert!( signing_key - .verify_prehashed(prehashed_bad2, Some(context), &good_sig) + .verify_prehashed(prehashed_bad2.clone(), Some(context), &good_sig) .is_err(), "Verification of a signature on a different message passed!" ); + assert!( + verifying_key + .verify_prehashed_strict(prehashed_bad2, Some(context), &good_sig) + .is_err(), + "Strict verification of a signature on a different message passed!" + ); } #[cfg(feature = "batch")]