From 3f95fb80d2e8fa6475ea34a6ccdc6ffe1564ac11 Mon Sep 17 00:00:00 2001 From: Nick Mathewson Date: Sun, 26 Dec 2021 10:38:57 -0500 Subject: [PATCH] Draft: New facility for exposing "bundled signatures". Sometimes it's useful to save up one or more signatures and have them verified later on. For example, in Arti, we sometimes want to parse a large document containing many internal signatures, but save the verification of the signatures for another thread. We can't use precomputed hashes for this, since the protocol is already specified. Thus, our only choice for now is to carry around the original message--either by copy or by reference--until we're ready to verify the signature on it. With this patch, ed25519-dalek exposes a new type, BundledSignature. A BundledSignature contains the public key, the InternalSignature, and the scalar `k` needed to verify the signature against the message. To avoid code bloat and to reuse testing, it uses these objects internally to implement signature verification. (According to `cargo bench`, there is no performance loss.) --- src/errors.rs | 57 ++++++++----- src/public.rs | 228 ++++++++++++++++++++++++++++++++++---------------- 2 files changed, 195 insertions(+), 90 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index b66fae0..64c9364 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -38,37 +38,56 @@ pub(crate) enum InternalError { VerifyError, /// Two arrays did not match in size, making the called signature /// verification method impossible. - ArrayLengthError{ name_a: &'static str, length_a: usize, - name_b: &'static str, length_b: usize, - name_c: &'static str, length_c: usize, }, + ArrayLengthError { + name_a: &'static str, + length_a: usize, + name_b: &'static str, + length_b: usize, + name_c: &'static str, + length_c: usize, + }, /// An ed25519ph signature can only take up to 255 octets of context. PrehashedContextLengthError, + /// A public key did not match the key provided in a bundled signature. + WrongKeyError, } impl Display for InternalError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { - InternalError::PointDecompressionError - => write!(f, "Cannot decompress Edwards point"), - InternalError::ScalarFormatError - => write!(f, "Cannot use scalar with high-bit set"), - InternalError::BytesLengthError{ name: n, length: l} - => write!(f, "{} must be {} bytes in length", n, l), - InternalError::VerifyError - => write!(f, "Verification equation was not satisfied"), - InternalError::ArrayLengthError{ name_a: na, length_a: la, - name_b: nb, length_b: lb, - name_c: nc, length_c: lc, } - => write!(f, "Arrays must be the same length: {} has length {}, - {} has length {}, {} has length {}.", na, la, nb, lb, nc, lc), - InternalError::PrehashedContextLengthError - => write!(f, "An ed25519ph signature can only take up to 255 octets of context"), + InternalError::PointDecompressionError => write!(f, "Cannot decompress Edwards point"), + InternalError::ScalarFormatError => write!(f, "Cannot use scalar with high-bit set"), + InternalError::BytesLengthError { name: n, length: l } => { + write!(f, "{} must be {} bytes in length", n, l) + } + InternalError::VerifyError => write!(f, "Verification equation was not satisfied"), + InternalError::ArrayLengthError { + name_a: na, + length_a: la, + name_b: nb, + length_b: lb, + name_c: nc, + length_c: lc, + } => write!( + f, + "Arrays must be the same length: {} has length {}, + {} has length {}, {} has length {}.", + na, la, nb, lb, nc, lc + ), + InternalError::PrehashedContextLengthError => write!( + f, + "An ed25519ph signature can only take up to 255 octets of context" + ), + InternalError::WrongKeyError => write!( + f, + "Provided public key did not match key in bundled signature" + ), } } } #[cfg(feature = "std")] -impl Error for InternalError { } +impl Error for InternalError {} /// Errors which may occur while processing signatures and keypairs. /// diff --git a/src/public.rs b/src/public.rs index 342adf6..f3730e0 100644 --- a/src/public.rs +++ b/src/public.rs @@ -28,7 +28,7 @@ use serde::de::Error as SerdeError; #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; #[cfg(feature = "serde")] -use serde_bytes::{Bytes as SerdeBytes, ByteBuf as SerdeByteBuf}; +use serde_bytes::{ByteBuf as SerdeByteBuf, Bytes as SerdeBytes}; use crate::constants::*; use crate::errors::*; @@ -131,7 +131,8 @@ impl PublicKey { return Err(InternalError::BytesLengthError { name: "PublicKey", length: PUBLIC_KEY_LENGTH, - }.into()); + } + .into()); } let mut bits: [u8; 32] = [0u8; 32]; bits.copy_from_slice(&bytes[..32]); @@ -160,6 +161,43 @@ impl PublicKey { PublicKey(compressed, point) } + /// Create a [`BundledSignature`] object containing the information needed + /// to validate a provided `signature` on a provided `prehashed_message` and + /// `context with this key. + /// + /// Verifying the resulting `BundledSignature` object has the same behavior + /// as if you had called `verify_prehashed()`. + #[allow(non_snake_case)] + pub fn prepare_bundled_signature_prehashed( + &self, + prehashed_message: D, + context: Option<&[u8]>, + signature: &ed25519::Signature, + ) -> Result + where + D: Digest, + { + let signature = InternalSignature::try_from(signature)?; + + let mut h: Sha512 = Sha512::default(); + + let ctx: &[u8] = context.unwrap_or(b""); + debug_assert!( + ctx.len() <= 255, + "The context must not be longer than 255 octets." + ); + + 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()); + + Ok(BundledSignature::new(*self, h, signature)) + } + /// Verify a `signature` on a `prehashed_message` using the Ed25519ph algorithm. /// /// # Inputs @@ -178,7 +216,6 @@ impl PublicKey { /// `Keypair` on the `prehashed_message`. /// /// [rfc8032]: https://tools.ietf.org/html/rfc8032#section-5.1 - #[allow(non_snake_case)] pub fn verify_prehashed( &self, prehashed_message: D, @@ -188,33 +225,42 @@ impl PublicKey { where D: Digest, { + self.prepare_bundled_signature_prehashed(prehashed_message, context, signature)? + .verify() + } + + /// Create a [`BundledSignature`] object containing the information needed + /// to strictly validate a provided `signature` on a provided `message` with this + /// key. + /// + /// Verifying the resulting `BundledSignature` object has the same behavior + /// as if you had called `verify_strict()`. + #[allow(non_snake_case)] + pub fn prepare_bundled_signature_strict( + &self, + message: &[u8], + signature: &ed25519::Signature, + ) -> Result { let signature = InternalSignature::try_from(signature)?; - let mut h: Sha512 = Sha512::default(); - let R: EdwardsPoint; - let k: Scalar; + let mut h: Sha512 = Sha512::new(); + let signature_R: EdwardsPoint; - let ctx: &[u8] = context.unwrap_or(b""); - debug_assert!(ctx.len() <= 255, "The context must not be longer than 255 octets."); + match signature.R.decompress() { + None => return Err(InternalError::VerifyError.into()), + Some(x) => signature_R = x, + } - let minus_A: EdwardsPoint = -self.1; + // 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::VerifyError.into()); + } - 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()); - - k = Scalar::from_hash(h); - R = EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &(minus_A), &signature.s); + h.update(&message); - if R.compress() == signature.R { - Ok(()) - } else { - Err(InternalError::VerifyError.into()) - } + Ok(BundledSignature::new(*self, h, signature)) } /// Strictly verify a signature on a message with this keypair's public key. @@ -279,43 +325,46 @@ impl PublicKey { /// # Return /// /// Returns `Ok(())` if the signature is valid, and `Err` otherwise. - #[allow(non_snake_case)] pub fn verify_strict( &self, message: &[u8], signature: &ed25519::Signature, - ) -> Result<(), SignatureError> - { + ) -> Result<(), SignatureError> { + self.prepare_bundled_signature_strict(message, signature)? + .verify() + } + + /// Create a [`BundledSignature`] object containing the information needed + /// to validate a provided `signature` on a provided `message` with this + /// key. + /// + /// Verifying the resulting `BundledSignature` object has the same behavior + /// as if you had called `verify()`. + #[allow(non_snake_case)] + pub fn prepare_bundled_signature( + &self, + message: &[u8], + signature: &ed25519::Signature, + ) -> Result { let signature = InternalSignature::try_from(signature)?; let mut h: Sha512 = Sha512::new(); - let R: EdwardsPoint; - let k: Scalar; - let minus_A: EdwardsPoint = -self.1; - let signature_R: EdwardsPoint; - - match signature.R.decompress() { - None => return Err(InternalError::VerifyError.into()), - Some(x) => signature_R = x, - } - - // 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::VerifyError.into()); - } h.update(signature.R.as_bytes()); h.update(self.as_bytes()); h.update(&message); - k = Scalar::from_hash(h); - R = EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &(minus_A), &signature.s); + Ok(BundledSignature::new(*self, h, signature)) + } - if R == signature_R { - Ok(()) - } else { - Err(InternalError::VerifyError.into()) + /// Check whether a bundled signature/message/key combination is valid, and + /// its key matches this key. + pub fn verify_bundled(&self, bundled: &BundledSignature) -> Result<(), SignatureError> { + if !bundled.matches_key(self) { + return Err(InternalError::WrongKeyError.into()); } + + bundled.verify() } } @@ -325,32 +374,8 @@ impl Verifier for PublicKey { /// # Return /// /// Returns `Ok(())` if the signature is valid, and `Err` otherwise. - #[allow(non_snake_case)] - fn verify( - &self, - message: &[u8], - signature: &ed25519::Signature - ) -> Result<(), SignatureError> - { - let signature = InternalSignature::try_from(signature)?; - - let mut h: Sha512 = Sha512::new(); - let R: EdwardsPoint; - let k: Scalar; - let minus_A: EdwardsPoint = -self.1; - - h.update(signature.R.as_bytes()); - h.update(self.as_bytes()); - h.update(&message); - - k = Scalar::from_hash(h); - R = EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &(minus_A), &signature.s); - - if R.compress() == signature.R { - Ok(()) - } else { - Err(InternalError::VerifyError.into()) - } + fn verify(&self, message: &[u8], signature: &ed25519::Signature) -> Result<(), SignatureError> { + self.prepare_bundled_signature(message, signature)?.verify() } } @@ -374,3 +399,64 @@ impl<'d> Deserialize<'d> for PublicKey { PublicKey::from_bytes(bytes.as_ref()).map_err(SerdeError::custom) } } + +/// A `BundledSignature` contains a signature, plus the information from the +/// message and public key needed to check it. +/// +/// Under some circumstances, you may want want to bundle the information needed +/// to verify a signature later on (in another thread, say). That's easy enough +/// if your protocol uses the "precomputed hash" variant of ed25519, but +/// otherwise it can become unwieldy (say, if your message is very large, and +/// passing it by reference is inconvenient). +/// +/// Instead, you can use one of the `prepare_bundled_signature*()` functions +/// from [`PublicKey`] to create a (comparatively) small [`BundledSignature`]. +/// The `BundledSignature` contains the public key, the signature, and the +/// necessary SHA512 hash that Ed25519 uses internally to verify that the +/// signature is correct for the given public key and message. +#[derive(Clone, Debug)] +pub struct BundledSignature { + public_key: PublicKey, + k: Scalar, + signature: InternalSignature, +} + +impl BundledSignature { + /// Construct a new BundledSignature from its parts. + fn new(public_key: PublicKey, h: Sha512, signature: InternalSignature) -> Self { + let k = Scalar::from_hash(h); + Self { + public_key, + k, + signature, + } + } + + /// Check whether this signature/message/key combination contains a valid + /// signature. + /// + /// # Return + /// + /// Returns `Ok(())` if the signature is valid, and `Err` otherwise. + #[allow(non_snake_case)] + #[inline] + pub fn verify(&self) -> Result<(), SignatureError> { + let minus_A: EdwardsPoint = -self.public_key.1; + let R = EdwardsPoint::vartime_double_scalar_mul_basepoint( + &self.k, + &(minus_A), + &self.signature.s, + ); + + if R.compress() == self.signature.R { + Ok(()) + } else { + Err(InternalError::VerifyError.into()) + } + } + + /// Return true if this BundledSignature was constructed with the public key `key`. + pub fn matches_key(&self, key: &PublicKey) -> bool { + &self.public_key == key + } +}