Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)).
Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
2 changes: 1 addition & 1 deletion miden-crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
58 changes: 54 additions & 4 deletions miden-crypto/src/dsa/ecdsa_k256_keccak/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<Self, DeserializationError> {
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 {
Expand Down Expand Up @@ -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<Self, DeserializationError> {
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<Self, DeserializationError> {
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,
})
}
}

Expand Down Expand Up @@ -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 })
}
}
}

Expand Down
Loading
Loading