Repository: dalek-cryptography/ed25519-dalek (language: rust)

Issue:
Reintroduce ExpandedSecretKey funcionality
Hi there.

We're using the `dalek` crates in [vodozemac](https://github.com/matrix-org/vodozemac/) as the core for our double ratchet implementation. This is a relatively new library intended to replace our old C based [libolm](https://gitlab.matrix.org/matrix-org/olm/) library.

Now our problem is that libolm always expands Ed25519 secret keys, which are stored long-term.

As we have many users already using libolm, we would like to transfer this expanded Ed25519 key when we transition our users from libolm to vodozemac. We've looked into updating our `ed25519-dalek` dependency, but found that it is no longer possible to construct an `ExpandedSecretKey` as the type is no longer public, as per https://github.com/dalek-cryptography/ed25519-dalek/pull/205.

Is there any way to restore this functionality?


Discussion / hints:
Per #205, `ExpandedSecretKey` has an unsafe API that was removed to fix a security vulnerability.

It also doesn't conform to any standard interface for Ed25519 keys, such as the one specified in RFC8032.
I can definitely see how this API is misuse-prone and aware it isn't a standard interface. We've long considered this behaviour of libolm to be [problematic](https://github.com/matrix-org/olm/issues/53).

Still, it's perfectly possible to use it correctly and our crate packages it up safely, without re-exposing the insecure interface.

Given this is a backward-incompatible change, resulting in a relatively critical loss of functionality preventing us from ever seamlessly migrating, perhaps it could be considered for reintroduction under a feature flag?

Alternatively, many RustCrypto crates offer low level and easy to misuse interfaces under crates or submodules marked as `hazmat`. Maybe even marking the constructor as `unsafe`?
It's possible, although the @RustCrypto `hazmat` features generally exist as implementation building blocks of higher level constructions, whereas this would be supporting a nonstandard key format which is not allowed under RFC8032 or NIST  SP 800-186.

I would suggest leaving it off the table for a 2.0 release and circling back afterward.

Alternatively, you can maintain a fork. I imagine you'll have to do this for many different libraries for different languages, since it's not a standard API.
An option that might be more in the spirit of a "hazmat" API would be a low-level signing API which accepts parameterized inputs (i.e. private scalar, derivation input, public key Edwards point, message hash), which could potentially also be used to implement constructions like the various flavors of Ed25519-BIP32
It's my understanding that `ExpandedSecretKey` was only insecure due to its `sign_` interface accepting the public key as an argument, rather than the existence of `ExpandedSecretKey` itself being somehow problematic. Rather than outright removing it, couldn't we add `ExpandedSecretKey` back (as suggested by [this comment](https://github.com/dalek-cryptography/ed25519-dalek/pull/205#issuecomment-1279453654)) with a different `sign` interface that doesn't accept the public key but derives it in the implementation? 

> Alternatively, you can maintain a fork. I imagine you'll have to do this for many different libraries for different languages, since it's not a standard API.

Besides libolm itself, which we're trying to sunset, I don't have to maintain any forks. We are using a single library for all our target languages. It would be nice to keep it this way.


>  with a different `sign` interface that doesn't accept the public key but derives it in the implementation? 

That would add an extra scalarmult to each signing operation, which would mean it couldn't be composed in terms of the existing implementation, where the corresponding public key is stored in `SigningKey`.

Effectively, we'd be adding an API just to support your nonstandard use case.

You can always vendor your own `ExpandedSecretKey` into your project?
@poljar Trying to determine how much burden this would put on you. Precisely what functionality do you need out of `ExpandedSecretKey`? If it's signing and that's it, then I think it'd be reasonable to keep the old def and vendor the `sign()` function from [here](https://github.com/dalek-cryptography/ed25519-dalek/blob/80aac08c1ca4a4a14912707650413b59c989e79a/src/signing.rs#L769-L787). Are there other things you need?
@rozbb yeah, that's more or less what I was proposing here, although perhaps even going lower level with it: https://github.com/dalek-cryptography/ed25519-dalek/issues/298#issuecomment-1499432724

Doing that would be a universal solution for everyone who wants to do something nonstandard, so we're not adding one-off nonstandard features every time they come up.

Fix patch:
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<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();
+    }
+}
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<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(),
         }
     }
 }
 
+//
+// Signing functions. These are pub(crate) so that the `hazmat` module can use them
+//
+
 impl ExpandedSecretKey {
-    /// Sign a message with this `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)]
-    pub(crate) fn sign(&self, message: &[u8], verifying_key: &VerifyingKey) -> Signature {
-        let mut h: Sha512 = Sha512::new();
+    #[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.nonce);
+        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].
-    ///
-    /// # 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
+    /// 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`.
     ///
-    /// 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])
diff --git a/src/verifying.rs b/src/verifying.rs
index 7de4fd1..083ba6e 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<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,44 +234,64 @@ 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()
     }
 
-    /// Verify a `signature` on a `prehashed_message` using the Ed25519ph algorithm.
-    ///
-    /// # 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.
-    /// * `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.
-    /// * `signature` is a purported Ed25519ph signature on the `prehashed_message`.
+    /// 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`.
     ///
-    /// # Returns
+    /// 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`.
     ///
-    /// Returns `true` if the `signature` was a valid signature created by this
-    /// `Keypair` on the `prehashed_message`.
+    /// 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 fn verify_prehashed<D>(
+    pub(crate) fn raw_verify_prehashed<CtxDigest, MsgDigest>(
         &self,
-        prehashed_message: D,
+        prehashed_message: MsgDigest,
         context: Option<&[u8]>,
         signature: &ed25519::Signature,
     ) -> Result<(), SignatureError>
     where
-        D: Digest<OutputSize = U64>,
+        CtxDigest: Digest<OutputSize = U64>,
+        MsgDigest: Digest<OutputSize = U64>,
     {
         let signature = InternalSignature::try_from(signature)?;
 
@@ -267,7 +302,7 @@ impl VerifyingKey {
         );
 
         let message = prehashed_message.finalize();
-        let expected_R = self.recompute_r(Some(ctx), &signature, &message);
+        let expected_R = self.recompute_R::<CtxDigest>(Some(ctx), &signature, &message);
 
         if expected_R == signature.R {
             Ok(())
@@ -276,6 +311,43 @@ impl VerifyingKey {
         }
     }
 
+    /// Verify a `signature` on a `prehashed_message` using the Ed25519ph algorithm.
+    ///
+    /// # 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.
+    /// * `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.
+    /// * `signature` is a purported Ed25519ph signature on the `prehashed_message`.
+    ///
+    /// # Returns
+    ///
+    /// 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.
+    #[cfg(feature = "digest")]
+    #[allow(non_snake_case)]
+    pub fn verify_prehashed<MsgDigest>(
+        &self,
+        prehashed_message: MsgDigest,
+        context: Option<&[u8]>,
+        signature: &ed25519::Signature,
+    ) -> Result<(), SignatureError>
+    where
+        MsgDigest: Digest<OutputSize = U64>,
+    {
+        self.raw_verify_prehashed::<Sha512, MsgDigest>(prehashed_message, context, signature)
+    }
+
     /// Strictly verify a signature on a message with this keypair's public key.
     ///
     /// # On The (Multiple) Sources of Malleability in Ed25519 Signatures
@@ -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()


Test patch: