Skip to main content

miden_crypto/dsa/ecdsa_k256_keccak/
mod.rs

1//! ECDSA (Elliptic Curve Digital Signature Algorithm) signature implementation over secp256k1
2//! curve using Keccak to hash the messages when signing.
3
4use alloc::{string::ToString, vec::Vec};
5use core::fmt;
6
7use k256::{
8    ecdh::diffie_hellman,
9    ecdsa,
10    ecdsa::{RecoveryId, VerifyingKey, signature::hazmat::PrehashVerifier},
11    elliptic_curve::{Generate, scalar::IsHigh},
12    pkcs8::DecodePublicKey,
13};
14use miden_crypto_derive::{SilentDebug, SilentDisplay};
15use rand::CryptoRng;
16use thiserror::Error;
17
18use crate::{
19    Felt, SequentialCommit, Word,
20    ecdh::k256::{EphemeralPublicKey, SharedSecret},
21    utils::{
22        ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
23        bytes_to_packed_u32_elements,
24        zeroize::{Zeroize, ZeroizeOnDrop},
25    },
26};
27
28mod tests;
29
30// CONSTANTS
31// ================================================================================================
32
33/// Length of secret key in bytes
34const SECRET_KEY_BYTES: usize = 32;
35/// Length of public key in bytes when using compressed format encoding
36pub(crate) const PUBLIC_KEY_BYTES: usize = 33;
37/// Length of signature in bytes using our custom serialization
38const SIGNATURE_BYTES: usize = 65;
39/// Length of signature in bytes using standard serialization i.e., `SEC1`
40const SIGNATURE_STANDARD_BYTES: usize = 64;
41/// Length of scalars for the `secp256k1` curve
42const SCALARS_SIZE_BYTES: usize = 32;
43
44// SECRET KEY
45// ================================================================================================
46
47/// Secret key for ECDSA signature verification over secp256k1 curve.
48#[derive(Clone, SilentDebug, SilentDisplay)]
49struct SecretKey {
50    inner: ecdsa::SigningKey,
51}
52
53impl SecretKey {
54    /// Generates a new secret key using the provided random number generator.
55    fn with_rng<R: CryptoRng>(rng: &mut R) -> Self {
56        let signing_key = ecdsa::SigningKey::generate_from_rng(rng);
57
58        Self { inner: signing_key }
59    }
60
61    /// Gets the corresponding public key for this secret key.
62    fn public_key(&self) -> PublicKey {
63        let verifying_key = self.inner.verifying_key();
64        PublicKey { inner: *verifying_key }
65    }
66
67    /// Signs a message (represented as a Word) with this secret key.
68    fn sign(&self, message: Word) -> Signature {
69        let message_digest = hash_message(message);
70        self.sign_prehash(message_digest)
71    }
72
73    /// Signs a pre-hashed message with this secret key.
74    fn sign_prehash(&self, message_digest: [u8; 32]) -> Signature {
75        let (signature_inner, recovery_id) = self
76            .inner
77            .sign_prehash_recoverable(&message_digest)
78            .expect("failed to generate signature");
79
80        let (r, s) = signature_inner.split_scalars();
81
82        Signature {
83            r: r.to_bytes().into(),
84            s: s.to_bytes().into(),
85            v: recovery_id.into(),
86        }
87    }
88
89    /// Computes a Diffie-Hellman shared secret from this secret key and the ephemeral public key
90    /// generated by the other party.
91    fn get_shared_secret(&self, pk_e: EphemeralPublicKey) -> SharedSecret {
92        let shared_secret_inner = diffie_hellman(self.inner.as_nonzero_scalar(), pk_e.as_affine());
93
94        SharedSecret::new(shared_secret_inner)
95    }
96}
97
98// SAFETY: The inner `k256::ecdsa::SigningKey` already implements `ZeroizeOnDrop`,
99// which ensures that the secret key material is securely zeroized when dropped.
100impl ZeroizeOnDrop for SecretKey {}
101
102impl PartialEq for SecretKey {
103    fn eq(&self, other: &Self) -> bool {
104        use subtle::ConstantTimeEq;
105        self.to_bytes().ct_eq(&other.to_bytes()).into()
106    }
107}
108
109impl Eq for SecretKey {}
110
111// SIGNING KEY
112// ================================================================================================
113
114/// A secret key for ECDSA signature verification over the secp256k1 curve.
115#[derive(Clone, Eq, PartialEq, SilentDebug, SilentDisplay)] // Safe as SecretKey has const-time eq
116pub struct SigningKey(SecretKey);
117
118impl SigningKey {
119    /// Generates a new random signing key using the OS random number generator.
120    ///
121    /// This is cryptographically secure as long as [`rand::rng`] remains so.
122    #[cfg(feature = "std")]
123    #[allow(clippy::new_without_default)]
124    pub fn new() -> Self {
125        let mut rng = rand::rng();
126        Self::with_rng(&mut rng)
127    }
128
129    /// Generates a new signing key using the provided random number generator.
130    pub fn with_rng<R: CryptoRng>(rng: &mut R) -> Self {
131        Self(SecretKey::with_rng(rng))
132    }
133
134    /// Gets the public key that corresponds to this signing key.
135    pub fn public_key(&self) -> PublicKey {
136        self.0.public_key()
137    }
138
139    /// Signs a message (represented as a word) with this signing key.
140    pub fn sign(&self, message: Word) -> Signature {
141        self.0.sign(message)
142    }
143
144    /// Signs a pre-hashed message with this signing key.
145    pub fn sign_prehash(&self, message_digest: [u8; 32]) -> Signature {
146        self.0.sign_prehash(message_digest)
147    }
148}
149
150impl From<SecretKey> for SigningKey {
151    fn from(secret_key: SecretKey) -> Self {
152        Self(secret_key)
153    }
154}
155
156// SAFETY: The inner `SecretKey` already implements `ZeroizeOnDrop` which ensures that the secret
157// key material is securely zeroized when dropped.
158impl ZeroizeOnDrop for SigningKey {}
159
160impl Serializable for SigningKey {
161    fn write_into<W: ByteWriter>(&self, target: &mut W) {
162        self.0.write_into(target);
163    }
164}
165
166impl Deserializable for SigningKey {
167    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
168        Ok(Self(SecretKey::read_from(source)?))
169    }
170}
171
172// KEY EXCHANGE KEY
173// ================================================================================================
174
175/// A secret key for ECDH key-exchange over the secp256k1 curve.
176#[derive(Clone, Eq, PartialEq, SilentDebug, SilentDisplay)] // Safe as SecretKey has const-time eq
177pub struct KeyExchangeKey(SecretKey);
178
179impl KeyExchangeKey {
180    /// Generates a new random key exchange key using the OS random number generator.
181    ///
182    /// This is cryptographically secure as long as [`rand::rng`] remains so.
183    #[cfg(feature = "std")]
184    #[allow(clippy::new_without_default)]
185    pub fn new() -> Self {
186        let mut rng = rand::rng();
187        Self::with_rng(&mut rng)
188    }
189
190    /// Generates a new signing key using the provided random number generator.
191    pub fn with_rng<R: CryptoRng>(rng: &mut R) -> Self {
192        Self(SecretKey::with_rng(rng))
193    }
194
195    /// Gets the public key that corresponds to this key exchange key.
196    pub fn public_key(&self) -> PublicKey {
197        self.0.public_key()
198    }
199
200    /// Computes a Diffie-Hellman shared secret from this secret key and the ephemeral public key
201    /// generated by the other party.
202    pub fn get_shared_secret(&self, pk_e: EphemeralPublicKey) -> SharedSecret {
203        self.0.get_shared_secret(pk_e)
204    }
205}
206
207impl From<SecretKey> for KeyExchangeKey {
208    fn from(value: SecretKey) -> Self {
209        Self(value)
210    }
211}
212
213// SAFETY: The inner `SecretKey` already implements `ZeroizeOnDrop` which ensures that the secret
214// key material is securely zeroized when dropped.
215impl ZeroizeOnDrop for KeyExchangeKey {}
216
217impl Serializable for KeyExchangeKey {
218    fn write_into<W: ByteWriter>(&self, target: &mut W) {
219        self.0.write_into(target);
220    }
221}
222
223impl Deserializable for KeyExchangeKey {
224    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
225        Ok(Self(SecretKey::read_from(source)?))
226    }
227}
228
229// PUBLIC KEY
230// ================================================================================================
231
232/// Public key for ECDSA signature verification over secp256k1 curve.
233#[derive(Debug, Clone, PartialEq, Eq)]
234pub struct PublicKey {
235    pub(crate) inner: VerifyingKey,
236}
237
238impl PublicKey {
239    /// Returns a commitment to the public key using the Poseidon2 hash function.
240    ///
241    /// The commitment is computed by first converting the public key to field elements (4 bytes
242    /// per element), and then computing a sequential hash of the elements.
243    pub fn to_commitment(&self) -> Word {
244        <Self as SequentialCommit>::to_commitment(self)
245    }
246
247    /// Verifies a signature against this public key and message.
248    pub fn verify(&self, message: Word, signature: &Signature) -> bool {
249        let message_digest = hash_message(message);
250        self.verify_prehash(message_digest, signature)
251    }
252
253    /// Verifies a signature against this public key and pre-hashed message.
254    pub fn verify_prehash(&self, message_digest: [u8; 32], signature: &Signature) -> bool {
255        let signature_inner = ecdsa::Signature::from_scalars(*signature.r(), *signature.s());
256
257        match signature_inner {
258            Ok(signature) => self.inner.verify_prehash(&message_digest, &signature).is_ok(),
259            Err(_) => false,
260        }
261    }
262
263    /// Recovers from the signature the public key associated to the secret key used to sign the
264    /// message.
265    pub fn recover_from(message: Word, signature: &Signature) -> Result<Self, PublicKeyError> {
266        let message_digest = hash_message(message);
267        let signature_data = ecdsa::Signature::from_scalars(*signature.r(), *signature.s())
268            .map_err(|_| PublicKeyError::RecoveryFailed)?;
269
270        let verifying_key = VerifyingKey::recover_from_prehash(
271            &message_digest,
272            &signature_data,
273            RecoveryId::from_byte(signature.v()).ok_or(PublicKeyError::RecoveryFailed)?,
274        )
275        .map_err(|_| PublicKeyError::RecoveryFailed)?;
276
277        Ok(Self { inner: verifying_key })
278    }
279
280    /// Creates a public key from SPKI ASN.1 DER format bytes.
281    ///
282    /// # Arguments
283    /// * `bytes` - SPKI ASN.1 DER format bytes
284    pub fn from_der(bytes: &[u8]) -> Result<Self, DeserializationError> {
285        let verifying_key = VerifyingKey::from_public_key_der(bytes)
286            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
287        Ok(PublicKey { inner: verifying_key })
288    }
289}
290
291impl SequentialCommit for PublicKey {
292    type Commitment = Word;
293
294    fn to_elements(&self) -> Vec<Felt> {
295        bytes_to_packed_u32_elements(&self.to_bytes())
296    }
297}
298
299#[derive(Debug, Error)]
300pub enum PublicKeyError {
301    #[error("Could not recover the public key from the message and signature")]
302    RecoveryFailed,
303}
304
305// SIGNATURE
306// ================================================================================================
307
308/// ECDSA signature over secp256k1 curve using Keccak to hash the messages when signing.
309///
310/// ## Serialization Formats
311///
312/// This implementation supports 2 serialization formats:
313///
314/// ### Custom Format (65 bytes):
315/// - Bytes 0-31: r component (32 bytes, big-endian)
316/// - Bytes 32-63: s component (32 bytes, big-endian)
317/// - Byte 64: recovery ID (v) - values 0-3
318///
319/// ### SEC1 Format (64 bytes):
320/// - Bytes 0-31: r component (32 bytes, big-endian)
321/// - Bytes 32-63: s component (32 bytes, big-endian)
322/// - Note: Recovery ID
323///
324/// # Examples
325///
326/// ```
327/// use miden_crypto::{
328///     Felt, Word,
329///     dsa::ecdsa_k256_keccak::{Signature, SigningKey},
330///     rand::test_utils::seeded_rng,
331/// };
332/// use miden_serde_utils::{Deserializable, Serializable};
333///
334/// let mut rng = seeded_rng([7; 32]);
335/// let signing_key = SigningKey::with_rng(&mut rng);
336/// let message = Word::new([
337///     Felt::new_unchecked(1),
338///     Felt::new_unchecked(2),
339///     Felt::new_unchecked(3),
340///     Felt::new_unchecked(4),
341/// ]);
342///
343/// let signature = signing_key.sign(message);
344/// let encoded = signature.to_bytes();
345///
346/// assert_eq!(encoded.len(), 65);
347/// assert_eq!(Signature::read_from_bytes(&encoded).unwrap(), signature);
348/// ```
349#[derive(Debug, Clone, PartialEq, Eq)]
350pub struct Signature {
351    r: [u8; SCALARS_SIZE_BYTES],
352    s: [u8; SCALARS_SIZE_BYTES],
353    v: u8,
354}
355
356impl Signature {
357    /// Returns the `r` scalar of this signature.
358    pub fn r(&self) -> &[u8; SCALARS_SIZE_BYTES] {
359        &self.r
360    }
361
362    /// Returns the `s` scalar of this signature.
363    pub fn s(&self) -> &[u8; SCALARS_SIZE_BYTES] {
364        &self.s
365    }
366
367    /// Returns the `v` component of this signature, which is a `u8` representing the recovery id.
368    pub fn v(&self) -> u8 {
369        self.v
370    }
371
372    /// Verifies this signature against a message and public key.
373    pub fn verify(&self, message: Word, pub_key: &PublicKey) -> bool {
374        pub_key.verify(message, self)
375    }
376
377    /// Converts signature to SEC1 format (standard 64-byte r||s format).
378    ///
379    /// This format is the standard one used by most ECDSA libraries but loses the recovery ID.
380    pub fn to_sec1_bytes(&self) -> [u8; SIGNATURE_STANDARD_BYTES] {
381        let mut bytes = [0u8; 2 * SCALARS_SIZE_BYTES];
382        bytes[0..SCALARS_SIZE_BYTES].copy_from_slice(self.r());
383        bytes[SCALARS_SIZE_BYTES..2 * SCALARS_SIZE_BYTES].copy_from_slice(self.s());
384        bytes
385    }
386
387    /// Creates a signature from SEC1 format bytes with a given recovery id.
388    ///
389    /// # Arguments
390    /// * `bytes` - 64-byte array containing r and s components
391    /// * `recovery_id` - recovery ID (0-3)
392    pub fn from_sec1_bytes_and_recovery_id(
393        bytes: [u8; SIGNATURE_STANDARD_BYTES],
394        recovery_id: u8,
395    ) -> Result<Self, DeserializationError> {
396        let mut r = [0u8; SCALARS_SIZE_BYTES];
397        let mut s = [0u8; SCALARS_SIZE_BYTES];
398        r.copy_from_slice(&bytes[0..SCALARS_SIZE_BYTES]);
399        s.copy_from_slice(&bytes[SCALARS_SIZE_BYTES..2 * SCALARS_SIZE_BYTES]);
400
401        if recovery_id > 3 {
402            return Err(DeserializationError::InvalidValue(r#"Invalid recovery ID"#.to_string()));
403        }
404
405        Ok(Signature { r, s, v: recovery_id })
406    }
407
408    /// Creates a signature from ASN.1 DER format bytes with a given recovery id.
409    ///
410    /// # Arguments
411    /// * `bytes` - ASN.1 DER format bytes
412    /// * `recovery_id` - recovery ID (0-3)
413    pub fn from_der(bytes: &[u8], mut recovery_id: u8) -> Result<Self, DeserializationError> {
414        if recovery_id > 3 {
415            return Err(DeserializationError::InvalidValue(r#"Invalid recovery ID"#.to_string()));
416        }
417
418        let sig = ecdsa::Signature::from_der(bytes)
419            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
420
421        // Normalize signature into "low s" form.
422        // See https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki.
423        let high_s = sig.s().is_high();
424        if bool::from(high_s) {
425            // Replacing s with (n - s) corresponds to negating the ephemeral point R
426            // (i.e. R -> -R), which flips the y-parity of R. A recoverable signature's
427            // `v` encodes that y-parity in its LSB, so we must toggle only that bit to
428            // preserve recoverability.
429            recovery_id ^= 1;
430        }
431        let sig = sig.normalize_s();
432
433        let (r, s) = sig.split_scalars();
434
435        Ok(Signature {
436            r: <[u8; SCALARS_SIZE_BYTES]>::from(r.to_bytes()),
437            s: <[u8; SCALARS_SIZE_BYTES]>::from(s.to_bytes()),
438            v: recovery_id,
439        })
440    }
441}
442
443// SERIALIZATION / DESERIALIZATION
444// ================================================================================================
445
446impl Serializable for SecretKey {
447    fn write_into<W: ByteWriter>(&self, target: &mut W) {
448        let mut buffer = Vec::with_capacity(SECRET_KEY_BYTES);
449        let sk_bytes: [u8; SECRET_KEY_BYTES] = self.inner.to_bytes().into();
450        buffer.extend_from_slice(&sk_bytes);
451
452        target.write_bytes(&buffer);
453    }
454}
455
456impl Deserializable for SecretKey {
457    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
458        let mut bytes: [u8; SECRET_KEY_BYTES] = source.read_array()?;
459
460        let signing_key = ecdsa::SigningKey::from_slice(&bytes)
461            .map_err(|_| DeserializationError::InvalidValue("Invalid secret key".to_string()))?;
462        bytes.zeroize();
463
464        Ok(Self { inner: signing_key })
465    }
466}
467
468impl Serializable for PublicKey {
469    fn write_into<W: ByteWriter>(&self, target: &mut W) {
470        // Compressed format
471        let encoded = self.inner.to_sec1_point(true);
472
473        target.write_bytes(encoded.as_bytes());
474    }
475}
476
477impl Deserializable for PublicKey {
478    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
479        let bytes: [u8; PUBLIC_KEY_BYTES] = source.read_array()?;
480
481        let verifying_key = VerifyingKey::from_sec1_bytes(&bytes)
482            .map_err(|_| DeserializationError::InvalidValue("Invalid public key".to_string()))?;
483
484        Ok(Self { inner: verifying_key })
485    }
486}
487
488impl fmt::Display for PublicKey {
489    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
490        crate::utils::write_hex(f, &self.to_bytes())
491    }
492}
493
494impl Serializable for Signature {
495    fn write_into<W: ByteWriter>(&self, target: &mut W) {
496        let mut bytes = [0u8; SIGNATURE_BYTES];
497        bytes[0..SCALARS_SIZE_BYTES].copy_from_slice(self.r());
498        bytes[SCALARS_SIZE_BYTES..2 * SCALARS_SIZE_BYTES].copy_from_slice(self.s());
499        bytes[2 * SCALARS_SIZE_BYTES] = self.v();
500        target.write_bytes(&bytes);
501    }
502}
503
504impl Deserializable for Signature {
505    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
506        let r: [u8; SCALARS_SIZE_BYTES] = source.read_array()?;
507        let s: [u8; SCALARS_SIZE_BYTES] = source.read_array()?;
508        let v: u8 = source.read_u8()?;
509
510        if v > 3 {
511            Err(DeserializationError::InvalidValue(r#"Invalid recovery ID"#.to_string()))
512        } else {
513            Ok(Signature { r, s, v })
514        }
515    }
516}
517
518impl fmt::Display for Signature {
519    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
520        crate::utils::write_hex(f, &self.to_bytes())
521    }
522}
523
524// HELPER
525// ================================================================================================
526
527/// Hashes a word message using Keccak.
528fn hash_message(message: Word) -> [u8; 32] {
529    use sha3::{Digest, Keccak256};
530    let mut hasher = Keccak256::new();
531    let message_bytes: [u8; 32] = message.into();
532    hasher.update(message_bytes);
533    hasher.finalize().into()
534}