diff --git a/src/lib.rs b/src/lib.rs index a7cfac4..a408b84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -263,6 +263,7 @@ mod errors; mod signature; mod signing; mod verifying; +mod streaming_verifying; #[cfg(feature = "hazmat")] pub mod hazmat; diff --git a/src/signing.rs b/src/signing.rs index 500f8b5..9a9407b 100644 --- a/src/signing.rs +++ b/src/signing.rs @@ -44,6 +44,7 @@ use crate::{ verifying::VerifyingKey, Signature, }; +use crate::streaming_verifying::StreamingVerifier; /// ed25519 secret key as defined in [RFC8032 ยง 5.1.5]: /// @@ -473,6 +474,13 @@ impl SigningKey { self.verifying_key.verify_strict(message, signature) } + /// Constructs stream verifier with candidate `signature`. + /// + /// See [`VerifyingKey::verify_streaming`] for more details. + pub fn verify_streaming(&self, signature: &ed25519::Signature) -> Result { + self.verifying_key.verify_streaming(signature) + } + /// Convert this signing key into a Curve25519 scalar. /// /// This is useful for e.g. performing X25519 Diffie-Hellman using diff --git a/src/streaming_verifying.rs b/src/streaming_verifying.rs new file mode 100644 index 0000000..ac96a61 --- /dev/null +++ b/src/streaming_verifying.rs @@ -0,0 +1,172 @@ +use crate::{SignatureError, VerifyingKey}; +use crate::Signature; +use crate::hazmat; +use sha2::{Sha512, Digest}; + +pub struct StreamingVerifier { + /// Public key to verify with. + pub(crate) public_key: VerifyingKey, + + /// Candidate signature to verify against. + pub(crate) signature: Signature, + + /// Hash state. + pub(crate) hasher: Sha512, +} + +impl StreamingVerifier { + // Note: I changed the signature parameter to use Signature + // instead of InternalSignature, because raw_verify consumes &Signature. + /// Constructs a new stream verifier. + /// + /// Seeds hash state with public key and signature components. + pub(crate) fn new(public_key: VerifyingKey, signature: Signature) -> Self { + let mut hasher = Sha512::new(); + hasher.update(signature.r_bytes()); + hasher.update(public_key.as_bytes()); + + Self { public_key, hasher, signature } + } + + // Note: I changed the chunk parameter to use &[u8] instead of + // impl AsRef<[u8]> because I think it's better. :V + /// Digest message chunk. + pub fn update(&mut self, chunk: &[u8]) { + self.hasher.update(chunk); + } + + pub fn finalize_and_verify(self) -> Result<(), SignatureError> { + hazmat::raw_verify::(&self.public_key, self.hasher.finalize().as_slice(), &self.signature) + } +} + +// So, in normal usage, hazmat::raw_verify uses CtxDigest to hash +// together the things we already did in the process of running through new() and a series +// of update()s. +// +// Here, we workaround the fact that there is no method in hazmat to provide +// that hash already computed (raw_verify_prehashed is not it), +// by making a hasher that plucks out the hash when provided as the message +// inside verifying::compute_challenge(). +// +// It would be *much* better if such a method were just added to hazmat, +// as we'd avoid the need for this whole module. +mod dirty_workaround { + use curve25519_dalek::digest::{Update, FixedOutput, FixedOutputReset, Output, Reset}; + use curve25519_dalek::digest::typenum::Unsigned; + use curve25519_dalek::digest::generic_array::typenum::U64; + use sha2::Digest; + use sha2::digest::OutputSizeUser; + + /// The number of bytes which precede the message + /// argument to hazmat::raw_verify being fed to this hasher. + /// It is 2 * (the length of CompressedEdwardsY). + const PREFIX_BYTES: usize = 64; + /// The number of bytes which we need to sneak through + /// the message argument into our hash. + const HASH_LENGTH: usize = 64; + + pub(crate) struct DirtyWorkaround { + step: usize, + hash: [u8; HASH_LENGTH], + } + + impl Default for DirtyWorkaround { + fn default() -> Self { + Self { + step: 0, + hash: [0; HASH_LENGTH], + } + } + } + + impl OutputSizeUser for DirtyWorkaround { + type OutputSize = U64; + } + + impl Update for DirtyWorkaround { + fn update(&mut self, data: &[u8]) { + if self.step >= PREFIX_BYTES && self.step < PREFIX_BYTES + HASH_LENGTH { + let buf = &mut self.hash[self.step - PREFIX_BYTES..]; + buf.copy_from_slice(data); + } + if self.step == PREFIX_BYTES { + self.hash.copy_from_slice(data.as_ref()); + } else if self.step > PREFIX_BYTES + HASH_LENGTH { + unreachable!("this should never happen") + } + self.step += data.len(); + } + } + + impl Reset for DirtyWorkaround { + fn reset(&mut self) { + self.step = 0; + self.hash = [0; HASH_LENGTH]; + } + } + + impl FixedOutput for DirtyWorkaround { + fn finalize_into(self, out: &mut Output) { + out.copy_from_slice(&self.hash); + } + } + + impl FixedOutputReset for DirtyWorkaround { + fn finalize_into_reset(&mut self, out: &mut Output) { + out.copy_from_slice(&self.hash); + ::reset(self); + } + } + + impl Digest for DirtyWorkaround { + fn new() -> Self { + Self::default() + } + + fn new_with_prefix(data: impl AsRef<[u8]>) -> Self { + let mut hasher = Self::new(); + ::update(&mut hasher, data); + hasher + } + + fn update(&mut self, data: impl AsRef<[u8]>) { + ::update(self, data.as_ref()) + } + + fn chain_update(mut self, data: impl AsRef<[u8]>) -> Self { + ::update(&mut self, data); + self + } + + fn finalize(self) -> Output { + let mut out = Output::::default(); + ::finalize_into(self, &mut out); + out + } + + fn finalize_into(self, out: &mut Output) { + out.copy_from_slice(&self.hash); + } + + fn finalize_reset(&mut self) -> Output where Self: FixedOutputReset { + ::finalize_fixed_reset(self) + } + + fn finalize_into_reset(&mut self, out: &mut Output) where Self: FixedOutputReset { + ::finalize_into_reset(self, out) + } + + fn reset(&mut self) where Self: Reset { + ::reset(self) + } + + fn output_size() -> usize { + Self::OutputSize::to_usize() + } + + fn digest(data: impl AsRef<[u8]>) -> Output { + Self::new_with_prefix(data).finalize() + } + } +} \ No newline at end of file diff --git a/src/verifying.rs b/src/verifying.rs index e97e203..9e2de6c 100644 --- a/src/verifying.rs +++ b/src/verifying.rs @@ -42,6 +42,7 @@ use crate::{ signature::InternalSignature, signing::SigningKey, }; +use crate::streaming_verifying::StreamingVerifier; /// An ed25519 public key. /// @@ -436,6 +437,18 @@ 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_streaming(&self, signature: &ed25519::Signature) -> Result { + // TODO: ensure that check_scalar is run + // this is normally done via Signature -> InternalSignature + Ok(StreamingVerifier::new(*self, *signature)) + } + /// Verify a `signature` on a `prehashed_message` using the Ed25519ph algorithm, /// using strict signture checking as defined by [`Self::verify_strict`]. ///