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}