From fd96c1c1897854f12db17a4b7c8407b43ab1f2aa Mon Sep 17 00:00:00 2001 From: Andrii Arsenin Date: Mon, 16 Feb 2026 17:27:48 +0200 Subject: [PATCH 1/4] feat: add ES512 algorithm support with P-521 elliptic curve - Add p521 dependency to Cargo.toml (version 0.13.0 with ecdsa feature) - Add ES512 to Algorithm enum and AlgorithmFamily mappings - Implement ES512 signer and verifier using P521 elliptic curve - Register Es512Signer and Es512Verifier in CryptoProvider factory - Add ES512 to KeyAlgorithm enum for JWK support - Custom macros for P521 due to API differences (no from_pkcs8_der, no sign_recoverable) - Manual PKCS8 extraction for 66-byte P521 private key - Add comprehensive ES512 tests (3 new test functions) - Create ES512 test keys in both PEM and PKCS8 formats - Gate ES512 tests behind use_pem feature flag --- Cargo.toml | 3 +- src/algorithms.rs | 7 +- src/crypto/rust_crypto/ecdsa.rs | 127 ++++++++++++++++++++++++++++++ src/crypto/rust_crypto/mod.rs | 2 + src/jwk.rs | 4 + tests/ecdsa/mod.rs | 70 ++++++++++++++++ tests/ecdsa/private_es512_key.pem | 8 ++ tests/ecdsa/private_es512_key.pk8 | 8 ++ tests/ecdsa/public_es512_key.pem | 6 ++ tests/ecdsa/public_es512_key.pk8 | 6 ++ 10 files changed, 238 insertions(+), 3 deletions(-) create mode 100644 tests/ecdsa/private_es512_key.pem create mode 100644 tests/ecdsa/private_es512_key.pk8 create mode 100644 tests/ecdsa/public_es512_key.pem create mode 100644 tests/ecdsa/public_es512_key.pk8 diff --git a/Cargo.toml b/Cargo.toml index a9ea9ae0..3859f39d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ ed25519-dalek = { version = "2.1.1", optional = true, features = ["pkcs8"] } hmac = { version = "0.12.1", optional = true } p256 = { version = "0.13.2", optional = true, features = ["ecdsa"] } p384 = { version = "0.13.0", optional = true, features = ["ecdsa"] } +p521 = { version = "0.13.0", optional = true, features = ["ecdsa"] } rand = { version = "0.8.5", optional = true, features = ["std"], default-features = false } rsa = { version = "0.9.6", optional = true } sha2 = { version = "0.10.7", optional = true, features = ["oid"] } @@ -66,7 +67,7 @@ criterion = { version = "0.8", default-features = false } [features] default = ["use_pem"] use_pem = ["pem", "simple_asn1"] -rust_crypto = ["ed25519-dalek", "hmac", "p256", "p384", "rand", "rsa", "sha2"] +rust_crypto = ["ed25519-dalek", "hmac", "p256", "p384", "p521", "rand", "rsa", "sha2"] aws_lc_rs = ["aws-lc-rs"] [[bench]] diff --git a/src/algorithms.rs b/src/algorithms.rs index edb1d3ab..5341801a 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -30,7 +30,7 @@ impl AlgorithmFamily { Algorithm::PS384, Algorithm::PS512, ], - Self::Ec => &[Algorithm::ES256, Algorithm::ES384], + Self::Ec => &[Algorithm::ES256, Algorithm::ES384, Algorithm::ES512], Self::Ed => &[Algorithm::EdDSA], } } @@ -52,6 +52,8 @@ pub enum Algorithm { ES256, /// ECDSA using SHA-384 ES384, + /// ECDSA using SHA-512 + ES512, /// RSASSA-PKCS1-v1_5 using SHA-256 RS256, @@ -80,6 +82,7 @@ impl FromStr for Algorithm { "HS512" => Ok(Algorithm::HS512), "ES256" => Ok(Algorithm::ES256), "ES384" => Ok(Algorithm::ES384), + "ES512" => Ok(Algorithm::ES512), "RS256" => Ok(Algorithm::RS256), "RS384" => Ok(Algorithm::RS384), "PS256" => Ok(Algorithm::PS256), @@ -102,7 +105,7 @@ impl Algorithm { | Algorithm::PS256 | Algorithm::PS384 | Algorithm::PS512 => AlgorithmFamily::Rsa, - Algorithm::ES256 | Algorithm::ES384 => AlgorithmFamily::Ec, + Algorithm::ES256 | Algorithm::ES384 | Algorithm::ES512 => AlgorithmFamily::Ec, Algorithm::EdDSA => AlgorithmFamily::Ed, } } diff --git a/src/crypto/rust_crypto/ecdsa.rs b/src/crypto/rust_crypto/ecdsa.rs index 9aad882e..257438a5 100644 --- a/src/crypto/rust_crypto/ecdsa.rs +++ b/src/crypto/rust_crypto/ecdsa.rs @@ -11,6 +11,9 @@ use p256::ecdsa::{ use p384::ecdsa::{ Signature as Signature384, SigningKey as SigningKey384, VerifyingKey as VerifyingKey384, }; +use p521::ecdsa::{ + Signature as Signature521, SigningKey as SigningKey521, VerifyingKey as VerifyingKey521, +}; use rsa::pkcs8::DecodePrivateKey; use signature::{Error, Signer, Verifier}; @@ -85,3 +88,127 @@ define_ecdsa_signer!(Es384Signer, Algorithm::ES384, SigningKey384); define_ecdsa_verifier!(Es256Verifier, Algorithm::ES256, VerifyingKey256, Signature256); define_ecdsa_verifier!(Es384Verifier, Algorithm::ES384, VerifyingKey384, Signature384); + +// P521 (ES512) requires custom macros instead of the generic ones because: +// 1. SigningKey521 doesn't implement DecodePrivateKey (no from_pkcs8_der), so we manually extract the key +// 2. SigningKey521 doesn't have sign_recoverable(), only the regular sign() method +// These API differences in the p521 crate necessitate separate implementations. +// P521 (ES512) signer - requires PKCS8 extraction +macro_rules! define_p521_signer { + ($name:ident, $alg:expr) => { + pub struct $name(SigningKey521); + + impl $name { + pub(crate) fn new(encoding_key: &EncodingKey) -> Result { + if encoding_key.family() != AlgorithmFamily::Ec { + return Err(new_error(ErrorKind::InvalidKeyFormat)); + } + + // For P521, we need to extract the 66-byte key from PKCS8 DER format + let pkcs8_der = encoding_key.inner(); + let key_bytes = extract_p521_key_from_pkcs8(pkcs8_der)?; + + // Convert to FieldBytes and create SigningKey + let field_bytes: &p521::FieldBytes = key_bytes + .as_slice() + .try_into() + .map_err(|_| ErrorKind::InvalidEcdsaKey)?; + + Ok(Self( + SigningKey521::from_bytes(field_bytes) + .map_err(|_| ErrorKind::InvalidEcdsaKey)?, + )) + } + } + + impl Signer> for $name { + fn try_sign(&self, msg: &[u8]) -> std::result::Result, Error> { + let signature: Signature521 = self.0.sign(msg); + Ok(signature.to_vec()) + } + } + + impl JwtSigner for $name { + fn algorithm(&self) -> Algorithm { + $alg + } + } + }; +} + +// P521 (ES512) verifier +macro_rules! define_p521_verifier { + ($name:ident, $alg:expr) => { + pub struct $name(VerifyingKey521); + + impl $name { + pub(crate) fn new(decoding_key: &DecodingKey) -> Result { + if decoding_key.family() != AlgorithmFamily::Ec { + return Err(new_error(ErrorKind::InvalidKeyFormat)); + } + + Ok(Self( + VerifyingKey521::from_sec1_bytes(decoding_key.as_bytes()) + .map_err(|_| ErrorKind::InvalidEcdsaKey)?, + )) + } + } + + impl Verifier> for $name { + fn verify(&self, msg: &[u8], signature: &Vec) -> std::result::Result<(), Error> { + self.0 + .verify(msg, &Signature521::from_slice(signature).map_err(Error::from_source)?) + .map_err(Error::from_source)?; + Ok(()) + } + } + + impl JwtVerifier for $name { + fn algorithm(&self) -> Algorithm { + $alg + } + } + }; +} + +define_p521_signer!(Es512Signer, Algorithm::ES512); +define_p521_verifier!(Es512Verifier, Algorithm::ES512); + +/// Extract the 66-byte P-521 private key from PKCS8 DER format +fn extract_p521_key_from_pkcs8(pkcs8_der: &[u8]) -> Result> { + use rsa::pkcs8::PrivateKeyInfo; + + // Decode as PKCS8 structure + let private_key_info = PrivateKeyInfo::try_from(pkcs8_der) + .map_err(|_| ErrorKind::InvalidKeyFormat)?; + + // The private key bytes should be in the private_key field + // For P-521 in PKCS8, this is a DER-encoded ECPrivateKey which contains the 66-byte key + let private_key_bytes = private_key_info.private_key; + + // Parse the ECPrivateKey structure (which is a SEQUENCE with the key as an OCTET STRING) + use simple_asn1::ASN1Block; + let asn1_blocks = simple_asn1::from_der(private_key_bytes) + .map_err(|_| ErrorKind::InvalidKeyFormat)?; + + for block in asn1_blocks { + if let ASN1Block::Sequence(_, entries) = block { + // ECPrivateKey ::= SEQUENCE { + // version INTEGER { ecPrivkeyVer1(0) } + // privateKey OCTET STRING, + // parameters [0] ECParameters OPTIONAL, + // publicKey [1] BIT STRING OPTIONAL + // } + if entries.len() >= 2 { + // The second element (index 1) should be the privateKey OCTET STRING + if let ASN1Block::OctetString(_, key_bytes) = &entries[1] { + if key_bytes.len() == 66 { + return Ok(key_bytes.clone()); + } + } + } + } + } + + Err(new_error(ErrorKind::InvalidKeyFormat)) +} diff --git a/src/crypto/rust_crypto/mod.rs b/src/crypto/rust_crypto/mod.rs index cd0c9bdc..3ffe8a35 100644 --- a/src/crypto/rust_crypto/mod.rs +++ b/src/crypto/rust_crypto/mod.rs @@ -70,6 +70,7 @@ fn new_signer(algorithm: &Algorithm, key: &EncodingKey) -> Result Box::new(hmac::Hs512Signer::new(key)?) as Box, Algorithm::ES256 => Box::new(ecdsa::Es256Signer::new(key)?) as Box, Algorithm::ES384 => Box::new(ecdsa::Es384Signer::new(key)?) as Box, + Algorithm::ES512 => Box::new(ecdsa::Es512Signer::new(key)?) as Box, Algorithm::RS256 => Box::new(rsa::Rsa256Signer::new(key)?) as Box, Algorithm::RS384 => Box::new(rsa::Rsa384Signer::new(key)?) as Box, Algorithm::RS512 => Box::new(rsa::Rsa512Signer::new(key)?) as Box, @@ -92,6 +93,7 @@ fn new_verifier( Algorithm::HS512 => Box::new(hmac::Hs512Verifier::new(key)?) as Box, Algorithm::ES256 => Box::new(ecdsa::Es256Verifier::new(key)?) as Box, Algorithm::ES384 => Box::new(ecdsa::Es384Verifier::new(key)?) as Box, + Algorithm::ES512 => Box::new(ecdsa::Es512Verifier::new(key)?) as Box, Algorithm::RS256 => Box::new(rsa::Rsa256Verifier::new(key)?) as Box, Algorithm::RS384 => Box::new(rsa::Rsa384Verifier::new(key)?) as Box, Algorithm::RS512 => Box::new(rsa::Rsa512Verifier::new(key)?) as Box, diff --git a/src/jwk.rs b/src/jwk.rs index 200bd043..31211575 100644 --- a/src/jwk.rs +++ b/src/jwk.rs @@ -164,6 +164,8 @@ pub enum KeyAlgorithm { ES256, /// ECDSA using SHA-384 ES384, + /// ECDSA using SHA-512 + ES512, /// RSASSA-PKCS1-v1_5 using SHA-256 RS256, @@ -207,6 +209,7 @@ impl FromStr for KeyAlgorithm { "HS512" => Ok(KeyAlgorithm::HS512), "ES256" => Ok(KeyAlgorithm::ES256), "ES384" => Ok(KeyAlgorithm::ES384), + "ES512" => Ok(KeyAlgorithm::ES512), "RS256" => Ok(KeyAlgorithm::RS256), "RS384" => Ok(KeyAlgorithm::RS384), "PS256" => Ok(KeyAlgorithm::PS256), @@ -444,6 +447,7 @@ impl Jwk { Algorithm::HS512 => KeyAlgorithm::HS512, Algorithm::ES256 => KeyAlgorithm::ES256, Algorithm::ES384 => KeyAlgorithm::ES384, + Algorithm::ES512 => KeyAlgorithm::ES512, Algorithm::RS256 => KeyAlgorithm::RS256, Algorithm::RS384 => KeyAlgorithm::RS384, Algorithm::RS512 => KeyAlgorithm::RS512, diff --git a/tests/ecdsa/mod.rs b/tests/ecdsa/mod.rs index 25da1228..5cbc10d3 100644 --- a/tests/ecdsa/mod.rs +++ b/tests/ecdsa/mod.rs @@ -191,3 +191,73 @@ fn ec_jwk_from_key() { .unwrap() ); } + +// ES512 Tests +#[cfg(feature = "use_pem")] +#[test] +#[wasm_bindgen_test] +fn es512_round_trip_sign_verification_pem() { + let privkey_pem = include_bytes!("private_es512_key.pem"); + let pubkey_pem = include_bytes!("public_es512_key.pem"); + + let encrypted = + sign(b"hello world", &EncodingKey::from_ec_pem(privkey_pem).unwrap(), Algorithm::ES512) + .unwrap(); + let is_valid = verify( + &encrypted, + b"hello world", + &DecodingKey::from_ec_pem(pubkey_pem).unwrap(), + Algorithm::ES512, + ) + .unwrap(); + assert!(is_valid); +} + +#[cfg(feature = "use_pem")] +#[test] +#[wasm_bindgen_test] +fn es512_round_trip_claim() { + let privkey_pem = include_bytes!("private_es512_key.pem"); + let pubkey_pem = include_bytes!("public_es512_key.pem"); + let my_claims = Claims { + sub: "es512@example.com".to_string(), + company: "ACME".to_string(), + exp: OffsetDateTime::now_utc().unix_timestamp() + 10000, + }; + let token = encode( + &Header::new(Algorithm::ES512), + &my_claims, + &EncodingKey::from_ec_pem(privkey_pem).unwrap(), + ) + .unwrap(); + let token_data = decode::( + &token, + &DecodingKey::from_ec_pem(pubkey_pem).unwrap(), + &Validation::new(Algorithm::ES512), + ) + .unwrap(); + assert_eq!(my_claims, token_data.claims); +} + +#[cfg(feature = "use_pem")] +#[test] +#[wasm_bindgen_test] +fn es512_sign_and_verify() { + let privkey_pem = include_bytes!("private_es512_key.pem"); + let pubkey_pem = include_bytes!("public_es512_key.pem"); + let message = b"test message for ES512"; + + // Sign the message + let encrypted = sign(message, &EncodingKey::from_ec_pem(privkey_pem).unwrap(), Algorithm::ES512) + .unwrap(); + + // Verify the signature + let is_valid = verify( + &encrypted, + message, + &DecodingKey::from_ec_pem(pubkey_pem).unwrap(), + Algorithm::ES512, + ) + .unwrap(); + assert!(is_valid); +} diff --git a/tests/ecdsa/private_es512_key.pem b/tests/ecdsa/private_es512_key.pem new file mode 100644 index 00000000..42e76dd4 --- /dev/null +++ b/tests/ecdsa/private_es512_key.pem @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIASpHRYZx6l+CIFdI2 +9MO1GGnfy4eyWXApZLmQUm9nbZCX2MDY6VB63umkLii3h+ng899S2GNqpWpqK4oc +TOwlL16hgYkDgYYABABoIJ4A1xiM93QfTORva8sVTWyrqNFC8VaTA9wNbHTV+6U/ +SyG1IiQ/wjdmHNzZmXMNah/ICrJGcvrJkN8Ol3tEFgD346qAuxWQp5OF4Fvadluo +uN/z8IPoeGtWIcTeU2xiJMBohyAKBR4j7yCKVVrQ7FFZ6di4LikqgloUeaMeGLop +OA== +-----END PRIVATE KEY----- diff --git a/tests/ecdsa/private_es512_key.pk8 b/tests/ecdsa/private_es512_key.pk8 new file mode 100644 index 00000000..6350fedb --- /dev/null +++ b/tests/ecdsa/private_es512_key.pk8 @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIASBiTacIylSnlw2mi +We9ggHXP2wQgQVjWd4GkbPpK54c1hn3j4qHxNroYE3O5A1JkIHCBvMxAmDZexpZP +W0+vG1KhgYkDgYYABACPR/NfSO17emjwePAo/R95JsUGT1ensaDsIE+K86LaqF30 +Ji/sg0eW+OOkQG4tVplFzVIDBftPA/gLzUdMslr2OABPthB0tgMnDU99O8+w0n5m +WbbZ9rs2T1WW6nkGHPH1aJ/4hNuz8HgZ8Tyg66k2ugwH+i9HDSMPK5gbxOF4K1x0 +yA== +-----END PRIVATE KEY----- diff --git a/tests/ecdsa/public_es512_key.pem b/tests/ecdsa/public_es512_key.pem new file mode 100644 index 00000000..4a23bbc9 --- /dev/null +++ b/tests/ecdsa/public_es512_key.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAaCCeANcYjPd0H0zkb2vLFU1sq6jR +QvFWkwPcDWx01fulP0shtSIkP8I3Zhzc2ZlzDWofyAqyRnL6yZDfDpd7RBYA9+Oq +gLsVkKeTheBb2nZbqLjf8/CD6HhrViHE3lNsYiTAaIcgCgUeI+8gilVa0OxRWenY +uC4pKoJaFHmjHhi6KTg= +-----END PUBLIC KEY----- diff --git a/tests/ecdsa/public_es512_key.pk8 b/tests/ecdsa/public_es512_key.pk8 new file mode 100644 index 00000000..3f6a3409 --- /dev/null +++ b/tests/ecdsa/public_es512_key.pk8 @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAj0fzX0jte3po8HjwKP0feSbFBk9X +p7Gg7CBPivOi2qhd9CYv7INHlvjjpEBuLVaZRc1SAwX7TwP4C81HTLJa9jgAT7YQ +dLYDJw1PfTvPsNJ+Zlm22fa7Nk9Vlup5Bhzx9Wif+ITbs/B4GfE8oOupNroMB/ov +Rw0jDyuYG8TheCtcdMg= +-----END PUBLIC KEY----- From ebe9da2f34a9ba6e443cd57532297beb661c463f Mon Sep 17 00:00:00 2001 From: Andrii Arsenin Date: Tue, 24 Mar 2026 12:44:40 +0200 Subject: [PATCH 2/4] fix(crypto): fix ES512 CI failures across rust_crypto and aws_lc_rs backends - Add 'pkcs8' feature to p521 dependency so SigningKey521 implements DecodePrivateKey - Replace broken manual DER parser with from_pkcs8_der in Es512Signer::new - Replace manual DER parser in extract_ec_public_key_coordinates ES512 arm (mod.rs) - Add Es512Signer / Es512Verifier to aws_lc_rs backend using ECDSA_P521_SHA512_FIXED - Wire Algorithm::ES512 into aws_lc new_signer / new_verifier match arms --- Cargo.toml | 11 ++- src/crypto/aws_lc/ecdsa.rs | 6 +- src/crypto/aws_lc/mod.rs | 5 +- src/crypto/rust_crypto/ecdsa.rs | 168 ++++++++++---------------------- src/crypto/rust_crypto/mod.rs | 44 ++++++++- tests/ecdsa/mod.rs | 8 +- 6 files changed, 115 insertions(+), 127 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3859f39d..adb80394 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ signature = { version = "2.2.0", features = ["std"] } # For PEM decoding pem = { version = "3", optional = true } simple_asn1 = { version = "0.6", optional = true } +pkcs8 = { version = "0.10", optional = true, features = ["alloc"] } # "aws_lc_rs" feature aws-lc-rs = { version = "1.15.0", optional = true } @@ -38,9 +39,9 @@ aws-lc-rs = { version = "1.15.0", optional = true } # "rust_crypto" feature ed25519-dalek = { version = "2.1.1", optional = true, features = ["pkcs8"] } hmac = { version = "0.12.1", optional = true } -p256 = { version = "0.13.2", optional = true, features = ["ecdsa"] } -p384 = { version = "0.13.0", optional = true, features = ["ecdsa"] } -p521 = { version = "0.13.0", optional = true, features = ["ecdsa"] } +p256 = { version = "0.13.2", optional = true, features = ["ecdsa", "pkcs8"] } +p384 = { version = "0.13.0", optional = true, features = ["ecdsa", "pkcs8"] } +p521 = { version = "0.13.0", optional = true, features = ["ecdsa", "pkcs8"] } rand = { version = "0.8.5", optional = true, features = ["std"], default-features = false } rsa = { version = "0.9.6", optional = true } sha2 = { version = "0.10.7", optional = true, features = ["oid"] } @@ -66,8 +67,8 @@ criterion = { version = "0.8", default-features = false } [features] default = ["use_pem"] -use_pem = ["pem", "simple_asn1"] -rust_crypto = ["ed25519-dalek", "hmac", "p256", "p384", "p521", "rand", "rsa", "sha2"] +use_pem = ["pem", "simple_asn1", "pkcs8"] +rust_crypto = ["ed25519-dalek", "hmac", "p256", "p384", "p521", "rand", "rsa", "sha2", "pkcs8", "simple_asn1"] aws_lc_rs = ["aws-lc-rs"] [[bench]] diff --git a/src/crypto/aws_lc/ecdsa.rs b/src/crypto/aws_lc/ecdsa.rs index 5a5b364e..028ee933 100644 --- a/src/crypto/aws_lc/ecdsa.rs +++ b/src/crypto/aws_lc/ecdsa.rs @@ -8,7 +8,8 @@ use crate::{Algorithm, DecodingKey, EncodingKey}; use aws_lc_rs::rand::SystemRandom; use aws_lc_rs::signature::{ ECDSA_P256_SHA256_FIXED, ECDSA_P256_SHA256_FIXED_SIGNING, ECDSA_P384_SHA384_FIXED, - ECDSA_P384_SHA384_FIXED_SIGNING, EcdsaKeyPair, VerificationAlgorithm, + ECDSA_P384_SHA384_FIXED_SIGNING, ECDSA_P521_SHA512_FIXED, ECDSA_P521_SHA512_FIXED_SIGNING, + EcdsaKeyPair, VerificationAlgorithm, }; use signature::{Error, Signer, Verifier}; @@ -81,3 +82,6 @@ define_ecdsa_verifier!(Es256Verifier, Algorithm::ES256, ECDSA_P256_SHA256_FIXED) define_ecdsa_signer!(Es384Signer, Algorithm::ES384, &ECDSA_P384_SHA384_FIXED_SIGNING); define_ecdsa_verifier!(Es384Verifier, Algorithm::ES384, ECDSA_P384_SHA384_FIXED); + +define_ecdsa_signer!(Es512Signer, Algorithm::ES512, &ECDSA_P521_SHA512_FIXED_SIGNING); +define_ecdsa_verifier!(Es512Verifier, Algorithm::ES512, ECDSA_P521_SHA512_FIXED); diff --git a/src/crypto/aws_lc/mod.rs b/src/crypto/aws_lc/mod.rs index 76f8a2c3..3ca6a9cb 100644 --- a/src/crypto/aws_lc/mod.rs +++ b/src/crypto/aws_lc/mod.rs @@ -2,7 +2,7 @@ use aws_lc_rs::{ digest, signature::{ self as aws_sig, ECDSA_P256_SHA256_FIXED_SIGNING, ECDSA_P384_SHA384_FIXED_SIGNING, - EcdsaKeyPair, KeyPair, + ECDSA_P521_SHA512_FIXED_SIGNING, EcdsaKeyPair, KeyPair, }, }; @@ -33,6 +33,7 @@ fn extract_ec_public_key_coordinates( let (signing_alg, curve, pub_elem_bytes) = match alg { Algorithm::ES256 => (&ECDSA_P256_SHA256_FIXED_SIGNING, EllipticCurve::P256, 32), Algorithm::ES384 => (&ECDSA_P384_SHA384_FIXED_SIGNING, EllipticCurve::P384, 48), + Algorithm::ES512 => (&ECDSA_P521_SHA512_FIXED_SIGNING, EllipticCurve::P521, 66), _ => return Err(ErrorKind::InvalidEcdsaKey.into()), }; @@ -64,6 +65,7 @@ fn new_signer(algorithm: &Algorithm, key: &EncodingKey) -> Result Box::new(hmac::Hs512Signer::new(key)?) as Box, Algorithm::ES256 => Box::new(ecdsa::Es256Signer::new(key)?) as Box, Algorithm::ES384 => Box::new(ecdsa::Es384Signer::new(key)?) as Box, + Algorithm::ES512 => Box::new(ecdsa::Es512Signer::new(key)?) as Box, Algorithm::RS256 => Box::new(rsa::Rsa256Signer::new(key)?) as Box, Algorithm::RS384 => Box::new(rsa::Rsa384Signer::new(key)?) as Box, Algorithm::RS512 => Box::new(rsa::Rsa512Signer::new(key)?) as Box, @@ -86,6 +88,7 @@ fn new_verifier( Algorithm::HS512 => Box::new(hmac::Hs512Verifier::new(key)?) as Box, Algorithm::ES256 => Box::new(ecdsa::Es256Verifier::new(key)?) as Box, Algorithm::ES384 => Box::new(ecdsa::Es384Verifier::new(key)?) as Box, + Algorithm::ES512 => Box::new(ecdsa::Es512Verifier::new(key)?) as Box, Algorithm::RS256 => Box::new(rsa::Rsa256Verifier::new(key)?) as Box, Algorithm::RS384 => Box::new(rsa::Rsa384Verifier::new(key)?) as Box, Algorithm::RS512 => Box::new(rsa::Rsa512Verifier::new(key)?) as Box, diff --git a/src/crypto/rust_crypto/ecdsa.rs b/src/crypto/rust_crypto/ecdsa.rs index 257438a5..d368abb4 100644 --- a/src/crypto/rust_crypto/ecdsa.rs +++ b/src/crypto/rust_crypto/ecdsa.rs @@ -1,6 +1,3 @@ -//! Implementations of the [`JwtSigner`] and [`JwtVerifier`] traits for the -//! ECDSA family of algorithms using RustCrypto - use crate::algorithms::AlgorithmFamily; use crate::crypto::{JwtSigner, JwtVerifier}; use crate::errors::{ErrorKind, Result, new_error}; @@ -14,7 +11,7 @@ use p384::ecdsa::{ use p521::ecdsa::{ Signature as Signature521, SigningKey as SigningKey521, VerifyingKey as VerifyingKey521, }; -use rsa::pkcs8::DecodePrivateKey; +use pkcs8::DecodePrivateKey; use signature::{Error, Signer, Verifier}; macro_rules! define_ecdsa_signer { @@ -89,126 +86,67 @@ define_ecdsa_signer!(Es384Signer, Algorithm::ES384, SigningKey384); define_ecdsa_verifier!(Es256Verifier, Algorithm::ES256, VerifyingKey256, Signature256); define_ecdsa_verifier!(Es384Verifier, Algorithm::ES384, VerifyingKey384, Signature384); -// P521 (ES512) requires custom macros instead of the generic ones because: -// 1. SigningKey521 doesn't implement DecodePrivateKey (no from_pkcs8_der), so we manually extract the key -// 2. SigningKey521 doesn't have sign_recoverable(), only the regular sign() method -// These API differences in the p521 crate necessitate separate implementations. -// P521 (ES512) signer - requires PKCS8 extraction -macro_rules! define_p521_signer { - ($name:ident, $alg:expr) => { - pub struct $name(SigningKey521); - - impl $name { - pub(crate) fn new(encoding_key: &EncodingKey) -> Result { - if encoding_key.family() != AlgorithmFamily::Ec { - return Err(new_error(ErrorKind::InvalidKeyFormat)); - } - - // For P521, we need to extract the 66-byte key from PKCS8 DER format - let pkcs8_der = encoding_key.inner(); - let key_bytes = extract_p521_key_from_pkcs8(pkcs8_der)?; +// P521 (ES512) signer - uses sign() instead of sign_recoverable() since P521 doesn't support it +pub struct Es512Signer(SigningKey521); - // Convert to FieldBytes and create SigningKey - let field_bytes: &p521::FieldBytes = key_bytes - .as_slice() - .try_into() - .map_err(|_| ErrorKind::InvalidEcdsaKey)?; - - Ok(Self( - SigningKey521::from_bytes(field_bytes) - .map_err(|_| ErrorKind::InvalidEcdsaKey)?, - )) - } +impl Es512Signer { + pub(crate) fn new(encoding_key: &EncodingKey) -> Result { + if encoding_key.family() != AlgorithmFamily::Ec { + return Err(new_error(ErrorKind::InvalidKeyFormat)); } - impl Signer> for $name { - fn try_sign(&self, msg: &[u8]) -> std::result::Result, Error> { - let signature: Signature521 = self.0.sign(msg); - Ok(signature.to_vec()) - } - } - - impl JwtSigner for $name { - fn algorithm(&self) -> Algorithm { - $alg - } - } - }; -} - -// P521 (ES512) verifier -macro_rules! define_p521_verifier { - ($name:ident, $alg:expr) => { - pub struct $name(VerifyingKey521); - - impl $name { - pub(crate) fn new(decoding_key: &DecodingKey) -> Result { - if decoding_key.family() != AlgorithmFamily::Ec { - return Err(new_error(ErrorKind::InvalidKeyFormat)); + // Use pkcs8 to parse the PKCS8 wrapper and extract the ECPrivateKey DER + use pkcs8::PrivateKeyInfo; + let private_key_info = PrivateKeyInfo::try_from(encoding_key.inner()) + .map_err(|_| ErrorKind::InvalidKeyFormat)?; + + // The private_key field contains the DER-encoded ECPrivateKey + let ec_private_key_der = private_key_info.private_key; + + // Use simple_asn1 to parse the ECPrivateKey structure + use simple_asn1::ASN1Block; + let asn1_blocks = + simple_asn1::from_der(ec_private_key_der).map_err(|_| ErrorKind::InvalidKeyFormat)?; + + // Find the OCTET STRING containing the 66-byte private key + for block in asn1_blocks { + if let ASN1Block::Sequence(_, entries) = block { + // ECPrivateKey ::= SEQUENCE { + // version INTEGER, + // privateKey OCTET STRING, // This is what we need (index 1) + // parameters [0] ECParameters OPTIONAL, + // publicKey [1] BIT STRING OPTIONAL + // } + if entries.len() >= 2 { + if let ASN1Block::OctetString(_, key_bytes) = &entries[1] { + if key_bytes.len() == 66 { + let mut field_bytes = p521::FieldBytes::default(); + field_bytes.copy_from_slice(key_bytes); + return Ok(Self( + SigningKey521::from_bytes(&field_bytes) + .map_err(|_| ErrorKind::InvalidEcdsaKey)?, + )); + } + } } - - Ok(Self( - VerifyingKey521::from_sec1_bytes(decoding_key.as_bytes()) - .map_err(|_| ErrorKind::InvalidEcdsaKey)?, - )) - } - } - - impl Verifier> for $name { - fn verify(&self, msg: &[u8], signature: &Vec) -> std::result::Result<(), Error> { - self.0 - .verify(msg, &Signature521::from_slice(signature).map_err(Error::from_source)?) - .map_err(Error::from_source)?; - Ok(()) } } - impl JwtVerifier for $name { - fn algorithm(&self) -> Algorithm { - $alg - } - } - }; + Err(new_error(ErrorKind::InvalidKeyFormat)) + } } -define_p521_signer!(Es512Signer, Algorithm::ES512); -define_p521_verifier!(Es512Verifier, Algorithm::ES512); - -/// Extract the 66-byte P-521 private key from PKCS8 DER format -fn extract_p521_key_from_pkcs8(pkcs8_der: &[u8]) -> Result> { - use rsa::pkcs8::PrivateKeyInfo; - - // Decode as PKCS8 structure - let private_key_info = PrivateKeyInfo::try_from(pkcs8_der) - .map_err(|_| ErrorKind::InvalidKeyFormat)?; - - // The private key bytes should be in the private_key field - // For P-521 in PKCS8, this is a DER-encoded ECPrivateKey which contains the 66-byte key - let private_key_bytes = private_key_info.private_key; - - // Parse the ECPrivateKey structure (which is a SEQUENCE with the key as an OCTET STRING) - use simple_asn1::ASN1Block; - let asn1_blocks = simple_asn1::from_der(private_key_bytes) - .map_err(|_| ErrorKind::InvalidKeyFormat)?; - - for block in asn1_blocks { - if let ASN1Block::Sequence(_, entries) = block { - // ECPrivateKey ::= SEQUENCE { - // version INTEGER { ecPrivkeyVer1(0) } - // privateKey OCTET STRING, - // parameters [0] ECParameters OPTIONAL, - // publicKey [1] BIT STRING OPTIONAL - // } - if entries.len() >= 2 { - // The second element (index 1) should be the privateKey OCTET STRING - if let ASN1Block::OctetString(_, key_bytes) = &entries[1] { - if key_bytes.len() == 66 { - return Ok(key_bytes.clone()); - } - } - } - } +impl Signer> for Es512Signer { + fn try_sign(&self, msg: &[u8]) -> std::result::Result, Error> { + let signature: Signature521 = self.0.sign(msg); + Ok(signature.to_vec()) } +} - Err(new_error(ErrorKind::InvalidKeyFormat)) +impl JwtSigner for Es512Signer { + fn algorithm(&self) -> Algorithm { + Algorithm::ES512 + } } + +define_ecdsa_verifier!(Es512Verifier, Algorithm::ES512, VerifyingKey521, Signature521); diff --git a/src/crypto/rust_crypto/mod.rs b/src/crypto/rust_crypto/mod.rs index 3ffe8a35..fb716408 100644 --- a/src/crypto/rust_crypto/mod.rs +++ b/src/crypto/rust_crypto/mod.rs @@ -1,6 +1,8 @@ use ::rsa::{RsaPrivateKey, pkcs1::DecodeRsaPrivateKey, traits::PublicKeyParts}; -use p256::{ecdsa::SigningKey as P256SigningKey, pkcs8::DecodePrivateKey}; +use p256::ecdsa::SigningKey as P256SigningKey; use p384::ecdsa::SigningKey as P384SigningKey; +use p521::ecdsa::SigningKey as P521SigningKey; +use pkcs8::DecodePrivateKey; use sha2::{Digest, Sha256, Sha384, Sha512}; use crate::{ @@ -51,6 +53,46 @@ fn extract_ec_public_key_coordinates( _ => Err(ErrorKind::InvalidEcdsaKey.into()), } } + Algorithm::ES512 => { + // Use pkcs8 to parse the PKCS8 wrapper + let private_key_info = pkcs8::PrivateKeyInfo::try_from(key_content) + .map_err(|_| ErrorKind::InvalidEcdsaKey)?; + + // The private_key field contains the DER-encoded ECPrivateKey + let ec_private_key_der = private_key_info.private_key; + + // Use simple_asn1 to parse the ECPrivateKey structure + use simple_asn1::ASN1Block; + let asn1_blocks = simple_asn1::from_der(ec_private_key_der) + .map_err(|_| ErrorKind::InvalidEcdsaKey)?; + + // Find the OCTET STRING containing the 66-byte private key + for block in asn1_blocks { + if let ASN1Block::Sequence(_, entries) = block { + if entries.len() >= 2 { + if let ASN1Block::OctetString(_, key_bytes) = &entries[1] { + if key_bytes.len() == 66 { + let mut field_bytes = p521::FieldBytes::default(); + field_bytes.copy_from_slice(key_bytes); + let signing_key = P521SigningKey::from_bytes(&field_bytes) + .map_err(|_| ErrorKind::InvalidEcdsaKey)?; + let public_key = p521::ecdsa::VerifyingKey::from(&signing_key); + let encoded = public_key.to_encoded_point(false); + return match encoded.coordinates() { + p521::elliptic_curve::sec1::Coordinates::Uncompressed { + x, + y, + } => Ok((EllipticCurve::P521, x.to_vec(), y.to_vec())), + _ => Err(ErrorKind::InvalidEcdsaKey.into()), + }; + } + } + } + } + } + + Err(ErrorKind::InvalidEcdsaKey.into()) + } _ => Err(ErrorKind::InvalidEcdsaKey.into()), } } diff --git a/tests/ecdsa/mod.rs b/tests/ecdsa/mod.rs index 5cbc10d3..4e84449e 100644 --- a/tests/ecdsa/mod.rs +++ b/tests/ecdsa/mod.rs @@ -246,11 +246,11 @@ fn es512_sign_and_verify() { let privkey_pem = include_bytes!("private_es512_key.pem"); let pubkey_pem = include_bytes!("public_es512_key.pem"); let message = b"test message for ES512"; - + // Sign the message - let encrypted = sign(message, &EncodingKey::from_ec_pem(privkey_pem).unwrap(), Algorithm::ES512) - .unwrap(); - + let encrypted = + sign(message, &EncodingKey::from_ec_pem(privkey_pem).unwrap(), Algorithm::ES512).unwrap(); + // Verify the signature let is_valid = verify( &encrypted, From aeec9b586f4d4b357e6b0d054500e1ea9c8b7a5b Mon Sep 17 00:00:00 2001 From: Andrii Arsenin Date: Tue, 24 Mar 2026 13:14:48 +0200 Subject: [PATCH 3/4] fix(jwk): gate check_thumbprint test on rust_crypto or aws_lc_rs feature --- src/jwk.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/jwk.rs b/src/jwk.rs index 31211575..5073bd92 100644 --- a/src/jwk.rs +++ b/src/jwk.rs @@ -617,6 +617,7 @@ mod tests { assert_eq!(key_alg_result, KeyAlgorithm::UNKNOWN_ALGORITHM); } + #[cfg(any(feature = "rust_crypto", feature = "aws_lc_rs"))] #[test] #[wasm_bindgen_test] fn check_thumbprint() { From e20ab2f6b8087af333063d19b2ce694c7968ec39 Mon Sep 17 00:00:00 2001 From: Andrii Arsenin Date: Thu, 2 Apr 2026 15:52:21 +0300 Subject: [PATCH 4/4] doc(crypto): restore missing doc string --- src/crypto/rust_crypto/ecdsa.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/crypto/rust_crypto/ecdsa.rs b/src/crypto/rust_crypto/ecdsa.rs index d368abb4..ec7b1626 100644 --- a/src/crypto/rust_crypto/ecdsa.rs +++ b/src/crypto/rust_crypto/ecdsa.rs @@ -1,3 +1,5 @@ +//! Implementations of the [`JwtSigner`] and [`JwtVerifier`] traits for the +//! ECDSA family of algorithms using RustCrypto use crate::algorithms::AlgorithmFamily; use crate::crypto::{JwtSigner, JwtVerifier}; use crate::errors::{ErrorKind, Result, new_error};