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}