mirror of
https://github.com/saymrwulf/curve25519-dalek-source.git
synced 2026-09-04 20:24:10 +00:00
Verify by digest update + StreamVerifier (#735)
* Replace recompute_R with a separate RCompute This struct can be use to implement verifiers with incremental updates * 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. * Tests for raw_sign_byupdate, raw_verify_byupdate * Add StreamVerifier * Make StreamVerifier use RCompute This allows it to use the same implementation as non-stream signature verification. * Guard StreamVerifier behind hazmat feature * docs: disambiguate unsafety Co-authored-by: Tony Arcieri <bascule@gmail.com> * chore: relax F bounds on raw_verify_byupdate * chore: remove raw_sign_byupdate and raw_verify_byupdate * chore: address clippy lints within new code * docs: fixup changelog * test: invert new chunked test * chore: revert raw_sign --------- Co-authored-by: Matt Johnston <matt@ucc.asn.au> Co-authored-by: Tony Arcieri <bascule@gmail.com>
This commit is contained in:
parent
dcd39743ea
commit
6dc7a1c7c5
5 changed files with 203 additions and 56 deletions
|
|
@ -8,6 +8,8 @@ Entries are listed in reverse chronological order per undeprecated major series.
|
|||
|
||||
# Unreleased
|
||||
|
||||
* Add `SigningKey::verify_stream()`, and `VerifyingKey::verify_stream()`
|
||||
|
||||
# 2.x series
|
||||
|
||||
## 2.1.1
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ use signature::DigestSigner;
|
|||
#[cfg(feature = "zeroize")]
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
#[cfg(feature = "hazmat")]
|
||||
use crate::verifying::StreamVerifier;
|
||||
use crate::{
|
||||
constants::{KEYPAIR_LENGTH, SECRET_KEY_LENGTH},
|
||||
errors::{InternalError, SignatureError},
|
||||
|
|
@ -483,6 +485,17 @@ impl SigningKey {
|
|||
self.verifying_key.verify_strict(message, signature)
|
||||
}
|
||||
|
||||
/// Constructs stream verifier with candidate `signature`.
|
||||
///
|
||||
/// See [`VerifyingKey::verify_stream()`] for more details.
|
||||
#[cfg(feature = "hazmat")]
|
||||
pub fn verify_stream(
|
||||
&self,
|
||||
signature: &ed25519::Signature,
|
||||
) -> Result<StreamVerifier, SignatureError> {
|
||||
self.verifying_key.verify_stream(signature)
|
||||
}
|
||||
|
||||
/// Convert this signing key into a byte representation of an unreduced, unclamped Curve25519
|
||||
/// scalar. This is NOT the same thing as `self.to_scalar().to_bytes()`, since `to_scalar()`
|
||||
/// performs a clamping step, which changes the value of the resulting scalar.
|
||||
|
|
|
|||
|
|
@ -42,6 +42,11 @@ use crate::{
|
|||
signing::SigningKey,
|
||||
};
|
||||
|
||||
#[cfg(feature = "hazmat")]
|
||||
mod stream;
|
||||
#[cfg(feature = "hazmat")]
|
||||
pub use self::stream::StreamVerifier;
|
||||
|
||||
/// An ed25519 public key.
|
||||
///
|
||||
/// # Note
|
||||
|
|
@ -186,58 +191,8 @@ impl VerifyingKey {
|
|||
self.point.is_small_order()
|
||||
}
|
||||
|
||||
// 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<CtxDigest>(
|
||||
context: Option<&[u8]>,
|
||||
R: &CompressedEdwardsY,
|
||||
A: &CompressedEdwardsY,
|
||||
M: &[u8],
|
||||
) -> Scalar
|
||||
where
|
||||
CtxDigest: Digest<OutputSize = U64>,
|
||||
{
|
||||
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<CtxDigest>(
|
||||
&self,
|
||||
context: Option<&[u8]>,
|
||||
signature: &InternalSignature,
|
||||
M: &[u8],
|
||||
) -> CompressedEdwardsY
|
||||
where
|
||||
CtxDigest: Digest<OutputSize = U64>,
|
||||
{
|
||||
let k = Self::compute_challenge::<CtxDigest>(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::RCompute`]). `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
|
||||
|
|
@ -253,7 +208,7 @@ impl VerifyingKey {
|
|||
{
|
||||
let signature = InternalSignature::try_from(signature)?;
|
||||
|
||||
let expected_R = self.recompute_R::<CtxDigest>(None, &signature, message);
|
||||
let expected_R = RCompute::<CtxDigest>::compute(self, signature, None, message);
|
||||
if expected_R == signature.R {
|
||||
Ok(())
|
||||
} else {
|
||||
|
|
@ -289,7 +244,8 @@ impl VerifyingKey {
|
|||
);
|
||||
|
||||
let message = prehashed_message.finalize();
|
||||
let expected_R = self.recompute_R::<CtxDigest>(Some(ctx), &signature, &message);
|
||||
|
||||
let expected_R = RCompute::<CtxDigest>::compute(self, signature, Some(ctx), &message);
|
||||
|
||||
if expected_R == signature.R {
|
||||
Ok(())
|
||||
|
|
@ -415,7 +371,7 @@ impl VerifyingKey {
|
|||
return Err(InternalError::Verify.into());
|
||||
}
|
||||
|
||||
let expected_R = self.recompute_R::<Sha512>(None, &signature, message);
|
||||
let expected_R = RCompute::<Sha512>::compute(self, signature, None, message);
|
||||
if expected_R == signature.R {
|
||||
Ok(())
|
||||
} else {
|
||||
|
|
@ -423,8 +379,22 @@ 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.
|
||||
#[cfg(feature = "hazmat")]
|
||||
pub fn verify_stream(
|
||||
&self,
|
||||
signature: &ed25519::Signature,
|
||||
) -> Result<StreamVerifier, SignatureError> {
|
||||
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
|
||||
///
|
||||
|
|
@ -477,7 +447,7 @@ impl VerifyingKey {
|
|||
}
|
||||
|
||||
let message = prehashed_message.finalize();
|
||||
let expected_R = self.recompute_R::<Sha512>(Some(ctx), &signature, &message);
|
||||
let expected_R = RCompute::<Sha512>::compute(self, signature, Some(ctx), &message);
|
||||
|
||||
if expected_R == signature.R {
|
||||
Ok(())
|
||||
|
|
@ -511,6 +481,79 @@ impl VerifyingKey {
|
|||
}
|
||||
}
|
||||
|
||||
/// Helper for verification. Computes the _expected_ R component of the signature. The
|
||||
/// caller compares this to the real R component.
|
||||
/// This computes `H(R || A || M)` where `H` is the 512-bit hash function
|
||||
/// given by `CtxDigest` (this is SHA-512 in spec-compliant Ed25519).
|
||||
///
|
||||
/// For pre-hashed 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 RCompute<CtxDigest> {
|
||||
key: VerifyingKey,
|
||||
signature: InternalSignature,
|
||||
h: CtxDigest,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
impl<CtxDigest> RCompute<CtxDigest>
|
||||
where
|
||||
CtxDigest: Digest<OutputSize = U64>,
|
||||
{
|
||||
/// If `prehash_ctx.is_some()`, this does the prehashed variant of the computation using its
|
||||
/// contents.
|
||||
pub(crate) 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(crate) 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(crate) fn update(&mut self, m: &[u8]) {
|
||||
self.h.update(m)
|
||||
}
|
||||
|
||||
pub(crate) 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<ed25519::Signature> for VerifyingKey {
|
||||
/// Verify a signature on a message with this keypair's public key.
|
||||
///
|
||||
|
|
|
|||
45
ed25519-dalek/src/verifying/stream.rs
Normal file
45
ed25519-dalek/src/verifying/stream.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
use curve25519_dalek::edwards::CompressedEdwardsY;
|
||||
use sha2::Sha512;
|
||||
|
||||
use crate::verifying::RCompute;
|
||||
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()
|
||||
#[allow(non_snake_case)]
|
||||
pub struct StreamVerifier {
|
||||
cr: RCompute<Sha512>,
|
||||
sig_R: CompressedEdwardsY,
|
||||
}
|
||||
|
||||
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 {
|
||||
Self {
|
||||
cr: RCompute::new(&public_key, signature, None),
|
||||
sig_R: signature.R,
|
||||
}
|
||||
}
|
||||
|
||||
/// Digest message chunk.
|
||||
pub fn update(&mut self, chunk: impl AsRef<[u8]>) {
|
||||
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 expected_R = self.cr.finish();
|
||||
|
||||
if expected_R == self.sig_R {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(InternalError::Verify.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -334,6 +334,50 @@ mod integrations {
|
|||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "digest")]
|
||||
#[test]
|
||||
fn sign_verify_digest_equivalence() {
|
||||
// TestSignVerify
|
||||
|
||||
let mut csprng = OsRng {};
|
||||
|
||||
let good: &[u8] = "test message".as_bytes();
|
||||
let bad: &[u8] = "wrong message".as_bytes();
|
||||
|
||||
let keypair: SigningKey = SigningKey::generate(&mut csprng);
|
||||
let good_sig: Signature = keypair.sign(good);
|
||||
let bad_sig: Signature = 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("test ");
|
||||
verifier.update("message");
|
||||
assert!(
|
||||
verifier.finalize_and_verify().is_ok(),
|
||||
"Verification of a valid signature failed!"
|
||||
);
|
||||
|
||||
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() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue