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