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