diff --git a/CHANGELOG.md b/CHANGELOG.md index 52ccb3a1b9..f287eb5c57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.22.6 (2026-03-13) + +- Added `Signature::from_der()` for ECDSA signatures over secp256k1 ([#842](https://github.com/0xMiden/crypto/pull/842)). +- Added `PublicKey::from_der()` for ECDSA public keys over secp256k1 ([#855](https://github.com/0xMiden/crypto/pull/855)). + ## 0.22.5 (2026-03-11) - Expose `StorageError` and `SubtreeUpdate` as prep. to externalize the `LargeSmt` RocksDB backend ([#850](https://github.com/0xMiden/crypto/pull/850)). diff --git a/Cargo.lock b/Cargo.lock index 1d9d7cd27e..14cdd27d72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -999,7 +999,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "miden-crypto" -version = "0.22.5" +version = "0.22.6" dependencies = [ "assert_matches", "blake3", @@ -1058,7 +1058,7 @@ dependencies = [ [[package]] name = "miden-crypto-derive" -version = "0.22.5" +version = "0.22.6" dependencies = [ "quote", "syn", @@ -1066,7 +1066,7 @@ dependencies = [ [[package]] name = "miden-field" -version = "0.22.5" +version = "0.22.6" dependencies = [ "miden-serde-utils", "num-bigint", @@ -1083,7 +1083,7 @@ dependencies = [ [[package]] name = "miden-serde-utils" -version = "0.22.5" +version = "0.22.6" dependencies = [ "p3-field", "p3-goldilocks", diff --git a/Cargo.toml b/Cargo.toml index 903aa5cae7..4444a7118a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["crypto", "hash", "merkle", "miden"] license = "MIT OR Apache-2.0" repository = "https://github.com/0xMiden/crypto" rust-version = "1.90" -version = "0.22.5" +version = "0.22.6" [workspace.dependencies] miden-crypto-derive = { path = "miden-crypto-derive", version = "0.22" } diff --git a/miden-crypto/Cargo.toml b/miden-crypto/Cargo.toml index 3649c283c0..790371265f 100644 --- a/miden-crypto/Cargo.toml +++ b/miden-crypto/Cargo.toml @@ -103,7 +103,7 @@ ed25519-dalek = { features = ["zeroize"], version = "2" } flume = { version = "0.11.1" } hashbrown = { features = ["serde"], optional = true, version = "0.16" } hkdf = { default-features = false, version = "0.12" } -k256 = { features = ["ecdh", "ecdsa"], version = "0.13" } +k256 = { features = ["ecdh", "ecdsa", "pkcs8"], version = "0.13" } num = { default-features = false, features = ["alloc", "libm"], version = "0.4" } num-complex = { default-features = false, version = "0.4" } proptest = { default-features = false, features = ["alloc"], optional = true, version = "1.7" } diff --git a/miden-crypto/src/dsa/ecdsa_k256_keccak/mod.rs b/miden-crypto/src/dsa/ecdsa_k256_keccak/mod.rs index ef99f4db03..47a116b841 100644 --- a/miden-crypto/src/dsa/ecdsa_k256_keccak/mod.rs +++ b/miden-crypto/src/dsa/ecdsa_k256_keccak/mod.rs @@ -6,6 +6,7 @@ use alloc::{string::ToString, vec::Vec}; use k256::{ ecdh::diffie_hellman, ecdsa::{RecoveryId, SigningKey, VerifyingKey, signature::hazmat::PrehashVerifier}, + pkcs8::DecodePublicKey, }; use miden_crypto_derive::{SilentDebug, SilentDisplay}; use rand::{CryptoRng, RngCore}; @@ -176,6 +177,16 @@ impl PublicKey { Ok(Self { inner: verifying_key }) } + + /// Creates a public key from SPKI ASN.1 DER format bytes. + /// + /// # Arguments + /// * `bytes` - SPKI ASN.1 DER format bytes + pub fn from_der(bytes: &[u8]) -> Result { + let verifying_key = VerifyingKey::from_public_key_der(bytes) + .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?; + Ok(PublicKey { inner: verifying_key }) + } } impl SequentialCommit for PublicKey { @@ -255,18 +266,53 @@ impl Signature { /// * `recovery_id` - recovery ID (0-3) pub fn from_sec1_bytes_and_recovery_id( bytes: [u8; SIGNATURE_STANDARD_BYTES], - v: u8, + recovery_id: u8, ) -> Result { let mut r = [0u8; SCALARS_SIZE_BYTES]; let mut s = [0u8; SCALARS_SIZE_BYTES]; r.copy_from_slice(&bytes[0..SCALARS_SIZE_BYTES]); s.copy_from_slice(&bytes[SCALARS_SIZE_BYTES..2 * SCALARS_SIZE_BYTES]); - if v > 3 { + if recovery_id > 3 { return Err(DeserializationError::InvalidValue(r#"Invalid recovery ID"#.to_string())); } - Ok(Signature { r, s, v }) + Ok(Signature { r, s, v: recovery_id }) + } + + /// Creates a signature from ASN.1 DER format bytes with a given recovery id. + /// + /// # Arguments + /// * `bytes` - ASN.1 DER format bytes + /// * `recovery_id` - recovery ID (0-3) + pub fn from_der(bytes: &[u8], mut recovery_id: u8) -> Result { + if recovery_id > 3 { + return Err(DeserializationError::InvalidValue(r#"Invalid recovery ID"#.to_string())); + } + + let sig = k256::ecdsa::Signature::from_der(bytes) + .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?; + + // Normalize signature into "low s" form. + // See https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki. + let sig = if let Some(norm) = sig.normalize_s() { + // Replacing s with (n - s) corresponds to negating the ephemeral point R + // (i.e. R -> -R), which flips the y-parity of R. A recoverable signature's + // `v` encodes that y-parity in its LSB, so we must toggle only that bit to + // preserve recoverability. + recovery_id ^= 1; + norm + } else { + sig + }; + + let (r, s) = sig.split_scalars(); + + Ok(Signature { + r: r.to_bytes().into(), + s: s.to_bytes().into(), + v: recovery_id, + }) } } @@ -331,7 +377,11 @@ impl Deserializable for Signature { let s: [u8; SCALARS_SIZE_BYTES] = source.read_array()?; let v: u8 = source.read_u8()?; - Ok(Signature { r, s, v }) + if v > 3 { + Err(DeserializationError::InvalidValue(r#"Invalid recovery ID"#.to_string())) + } else { + Ok(Signature { r, s, v }) + } } } diff --git a/miden-crypto/src/dsa/ecdsa_k256_keccak/tests.rs b/miden-crypto/src/dsa/ecdsa_k256_keccak/tests.rs index 96d17076cd..fce5fb2691 100644 --- a/miden-crypto/src/dsa/ecdsa_k256_keccak/tests.rs +++ b/miden-crypto/src/dsa/ecdsa_k256_keccak/tests.rs @@ -122,3 +122,294 @@ fn test_signature_serde() { assert!(!slice_reader.has_more_bytes()); assert_eq!(sig0, sig0_deserialized); } + +#[test] +fn test_signature_from_der_success() { + // DER-encoded form of an ASN.1 SEQUENCE containing two INTEGER values. + let der: [u8; 8] = [ + 0x30, 0x06, // Sequence tag and length of sequence contents. + 0x02, 0x01, 0x01, // Integer 1. + 0x02, 0x01, 0x09, // Integer 2. + ]; + let v = 2u8; + + let sig = Signature::from_der(&der, v).expect("from_der should parse valid DER"); + + // Expect r = 1 and s = 9 in 32-byte big-endian form. + let mut expected_r = [0u8; 32]; + expected_r[31] = 1; + let mut expected_s = [0u8; 32]; + expected_s[31] = 9; + + assert_eq!(sig.r(), &expected_r); + assert_eq!(sig.s(), &expected_s); + assert_eq!(sig.v(), v); +} + +#[test] +fn test_signature_from_der_recovery_id_variation() { + // DER encoding with two integers both equal to 1. + let der: [u8; 8] = [0x30, 0x06, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01]; + + let sig_v0 = Signature::from_der(&der, 0).unwrap(); + let sig_v3 = Signature::from_der(&der, 3).unwrap(); + + // r and s must be identical; v differs, so signatures should not be equal. + assert_eq!(sig_v0.r(), sig_v3.r()); + assert_eq!(sig_v0.s(), sig_v3.s()); + assert_ne!(sig_v0.v(), sig_v3.v()); + assert_ne!(sig_v0, sig_v3); +} + +#[test] +fn test_signature_from_der_invalid() { + // Empty input should fail at DER parsing stage (der error). + match Signature::from_der(&[], 0) { + Err(super::DeserializationError::InvalidValue(_)) => {}, + other => panic!("expected InvalidValue for empty DER, got {:?}", other), + } + + // Malformed/truncated DER should also fail. + let der_bad: [u8; 2] = [0x30, 0x01]; + match Signature::from_der(&der_bad, 0) { + Err(super::DeserializationError::InvalidValue(_)) => {}, + other => panic!("expected InvalidValue for malformed DER, got {:?}", other), + } +} + +#[test] +fn test_signature_from_der_high_s_normalizes_and_flips_v() { + // Construct a DER signature with r = 3 and s = n - 2 (high-S), which requires a leading 0x00 + // in DER to force a positive INTEGER. + // + // secp256k1 curve order (n): + // n = FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141 + // We set s = n - 2 = ... D036413F (> n/2), so normalize_s() should trigger and flip recovery + // id. + let der: [u8; 40] = [ + 0x30, 0x26, // SEQUENCE, length 38 + 0x02, 0x01, 0x03, // INTEGER r = 3 + 0x02, 0x21, 0x00, // INTEGER s, length 33 with leading 0x00 to keep positive + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfe, 0xba, 0xae, 0xdc, 0xe6, 0xaf, 0x48, 0xa0, 0x3b, 0xbf, 0xd2, 0x5e, 0x8c, 0xd0, 0x36, + 0x41, 0x3f, + ]; + let v_initial: u8 = 2; + let sig = Signature::from_der(&der, v_initial).expect("from_der should parse valid high-S DER"); + + // After normalization: + // - v should have its parity bit flipped (XOR with 1). + // - s should be normalized to low-s; since s = n - 2, the normalized s is 2. + let mut expected_r = [0u8; 32]; + expected_r[31] = 3; + let mut expected_s_low = [0u8; 32]; + expected_s_low[31] = 2; + + assert_eq!(sig.r(), &expected_r); + assert_eq!(sig.s(), &expected_s_low); + assert_eq!(sig.v(), v_initial ^ 1); +} + +#[test] +fn test_public_key_from_der_success() { + // Build a valid SPKI DER for the compressed SEC1 point of our generated key. + let mut rng = seeded_rng([9u8; 32]); + let secret_key = SecretKey::with_rng(&mut rng); + let public_key = secret_key.public_key(); + let public_key_bytes = public_key.to_bytes(); // compressed SEC1 (33 bytes). + + // AlgorithmIdentifier: id-ecPublicKey + secp256k1 + let algo: [u8; 18] = [ + 0x30, 0x10, // SEQUENCE, length 16 + 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // OID 1.2.840.10045.2.1 + 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a, // OID 1.3.132.0.10 (secp256k1) + ]; + + // subjectPublicKey BIT STRING: 0 unused bits + compressed SEC1. + let mut spk = Vec::with_capacity(2 + 1 + public_key_bytes.len()); + spk.push(0x03); // BIT STRING + spk.push((1 + public_key_bytes.len()) as u8); // length + spk.push(0x00); // unused bits = 0 + spk.extend_from_slice(&public_key_bytes); + + // Outer SEQUENCE. + let mut der = Vec::with_capacity(2 + algo.len() + spk.len()); + der.push(0x30); // SEQUENCE + der.push((algo.len() + spk.len()) as u8); // total length + der.extend_from_slice(&algo); + der.extend_from_slice(&spk); + + let parsed = PublicKey::from_der(&der).expect("should parse valid SPKI DER"); + assert_eq!(parsed, public_key); +} + +#[test] +fn test_public_key_from_der_invalid() { + // Empty DER. + match PublicKey::from_der(&[]) { + Err(super::DeserializationError::InvalidValue(_)) => {}, + other => panic!("expected InvalidValue for empty DER, got {:?}", other), + } + + // Malformed: SEQUENCE with zero length (missing fields). + let der_bad: [u8; 2] = [0x30, 0x00]; + match PublicKey::from_der(&der_bad) { + Err(super::DeserializationError::InvalidValue(_)) => {}, + other => panic!("expected InvalidValue for malformed DER, got {:?}", other), + } +} + +#[test] +fn test_public_key_from_der_rejects_non_canonical_long_form_length() { + // Build a valid SPKI structure but encode the outer SEQUENCE length using non-canonical + // long-form (0x81 ) even though the length < 128. DER should reject this. + let mut rng = seeded_rng([10u8; 32]); + let secret_key = SecretKey::with_rng(&mut rng); + let public_key = secret_key.public_key(); + let public_key_bytes = public_key.to_bytes(); // compressed SEC1 (33 bytes) + + // AlgorithmIdentifier: id-ecPublicKey + secp256k1 + let algo: [u8; 18] = [ + 0x30, 0x10, // SEQUENCE, length 16 + 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // OID 1.2.840.10045.2.1 + 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a, // OID 1.3.132.0.10 (secp256k1) + ]; + + // subjectPublicKey BIT STRING: 0 unused bits + compressed SEC1 + let mut spk = Vec::with_capacity(2 + 1 + public_key_bytes.len()); + spk.push(0x03); // BIT STRING + spk.push((1 + public_key_bytes.len()) as u8); // length + spk.push(0x00); // unused bits = 0 + spk.extend_from_slice(&public_key_bytes); + + // Outer SEQUENCE using non-canonical long-form length (0x81) + let total_len = (algo.len() + spk.len()) as u8; // fits in one byte + let mut der = Vec::with_capacity(3 + algo.len() + spk.len()); + der.push(0x30); // SEQUENCE + der.push(0x81); // long-form length marker with one subsequent length byte + der.push(total_len); + der.extend_from_slice(&algo); + der.extend_from_slice(&spk); + + match PublicKey::from_der(&der) { + Err(super::DeserializationError::InvalidValue(_)) => {}, + other => { + panic!("expected InvalidValue for non-canonical long-form length, got {:?}", other) + }, + } +} + +#[test] +fn test_public_key_from_der_rejects_trailing_bytes() { + // Build a valid SPKI DER but append trailing bytes after the sequence; DER should reject. + let mut rng = seeded_rng([11u8; 32]); + let secret_key = SecretKey::with_rng(&mut rng); + let public_key = secret_key.public_key(); + let public_key_bytes = public_key.to_bytes(); // compressed SEC1 (33 bytes) + + // AlgorithmIdentifier: id-ecPublicKey + secp256k1. + let algo: [u8; 18] = [ + 0x30, 0x10, // SEQUENCE, length 16 + 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // OID 1.2.840.10045.2.1 + 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a, // OID 1.3.132.0.10 (secp256k1) + ]; + + // subjectPublicKey BIT STRING: 0 unused bits + compressed SEC1. + let mut spk = Vec::with_capacity(2 + 1 + public_key_bytes.len()); + spk.push(0x03); // BIT STRING + spk.push((1 + public_key_bytes.len()) as u8); // length + spk.push(0x00); // unused bits = 0 + spk.extend_from_slice(&public_key_bytes); + + // Outer SEQUENCE with short-form length. + let total_len = (algo.len() + spk.len()) as u8; + let mut der = Vec::with_capacity(2 + algo.len() + spk.len() + 2); + der.push(0x30); // SEQUENCE + der.push(total_len); + der.extend_from_slice(&algo); + der.extend_from_slice(&spk); + + // Append trailing junk. + der.push(0x00); + der.push(0x00); + + match PublicKey::from_der(&der) { + Err(super::DeserializationError::InvalidValue(_)) => {}, + other => panic!("expected InvalidValue for DER with trailing bytes, got {:?}", other), + } +} + +#[test] +fn test_public_key_from_der_rejects_wrong_curve_oid() { + // Same structure but with prime256v1 (P-256) curve OID instead of secp256k1. + let mut rng = seeded_rng([12u8; 32]); + let secret_key = SecretKey::with_rng(&mut rng); + let public_key = secret_key.public_key(); + let public_key_bytes = public_key.to_bytes(); // compressed SEC1 (33 bytes) + + // AlgorithmIdentifier: id-ecPublicKey + prime256v1 (1.2.840.10045.3.1.7). + // Completed prime256v1 OID tail for correctness + // Full DER OID bytes for 1.2.840.10045.3.1.7 are: 06 08 2A 86 48 CE 3D 03 01 07 + // We'll encode properly below with 8 length, then adjust the outer lengths accordingly. + + // AlgorithmIdentifier with correct OID encoding but wrong curve: + let algo_full: [u8; 21] = [ + 0x30, 0x12, // SEQUENCE, length 18 + 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // id-ecPublicKey + 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, // prime256v1 + ]; + + // subjectPublicKey BIT STRING. + let mut spk = Vec::with_capacity(2 + 1 + public_key_bytes.len()); + spk.push(0x03); + spk.push((1 + public_key_bytes.len()) as u8); + spk.push(0x00); + spk.extend_from_slice(&public_key_bytes); + + let mut der = Vec::with_capacity(2 + algo_full.len() + spk.len()); + der.push(0x30); + der.push((algo_full.len() + spk.len()) as u8); + der.extend_from_slice(&algo_full); + der.extend_from_slice(&spk); + + match PublicKey::from_der(&der) { + Err(super::DeserializationError::InvalidValue(_)) => {}, + other => panic!("expected InvalidValue for wrong curve OID, got {:?}", other), + } +} + +#[test] +fn test_public_key_from_der_rejects_wrong_algorithm_oid() { + // Use rsaEncryption (1.2.840.113549.1.1.1) instead of id-ecPublicKey. + let mut rng = seeded_rng([13u8; 32]); + let secret_key = SecretKey::with_rng(&mut rng); + let public_key = secret_key.public_key(); + let public_key_bytes = public_key.to_bytes(); + + // AlgorithmIdentifier: rsaEncryption + NULL parameter. + // OID bytes for 1.2.840.113549.1.1.1: 06 09 2A 86 48 86 F7 0D 01 01 01. + // NULL parameter: 05 00. + let algo_rsa: [u8; 15] = [ + 0x30, 0x0d, // SEQUENCE, length 13 + 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, // rsaEncryption + 0x05, 0x00, // NULL + ]; + + // subjectPublicKey BIT STRING with EC compressed point (intentionally mismatched with algo). + let mut spk = Vec::with_capacity(2 + 1 + public_key_bytes.len()); + spk.push(0x03); + spk.push((1 + public_key_bytes.len()) as u8); + spk.push(0x00); + spk.extend_from_slice(&public_key_bytes); + + let mut der = Vec::with_capacity(2 + algo_rsa.len() + spk.len()); + der.push(0x30); + der.push((algo_rsa.len() + spk.len()) as u8); + der.extend_from_slice(&algo_rsa); + der.extend_from_slice(&spk); + + match PublicKey::from_der(&der) { + Err(super::DeserializationError::InvalidValue(_)) => {}, + other => panic!("expected InvalidValue for wrong algorithm OID, got {:?}", other), + } +}