ed: Expose hazmat::raw_sign_byupdate() for streamed signing (#765)

* Added raw_sign_byupdate to hazmat; refactored other funcs to use that directly
This commit is contained in:
Michael Rosenberg 2025-06-03 09:14:22 -04:00 committed by GitHub
parent 6dc7a1c7c5
commit 08d7176d58
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 101 additions and 5 deletions

View file

@ -106,7 +106,7 @@ impl TryFrom<&[u8]> for ExpandedSecretKey {
/// calculate the pseudorandomness needed for signing. According to the Ed25519 spec, `CtxDigest =
/// Sha512`.
///
/// # ⚠️ Unsafe
/// # ⚠️ Cryptographically Unsafe
///
/// Do NOT use this function unless you absolutely must. Using the wrong values in
/// `ExpandedSecretKey` can leak your signing key. See
@ -127,7 +127,7 @@ where
/// `CtxDigest` is the digest function used to calculate the pseudorandomness needed for signing.
/// According to the Ed25519 spec, `MsgDigest = CtxDigest = Sha512`.
///
/// # ⚠️ Unsafe
/// # ⚠️ Cryptographically Unsafe
//
/// Do NOT use this function unless you absolutely must. Using the wrong values in
/// `ExpandedSecretKey` can leak your signing key. See
@ -169,6 +169,33 @@ 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 hasher argument. It will be
/// called twice. This closure MUST leave its hasher in the same state (i.e., must hash the same
/// values) after both calls. Otherwise it will produce an invalid signature.
///
/// `CtxDigest` is the digest used to calculate the pseudorandomness needed for signing. According
/// to the Ed25519 spec, `CtxDigest = Sha512`.
///
/// # ⚠️ Cryptographically 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`.
@ -263,4 +290,47 @@ 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");
}
}

View file

@ -833,6 +833,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,
@ -841,11 +842,36 @@ 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. This closure MUST leave its hasher in the same state (i.e., must hash the
/// same values) after both calls. Otherwise it will produce an invalid signature.
#[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();
@ -853,12 +879,12 @@ impl ExpandedSecretKey {
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