Skip to content

Commit 11cb33e

Browse files
authored
feat: implement ECDSA Public Key from_der() (#855)
1 parent df02075 commit 11cb33e

File tree

6 files changed

+224
-5
lines changed

6 files changed

+224
-5
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.19.8 (2026-03-02)
2+
3+
- Added `PublicKey::from_der()` for ECDSA public keys over secp256k1 ([#855](https://github.com/0xMiden/crypto/pull/855)).
4+
15
## 0.19.7 (2026-02-26)
26

37
- Exposed `StorageError` and `SubtreeUpdate` as prep. to externalize the `LargeSmt` RocksDB backend ([#850](https://github.com/0xMiden/crypto/pull/850)).

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ keywords = ["crypto", "hash", "merkle", "miden"]
1111
license = "MIT OR Apache-2.0"
1212
repository = "https://github.com/0xMiden/crypto"
1313
rust-version = "1.90"
14-
version = "0.19.7"
14+
version = "0.19.8"
1515

1616
[workspace.dependencies]
1717
miden-crypto-derive = { path = "miden-crypto-derive", version = "0.19" }

miden-crypto/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ ed25519-dalek = { features = ["zeroize"], version = "2" }
8585
flume = { version = "0.11" }
8686
hashbrown = { features = ["serde"], optional = true, version = "0.16" }
8787
hkdf = { default-features = false, version = "0.12" }
88-
k256 = { features = ["ecdh", "ecdsa"], version = "0.13" }
88+
k256 = { features = ["ecdh", "ecdsa", "pkcs8"], version = "0.13" }
8989
miden-crypto-derive.workspace = true
9090
num = { default-features = false, features = ["alloc", "libm"], version = "0.4" }
9191
num-complex = { default-features = false, version = "0.4" }

miden-crypto/src/dsa/ecdsa_k256_keccak/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use alloc::{string::ToString, vec::Vec};
66
use k256::{
77
ecdh::diffie_hellman,
88
ecdsa::{RecoveryId, SigningKey, VerifyingKey, signature::hazmat::PrehashVerifier},
9+
pkcs8::DecodePublicKey,
910
};
1011
use miden_crypto_derive::{SilentDebug, SilentDisplay};
1112
use rand::{CryptoRng, RngCore};
@@ -176,6 +177,16 @@ impl PublicKey {
176177

177178
Ok(Self { inner: verifying_key })
178179
}
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+
}
179190
}
180191

181192
impl SequentialCommit for PublicKey {

miden-crypto/src/dsa/ecdsa_k256_keccak/tests.rs

Lines changed: 205 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use rand::rng;
22

33
use super::*;
4-
use crate::Felt;
4+
use crate::{Felt, rand::test_utils::seeded_rng};
55

66
#[test]
77
fn test_key_generation() {
@@ -211,3 +211,207 @@ fn test_signature_from_der_high_s_normalizes_and_flips_v() {
211211
assert_eq!(sig.s(), &expected_s_low);
212212
assert_eq!(sig.v(), v_initial ^ 1);
213213
}
214+
215+
#[test]
216+
fn test_public_key_from_der_success() {
217+
// Build a valid SPKI DER for the compressed SEC1 point of our generated key.
218+
let mut rng = seeded_rng([9u8; 32]);
219+
let secret_key = SecretKey::with_rng(&mut rng);
220+
let public_key = secret_key.public_key();
221+
let public_key_bytes = public_key.to_bytes(); // compressed SEC1 (33 bytes).
222+
223+
// AlgorithmIdentifier: id-ecPublicKey + secp256k1
224+
let algo: [u8; 18] = [
225+
0x30, 0x10, // SEQUENCE, length 16
226+
0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // OID 1.2.840.10045.2.1
227+
0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a, // OID 1.3.132.0.10 (secp256k1)
228+
];
229+
230+
// subjectPublicKey BIT STRING: 0 unused bits + compressed SEC1.
231+
let mut spk = Vec::with_capacity(2 + 1 + public_key_bytes.len());
232+
spk.push(0x03); // BIT STRING
233+
spk.push((1 + public_key_bytes.len()) as u8); // length
234+
spk.push(0x00); // unused bits = 0
235+
spk.extend_from_slice(&public_key_bytes);
236+
237+
// Outer SEQUENCE.
238+
let mut der = Vec::with_capacity(2 + algo.len() + spk.len());
239+
der.push(0x30); // SEQUENCE
240+
der.push((algo.len() + spk.len()) as u8); // total length
241+
der.extend_from_slice(&algo);
242+
der.extend_from_slice(&spk);
243+
244+
let parsed = PublicKey::from_der(&der).expect("should parse valid SPKI DER");
245+
assert_eq!(parsed, public_key);
246+
}
247+
248+
#[test]
249+
fn test_public_key_from_der_invalid() {
250+
// Empty DER.
251+
match PublicKey::from_der(&[]) {
252+
Err(super::DeserializationError::InvalidValue(_)) => {},
253+
other => panic!("expected InvalidValue for empty DER, got {:?}", other),
254+
}
255+
256+
// Malformed: SEQUENCE with zero length (missing fields).
257+
let der_bad: [u8; 2] = [0x30, 0x00];
258+
match PublicKey::from_der(&der_bad) {
259+
Err(super::DeserializationError::InvalidValue(_)) => {},
260+
other => panic!("expected InvalidValue for malformed DER, got {:?}", other),
261+
}
262+
}
263+
264+
#[test]
265+
fn test_public_key_from_der_rejects_non_canonical_long_form_length() {
266+
// Build a valid SPKI structure but encode the outer SEQUENCE length using non-canonical
267+
// long-form (0x81 <len>) even though the length < 128. DER should reject this.
268+
let mut rng = seeded_rng([10u8; 32]);
269+
let secret_key = SecretKey::with_rng(&mut rng);
270+
let public_key = secret_key.public_key();
271+
let public_key_bytes = public_key.to_bytes(); // compressed SEC1 (33 bytes)
272+
273+
// AlgorithmIdentifier: id-ecPublicKey + secp256k1
274+
let algo: [u8; 18] = [
275+
0x30, 0x10, // SEQUENCE, length 16
276+
0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // OID 1.2.840.10045.2.1
277+
0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a, // OID 1.3.132.0.10 (secp256k1)
278+
];
279+
280+
// subjectPublicKey BIT STRING: 0 unused bits + compressed SEC1
281+
let mut spk = Vec::with_capacity(2 + 1 + public_key_bytes.len());
282+
spk.push(0x03); // BIT STRING
283+
spk.push((1 + public_key_bytes.len()) as u8); // length
284+
spk.push(0x00); // unused bits = 0
285+
spk.extend_from_slice(&public_key_bytes);
286+
287+
// Outer SEQUENCE using non-canonical long-form length (0x81)
288+
let total_len = (algo.len() + spk.len()) as u8; // fits in one byte
289+
let mut der = Vec::with_capacity(3 + algo.len() + spk.len());
290+
der.push(0x30); // SEQUENCE
291+
der.push(0x81); // long-form length marker with one subsequent length byte
292+
der.push(total_len);
293+
der.extend_from_slice(&algo);
294+
der.extend_from_slice(&spk);
295+
296+
match PublicKey::from_der(&der) {
297+
Err(super::DeserializationError::InvalidValue(_)) => {},
298+
other => {
299+
panic!("expected InvalidValue for non-canonical long-form length, got {:?}", other)
300+
},
301+
}
302+
}
303+
304+
#[test]
305+
fn test_public_key_from_der_rejects_trailing_bytes() {
306+
// Build a valid SPKI DER but append trailing bytes after the sequence; DER should reject.
307+
let mut rng = seeded_rng([11u8; 32]);
308+
let secret_key = SecretKey::with_rng(&mut rng);
309+
let public_key = secret_key.public_key();
310+
let public_key_bytes = public_key.to_bytes(); // compressed SEC1 (33 bytes)
311+
312+
// AlgorithmIdentifier: id-ecPublicKey + secp256k1.
313+
let algo: [u8; 18] = [
314+
0x30, 0x10, // SEQUENCE, length 16
315+
0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // OID 1.2.840.10045.2.1
316+
0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a, // OID 1.3.132.0.10 (secp256k1)
317+
];
318+
319+
// subjectPublicKey BIT STRING: 0 unused bits + compressed SEC1.
320+
let mut spk = Vec::with_capacity(2 + 1 + public_key_bytes.len());
321+
spk.push(0x03); // BIT STRING
322+
spk.push((1 + public_key_bytes.len()) as u8); // length
323+
spk.push(0x00); // unused bits = 0
324+
spk.extend_from_slice(&public_key_bytes);
325+
326+
// Outer SEQUENCE with short-form length.
327+
let total_len = (algo.len() + spk.len()) as u8;
328+
let mut der = Vec::with_capacity(2 + algo.len() + spk.len() + 2);
329+
der.push(0x30); // SEQUENCE
330+
der.push(total_len);
331+
der.extend_from_slice(&algo);
332+
der.extend_from_slice(&spk);
333+
334+
// Append trailing junk.
335+
der.push(0x00);
336+
der.push(0x00);
337+
338+
match PublicKey::from_der(&der) {
339+
Err(super::DeserializationError::InvalidValue(_)) => {},
340+
other => panic!("expected InvalidValue for DER with trailing bytes, got {:?}", other),
341+
}
342+
}
343+
344+
#[test]
345+
fn test_public_key_from_der_rejects_wrong_curve_oid() {
346+
// Same structure but with prime256v1 (P-256) curve OID instead of secp256k1.
347+
let mut rng = seeded_rng([12u8; 32]);
348+
let secret_key = SecretKey::with_rng(&mut rng);
349+
let public_key = secret_key.public_key();
350+
let public_key_bytes = public_key.to_bytes(); // compressed SEC1 (33 bytes)
351+
352+
// AlgorithmIdentifier: id-ecPublicKey + prime256v1 (1.2.840.10045.3.1.7).
353+
// Completed prime256v1 OID tail for correctness
354+
// Full DER OID bytes for 1.2.840.10045.3.1.7 are: 06 08 2A 86 48 CE 3D 03 01 07
355+
// We'll encode properly below with 8 length, then adjust the outer lengths accordingly.
356+
357+
// AlgorithmIdentifier with correct OID encoding but wrong curve:
358+
let algo_full: [u8; 21] = [
359+
0x30, 0x12, // SEQUENCE, length 18
360+
0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // id-ecPublicKey
361+
0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, // prime256v1
362+
];
363+
364+
// subjectPublicKey BIT STRING.
365+
let mut spk = Vec::with_capacity(2 + 1 + public_key_bytes.len());
366+
spk.push(0x03);
367+
spk.push((1 + public_key_bytes.len()) as u8);
368+
spk.push(0x00);
369+
spk.extend_from_slice(&public_key_bytes);
370+
371+
let mut der = Vec::with_capacity(2 + algo_full.len() + spk.len());
372+
der.push(0x30);
373+
der.push((algo_full.len() + spk.len()) as u8);
374+
der.extend_from_slice(&algo_full);
375+
der.extend_from_slice(&spk);
376+
377+
match PublicKey::from_der(&der) {
378+
Err(super::DeserializationError::InvalidValue(_)) => {},
379+
other => panic!("expected InvalidValue for wrong curve OID, got {:?}", other),
380+
}
381+
}
382+
383+
#[test]
384+
fn test_public_key_from_der_rejects_wrong_algorithm_oid() {
385+
// Use rsaEncryption (1.2.840.113549.1.1.1) instead of id-ecPublicKey.
386+
let mut rng = seeded_rng([13u8; 32]);
387+
let secret_key = SecretKey::with_rng(&mut rng);
388+
let public_key = secret_key.public_key();
389+
let public_key_bytes = public_key.to_bytes();
390+
391+
// AlgorithmIdentifier: rsaEncryption + NULL parameter.
392+
// OID bytes for 1.2.840.113549.1.1.1: 06 09 2A 86 48 86 F7 0D 01 01 01.
393+
// NULL parameter: 05 00.
394+
let algo_rsa: [u8; 15] = [
395+
0x30, 0x0d, // SEQUENCE, length 13
396+
0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, // rsaEncryption
397+
0x05, 0x00, // NULL
398+
];
399+
400+
// subjectPublicKey BIT STRING with EC compressed point (intentionally mismatched with algo).
401+
let mut spk = Vec::with_capacity(2 + 1 + public_key_bytes.len());
402+
spk.push(0x03);
403+
spk.push((1 + public_key_bytes.len()) as u8);
404+
spk.push(0x00);
405+
spk.extend_from_slice(&public_key_bytes);
406+
407+
let mut der = Vec::with_capacity(2 + algo_rsa.len() + spk.len());
408+
der.push(0x30);
409+
der.push((algo_rsa.len() + spk.len()) as u8);
410+
der.extend_from_slice(&algo_rsa);
411+
der.extend_from_slice(&spk);
412+
413+
match PublicKey::from_der(&der) {
414+
Err(super::DeserializationError::InvalidValue(_)) => {},
415+
other => panic!("expected InvalidValue for wrong algorithm OID, got {:?}", other),
416+
}
417+
}

0 commit comments

Comments
 (0)