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