Skip to content
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

Sign/verify by digest update, StreamVerifier refactoring #556

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions ed25519-dalek/CHANGELOG.md
Expand Up @@ -8,6 +8,10 @@ Entries are listed in reverse chronological order per undeprecated major series.

# Unreleased

* Add `SigningKey::verify_stream()`, and `VerifyingKey::verify_stream()`
* Add `hazmat` `raw_sign_byupdate()` and `raw_verify_byupdate()` to allow
passing a message incrementally.

# 2.x series

## 2.1.0
Expand Down
155 changes: 155 additions & 0 deletions ed25519-dalek/src/hazmat.rs
Expand Up @@ -169,6 +169,34 @@ where
esk.raw_sign_prehashed::<CtxDigest, MsgDigest>(prehashed_message, verifying_key, context)
}

/// Compute an ordinary Ed25519 signature, with the message contents provided incrementally
/// by updating a digest instance.
///
/// The `msg_update` closure provides the message content, updating a hash argument.
/// It will be called twice.
///
/// `CtxDigest` is the digest used to
/// calculate the pseudorandomness needed for signing. According to the Ed25519 spec, `CtxDigest =
/// Sha512`.
///
///
/// # ⚠️ 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`.
Expand Down Expand Up @@ -202,6 +230,26 @@ where
vk.raw_verify_prehashed::<CtxDigest, MsgDigest>(prehashed_message, context, signature)
}

/// Performs an ordinary Ed25519 verification check, with the message passed incrementally.
///
/// Instead of passing the message directly ([`raw_verify()`]), the caller
/// provides a `msg_update` closure that will be called to feed the
/// hash of the message being verified.
///
/// `CtxDigest` is the digest used to calculate the pseudorandomness needed for signing.
/// According to the Ed25519 spec, `CtxDigest = Sha512`.
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)]
Expand Down Expand Up @@ -263,4 +311,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
Expand Up @@ -44,7 +44,7 @@ use crate::{
errors::{InternalError, SignatureError},
hazmat::ExpandedSecretKey,
signature::InternalSignature,
verifying::VerifyingKey,
verifying::{StreamVerifier, VerifyingKey},
Signature,
};

Expand Down Expand Up @@ -483,6 +483,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 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.
Expand Down Expand Up @@ -806,6 +816,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,
Expand All @@ -814,24 +825,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
Expand Down