Skip to content

Commit 6dc7a1c

Browse files
robjtedemkjtarcieri
authored
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>
1 parent dcd3974 commit 6dc7a1c

File tree

5 files changed

+203
-56
lines changed

5 files changed

+203
-56
lines changed

ed25519-dalek/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Entries are listed in reverse chronological order per undeprecated major series.
88

99
# Unreleased
1010

11+
* Add `SigningKey::verify_stream()`, and `VerifyingKey::verify_stream()`
12+
1113
# 2.x series
1214

1315
## 2.1.1

ed25519-dalek/src/signing.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ use signature::DigestSigner;
3939
#[cfg(feature = "zeroize")]
4040
use zeroize::{Zeroize, ZeroizeOnDrop};
4141

42+
#[cfg(feature = "hazmat")]
43+
use crate::verifying::StreamVerifier;
4244
use crate::{
4345
constants::{KEYPAIR_LENGTH, SECRET_KEY_LENGTH},
4446
errors::{InternalError, SignatureError},
@@ -483,6 +485,17 @@ impl SigningKey {
483485
self.verifying_key.verify_strict(message, signature)
484486
}
485487

488+
/// Constructs stream verifier with candidate `signature`.
489+
///
490+
/// See [`VerifyingKey::verify_stream()`] for more details.
491+
#[cfg(feature = "hazmat")]
492+
pub fn verify_stream(
493+
&self,
494+
signature: &ed25519::Signature,
495+
) -> Result<StreamVerifier, SignatureError> {
496+
self.verifying_key.verify_stream(signature)
497+
}
498+
486499
/// Convert this signing key into a byte representation of an unreduced, unclamped Curve25519
487500
/// scalar. This is NOT the same thing as `self.to_scalar().to_bytes()`, since `to_scalar()`
488501
/// performs a clamping step, which changes the value of the resulting scalar.

ed25519-dalek/src/verifying.rs

Lines changed: 99 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ use crate::{
4242
signing::SigningKey,
4343
};
4444

45+
#[cfg(feature = "hazmat")]
46+
mod stream;
47+
#[cfg(feature = "hazmat")]
48+
pub use self::stream::StreamVerifier;
49+
4550
/// An ed25519 public key.
4651
///
4752
/// # Note
@@ -186,58 +191,8 @@ impl VerifyingKey {
186191
self.point.is_small_order()
187192
}
188193

189-
// A helper function that computes `H(R || A || M)` where `H` is the 512-bit hash function
190-
// given by `CtxDigest` (this is SHA-512 in spec-compliant Ed25519). If `context.is_some()`,
191-
// this does the prehashed variant of the computation using its contents.
192-
#[allow(non_snake_case)]
193-
fn compute_challenge<CtxDigest>(
194-
context: Option<&[u8]>,
195-
R: &CompressedEdwardsY,
196-
A: &CompressedEdwardsY,
197-
M: &[u8],
198-
) -> Scalar
199-
where
200-
CtxDigest: Digest<OutputSize = U64>,
201-
{
202-
let mut h = CtxDigest::new();
203-
if let Some(c) = context {
204-
h.update(b"SigEd25519 no Ed25519 collisions");
205-
h.update([1]); // Ed25519ph
206-
h.update([c.len() as u8]);
207-
h.update(c);
208-
}
209-
h.update(R.as_bytes());
210-
h.update(A.as_bytes());
211-
h.update(M);
212-
213-
Scalar::from_hash(h)
214-
}
215-
216-
// Helper function for verification. Computes the _expected_ R component of the signature. The
217-
// caller compares this to the real R component. If `context.is_some()`, this does the
218-
// prehashed variant of the computation using its contents.
219-
// Note that this returns the compressed form of R and the caller does a byte comparison. This
220-
// means that all our verification functions do not accept non-canonically encoded R values.
221-
// See the validation criteria blog post for more details:
222-
// https://hdevalence.ca/blog/2020-10-04-its-25519am
223-
#[allow(non_snake_case)]
224-
fn recompute_R<CtxDigest>(
225-
&self,
226-
context: Option<&[u8]>,
227-
signature: &InternalSignature,
228-
M: &[u8],
229-
) -> CompressedEdwardsY
230-
where
231-
CtxDigest: Digest<OutputSize = U64>,
232-
{
233-
let k = Self::compute_challenge::<CtxDigest>(context, &signature.R, &self.compressed, M);
234-
let minus_A: EdwardsPoint = -self.point;
235-
// Recall the (non-batched) verification equation: -[k]A + [s]B = R
236-
EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &(minus_A), &signature.s).compress()
237-
}
238-
239194
/// The ordinary non-batched Ed25519 verification check, rejecting non-canonical R values. (see
240-
/// [`Self::recompute_R`]). `CtxDigest` is the digest used to calculate the pseudorandomness
195+
/// [`Self::RCompute`]). `CtxDigest` is the digest used to calculate the pseudorandomness
241196
/// needed for signing. According to the spec, `CtxDigest = Sha512`.
242197
///
243198
/// This definition is loose in its parameters so that end-users of the `hazmat` module can
@@ -253,7 +208,7 @@ impl VerifyingKey {
253208
{
254209
let signature = InternalSignature::try_from(signature)?;
255210

256-
let expected_R = self.recompute_R::<CtxDigest>(None, &signature, message);
211+
let expected_R = RCompute::<CtxDigest>::compute(self, signature, None, message);
257212
if expected_R == signature.R {
258213
Ok(())
259214
} else {
@@ -289,7 +244,8 @@ impl VerifyingKey {
289244
);
290245

291246
let message = prehashed_message.finalize();
292-
let expected_R = self.recompute_R::<CtxDigest>(Some(ctx), &signature, &message);
247+
248+
let expected_R = RCompute::<CtxDigest>::compute(self, signature, Some(ctx), &message);
293249

294250
if expected_R == signature.R {
295251
Ok(())
@@ -415,16 +371,30 @@ impl VerifyingKey {
415371
return Err(InternalError::Verify.into());
416372
}
417373

418-
let expected_R = self.recompute_R::<Sha512>(None, &signature, message);
374+
let expected_R = RCompute::<Sha512>::compute(self, signature, None, message);
419375
if expected_R == signature.R {
420376
Ok(())
421377
} else {
422378
Err(InternalError::Verify.into())
423379
}
424380
}
425381

382+
/// Constructs stream verifier with candidate `signature`.
383+
///
384+
/// Useful for cases where the whole message is not available all at once, allowing the
385+
/// internal signature state to be updated incrementally and verified at the end. In some cases,
386+
/// this will reduce the need for additional allocations.
387+
#[cfg(feature = "hazmat")]
388+
pub fn verify_stream(
389+
&self,
390+
signature: &ed25519::Signature,
391+
) -> Result<StreamVerifier, SignatureError> {
392+
let signature = InternalSignature::try_from(signature)?;
393+
Ok(StreamVerifier::new(*self, signature))
394+
}
395+
426396
/// Verify a `signature` on a `prehashed_message` using the Ed25519ph algorithm,
427-
/// using strict signture checking as defined by [`Self::verify_strict`].
397+
/// using strict signature checking as defined by [`Self::verify_strict`].
428398
///
429399
/// # Inputs
430400
///
@@ -477,7 +447,7 @@ impl VerifyingKey {
477447
}
478448

479449
let message = prehashed_message.finalize();
480-
let expected_R = self.recompute_R::<Sha512>(Some(ctx), &signature, &message);
450+
let expected_R = RCompute::<Sha512>::compute(self, signature, Some(ctx), &message);
481451

482452
if expected_R == signature.R {
483453
Ok(())
@@ -511,6 +481,79 @@ impl VerifyingKey {
511481
}
512482
}
513483

484+
/// Helper for verification. Computes the _expected_ R component of the signature. The
485+
/// caller compares this to the real R component.
486+
/// This computes `H(R || A || M)` where `H` is the 512-bit hash function
487+
/// given by `CtxDigest` (this is SHA-512 in spec-compliant Ed25519).
488+
///
489+
/// For pre-hashed variants a `h` with the context already included can be provided.
490+
/// Note that this returns the compressed form of R and the caller does a byte comparison. This
491+
/// means that all our verification functions do not accept non-canonically encoded R values.
492+
/// See the validation criteria blog post for more details:
493+
/// https://hdevalence.ca/blog/2020-10-04-its-25519am
494+
pub(crate) struct RCompute<CtxDigest> {
495+
key: VerifyingKey,
496+
signature: InternalSignature,
497+
h: CtxDigest,
498+
}
499+
500+
#[allow(non_snake_case)]
501+
impl<CtxDigest> RCompute<CtxDigest>
502+
where
503+
CtxDigest: Digest<OutputSize = U64>,
504+
{
505+
/// If `prehash_ctx.is_some()`, this does the prehashed variant of the computation using its
506+
/// contents.
507+
pub(crate) fn compute(
508+
key: &VerifyingKey,
509+
signature: InternalSignature,
510+
prehash_ctx: Option<&[u8]>,
511+
message: &[u8],
512+
) -> CompressedEdwardsY {
513+
let mut c = Self::new(key, signature, prehash_ctx);
514+
c.update(message);
515+
c.finish()
516+
}
517+
518+
pub(crate) fn new(
519+
key: &VerifyingKey,
520+
signature: InternalSignature,
521+
prehash_ctx: Option<&[u8]>,
522+
) -> Self {
523+
let R = &signature.R;
524+
let A = &key.compressed;
525+
526+
let mut h = CtxDigest::new();
527+
if let Some(c) = prehash_ctx {
528+
h.update(b"SigEd25519 no Ed25519 collisions");
529+
h.update([1]); // Ed25519ph
530+
h.update([c.len() as u8]);
531+
h.update(c);
532+
}
533+
534+
h.update(R.as_bytes());
535+
h.update(A.as_bytes());
536+
Self {
537+
key: *key,
538+
signature,
539+
h,
540+
}
541+
}
542+
543+
pub(crate) fn update(&mut self, m: &[u8]) {
544+
self.h.update(m)
545+
}
546+
547+
pub(crate) fn finish(self) -> CompressedEdwardsY {
548+
let k = Scalar::from_hash(self.h);
549+
550+
let minus_A: EdwardsPoint = -self.key.point;
551+
// Recall the (non-batched) verification equation: -[k]A + [s]B = R
552+
EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &(minus_A), &self.signature.s)
553+
.compress()
554+
}
555+
}
556+
514557
impl Verifier<ed25519::Signature> for VerifyingKey {
515558
/// Verify a signature on a message with this keypair's public key.
516559
///

ed25519-dalek/src/verifying/stream.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use curve25519_dalek::edwards::CompressedEdwardsY;
2+
use sha2::Sha512;
3+
4+
use crate::verifying::RCompute;
5+
use crate::{signature::InternalSignature, InternalError, SignatureError, VerifyingKey};
6+
7+
/// An IUF verifier for ed25519.
8+
///
9+
/// Created with [`VerifyingKey::verify_stream()`] or [`SigningKey::verify_stream()`].
10+
///
11+
/// [`SigningKey::verify_stream()`]: super::SigningKey::verify_stream()
12+
#[allow(non_snake_case)]
13+
pub struct StreamVerifier {
14+
cr: RCompute<Sha512>,
15+
sig_R: CompressedEdwardsY,
16+
}
17+
18+
impl StreamVerifier {
19+
/// Constructs new stream verifier.
20+
///
21+
/// Seeds hash state with public key and signature components.
22+
pub(crate) fn new(public_key: VerifyingKey, signature: InternalSignature) -> Self {
23+
Self {
24+
cr: RCompute::new(&public_key, signature, None),
25+
sig_R: signature.R,
26+
}
27+
}
28+
29+
/// Digest message chunk.
30+
pub fn update(&mut self, chunk: impl AsRef<[u8]>) {
31+
self.cr.update(chunk.as_ref());
32+
}
33+
34+
/// Finalize verifier and check against candidate signature.
35+
#[allow(non_snake_case)]
36+
pub fn finalize_and_verify(self) -> Result<(), SignatureError> {
37+
let expected_R = self.cr.finish();
38+
39+
if expected_R == self.sig_R {
40+
Ok(())
41+
} else {
42+
Err(InternalError::Verify.into())
43+
}
44+
}
45+
}

ed25519-dalek/tests/ed25519.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,50 @@ mod integrations {
334334
);
335335
}
336336

337+
#[cfg(feature = "digest")]
338+
#[test]
339+
fn sign_verify_digest_equivalence() {
340+
// TestSignVerify
341+
342+
let mut csprng = OsRng {};
343+
344+
let good: &[u8] = "test message".as_bytes();
345+
let bad: &[u8] = "wrong message".as_bytes();
346+
347+
let keypair: SigningKey = SigningKey::generate(&mut csprng);
348+
let good_sig: Signature = keypair.sign(good);
349+
let bad_sig: Signature = keypair.sign(bad);
350+
351+
let mut verifier = keypair.verify_stream(&good_sig).unwrap();
352+
verifier.update(good);
353+
assert!(
354+
verifier.finalize_and_verify().is_ok(),
355+
"Verification of a valid signature failed!"
356+
);
357+
358+
let mut verifier = keypair.verify_stream(&bad_sig).unwrap();
359+
verifier.update(good);
360+
assert!(
361+
verifier.finalize_and_verify().is_err(),
362+
"Verification of a signature on a different message passed!"
363+
);
364+
365+
let mut verifier = keypair.verify_stream(&good_sig).unwrap();
366+
verifier.update("test ");
367+
verifier.update("message");
368+
assert!(
369+
verifier.finalize_and_verify().is_ok(),
370+
"Verification of a valid signature failed!"
371+
);
372+
373+
let mut verifier = keypair.verify_stream(&good_sig).unwrap();
374+
verifier.update(bad);
375+
assert!(
376+
verifier.finalize_and_verify().is_err(),
377+
"Verification of a signature on a different message passed!"
378+
);
379+
}
380+
337381
#[cfg(feature = "digest")]
338382
#[test]
339383
fn ed25519ph_sign_verify() {

0 commit comments

Comments
 (0)