Adding verify_prehashed_strict() (#212)

Combines `verify_prehashed` and `verify_strict` to allow strict
verification with prehashed values.
This commit is contained in:
andrew lyon 2023-01-07 08:21:54 -08:00 committed by GitHub
parent 461a2d7e05
commit 4f218d8e67
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 268 additions and 84 deletions

View file

@ -141,6 +141,28 @@ impl VerifyingKey {
VerifyingKey(compressed, point) 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. /// Verify a `signature` on a `prehashed_message` using the Ed25519ph algorithm.
/// ///
/// # Inputs /// # Inputs
@ -171,8 +193,6 @@ impl VerifyingKey {
{ {
let signature = InternalSignature::try_from(signature)?; let signature = InternalSignature::try_from(signature)?;
let mut h: Sha512 = Sha512::default();
let ctx: &[u8] = context.unwrap_or(b""); let ctx: &[u8] = context.unwrap_or(b"");
debug_assert!( debug_assert!(
ctx.len() <= 255, ctx.len() <= 255,
@ -180,16 +200,12 @@ impl VerifyingKey {
); );
let minus_A: EdwardsPoint = -self.1; let minus_A: EdwardsPoint = -self.1;
let k = Self::compute_challenge(
h.update(b"SigEd25519 no Ed25519 collisions"); Some(ctx),
h.update([1]); // Ed25519ph &signature.R,
h.update([ctx.len() as u8]); &self.0,
h.update(ctx); prehashed_message.finalize().as_slice(),
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 R: EdwardsPoint = let R: EdwardsPoint =
EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &(minus_A), &signature.s); EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &(minus_A), &signature.s);
@ -270,24 +286,18 @@ impl VerifyingKey {
) -> Result<(), SignatureError> { ) -> Result<(), SignatureError> {
let signature = InternalSignature::try_from(signature)?; let signature = InternalSignature::try_from(signature)?;
let mut h: Sha512 = Sha512::new(); let signature_R = signature
let minus_A: EdwardsPoint = -self.1; .R
.decompress()
let signature_R: EdwardsPoint = match signature.R.decompress() { .ok_or_else(|| SignatureError::from(InternalError::Verify))?;
None => return Err(InternalError::Verify.into()),
Some(x) => x,
};
// Logical OR is fine here as we're not trying to be constant time. // 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() { if signature_R.is_small_order() || self.1.is_small_order() {
return Err(InternalError::Verify.into()); return Err(InternalError::Verify.into());
} }
h.update(signature.R.as_bytes()); let minus_A: EdwardsPoint = -self.1;
h.update(self.as_bytes()); let k = Self::compute_challenge(None, &signature.R, &self.0, message);
h.update(message);
let k = Scalar::from_hash(h);
let R: EdwardsPoint = let R: EdwardsPoint =
EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &(minus_A), &signature.s); EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &(minus_A), &signature.s);
@ -297,6 +307,67 @@ impl VerifyingKey {
Err(InternalError::Verify.into()) 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<D>(
&self,
prehashed_message: D,
context: Option<&[u8]>,
signature: &ed25519::Signature,
) -> Result<(), SignatureError>
where
D: Digest<OutputSize = U64>,
{
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<ed25519::Signature> for VerifyingKey { impl Verifier<ed25519::Signature> for VerifyingKey {
@ -309,14 +380,8 @@ impl Verifier<ed25519::Signature> for VerifyingKey {
fn verify(&self, message: &[u8], signature: &ed25519::Signature) -> Result<(), SignatureError> { fn verify(&self, message: &[u8], signature: &ed25519::Signature) -> Result<(), SignatureError> {
let signature = InternalSignature::try_from(signature)?; let signature = InternalSignature::try_from(signature)?;
let mut h: Sha512 = Sha512::new();
let minus_A: EdwardsPoint = -self.1; let minus_A: EdwardsPoint = -self.1;
let k = Self::compute_challenge(None, &signature.R, &self.0, message);
h.update(signature.R.as_bytes());
h.update(self.as_bytes());
h.update(message);
let k = Scalar::from_hash(h);
let R: EdwardsPoint = let R: EdwardsPoint =
EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &(minus_A), &signature.s); EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &(minus_A), &signature.s);

View file

@ -21,16 +21,23 @@ use sha2::Sha512;
#[cfg(test)] #[cfg(test)]
mod vectors { mod vectors {
use curve25519_dalek::{edwards::EdwardsPoint, scalar::Scalar};
use sha2::{digest::Digest, Sha512};
use std::convert::TryFrom;
use std::fs::File;
use std::io::BufRead;
use std::io::BufReader;
use super::*; use super::*;
use curve25519_dalek::{
constants::ED25519_BASEPOINT_POINT,
edwards::{CompressedEdwardsY, EdwardsPoint},
scalar::Scalar,
traits::IsIdentity,
};
use sha2::{digest::Digest, Sha512};
use std::{
convert::TryFrom,
fs::File,
io::{BufRead, BufReader},
ops::Neg,
};
// TESTVECTORS is taken from sign.input.gz in agl's ed25519 Golang // TESTVECTORS is taken from sign.input.gz in agl's ed25519 Golang
// package. It is a selection of test cases from // package. It is a selection of test cases from
// http://ed25519.cr.yp.to/python/sign.input // http://ed25519.cr.yp.to/python/sign.input
@ -81,6 +88,13 @@ mod vectors {
"Signature verification failed on line {}", "Signature verification failed on line {}",
lineno 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!( assert!(
signing_key signing_key
.verify_prehashed(prehash_for_verifying, None, &sig2) .verify_prehashed(prehash_for_verifying.clone(), None, &sig2)
.is_ok(), .is_ok(),
"Could not verify ed25519ph signature!" "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] // Taken from curve25519_dalek::constants::EIGHT_TORSION[4]
const EIGHT_TORSION_4: [u8; 32] = [ const EIGHT_TORSION_4: [u8; 32] = [
236, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 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, 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 { // Computes the prehashed or non-prehashed challenge, depending on whether context is given
let k_bytes = Sha512::default() fn compute_challenge(
.chain_update(&signature_r.compress().as_bytes()) message: &[u8],
.chain_update(&pub_key.compress().as_bytes()[..]) pub_key: &EdwardsPoint,
.chain_update(&message); signature_r: &EdwardsPoint,
let mut k_output = [0u8; 64]; context: Option<&[u8]>,
k_output.copy_from_slice(k_bytes.finalize().as_slice()); ) -> Scalar {
Scalar::from_bytes_mod_order_wide(&k_output) 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<u8> { fn serialize_signature(r: &EdwardsPoint, s: &Scalar) -> Vec<u8> {
[&r.compress().as_bytes()[..], &s.as_bytes()[..]].concat() [&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] #[test]
fn repudiation() { fn repudiation() {
use curve25519_dalek::traits::IsIdentity;
use std::ops::Neg;
let message1 = b"Send 100 USD to Alice"; let message1 = b"Send 100 USD to Alice";
let message2 = b"Send 100000 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(); let mut s: Scalar = non_null_scalar();
let pubkey = WEAK_PUBKEY.decompress().unwrap();
let mut r = pick_r(s);
fn pick_r_and_pubkey(s: Scalar) -> (EdwardsPoint, EdwardsPoint) { // Find an R such that
let r0 = s * curve25519_dalek::constants::ED25519_BASEPOINT_POINT; // H(R || A || M₁) · A == A == H(R || A || M₂) · A
// Pick a torsion point of order 2 // This happens with high probability when A is low order.
let pub_key = curve25519_dalek::edwards::CompressedEdwardsY(EIGHT_TORSION_4) while !(pubkey.neg() + compute_challenge(message1, &pubkey, &r, None) * pubkey)
.decompress() .is_identity()
.unwrap(); || !(pubkey.neg() + compute_challenge(message2, &pubkey, &r, None) * pubkey)
let r = r0 + pub_key.neg(); .is_identity()
(r, pub_key) {
// 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());
while !(pub_key.neg() + compute_hram(message1, &pub_key, &r) * pub_key).is_identity() // Now check that the sigs fail under verify_strict. This is because verify_strict rejects
|| !(pub_key.neg() + compute_hram(message2, &pub_key, &r) * pub_key).is_identity() // small order pubkeys.
assert!(vk.verify_strict(message1, &sig).is_err());
assert!(vk.verify_strict(message2, &sig).is_err());
}
// 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(); s = non_null_scalar();
let key = pick_r_and_pubkey(s); r = pick_r(s);
r = key.0;
pub_key = key.1;
} }
// Check that verify_prehashed succeeds on both sigs
let signature = serialize_signature(&r, &s); 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(); let sig = Signature::try_from(&signature[..]).unwrap();
// The same signature verifies for both messages assert!(vk
assert!(pk.verify(message1, &sig).is_ok() && pk.verify(message2, &sig).is_ok()); .verify_prehashed(message1.clone(), context_str, &sig)
// But not with a strict signature: verify_strict refuses small order keys .is_ok());
assert!( assert!(vk
pk.verify_strict(message1, &sig).is_err() || pk.verify_strict(message2, &sig).is_err() .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; let mut csprng = OsRng;
signing_key = SigningKey::generate(&mut csprng); signing_key = SigningKey::generate(&mut csprng);
let verifying_key = signing_key.verifying_key();
good_sig = signing_key.sign(&good); good_sig = signing_key.sign(&good);
bad_sig = signing_key.sign(&bad); bad_sig = signing_key.sign(&bad);
@ -219,14 +307,26 @@ mod integrations {
signing_key.verify(&good, &good_sig).is_ok(), signing_key.verify(&good, &good_sig).is_ok(),
"Verification of a valid signature failed!" "Verification of a valid signature failed!"
); );
assert!(
verifying_key.verify_strict(&good, &good_sig).is_ok(),
"Strict verification of a valid signature failed!"
);
assert!( assert!(
signing_key.verify(&good, &bad_sig).is_err(), signing_key.verify(&good, &bad_sig).is_err(),
"Verification of a signature on a different message passed!" "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!( assert!(
signing_key.verify(&bad, &good_sig).is_err(), signing_key.verify(&bad, &good_sig).is_err(),
"Verification of a signature on a different message passed!" "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] #[test]
@ -256,6 +356,7 @@ mod integrations {
let context: &[u8] = b"testing testing 1 2 3"; let context: &[u8] = b"testing testing 1 2 3";
signing_key = SigningKey::generate(&mut csprng); signing_key = SigningKey::generate(&mut csprng);
let verifying_key = signing_key.verifying_key();
good_sig = signing_key good_sig = signing_key
.sign_prehashed(prehashed_good1, Some(context)) .sign_prehashed(prehashed_good1, Some(context))
.unwrap(); .unwrap();
@ -265,22 +366,40 @@ mod integrations {
assert!( assert!(
signing_key signing_key
.verify_prehashed(prehashed_good2, Some(context), &good_sig) .verify_prehashed(prehashed_good2.clone(), Some(context), &good_sig)
.is_ok(), .is_ok(),
"Verification of a valid signature failed!" "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!( assert!(
signing_key signing_key
.verify_prehashed(prehashed_good3, Some(context), &bad_sig) .verify_prehashed(prehashed_good3.clone(), Some(context), &bad_sig)
.is_err(), .is_err(),
"Verification of a signature on a different message passed!" "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!( assert!(
signing_key signing_key
.verify_prehashed(prehashed_bad2, Some(context), &good_sig) .verify_prehashed(prehashed_bad2.clone(), Some(context), &good_sig)
.is_err(), .is_err(),
"Verification of a signature on a different message passed!" "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")] #[cfg(feature = "batch")]