Skip to content

Sign/verify by digest update, StreamVerifier refactoring #583

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ed25519-dalek/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -34,6 +34,7 @@ Entries are listed in reverse chronological order per undeprecated major series.
* Add `pkcs` feature to support PKCS #8 (de)serialization of `SigningKey` and `VerifyingKey`
* Add `fast` feature to include basepoint tables
* Add tests for validation criteria
* Add `SigningKey::verify_stream()`, and `VerifyingKey::verify_stream()`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this entry should probably be moved up out of the 2.0.0 section

* Impl `DigestSigner`/`DigestVerifier` for `SigningKey`/`VerifyingKey`, respectively
* Impl `Hash` for `VerifyingKey`
* Impl `Clone`, `Drop`, and `ZeroizeOnDrop` for `SigningKey`
150 changes: 150 additions & 0 deletions ed25519-dalek/src/hazmat.rs
Original file line number Diff line number Diff line change
@@ -169,6 +169,30 @@ where
esk.raw_sign_prehashed::<CtxDigest, MsgDigest>(prehashed_message, verifying_key, context)
}

/// Compute an ordinary Ed25519 signature over the given message. `CtxDigest` is the digest used to
/// calculate the pseudorandomness needed for signing. According to the Ed25519 spec, `CtxDigest =
/// Sha512`.
///
/// The `msg_update` closure provides the message content, updating a hash argument.
/// It will be called twice.
///
/// # ⚠️ Unsafe
///
/// Do NOT use this function unless you absolutely must. Using the wrong values in
/// `ExpandedSecretKey` can leak your signing key. See
/// [here](https://github.com/MystenLabs/ed25519-unsafe-libs) for more details on this attack.
pub fn raw_sign_byupdate<CtxDigest, F>(
esk: &ExpandedSecretKey,
msg_update: F,
verifying_key: &VerifyingKey,
) -> Result<Signature, SignatureError>
where
CtxDigest: Digest<OutputSize = U64>,
F: Fn(&mut CtxDigest) -> Result<(), SignatureError>,
{
esk.raw_sign_byupdate::<CtxDigest, F>(msg_update, verifying_key)
}

/// The ordinary non-batched Ed25519 verification check, rejecting non-canonical R
/// values.`CtxDigest` is the digest used to calculate the pseudorandomness needed for signing.
/// According to the Ed25519 spec, `CtxDigest = Sha512`.
@@ -202,6 +226,25 @@ where
vk.raw_verify_prehashed::<CtxDigest, MsgDigest>(prehashed_message, context, signature)
}

/// The ordinary non-batched Ed25519 verification check, rejecting non-canonical R
/// values.`CtxDigest` is the digest used to calculate the pseudorandomness needed for signing.
/// According to the Ed25519 spec, `CtxDigest = Sha512`.
/// Instead of passing the message directly (`sign()`), the caller
/// provides a `msg_update` closure that will be called to feed the
/// hash of the message being signed.

pub fn raw_verify_byupdate<CtxDigest, F>(
vk: &VerifyingKey,
msg_update: F,
signature: &ed25519::Signature,
) -> Result<(), SignatureError>
where
CtxDigest: Digest<OutputSize = U64>,
F: Fn(&mut CtxDigest) -> Result<(), SignatureError>,
{
vk.raw_verify_byupdate::<CtxDigest, F>(msg_update, signature)
}

#[cfg(test)]
mod test {
#![allow(clippy::unwrap_used)]
@@ -263,4 +306,111 @@ mod test {
.unwrap();
raw_verify_prehashed::<CtxDigest, MsgDigest>(&vk, h, Some(ctx_str), &sig).unwrap();
}

#[test]
fn sign_byupdate() {
// Generate the keypair
let mut rng = OsRng;
let esk = ExpandedSecretKey::random(&mut rng);
let vk = VerifyingKey::from(&esk);

let msg = b"realistic";
// signatures are deterministic so we can compare with a good one
let good_sig = raw_sign::<CtxDigest>(&esk, msg, &vk);

let sig = raw_sign_byupdate::<CtxDigest, _>(
&esk,
|h| {
h.update(msg);
Ok(())
},
&vk,
);
assert!(sig.unwrap() == good_sig, "sign byupdate matches");

let sig = raw_sign_byupdate::<CtxDigest, _>(
&esk,
|h| {
h.update(msg);
Err(SignatureError::new())
},
&vk,
);
assert!(sig.is_err(), "sign byupdate failure propagates");

let sig = raw_sign_byupdate::<CtxDigest, _>(
&esk,
|h| {
h.update(&msg[..1]);
h.update(&msg[1..]);
Ok(())
},
&vk,
);
assert!(sig.unwrap() == good_sig, "sign byupdate two part");
}

#[test]
fn verify_byupdate() {
// Generate the keypair
let mut rng = OsRng;
let esk = ExpandedSecretKey::random(&mut rng);
let vk = VerifyingKey::from(&esk);

let msg = b"Torrens title";
let sig = raw_sign::<CtxDigest>(&esk, msg, &vk);
let wrong_sig = raw_sign::<CtxDigest>(&esk, b"nope", &vk);

let r = raw_verify_byupdate::<CtxDigest, _>(
&vk,
|h| {
h.update(msg);
Ok(())
},
&sig,
);
assert!(r.is_ok(), "verify byupdate success");

let r = raw_verify_byupdate::<CtxDigest, _>(
&vk,
|h| {
h.update(msg);
Ok(())
},
&wrong_sig,
);
assert!(r.is_err(), "verify byupdate wrong fails");

let r = raw_verify_byupdate::<CtxDigest, _>(
&vk,
|h| {
h.update(&msg[..5]);
h.update(&msg[5..]);
Ok(())
},
&sig,
);
assert!(r.is_ok(), "verify byupdate two-part");

let r = raw_verify_byupdate::<CtxDigest, _>(
&vk,
|h| {
h.update(msg);
h.update(b"X");
Ok(())
},
&sig,
);
assert!(r.is_err(), "verify byupdate extra fails");

let r = raw_verify_byupdate::<CtxDigest, _>(
&vk,
|h| {
h.update(msg);
Err(SignatureError::new())
},
&sig,
);
assert!(r.is_err(), "verify byupdate error propagates");
}
}
43 changes: 39 additions & 4 deletions ed25519-dalek/src/signing.rs
Original file line number Diff line number Diff line change
@@ -42,7 +42,7 @@ use crate::{
errors::{InternalError, SignatureError},
hazmat::ExpandedSecretKey,
signature::InternalSignature,
verifying::VerifyingKey,
verifying::{StreamVerifier, VerifyingKey},
Signature,
};

