miden_crypto/dsa/eddsa_25519_sha512/
mod.rs

1//! Ed25519 (EdDSA) signature implementation using Curve25519 and SHA-512 to hash
2//! the messages when signing.
3
4use alloc::{string::ToString, vec::Vec};
5
6use ed25519_dalek::{Signer, Verifier};
7use miden_crypto_derive::{SilentDebug, SilentDisplay};
8use rand::{CryptoRng, RngCore};
9use thiserror::Error;
10
11use crate::{
12    Felt, SequentialCommit, Word,
13    ecdh::x25519::{EphemeralPublicKey, SharedSecret},
14    utils::{
15        ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
16        bytes_to_packed_u32_elements,
17    },
18    zeroize::{Zeroize, ZeroizeOnDrop},
19};
20
21#[cfg(test)]
22mod tests;
23
24// CONSTANTS
25// ================================================================================================
26
27/// Length of secret key in bytes
28const SECRET_KEY_BYTES: usize = 32;
29/// Length of public key in bytes
30pub(crate) const PUBLIC_KEY_BYTES: usize = 32;
31/// Length of signature in bytes
32const SIGNATURE_BYTES: usize = 64;
33
34// SECRET KEY
35// ================================================================================================
36
37/// Secret key for EdDSA (Ed25519) signature verification over Curve25519.
38#[derive(Clone, SilentDebug, SilentDisplay)]
39pub struct SecretKey {
40    inner: ed25519_dalek::SigningKey,
41}
42
43impl SecretKey {
44    /// Generates a new random secret key using the OS random number generator.
45    #[cfg(feature = "std")]
46    #[allow(clippy::new_without_default)]
47    pub fn new() -> Self {
48        let mut rng = rand::rng();
49
50        Self::with_rng(&mut rng)
51    }
52
53    /// Generates a new secret key using RNG.
54    pub fn with_rng<R: CryptoRng + RngCore>(rng: &mut R) -> Self {
55        let mut seed = [0u8; SECRET_KEY_BYTES];
56        rand::RngCore::fill_bytes(rng, &mut seed);
57
58        let inner = ed25519_dalek::SigningKey::from_bytes(&seed);
59
60        // Zeroize the seed to prevent leaking secret material
61        seed.zeroize();
62
63        Self { inner }
64    }
65
66    /// Gets the corresponding public key for this secret key.
67    pub fn public_key(&self) -> PublicKey {
68        PublicKey { inner: self.inner.verifying_key() }
69    }
70
71    /// Signs a message (Word) with this secret key.
72    pub fn sign(&self, message: Word) -> Signature {
73        let message_bytes: [u8; 32] = message.into();
74        let sig = self.inner.sign(&message_bytes);
75        Signature { inner: sig }
76    }
77
78    /// Computes a Diffie-Hellman shared secret from this secret key and the ephemeral public key
79    /// generated by the other party.
80    pub fn get_shared_secret(&self, pk_e: EphemeralPublicKey) -> SharedSecret {
81        let shared = self.to_x25519().diffie_hellman(&pk_e.inner);
82        SharedSecret::new(shared)
83    }
84
85    /// Converts this Ed25519 secret key into an [`x25519_dalek::StaticSecret`].
86    ///
87    /// This conversion allows using the same underlying scalar from the Ed25519 secret key
88    /// for X25519 Diffie-Hellman key exchange. The returned `StaticSecret` can then be used
89    /// in key agreement protocols to establish a shared secret with another party's
90    /// X25519 public key.
91    fn to_x25519(&self) -> x25519_dalek::StaticSecret {
92        let mut scalar_bytes = self.inner.to_scalar_bytes();
93        let static_secret = x25519_dalek::StaticSecret::from(scalar_bytes);
94
95        // Zeroize the temporary scalar bytes
96        scalar_bytes.zeroize();
97
98        static_secret
99    }
100}
101
102// SAFETY: The inner `ed25519_dalek::SigningKey` already implements `ZeroizeOnDrop`,
103// which ensures that the secret key material is securely zeroized when dropped.
104impl ZeroizeOnDrop for SecretKey {}
105
106impl PartialEq for SecretKey {
107    fn eq(&self, other: &Self) -> bool {
108        use subtle::ConstantTimeEq;
109        self.inner.to_bytes().ct_eq(&other.inner.to_bytes()).into()
110    }
111}
112
113impl Eq for SecretKey {}
114
115// PUBLIC KEY
116// ================================================================================================
117
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct PublicKey {
120    pub(crate) inner: ed25519_dalek::VerifyingKey,
121}
122
123impl PublicKey {
124    /// Returns a commitment to the public key using the RPO256 hash function.
125    ///
126    /// The commitment is computed by first converting the public key to field elements (4 bytes
127    /// per element), and then computing a sequential hash of the elements.
128    pub fn to_commitment(&self) -> Word {
129        <Self as SequentialCommit>::to_commitment(self)
130    }
131
132    /// Verifies a signature against this public key and message.
133    pub fn verify(&self, message: Word, signature: &Signature) -> bool {
134        let message_bytes: [u8; 32] = message.into();
135        self.inner.verify(&message_bytes, &signature.inner).is_ok()
136    }
137
138    /// Computes the Ed25519 challenge hash from a message and signature.
139    ///
140    /// This method computes the 64-byte hash `SHA-512(R || A || message)` where:
141    /// - `R` is the signature's R component (first 32 bytes)
142    /// - `A` is the public key
143    /// - `message` is the message bytes
144    ///
145    /// The resulting 64-byte hash can be passed to `verify_with_unchecked_k()` which will
146    /// reduce it modulo the curve order L to produce the challenge scalar.
147    ///
148    /// # Use Case
149    ///
150    /// This method is useful when you want to separate the hashing phase from the
151    /// elliptic curve verification phase. You can:
152    /// 1. Compute the hash using this method (hashing phase)
153    /// 2. Verify using `verify_with_unchecked_k(hash, signature)` (EC phase)
154    ///
155    /// This is equivalent to calling `verify()` directly, but allows the two phases
156    /// to be executed separately or in different environments.
157    ///
158    /// # Arguments
159    /// * `message` - The message that was signed
160    /// * `signature` - The signature to compute the challenge hash from
161    ///
162    /// # Returns
163    /// A 64-byte hash that will be reduced modulo L in `verify_with_unchecked_k()`
164    ///
165    /// # Example
166    /// ```ignore
167    /// let k_hash = public_key.compute_challenge_k(message, &signature);
168    /// let is_valid = public_key.verify_with_unchecked_k(k_hash, &signature).is_ok();
169    /// // is_valid should equal public_key.verify(message, &signature)
170    /// ```
171    ///
172    /// # Not Ed25519ph / RFC 8032 Prehash
173    ///
174    /// This helper reproduces the *standard* Ed25519 challenge `H(R || A || M)` used when verifying
175    /// signatures. It does **not** implement the RFC 8032 Ed25519ph variant, which prepends a
176    /// domain separation string and optional context before hashing. Callers that require the
177    /// Ed25519ph flavour must implement the additional domain separation logic themselves.
178    pub fn compute_challenge_k(&self, message: Word, signature: &Signature) -> [u8; 64] {
179        use sha2::Digest;
180
181        let message_bytes: [u8; 32] = message.into();
182        let sig_bytes = signature.inner.to_bytes();
183        let r_bytes = &sig_bytes[0..32];
184
185        // Compute SHA-512(R || A || message)
186        let mut hasher = sha2::Sha512::new();
187        hasher.update(r_bytes);
188        hasher.update(self.inner.to_bytes());
189        hasher.update(message_bytes);
190        let k_hash = hasher.finalize();
191
192        k_hash.into()
193    }
194
195    /// Verifies a signature using a pre-computed challenge hash.
196    ///
197    /// # ⚠️ CRITICAL SECURITY WARNING ⚠️
198    ///
199    /// **THIS METHOD IS EXTREMELY DANGEROUS AND EASY TO MISUSE.**
200    ///
201    /// This method bypasses the standard Ed25519 verification process by accepting a pre-computed
202    /// challenge hash instead of computing it from the message. This breaks Ed25519's
203    /// security properties in the following ways:
204    ///
205    /// ## Security Risks:
206    ///
207    /// 1. **Signature Forgery**: An attacker who can control the hash value can forge signatures
208    ///    for arbitrary messages without knowing the private key.
209    ///
210    /// 2. **Breaks Message Binding**: Standard Ed25519 cryptographically binds the signature to the
211    ///    message via the hash `H(R || A || message)`. Accepting arbitrary hashes breaks this
212    ///    binding.
213    ///
214    /// 3. **Bypasses Standard Protocol**: If the hash is not computed correctly as `SHA-512(R || A
215    ///    || message)`, this method bypasses standard Ed25519 verification and the signature will
216    ///    not be compatible with Ed25519 semantics.
217    ///
218    /// ## When This Might Be Used:
219    ///
220    /// This method is only appropriate in very specific scenarios where:
221    /// - You have a trusted computation environment that computes the hash correctly as `SHA-512(R
222    ///   || A || message)` (see `compute_challenge_k()`)
223    /// - You need to separate the hashing phase from the EC verification phase (e.g., for different
224    ///   execution environments or performance optimization)
225    /// - You fully understand the security implications and have a threat model that accounts for
226    ///   them
227    ///
228    /// When the hash is computed correctly, this method implements standard Ed25519 verification.
229    ///
230    /// ## Standard Usage:
231    ///
232    /// For normal Ed25519 verification, use `verify()` instead.
233    ///
234    /// ## Performance
235    ///
236    /// This helper decompresses the signature's `R` component before performing group arithmetic
237    /// and reuses the cached Edwards form of the public key. Expect it to be slower than
238    /// calling `verify()` directly.
239    ///
240    /// # Arguments
241    /// * `k_hash` - A 64-byte hash (typically computed as `SHA-512(R || A || message)`)
242    /// * `signature` - The signature to verify
243    ///
244    /// # Returns
245    /// `Ok(())` if the verification equation `[s]B = R + [k]A` holds, or an error describing why
246    /// the verification failed.
247    ///
248    /// # Warning
249    /// Do NOT use this method unless you fully understand Ed25519's cryptographic properties,
250    /// have a specific need for this low-level operation, and are feeding it the exact
251    /// `SHA-512(R || A || message)` output (without the Ed25519ph domain separation string).
252    pub fn verify_with_unchecked_k(
253        &self,
254        k_hash: [u8; 64],
255        signature: &Signature,
256    ) -> Result<(), UncheckedVerificationError> {
257        use curve25519_dalek::{
258            edwards::{CompressedEdwardsY, EdwardsPoint},
259            scalar::Scalar,
260        };
261
262        // Reduce the 64-byte hash modulo L to get the challenge scalar
263        let k_scalar = Scalar::from_bytes_mod_order_wide(&k_hash);
264
265        // Extract signature components: R (first 32 bytes) and s (second 32 bytes)
266        let sig_bytes = signature.inner.to_bytes();
267        let r_bytes: [u8; 32] =
268            sig_bytes[..32].try_into().expect("signature R component is exactly 32 bytes");
269        let s_bytes: [u8; 32] =
270            sig_bytes[32..].try_into().expect("signature s component is exactly 32 bytes");
271
272        // RFC 8032 requires s to be canonical; reject non-canonical scalars to avoid malleability.
273        let s_candidate = Scalar::from_canonical_bytes(s_bytes);
274        if s_candidate.is_none().into() {
275            return Err(UncheckedVerificationError::NonCanonicalScalar);
276        }
277        let s_scalar = s_candidate.unwrap();
278
279        let r_compressed = CompressedEdwardsY(r_bytes);
280        let Some(r_point) = r_compressed.decompress() else {
281            return Err(UncheckedVerificationError::InvalidSignaturePoint);
282        };
283
284        let a_point = self.inner.to_edwards();
285
286        // Match the stricter ed25519-dalek semantics by rejecting small-order inputs instead of
287        // multiplying the whole equation by the cofactor. dalek leaves this check opt-in via
288        // `verify_strict()`; we enforce it here to guard this hazmat API against torsion exploits.
289        if r_point.is_small_order() {
290            return Err(UncheckedVerificationError::SmallOrderSignature);
291        }
292        if a_point.is_small_order() {
293            return Err(UncheckedVerificationError::SmallOrderPublicKey);
294        }
295
296        // Compute the verification equation: -[k]A + [s]B == R, mirroring dalek's raw_verify.
297        // Small-order points are rejected above and hence no need for multiplication by co-factor
298        let minus_a = -a_point;
299        let expected_r =
300            EdwardsPoint::vartime_double_scalar_mul_basepoint(&k_scalar, &minus_a, &s_scalar)
301                .compress();
302
303        if expected_r == r_compressed {
304            Ok(())
305        } else {
306            Err(UncheckedVerificationError::EquationMismatch)
307        }
308    }
309
310    /// Convert to a X25519 public key which can be used in a DH key exchange protocol.
311    ///
312    /// # ⚠️ Security Warning
313    ///
314    /// **Do not reuse the same secret key for both Ed25519 signatures and X25519 key exchange.**
315    /// This conversion is primarily intended for sealed box primitives where an Ed25519 public key
316    /// is used to generate the shared key for encryption given an ephemeral X25519 key pair.
317    ///
318    /// In all other uses, prefer generating dedicated X25519 keys directly.
319    pub(crate) fn to_x25519(&self) -> x25519_dalek::PublicKey {
320        let mont_point = self.inner.to_montgomery();
321        x25519_dalek::PublicKey::from(mont_point.to_bytes())
322    }
323}
324
325impl SequentialCommit for PublicKey {
326    type Commitment = Word;
327
328    fn to_elements(&self) -> Vec<Felt> {
329        bytes_to_packed_u32_elements(&self.to_bytes())
330    }
331}
332
333#[derive(Debug, Error)]
334pub enum PublicKeyError {
335    #[error("Could not verify with given public key and signature")]
336    VerificationFailed,
337}
338
339/// Errors that can arise when invoking [`PublicKey::verify_with_unchecked_k`].
340#[derive(Debug, Error)]
341pub enum UncheckedVerificationError {
342    #[error("challenge scalar is not canonical")]
343    NonCanonicalScalar,
344    #[error("signature R component failed to decompress")]
345    InvalidSignaturePoint,
346    #[error("small-order component detected in signature R")]
347    SmallOrderSignature,
348    #[error("small-order component detected in public key")]
349    SmallOrderPublicKey,
350    #[error("verification equation was not satisfied")]
351    EquationMismatch,
352}
353
354// SIGNATURE
355// ================================================================================================
356
357/// EdDSA (Ed25519) signature
358#[derive(Debug, Clone, PartialEq, Eq)]
359pub struct Signature {
360    inner: ed25519_dalek::Signature,
361}
362
363impl Signature {
364    /// Verify against (message, public key).
365    pub fn verify(&self, message: Word, pub_key: &PublicKey) -> bool {
366        pub_key.verify(message, self)
367    }
368}
369
370// SERIALIZATION / DESERIALIZATION
371// ================================================================================================
372
373impl Serializable for SecretKey {
374    fn write_into<W: ByteWriter>(&self, target: &mut W) {
375        target.write_bytes(&self.inner.to_bytes());
376    }
377}
378
379impl Deserializable for SecretKey {
380    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
381        let mut bytes: [u8; SECRET_KEY_BYTES] = source.read_array()?;
382        let inner = ed25519_dalek::SigningKey::from_bytes(&bytes);
383        bytes.zeroize();
384
385        Ok(Self { inner })
386    }
387}
388
389impl Serializable for PublicKey {
390    fn write_into<W: ByteWriter>(&self, target: &mut W) {
391        target.write_bytes(&self.inner.to_bytes());
392    }
393}
394
395impl Deserializable for PublicKey {
396    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
397        let bytes: [u8; PUBLIC_KEY_BYTES] = source.read_array()?;
398        let inner = ed25519_dalek::VerifyingKey::from_bytes(&bytes).map_err(|_| {
399            DeserializationError::InvalidValue("Invalid Ed25519 public key".to_string())
400        })?;
401        Ok(Self { inner })
402    }
403}
404
405impl Serializable for Signature {
406    fn write_into<W: ByteWriter>(&self, target: &mut W) {
407        target.write_bytes(&self.inner.to_bytes())
408    }
409}
410
411impl Deserializable for Signature {
412    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
413        let bytes: [u8; SIGNATURE_BYTES] = source.read_array()?;
414        let inner = ed25519_dalek::Signature::from_bytes(&bytes);
415        Ok(Self { inner })
416    }
417}