Add hazmat module with ExpandedSecretKey, raw_sign, raw_sign_prehashed (#299)

* Added raw_sign() and raw_sign_prehashed() functions

* Renamed `nonce` to `hash_prefix` in signing because it's really not a nonce

* Moved raw signing to hazmat module

* impl From<EdwardsPoint> for VerifyingKey

* Brought back ExpandedSecretKey; made raw_* functions take it as input

* Added remaining features to docs.rs feature set

* Removed redundant ExpandedSecretKey def; made raw signing use a generic CtxDigest

* Implemented raw_verify with generic CtxDigest

* Implemented raw_verify_prehashed with generic MsgDigest and CtxDigest

* Wrote hazmat tests; fixed errors; switched ordering of MsgDigest and CtxDigest

* Updated changelog

* ExpandedSecretKey::from_bytes takes an array and is now infallible

* Add TODO comment for split_array_ref

* Added from_slice and TryFrom<&[u8]> for ExpandedSecretKey

---------

Co-authored-by: Tony Arcieri <bascule@gmail.com>
This commit is contained in:
Michael Rosenberg 2023-05-15 00:50:38 -04:00 committed by GitHub
parent 90f10ed096
commit 4afbf09e1c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 526 additions and 171 deletions

View file

@ -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

22
Cargo.lock generated
View file

@ -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"

View file

@ -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"]

View file

@ -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

280
src/hazmat.rs Normal file
View file

@ -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 its 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<Self, SignatureError> {
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, Self::Error> {
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<CtxDigest>(
esk: &ExpandedSecretKey,
message: &[u8],
verifying_key: &VerifyingKey,
) -> Signature
where
CtxDigest: Digest<OutputSize = U64>,
{
esk.raw_sign::<CtxDigest>(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<Signature, SignatureError>
where
MsgDigest: Digest<OutputSize = U64>,
CtxDigest: Digest<OutputSize = U64>,
{
esk.raw_sign_prehashed::<CtxDigest, MsgDigest>(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<CtxDigest>(
vk: &VerifyingKey,
message: &[u8],
signature: &ed25519::Signature,
) -> Result<(), SignatureError>
where
CtxDigest: Digest<OutputSize = U64>,
{
vk.raw_verify::<CtxDigest>(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<CtxDigest, MsgDigest>(
vk: &VerifyingKey,
prehashed_message: MsgDigest,
context: Option<&[u8]>,
signature: &ed25519::Signature,
) -> Result<(), SignatureError>
where
MsgDigest: Digest<OutputSize = U64>,
CtxDigest: Digest<OutputSize = U64>,
{
vk.raw_verify_prehashed::<CtxDigest, MsgDigest>(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<R: RngCore + CryptoRng>(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::<CtxDigest>(&esk, msg, &vk);
raw_verify::<CtxDigest>(&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::<CtxDigest, MsgDigest>(&esk, h.clone(), &vk, Some(ctx_str))
.unwrap();
raw_verify_prehashed::<CtxDigest, MsgDigest>(&vk, h, Some(ctx_str), &sig).unwrap();
}
}

View file

@ -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")]

View file

@ -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<D>(
pub fn sign_prehashed<MsgDigest>(
&self,
prehashed_message: D,
prehashed_message: MsgDigest,
context: Option<&[u8]>,
) -> Result<Signature, SignatureError>
where
D: Digest<OutputSize = U64>,
MsgDigest: Digest<OutputSize = U64>,
{
ExpandedSecretKey::from(&self.secret_key).sign_prehashed(
ExpandedSecretKey::from(&self.secret_key).raw_sign_prehashed::<Sha512, MsgDigest>(
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<D>(
pub fn verify_prehashed<MsgDigest>(
&self,
prehashed_message: D,
prehashed_message: MsgDigest,
context: Option<&[u8]>,
signature: &Signature,
) -> Result<(), SignatureError>
where
D: Digest<OutputSize = U64>,
MsgDigest: Digest<OutputSize = U64>,
{
self.verifying_key
.verify_prehashed(prehashed_message, context, signature)
@ -485,7 +496,7 @@ impl Signer<Signature> for SigningKey {
/// Sign a message with this signing key's secret key.
fn try_sign(&self, message: &[u8]) -> Result<Signature, SignatureError> {
let expanded: ExpandedSecretKey = (&self.secret_key).into();
Ok(expanded.sign(message, &self.verifying_key))
Ok(expanded.raw_sign::<Sha512>(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<CtxDigest>(
&self,
message: &[u8],
verifying_key: &VerifyingKey,
) -> Signature
where
CtxDigest: Digest<OutputSize = U64>,
{
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<Signature, SignatureError>
where
D: Digest<OutputSize = U64>,
CtxDigest: Digest<OutputSize = U64>,
MsgDigest: Digest<OutputSize = U64>,
{
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])

View file

@ -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<EdwardsPoint> 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<CtxDigest>(
context: Option<&[u8]>,
R: &CompressedEdwardsY,
A: &CompressedEdwardsY,
M: &[u8],
) -> Scalar {
let mut h = Sha512::new();
) -> 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
@ -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<CtxDigest>(
&self,
context: Option<&[u8]>,
signature: &InternalSignature,
M: &[u8],
) -> CompressedEdwardsY {
let k = Self::compute_challenge(context, &signature.R, &self.compressed, M);
) -> 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
/// 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<CtxDigest>(
&self,
message: &[u8],
signature: &ed25519::Signature,
) -> Result<(), SignatureError>
where
CtxDigest: Digest<OutputSize = U64>,
{
let signature = InternalSignature::try_from(signature)?;
let expected_R = self.recompute_R::<CtxDigest>(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<CtxDigest, MsgDigest>(
&self,
prehashed_message: MsgDigest,
context: Option<&[u8]>,
signature: &ed25519::Signature,
) -> Result<(), SignatureError>
where
CtxDigest: Digest<OutputSize = U64>,
MsgDigest: Digest<OutputSize = U64>,
{
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::<CtxDigest>(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<D>(
pub fn verify_prehashed<MsgDigest>(
&self,
prehashed_message: D,
prehashed_message: MsgDigest,
context: Option<&[u8]>,
signature: &ed25519::Signature,
) -> Result<(), SignatureError>
where
D: Digest<OutputSize = U64>,
MsgDigest: Digest<OutputSize = U64>,
{
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::<Sha512, MsgDigest>(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::<Sha512>(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<D>(
pub fn verify_prehashed_strict<MsgDigest>(
&self,
prehashed_message: D,
prehashed_message: MsgDigest,
context: Option<&[u8]>,
signature: &ed25519::Signature,
) -> Result<(), SignatureError>
where
D: Digest<OutputSize = U64>,
MsgDigest: Digest<OutputSize = U64>,
{
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::<Sha512>(Some(ctx), &signature, &message);
if expected_R == signature.R {
Ok(())
@ -442,28 +521,20 @@ impl Verifier<ed25519::Signature> 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::<Sha512>(message, signature)
}
}
/// Equivalent to [`VerifyingKey::verify_prehashed`] with `context` set to [`None`].
#[cfg(feature = "digest")]
impl<D> DigestVerifier<D, ed25519::Signature> for VerifyingKey
impl<MsgDigest> DigestVerifier<MsgDigest, ed25519::Signature> for VerifyingKey
where
D: Digest<OutputSize = U64>,
MsgDigest: Digest<OutputSize = U64>,
{
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<D> DigestVerifier<D, ed25519::Signature> for Context<'_, '_, VerifyingKey>
impl<MsgDigest> DigestVerifier<MsgDigest, ed25519::Signature> for Context<'_, '_, VerifyingKey>
where
D: Digest<OutputSize = U64>,
MsgDigest: Digest<OutputSize = U64>,
{
fn verify_digest(
&self,
msg_digest: D,
msg_digest: MsgDigest,
signature: &ed25519::Signature,
) -> Result<(), SignatureError> {
self.key()