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}