Skip to content

Commit 93063a3

Browse files
authored
feat: implement ECDSA Public Key from_der() (#855)
1 parent 548e198 commit 93063a3

File tree

4 files changed

+217
-1
lines changed

4 files changed

+217
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- Use read_from_bytes_with_budget instead of read_from_bytes for deserialization from untrusted sources, setting the budget to the actual input byte slice length. ([#846](https://github.com/0xMiden/crypto/pull/846)).
1717
- [BREAKING] Added info context field to secret box, bind IES HKDF info to a stable context string, scheme identifier, and ephemeral public key bytes. ([#843](https://github.com/0xMiden/crypto/pull/843)).
1818
- [BREAKING] Removed `PartialEq`/`Eq` for AEAD `SecretKey` in non-test builds, fix various hygiene issues in dealing with secret keys ([#849](https://github.com/0xMiden/crypto/pull/849)).
19+
- Added `PublicKey::from_der()` for ECDSA public keys over secp256k1 ([#855](https://github.com/0xMiden/crypto/pull/855)).
1920

2021
## 0.22.3 (2026-02-23)
2122

miden-crypto/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ curve25519-dalek = { default-features = false, version = "4" }
104104
ed25519-dalek = { features = ["zeroize"], version = "2" }
105105
flume = { version = "0.11.1" }
106106
hkdf = { default-features = false, version = "0.12" }
107-
k256 = { features = ["ecdh", "ecdsa"], version = "0.13" }
107+
k256 = { features = ["ecdh", "ecdsa", "pkcs8"], version = "0.13" }
108108
num = { default-features = false, features = ["alloc", "libm"], version = "0.4" }
109109
num-complex = { default-features = false, version = "0.4" }
110110
proptest = { default-features = false, features = ["alloc"], optional = true, version = "1.7" }

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

0 commit comments

Comments
 (0)