Skip to main content

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