diff --git a/CHANGELOG.md b/CHANGELOG.md index 40ddabe..3657c20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Entries are listed in reverse chronological order per undeprecated major series. * Make all batch verification deterministic remove `batch_deterministic` ([#256](https://github.com/dalek-cryptography/ed25519-dalek/pull/256)) * Remove `ExpandedSecretKey` API ((#205)[https://github.com/dalek-cryptography/ed25519-dalek/pull/205]) * Rename `Keypair` → `SigningKey` and `PublicKey` → `VerifyingKey` +* Make `hazmat` feature to expose, `ExpandedSecretKey`, `raw_sign()`, `raw_sign_prehashed()`, `raw_verify()`, and `raw_verify_prehashed()` ### Other changes diff --git a/Cargo.lock b/Cargo.lock index 1eb6a42..5ef6955 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,6 +46,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.3" @@ -272,6 +281,7 @@ checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -290,6 +300,7 @@ name = "ed25519-dalek" version = "2.0.0-rc.2" dependencies = [ "bincode", + "blake2", "criterion", "curve25519-dalek", "ed25519", @@ -301,6 +312,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "sha3", "signature", "toml", "zeroize", @@ -736,6 +748,16 @@ dependencies = [ "cc", ] +[[package]] +name = "sha3" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c2bb1a323307527314a36bfb73f24febb08ce2b8a554bf4ffd6f51ad15198c" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "signature" version = "2.0.0" diff --git a/Cargo.toml b/Cargo.toml index 5c73858..cdbe10a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ rustdoc-args = [ "--html-in-header", "docs/assets/rustdoc-include-katex-header.html", "--cfg", "docsrs", ] -features = ["batch", "pkcs8"] +features = ["batch", "digest", "hazmat", "pem", "serde"] [dependencies] curve25519-dalek = { version = "=4.0.0-rc.2", default-features = false, features = ["digest"] } @@ -38,6 +38,8 @@ zeroize = { version = "1.5", default-features = false, optional = true } [dev-dependencies] curve25519-dalek = { version = "=4.0.0-rc.2", default-features = false, features = ["digest", "rand_core"] } +blake2 = "0.10" +sha3 = "0.10" hex = "0.4" bincode = "1.0" serde_json = "1.0" @@ -62,7 +64,9 @@ asm = ["sha2/asm"] batch = ["alloc", "merlin", "rand_core"] fast = ["curve25519-dalek/precomputed-tables"] digest = ["signature/digest"] -# This features turns off stricter checking for scalar malleability in signatures +# Exposes the hazmat module +hazmat = [] +# Turns off stricter checking for scalar malleability in signatures legacy_compatibility = [] pkcs8 = ["ed25519/pkcs8"] pem = ["alloc", "ed25519/pem", "pkcs8"] diff --git a/README.md b/README.md index feedee8..c5c279f 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ This crate is `#[no_std]` compatible with `default-features = false`. | `pkcs8` | | Enables [PKCS#8](https://en.wikipedia.org/wiki/PKCS_8) serialization/deserialization for `SigningKey` and `VerifyingKey` | | `pem` | | Enables PEM serialization support for PKCS#8 private keys and SPKI public keys. Also enables `alloc`. | | `legacy_compatibility` | | **Unsafe:** Disables certain signature checks. See [below](#malleability-and-the-legacy_compatibility-feature) | +| `hazmat` | | **Unsafe:** Exposes the `hazmat` module for raw signing/verifying. Misuse of these functions will expose the private key, as in the [signing oracle attack](https://github.com/MystenLabs/ed25519-unsafe-libs). | # Major Changes @@ -54,6 +55,7 @@ See [CHANGELOG.md](CHANGELOG.md) for a list of changes made in past version of t * Make all batch verification deterministic remove `batch_deterministic` ([#256](https://github.com/dalek-cryptography/ed25519-dalek/pull/256)) * Remove `ExpandedSecretKey` API ((#205)[https://github.com/dalek-cryptography/ed25519-dalek/pull/205]) * Rename `Keypair` → `SigningKey` and `PublicKey` → `VerifyingKey` +* Make `hazmat` feature to expose, `ExpandedSecretKey`, `raw_sign()`, `raw_sign_prehashed()`, `raw_verify()`, and `raw_verify_prehashed()` # Documentation diff --git a/src/hazmat.rs b/src/hazmat.rs new file mode 100644 index 0000000..5f16d3a --- /dev/null +++ b/src/hazmat.rs @@ -0,0 +1,280 @@ +//! Low-level interfaces to ed25519 functions +//! +//! # ⚠️ Warning: Hazmat +//! +//! These primitives are easy-to-misuse low-level interfaces. +//! +//! If you are an end user / non-expert in cryptography, **do not use any of these functions**. +//! Failure to use them correctly can lead to catastrophic failures including **full private key +//! recovery.** + +// Permit dead code because 1) this module is only public when the `hazmat` feature is set, and 2) +// even without `hazmat` we still need this module because this is where `ExpandedSecretKey` is +// defined. +#![allow(dead_code)] + +use crate::{InternalError, SignatureError}; + +use curve25519_dalek::Scalar; + +#[cfg(feature = "zeroize")] +use zeroize::{Zeroize, ZeroizeOnDrop}; + +// These are used in the functions that are made public when the hazmat feature is set +use crate::{Signature, VerifyingKey}; +use curve25519_dalek::digest::{generic_array::typenum::U64, Digest}; + +/// Contains the secret scalar and domain separator used for generating signatures. +/// +/// This is used internally for signing. +/// +/// In the usual Ed25519 signing algorithm, `scalar` and `hash_prefix` are defined such that +/// `scalar || hash_prefix = H(sk)` where `sk` is the signing key and `H` is SHA-512. +/// **WARNING:** Deriving the values for these fields in any other way can lead to full key +/// recovery, as documented in [`raw_sign`] and [`raw_sign_prehashed`]. +/// +/// Instances of this secret are automatically overwritten with zeroes when they fall out of scope. +pub struct ExpandedSecretKey { + /// The secret scalar used for signing + pub scalar: Scalar, + /// The domain separator used when hashing the message to generate the pseudorandom `r` value + pub hash_prefix: [u8; 32], +} + +#[cfg(feature = "zeroize")] +impl Drop for ExpandedSecretKey { + fn drop(&mut self) { + self.scalar.zeroize(); + self.hash_prefix.zeroize() + } +} + +#[cfg(feature = "zeroize")] +impl ZeroizeOnDrop for ExpandedSecretKey {} + +// Some conversion methods for `ExpandedSecretKey`. The signing methods are defined in +// `signing.rs`, since we need them even when `not(feature = "hazmat")` +impl ExpandedSecretKey { + /// Convert this `ExpandedSecretKey` into an array of 64 bytes. + pub fn to_bytes(&self) -> [u8; 64] { + let mut bytes: [u8; 64] = [0u8; 64]; + + bytes[..32].copy_from_slice(self.scalar.as_bytes()); + bytes[32..].copy_from_slice(&self.hash_prefix[..]); + bytes + } + + /// Construct an `ExpandedSecretKey` from an array of 64 bytes. + pub fn from_bytes(bytes: &[u8; 64]) -> Self { + // TODO: Use bytes.split_array_ref once it’s in MSRV. + let mut lower: [u8; 32] = [0u8; 32]; + let mut upper: [u8; 32] = [0u8; 32]; + + lower.copy_from_slice(&bytes[00..32]); + upper.copy_from_slice(&bytes[32..64]); + + ExpandedSecretKey { + scalar: Scalar::from_bytes_mod_order(lower), + hash_prefix: upper, + } + } + + /// Construct an `ExpandedSecretKey` from a slice of 64 bytes. + /// + /// # Returns + /// + /// A `Result` whose okay value is an EdDSA `ExpandedSecretKey` or whose error value is an + /// `SignatureError` describing the error that occurred, namely that the given slice's length + /// is not 64. + #[allow(clippy::unwrap_used)] + pub fn from_slice(bytes: &[u8]) -> Result { + if bytes.len() != 64 { + Err(InternalError::BytesLength { + name: "ExpandedSecretKey", + length: 64, + } + .into()) + } else { + // If the input is 64 bytes long, coerce it to a 64-byte array + Ok(Self::from_bytes(bytes.try_into().unwrap())) + } + } +} + +impl TryFrom<&[u8]> for ExpandedSecretKey { + type Error = SignatureError; + + fn try_from(bytes: &[u8]) -> Result { + Self::from_slice(bytes) + } +} + +/// 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`. +/// +/// # ⚠️ 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( + esk: &ExpandedSecretKey, + message: &[u8], + verifying_key: &VerifyingKey, +) -> Signature +where + CtxDigest: Digest, +{ + esk.raw_sign::(message, verifying_key) +} + +/// Compute a signature over the given prehashed message, the Ed25519ph algorithm defined in +/// [RFC8032 §5.1][rfc8032]. `MsgDigest` is the digest function used to hash the signed message. +/// `CtxDigest` is the digest function used to calculate the pseudorandomness needed for signing. +/// According to the Ed25519 spec, `MsgDigest = 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. +/// +/// # Inputs +/// +/// * `esk` is the [`ExpandedSecretKey`] being used for signing +/// * `prehashed_message` is an instantiated hash digest with 512-bits of +/// output which has had the message to be signed previously fed into its +/// state. +/// * `verifying_key` is a [`VerifyingKey`] which corresponds to this secret key. +/// * `context` is an optional context string, up to 255 bytes inclusive, +/// which may be used to provide additional domain separation. If not +/// set, this will default to an empty string. +/// +/// `scalar` and `hash_prefix` are usually selected such that `scalar || hash_prefix = H(sk)` where +/// `sk` is the signing key +/// +/// # Returns +/// +/// A `Result` whose `Ok` value is an Ed25519ph [`Signature`] on the +/// `prehashed_message` if the context was 255 bytes or less, otherwise +/// a `SignatureError`. +/// +/// [rfc8032]: https://tools.ietf.org/html/rfc8032#section-5.1 +#[cfg(feature = "digest")] +#[allow(non_snake_case)] +pub fn raw_sign_prehashed<'a, CtxDigest, MsgDigest>( + esk: &ExpandedSecretKey, + prehashed_message: MsgDigest, + verifying_key: &VerifyingKey, + context: Option<&'a [u8]>, +) -> Result +where + MsgDigest: Digest, + CtxDigest: Digest, +{ + esk.raw_sign_prehashed::(prehashed_message, verifying_key, context) +} + +/// 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`. +pub fn raw_verify( + vk: &VerifyingKey, + message: &[u8], + signature: &ed25519::Signature, +) -> Result<(), SignatureError> +where + CtxDigest: Digest, +{ + vk.raw_verify::(message, signature) +} + +/// The batched Ed25519 verification check, rejecting non-canonical R values. `MsgDigest` is the +/// digest used to hash the signed message. `CtxDigest` is the digest used to calculate the +/// pseudorandomness needed for signing. According to the Ed25519 spec, `MsgDigest = CtxDigest = +/// Sha512`. +#[cfg(feature = "digest")] +#[allow(non_snake_case)] +pub fn raw_verify_prehashed( + vk: &VerifyingKey, + prehashed_message: MsgDigest, + context: Option<&[u8]>, + signature: &ed25519::Signature, +) -> Result<(), SignatureError> +where + MsgDigest: Digest, + CtxDigest: Digest, +{ + vk.raw_verify_prehashed::(prehashed_message, context, signature) +} + +#[cfg(test)] +mod test { + use super::*; + + use curve25519_dalek::Scalar; + use rand::{rngs::OsRng, CryptoRng, RngCore}; + + // Pick distinct, non-spec 512-bit hash functions for message and sig-context hashing + type CtxDigest = blake2::Blake2b512; + type MsgDigest = sha3::Sha3_512; + + impl ExpandedSecretKey { + // Make a random expanded secret key for testing purposes. This is NOT how you generate + // expanded secret keys IRL. They're the hash of a seed. + fn random(mut rng: R) -> Self { + // The usual signing algorithm clamps its scalars + let scalar_bytes = [0u8; 32]; + let scalar = Scalar::from_bits_clamped(scalar_bytes); + + let mut hash_prefix = [0u8; 32]; + rng.fill_bytes(&mut hash_prefix); + + ExpandedSecretKey { + scalar, + hash_prefix, + } + } + } + + // Check that raw_sign and raw_verify work when a non-spec CtxDigest is used + #[test] + fn sign_verify_nonspec() { + // Generate the keypair + let mut rng = OsRng; + let esk = ExpandedSecretKey::random(&mut rng); + let vk = VerifyingKey::from(&esk); + + let msg = b"Then one day, a piano fell on my head"; + + // Sign and verify + let sig = raw_sign::(&esk, msg, &vk); + raw_verify::(&vk, msg, &sig).unwrap(); + } + + // Check that raw_sign_prehashed and raw_verify_prehashed work when distinct, non-spec + // MsgDigest and CtxDigest are used + #[cfg(feature = "digest")] + #[test] + fn sign_verify_prehashed_nonspec() { + use curve25519_dalek::digest::Digest; + + // Generate the keypair + let mut rng = OsRng; + let esk = ExpandedSecretKey::random(&mut rng); + let vk = VerifyingKey::from(&esk); + + // Hash the message + let msg = b"And then I got trampled by a herd of buffalo"; + let mut h = MsgDigest::new(); + h.update(msg); + + let ctx_str = &b"consequences"[..]; + + // Sign and verify prehashed + let sig = raw_sign_prehashed::(&esk, h.clone(), &vk, Some(ctx_str)) + .unwrap(); + raw_verify_prehashed::(&vk, h, Some(ctx_str), &sig).unwrap(); + } +} diff --git a/src/lib.rs b/src/lib.rs index 225d871..a7cfac4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -264,6 +264,11 @@ mod signature; mod signing; mod verifying; +#[cfg(feature = "hazmat")] +pub mod hazmat; +#[cfg(not(feature = "hazmat"))] +mod hazmat; + #[cfg(feature = "digest")] pub use curve25519_dalek::digest::Digest; #[cfg(feature = "digest")] diff --git a/src/signing.rs b/src/signing.rs index 93084b8..500f8b5 100644 --- a/src/signing.rs +++ b/src/signing.rs @@ -20,12 +20,11 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use sha2::Sha512; -#[cfg(feature = "digest")] -use curve25519_dalek::digest::generic_array::typenum::U64; -use curve25519_dalek::digest::Digest; -use curve25519_dalek::edwards::CompressedEdwardsY; -use curve25519_dalek::edwards::EdwardsPoint; -use curve25519_dalek::scalar::Scalar; +use curve25519_dalek::{ + digest::{generic_array::typenum::U64, Digest}, + edwards::{CompressedEdwardsY, EdwardsPoint}, + scalar::Scalar, +}; use ed25519::signature::{KeypairRef, Signer, Verifier}; @@ -37,11 +36,14 @@ use signature::DigestSigner; #[cfg(feature = "zeroize")] use zeroize::{Zeroize, ZeroizeOnDrop}; -use crate::constants::*; -use crate::errors::*; -use crate::signature::*; -use crate::verifying::*; -use crate::Signature; +use crate::{ + constants::{KEYPAIR_LENGTH, SECRET_KEY_LENGTH}, + errors::{InternalError, SignatureError}, + hazmat::ExpandedSecretKey, + signature::InternalSignature, + verifying::VerifyingKey, + Signature, +}; /// ed25519 secret key as defined in [RFC8032 § 5.1.5]: /// @@ -202,7 +204,9 @@ impl SigningKey { /// /// # Inputs /// - /// * `prehashed_message` is an instantiated SHA-512 digest of the message + /// * `prehashed_message` is an instantiated hash digest with 512-bits of + /// output which has had the message to be signed previously fed into its + /// state. /// * `context` is an optional context string, up to 255 bytes inclusive, /// which may be used to provide additional domain separation. If not /// set, this will default to an empty string. @@ -213,10 +217,10 @@ impl SigningKey { /// /// # Note /// - /// The RFC only permits SHA-512 to be used for prehashing. This function technically works, - /// and is probably safe to use, with any secure hash function with 512-bit digests, but - /// anything outside of SHA-512 is NOT specification-compliant. We expose [`crate::Sha512`] for - /// user convenience. + /// The RFC only permits SHA-512 to be used for prehashing, i.e., `MsgDigest = Sha512`. This + /// function technically works, and is probably safe to use, with any secure hash function with + /// 512-bit digests, but anything outside of SHA-512 is NOT specification-compliant. We expose + /// [`crate::Sha512`] for user convenience. /// /// # Examples /// @@ -297,15 +301,15 @@ impl SigningKey { /// [rfc8032]: https://tools.ietf.org/html/rfc8032#section-5.1 /// [terrible_idea]: https://github.com/isislovecruft/scripts/blob/master/gpgkey2bc.py #[cfg(feature = "digest")] - pub fn sign_prehashed( + pub fn sign_prehashed( &self, - prehashed_message: D, + prehashed_message: MsgDigest, context: Option<&[u8]>, ) -> Result where - D: Digest, + MsgDigest: Digest, { - ExpandedSecretKey::from(&self.secret_key).sign_prehashed( + ExpandedSecretKey::from(&self.secret_key).raw_sign_prehashed::( prehashed_message, &self.verifying_key, context, @@ -334,6 +338,13 @@ impl SigningKey { /// Returns `true` if the `signature` was a valid signature created by this /// [`SigningKey`] on the `prehashed_message`. /// + /// # Note + /// + /// The RFC only permits SHA-512 to be used for prehashing, i.e., `MsgDigest = Sha512`. This + /// function technically works, and is probably safe to use, with any secure hash function with + /// 512-bit digests, but anything outside of SHA-512 is NOT specification-compliant. We expose + /// [`crate::Sha512`] for user convenience. + /// /// # Examples /// #[cfg_attr(all(feature = "rand_core", feature = "digest"), doc = "```")] @@ -378,14 +389,14 @@ impl SigningKey { /// /// [rfc8032]: https://tools.ietf.org/html/rfc8032#section-5.1 #[cfg(feature = "digest")] - pub fn verify_prehashed( + pub fn verify_prehashed( &self, - prehashed_message: D, + prehashed_message: MsgDigest, context: Option<&[u8]>, signature: &Signature, ) -> Result<(), SignatureError> where - D: Digest, + MsgDigest: Digest, { self.verifying_key .verify_prehashed(prehashed_message, context, signature) @@ -485,7 +496,7 @@ impl Signer for SigningKey { /// Sign a message with this signing key's secret key. fn try_sign(&self, message: &[u8]) -> Result { let expanded: ExpandedSecretKey = (&self.secret_key).into(); - Ok(expanded.sign(message, &self.verifying_key)) + Ok(expanded.raw_sign::(message, &self.verifying_key)) } } @@ -697,57 +708,9 @@ impl<'d> Deserialize<'d> for SigningKey { } } -/// An "expanded" secret key. -/// -/// This is produced by using an hash function with 512-bits output to digest a -/// `SecretKey`. The output digest is then split in half, the lower half being -/// the actual `key` used to sign messages, after twiddling with some bits.¹ The -/// upper half is used a sort of half-baked, ill-designed² pseudo-domain-separation -/// "nonce"-like thing, which is used during signature production by -/// concatenating it with the message to be signed before the message is hashed. -/// -/// Instances of this secret are automatically overwritten with zeroes when they -/// fall out of scope. -// -// ¹ This results in a slight bias towards non-uniformity at one spectrum of -// the range of valid keys. Oh well: not my idea; not my problem. -// -// ² It is the author's view (specifically, isis agora lovecruft, in the event -// you'd like to complain about me, again) that this is "ill-designed" because -// this doesn't actually provide true hash domain separation, in that in many -// real-world applications a user wishes to have one key which is used in -// several contexts (such as within tor, which does domain separation -// manually by pre-concatenating static strings to messages to achieve more -// robust domain separation). In other real-world applications, such as -// bitcoind, a user might wish to have one master keypair from which others are -// derived (à la BIP32) and different domain separators between keys derived at -// different levels (and similarly for tree-based key derivation constructions, -// such as hash-based signatures). Leaving the domain separation to -// application designers, who thus far have produced incompatible, -// slightly-differing, ad hoc domain separation (at least those application -// designers who knew enough cryptographic theory to do so!), is therefore a -// bad design choice on the part of the cryptographer designing primitives -// which should be simple and as foolproof as possible to use for -// non-cryptographers. Further, later in the ed25519 signature scheme, as -// specified in RFC8032, the public key is added into *another* hash digest -// (along with the message, again); it is unclear to this author why there's -// not only one but two poorly-thought-out attempts at domain separation in the -// same signature scheme, and which both fail in exactly the same way. For a -// better-designed, Schnorr-based signature scheme, see Trevor Perrin's work on -// "generalised EdDSA" and "VXEdDSA". -pub(crate) struct ExpandedSecretKey { - pub(crate) scalar: Scalar, - pub(crate) nonce: [u8; 32], -} - -#[cfg(feature = "zeroize")] -impl Drop for ExpandedSecretKey { - fn drop(&mut self) { - self.scalar.zeroize(); - self.nonce.zeroize() - } -} - +/// The spec-compliant way to define an expanded secret key. This computes `SHA512(sk)`, clamps the +/// first 32 bytes and uses it as a scalar, and uses the second 32 bytes as a domain separator for +/// hashing. impl From<&SecretKey> for ExpandedSecretKey { #[allow(clippy::unwrap_used)] fn from(secret_key: &SecretKey) -> ExpandedSecretKey { @@ -758,24 +721,42 @@ impl From<&SecretKey> for ExpandedSecretKey { // The try_into here converts to fixed-size array ExpandedSecretKey { scalar: Scalar::from_bits_clamped(lower.try_into().unwrap()), - nonce: upper.try_into().unwrap(), + hash_prefix: upper.try_into().unwrap(), } } } -impl ExpandedSecretKey { - /// Sign a message with this `ExpandedSecretKey`. - #[allow(non_snake_case)] - pub(crate) fn sign(&self, message: &[u8], verifying_key: &VerifyingKey) -> Signature { - let mut h: Sha512 = Sha512::new(); +// +// Signing functions. These are pub(crate) so that the `hazmat` module can use them +// - h.update(self.nonce); +impl ExpandedSecretKey { + /// The plain, non-prehashed, signing function for Ed25519. `CtxDigest` is the digest used to + /// calculate the pseudorandomness needed for signing. According to the spec, `CtxDigest = + /// Sha512`, and `self` is derived via the method defined in `impl From<&SigningKey> for + /// 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)] + #[inline(always)] + pub(crate) fn raw_sign( + &self, + message: &[u8], + verifying_key: &VerifyingKey, + ) -> Signature + where + CtxDigest: Digest, + { + let mut h = CtxDigest::new(); + + h.update(self.hash_prefix); h.update(message); let r = Scalar::from_hash(h); let R: CompressedEdwardsY = EdwardsPoint::mul_base(&r).compress(); - h = Sha512::new(); + h = CtxDigest::new(); h.update(R.as_bytes()); h.update(verifying_key.as_bytes()); h.update(message); @@ -786,38 +767,27 @@ impl ExpandedSecretKey { InternalSignature { R, s }.into() } - /// Sign a `prehashed_message` with this `ExpandedSecretKey` using the - /// Ed25519ph algorithm defined in [RFC8032 §5.1][rfc8032]. + /// The prehashed signing function for Ed25519 (i.e., Ed25519ph). `CtxDigest` is the digest + /// function used to calculate the pseudorandomness needed for signing. `MsgDigest` is the + /// digest function used to hash the signed message. According to the spec, `MsgDigest = + /// CtxDigest = Sha512`, and `self` is derived via the method defined in `impl + /// From<&SigningKey> for ExpandedSecretKey`. /// - /// # Inputs - /// - /// * `prehashed_message` is an instantiated hash digest with 512-bits of - /// output which has had the message to be signed previously fed into its - /// state. - /// * `verifying_key` is a [`VerifyingKey`] which corresponds to this secret key. - /// * `context` is an optional context string, up to 255 bytes inclusive, - /// which may be used to provide additional domain separation. If not - /// set, this will default to an empty string. - /// - /// # Returns - /// - /// A `Result` whose `Ok` value is an Ed25519ph [`Signature`] on the - /// `prehashed_message` if the context was 255 bytes or less, otherwise - /// a `SignatureError`. - /// - /// [rfc8032]: https://tools.ietf.org/html/rfc8032#section-5.1 + /// This definition is loose in its parameters so that end-users of the `hazmat` module can + /// change how the `ExpandedSecretKey` is calculated and which `CtxDigest` function to use. #[cfg(feature = "digest")] #[allow(non_snake_case)] - pub(crate) fn sign_prehashed<'a, D>( + #[inline(always)] + pub(crate) fn raw_sign_prehashed<'a, CtxDigest, MsgDigest>( &self, - prehashed_message: D, + prehashed_message: MsgDigest, verifying_key: &VerifyingKey, context: Option<&'a [u8]>, ) -> Result where - D: Digest, + CtxDigest: Digest, + MsgDigest: Digest, { - let mut h: Sha512; let mut prehash: [u8; 64] = [0u8; 64]; let ctx: &[u8] = context.unwrap_or(b""); // By default, the context is an empty string. @@ -843,18 +813,18 @@ impl ExpandedSecretKey { // // This is a really fucking stupid bandaid, and the damned scheme is // still bleeding from malleability, for fuck's sake. - h = Sha512::new() + let mut h = CtxDigest::new() .chain_update(b"SigEd25519 no Ed25519 collisions") .chain_update([1]) // Ed25519ph .chain_update([ctx_len]) .chain_update(ctx) - .chain_update(self.nonce) + .chain_update(self.hash_prefix) .chain_update(&prehash[..]); let r = Scalar::from_hash(h); let R: CompressedEdwardsY = EdwardsPoint::mul_base(&r).compress(); - h = Sha512::new() + h = CtxDigest::new() .chain_update(b"SigEd25519 no Ed25519 collisions") .chain_update([1]) // Ed25519ph .chain_update([ctx_len]) diff --git a/src/verifying.rs b/src/verifying.rs index 5dbbefc..e97e203 100644 --- a/src/verifying.rs +++ b/src/verifying.rs @@ -13,13 +13,12 @@ use core::convert::TryFrom; use core::fmt::Debug; use core::hash::{Hash, Hasher}; -#[cfg(feature = "digest")] -use curve25519_dalek::digest::generic_array::typenum::U64; -use curve25519_dalek::digest::Digest; -use curve25519_dalek::edwards::CompressedEdwardsY; -use curve25519_dalek::edwards::EdwardsPoint; -use curve25519_dalek::montgomery::MontgomeryPoint; -use curve25519_dalek::scalar::Scalar; +use curve25519_dalek::{ + digest::{generic_array::typenum::U64, Digest}, + edwards::{CompressedEdwardsY, EdwardsPoint}, + montgomery::MontgomeryPoint, + scalar::Scalar, +}; use ed25519::signature::Verifier; @@ -36,10 +35,13 @@ use crate::context::Context; #[cfg(feature = "digest")] use signature::DigestVerifier; -use crate::constants::*; -use crate::errors::*; -use crate::signature::*; -use crate::signing::*; +use crate::{ + constants::PUBLIC_KEY_LENGTH, + errors::{InternalError, SignatureError}, + hazmat::ExpandedSecretKey, + signature::InternalSignature, + signing::SigningKey, +}; /// An ed25519 public key. /// @@ -100,6 +102,15 @@ impl From<&SigningKey> for VerifyingKey { } } +impl From for VerifyingKey { + fn from(point: EdwardsPoint) -> VerifyingKey { + VerifyingKey { + point, + compressed: point.compress(), + } + } +} + impl VerifyingKey { /// Convert this public key to a byte array. #[inline] @@ -188,16 +199,20 @@ impl VerifyingKey { VerifyingKey { compressed, point } } - // A helper function that computes H(R || A || M). If `context.is_some()`, this does the - // prehashed variant of the computation using its contents. + // 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( + fn compute_challenge( context: Option<&[u8]>, R: &CompressedEdwardsY, A: &CompressedEdwardsY, M: &[u8], - ) -> Scalar { - let mut h = Sha512::new(); + ) -> Scalar + where + CtxDigest: Digest, + { + let mut h = CtxDigest::new(); if let Some(c) = context { h.update(b"SigEd25519 no Ed25519 collisions"); h.update([1]); // Ed25519ph @@ -219,18 +234,83 @@ impl VerifyingKey { // 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( + fn recompute_R( &self, context: Option<&[u8]>, signature: &InternalSignature, M: &[u8], - ) -> CompressedEdwardsY { - let k = Self::compute_challenge(context, &signature.R, &self.compressed, M); + ) -> CompressedEdwardsY + where + CtxDigest: Digest, + { + let k = Self::compute_challenge::(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 + /// 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 + /// change how the `ExpandedSecretKey` is calculated and which hash function to use. + #[allow(non_snake_case)] + pub(crate) fn raw_verify( + &self, + message: &[u8], + signature: &ed25519::Signature, + ) -> Result<(), SignatureError> + where + CtxDigest: Digest, + { + let signature = InternalSignature::try_from(signature)?; + + let expected_R = self.recompute_R::(None, &signature, message); + if expected_R == signature.R { + Ok(()) + } else { + Err(InternalError::Verify.into()) + } + } + + /// The prehashed non-batched Ed25519 verification check, rejecting non-canonical R values. + /// (see [`Self::recompute_R`]). `CtxDigest` is the digest used to calculate the + /// pseudorandomness needed for signing. `MsgDigest` is the digest used to hash the signed + /// message. According to the spec, `MsgDigest = CtxDigest = Sha512`. + /// + /// 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. + #[cfg(feature = "digest")] + #[allow(non_snake_case)] + pub(crate) fn raw_verify_prehashed( + &self, + prehashed_message: MsgDigest, + context: Option<&[u8]>, + signature: &ed25519::Signature, + ) -> Result<(), SignatureError> + where + CtxDigest: Digest, + MsgDigest: Digest, + { + let signature = InternalSignature::try_from(signature)?; + + let ctx: &[u8] = context.unwrap_or(b""); + debug_assert!( + ctx.len() <= 255, + "The context must not be longer than 255 octets." + ); + + let message = prehashed_message.finalize(); + let expected_R = self.recompute_R::(Some(ctx), &signature, &message); + + if expected_R == signature.R { + Ok(()) + } else { + Err(InternalError::Verify.into()) + } + } + /// Verify a `signature` on a `prehashed_message` using the Ed25519ph algorithm. /// /// # Inputs @@ -246,34 +326,26 @@ impl VerifyingKey { /// # Returns /// /// Returns `true` if the `signature` was a valid signature created by this - /// `Keypair` on the `prehashed_message`. + /// [`SigningKey`] on the `prehashed_message`. + /// + /// # Note + /// + /// The RFC only permits SHA-512 to be used for prehashing, i.e., `MsgDigest = Sha512`. This + /// function technically works, and is probably safe to use, with any secure hash function with + /// 512-bit digests, but anything outside of SHA-512 is NOT specification-compliant. We expose + /// [`crate::Sha512`] for user convenience. #[cfg(feature = "digest")] #[allow(non_snake_case)] - pub fn verify_prehashed( + pub fn verify_prehashed( &self, - prehashed_message: D, + prehashed_message: MsgDigest, context: Option<&[u8]>, signature: &ed25519::Signature, ) -> Result<(), SignatureError> where - D: Digest, + MsgDigest: Digest, { - let signature = InternalSignature::try_from(signature)?; - - let ctx: &[u8] = context.unwrap_or(b""); - debug_assert!( - ctx.len() <= 255, - "The context must not be longer than 255 octets." - ); - - let message = prehashed_message.finalize(); - let expected_R = self.recompute_r(Some(ctx), &signature, &message); - - if expected_R == signature.R { - Ok(()) - } else { - Err(InternalError::Verify.into()) - } + self.raw_verify_prehashed::(prehashed_message, context, signature) } /// Strictly verify a signature on a message with this keypair's public key. @@ -356,7 +428,7 @@ impl VerifyingKey { return Err(InternalError::Verify.into()); } - let expected_R = self.recompute_r(None, &signature, message); + let expected_R = self.recompute_R::(None, &signature, message); if expected_R == signature.R { Ok(()) } else { @@ -380,17 +452,24 @@ impl VerifyingKey { /// # Returns /// /// Returns `true` if the `signature` was a valid signature created by this - /// `Keypair` on the `prehashed_message`. + /// [`SigningKey`] on the `prehashed_message`. + /// + /// # Note + /// + /// The RFC only permits SHA-512 to be used for prehashing, i.e., `MsgDigest = Sha512`. This + /// function technically works, and is probably safe to use, with any secure hash function with + /// 512-bit digests, but anything outside of SHA-512 is NOT specification-compliant. We expose + /// [`crate::Sha512`] for user convenience. #[cfg(feature = "digest")] #[allow(non_snake_case)] - pub fn verify_prehashed_strict( + pub fn verify_prehashed_strict( &self, - prehashed_message: D, + prehashed_message: MsgDigest, context: Option<&[u8]>, signature: &ed25519::Signature, ) -> Result<(), SignatureError> where - D: Digest, + MsgDigest: Digest, { let signature = InternalSignature::try_from(signature)?; @@ -411,7 +490,7 @@ impl VerifyingKey { } let message = prehashed_message.finalize(); - let expected_R = self.recompute_r(Some(ctx), &signature, &message); + let expected_R = self.recompute_R::(Some(ctx), &signature, &message); if expected_R == signature.R { Ok(()) @@ -442,28 +521,20 @@ impl Verifier for VerifyingKey { /// # Return /// /// Returns `Ok(())` if the signature is valid, and `Err` otherwise. - #[allow(non_snake_case)] fn verify(&self, message: &[u8], signature: &ed25519::Signature) -> Result<(), SignatureError> { - let signature = InternalSignature::try_from(signature)?; - - let expected_R = self.recompute_r(None, &signature, message); - if expected_R == signature.R { - Ok(()) - } else { - Err(InternalError::Verify.into()) - } + self.raw_verify::(message, signature) } } /// Equivalent to [`VerifyingKey::verify_prehashed`] with `context` set to [`None`]. #[cfg(feature = "digest")] -impl DigestVerifier for VerifyingKey +impl DigestVerifier for VerifyingKey where - D: Digest, + MsgDigest: Digest, { fn verify_digest( &self, - msg_digest: D, + msg_digest: MsgDigest, signature: &ed25519::Signature, ) -> Result<(), SignatureError> { self.verify_prehashed(msg_digest, None, signature) @@ -473,13 +544,13 @@ where /// Equivalent to [`VerifyingKey::verify_prehashed`] with `context` set to [`Some`] /// containing `self.value()`. #[cfg(feature = "digest")] -impl DigestVerifier for Context<'_, '_, VerifyingKey> +impl DigestVerifier for Context<'_, '_, VerifyingKey> where - D: Digest, + MsgDigest: Digest, { fn verify_digest( &self, - msg_digest: D, + msg_digest: MsgDigest, signature: &ed25519::Signature, ) -> Result<(), SignatureError> { self.key()