@@ -481,6 +481,16 @@ impl SigningKey {
self.verifying_key.verify_strict(message, signature)
}

/// Constructs stream verifier with candidate `signature`.
///
/// See [`VerifyingKey::verify_stream()`] for more details.
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 a(n) (unreduced) Curve25519 scalar.
///
/// This can be used for performing X25519 Diffie-Hellman using Ed25519 keys. The bytes output
@@ -766,6 +776,7 @@ impl ExpandedSecretKey {
/// This definition is loose in its parameters so that end-users of the `hazmat` module can
/// change how the `ExpandedSecretKey` is calculated and which hash function to use.
#[allow(non_snake_case)]
#[allow(clippy::unwrap_used)]
#[inline(always)]
pub(crate) fn raw_sign<CtxDigest>(
&self,
@@ -774,24 +785,48 @@ impl ExpandedSecretKey {
) -> Signature
where
CtxDigest: Digest<OutputSize = U64>,
{
// OK unwrap, update can't fail.
self.raw_sign_byupdate(
|h: &mut CtxDigest| {
h.update(message);
Ok(())
},
verifying_key,
)
.unwrap()
}

/// Sign a message provided in parts. The `msg_update` closure
/// will be called twice to hash the message parts.
#[allow(non_snake_case)]
#[inline(always)]
pub(crate) fn raw_sign_byupdate<CtxDigest, F>(
&self,
msg_update: F,
verifying_key: &VerifyingKey,
) -> Result<Signature, SignatureError>
where
CtxDigest: Digest<OutputSize = U64>,
F: Fn(&mut CtxDigest) -> Result<(), SignatureError>,
{
let mut h = CtxDigest::new();

h.update(self.hash_prefix);
h.update(message);
msg_update(&mut h)?;

let r = Scalar::from_hash(h);
let R: CompressedEdwardsY = EdwardsPoint::mul_base(&r).compress();

h = CtxDigest::new();
h.update(R.as_bytes());
h.update(verifying_key.as_bytes());
h.update(message);
msg_update(&mut h)?;

let k = Scalar::from_hash(h);
let s: Scalar = (k * self.scalar) + r;

InternalSignature { R, s }.into()
Ok(InternalSignature { R, s }.into())
}

/// The prehashed signing function for Ed25519 (i.e., Ed25519ph). `CtxDigest` is the digest
170 changes: 114 additions & 56 deletions ed25519-dalek/src/verifying.rs
Original file line number Diff line number Diff line change
@@ -43,6 +43,9 @@ use crate::{
signing::SigningKey,
};

mod stream;
pub use self::stream::StreamVerifier;

/// An ed25519 public key.
///
/// # Note
@@ -187,58 +190,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
@@ -254,7 +207,30 @@ 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 {
Err(InternalError::Verify.into())
}
}

#[allow(non_snake_case)]
pub(crate) fn raw_verify_byupdate<CtxDigest, F>(
&self,
msg_update: F,
signature: &ed25519::Signature,
) -> Result<(), SignatureError>
where
CtxDigest: Digest<OutputSize = U64>,
F: Fn(&mut CtxDigest) -> Result<(), SignatureError>,
{
let signature = InternalSignature::try_from(signature)?;

let mut c = RCompute::<CtxDigest>::new(self, signature, None);
msg_update(&mut c.h)?;
let expected_R = c.finish();

if expected_R == signature.R {
Ok(())
} else {
@@ -290,7 +266,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(())
@@ -416,16 +393,29 @@ 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 {
Err(InternalError::Verify.into())
}
}

/// 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_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
///
@@ -478,7 +468,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(())
@@ -507,6 +497,74 @@ impl VerifyingKey {
}
}

// Helper for verification. Computes the _expected_ R component of the signature. The
// caller compares this to the real R component.
// For prehashed 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>,
{
pub 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 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 fn update(&mut self, m: &[u8]) {
self.h.update(m)
}

pub 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 changes: 45 additions & 0 deletions ed25519-dalek/src/verifying/stream.rs
Original file line number Diff line number Diff line change
@@ -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())
}
}
}
39 changes: 39 additions & 0 deletions ed25519-dalek/tests/ed25519.rs
Original file line number Diff line number Diff line change
@@ -335,6 +335,45 @@ mod integrations {
);
}

#[cfg(feature = "digest")]
#[test]
fn sign_verify_digest_equivalence() {
// TestSignVerify
let keypair: SigningKey;
let good_sig: Signature;
let bad_sig: Signature;

let good: &[u8] = "test message".as_bytes();
let bad: &[u8] = "wrong message".as_bytes();

let mut csprng = OsRng {};

keypair = SigningKey::generate(&mut csprng);
good_sig = keypair.sign(&good);
bad_sig = 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(&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() {