diff --git a/Cargo.toml b/Cargo.toml index f62f3a5e..5090e53b 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.4", 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/README.md b/README.md index 09b9f719..48729ec0 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ This library currently supports the following: - PS512 - ES256 - ES384 +- ES512 - EdDSA diff --git a/src/algorithms.rs b/src/algorithms.rs index 94eb3637..f7093172 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -25,7 +25,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], } } @@ -47,6 +47,8 @@ pub enum Algorithm { ES256, /// ECDSA using SHA-384 ES384, + /// ECDSA using SHA-512 + ES512, /// RSASSA-PKCS1-v1_5 using SHA-256 RS256, @@ -75,6 +77,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), @@ -97,7 +100,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 f192adea..8fd704f7 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,137 @@ 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) signer - uses different API (no sign_recoverable, different 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)); + } + + // Extract the raw 66-byte key from PKCS8 DER format + let pkcs8_der = encoding_key.inner(); + let key_bytes = extract_p521_key_from_pkcs8(pkcs8_der)?; + + // Verify correct length and convert to fixed-size array safely + if key_bytes.len() != 66 { + return Err(new_error(ErrorKind::InvalidEcdsaKey)); + } + + // Safe conversion using slice_as_array pattern + let mut key_array = [0u8; 66]; + key_array.copy_from_slice(&key_bytes); + + // Convert array to GenericArray reference using From trait + let field_bytes: &p521::FieldBytes = key_array.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 +/// +/// P-521 keys in PKCS8 format have a different structure than the standard P256/P384: +/// PKCS8 ::= SEQUENCE { +/// version INTEGER, +/// algorithm AlgorithmIdentifier, +/// PrivateKey OCTET STRING +/// } +/// The PrivateKey octet string contains a DER-encoded ECPrivateKey SEQUENCE: +/// ECPrivateKey ::= SEQUENCE { +/// version INTEGER, +/// privateKey OCTET STRING (66 bytes for P-521) +/// } +fn extract_p521_key_from_pkcs8(pkcs8_der: &[u8]) -> Result> { + let asn1_blocks = simple_asn1::from_der(pkcs8_der) + .map_err(|_| ErrorKind::InvalidKeyFormat)?; + + for block in asn1_blocks { + if let simple_asn1::ASN1Block::Sequence(_, entries) = block { + // The third element (index 2) should be the privateKey OCTET STRING + if entries.len() >= 3 { + if let simple_asn1::ASN1Block::OctetString(_, value) = &entries[2] { + // The value is DER-encoded and contains a SEQUENCE with the actual key + if let Ok(inner_blocks) = simple_asn1::from_der(value) { + for inner_block in inner_blocks { + if let simple_asn1::ASN1Block::Sequence(_, inner_entries) = inner_block { + // Look for the OCTET STRING within this sequence + for inner_entry in inner_entries { + if let simple_asn1::ASN1Block::OctetString(_, key_value) = inner_entry { + // This should be our 66-byte key + if key_value.len() == 66 { + return Ok(key_value.to_vec()); + } + } + } + } + } + } + } + } + } + } + + Err(new_error(ErrorKind::InvalidKeyFormat)) +} diff --git a/src/decoding.rs b/src/decoding.rs index 51d793e7..9b9f47f3 100644 --- a/src/decoding.rs +++ b/src/decoding.rs @@ -16,7 +16,7 @@ use crate::validation::{Validation, validate}; // Crypto #[cfg(feature = "aws_lc_rs")] use crate::crypto::aws_lc::{ - ecdsa::{Es256Verifier, Es384Verifier}, + ecdsa::{Es256Verifier, Es384Verifier, Es512Verifier}, eddsa::EdDSAVerifier, hmac::{Hs256Verifier, Hs384Verifier, Hs512Verifier}, rsa::{ @@ -26,7 +26,7 @@ use crate::crypto::aws_lc::{ }; #[cfg(feature = "rust_crypto")] use crate::crypto::rust_crypto::{ - ecdsa::{Es256Verifier, Es384Verifier}, + ecdsa::{Es256Verifier, Es384Verifier, Es512Verifier}, eddsa::EdDSAVerifier, hmac::{Hs256Verifier, Hs384Verifier, Hs512Verifier}, rsa::{ @@ -326,6 +326,7 @@ pub fn jwt_verifier_factory( Algorithm::HS512 => Box::new(Hs512Verifier::new(key)?) as Box, Algorithm::ES256 => Box::new(Es256Verifier::new(key)?) as Box, Algorithm::ES384 => Box::new(Es384Verifier::new(key)?) as Box, + Algorithm::ES512 => Box::new(Es512Verifier::new(key)?) as Box, Algorithm::RS256 => Box::new(Rsa256Verifier::new(key)?) as Box, Algorithm::RS384 => Box::new(Rsa384Verifier::new(key)?) as Box, Algorithm::RS512 => Box::new(Rsa512Verifier::new(key)?) as Box, diff --git a/src/encoding.rs b/src/encoding.rs index 30a31953..207f78f5 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -17,7 +17,7 @@ use crate::serialization::{b64_encode, b64_encode_part}; // Crypto #[cfg(feature = "aws_lc_rs")] use crate::crypto::aws_lc::{ - ecdsa::{Es256Signer, Es384Signer}, + ecdsa::{Es256Signer, Es384Signer, Es512Signer}, eddsa::EdDSASigner, hmac::{Hs256Signer, Hs384Signer, Hs512Signer}, rsa::{ @@ -26,7 +26,7 @@ use crate::crypto::aws_lc::{ }; #[cfg(feature = "rust_crypto")] use crate::crypto::rust_crypto::{ - ecdsa::{Es256Signer, Es384Signer}, + ecdsa::{Es256Signer, Es384Signer, Es512Signer}, eddsa::EdDSASigner, hmac::{Hs256Signer, Hs384Signer, Hs512Signer}, rsa::{ @@ -202,6 +202,7 @@ pub(crate) fn jwt_signer_factory( Algorithm::HS512 => Box::new(Hs512Signer::new(key)?) as Box, Algorithm::ES256 => Box::new(Es256Signer::new(key)?) as Box, Algorithm::ES384 => Box::new(Es384Signer::new(key)?) as Box, + Algorithm::ES512 => Box::new(Es512Signer::new(key)?) as Box, Algorithm::RS256 => Box::new(Rsa256Signer::new(key)?) as Box, Algorithm::RS384 => Box::new(Rsa384Signer::new(key)?) as Box, Algorithm::RS512 => Box::new(Rsa512Signer::new(key)?) as Box, diff --git a/src/jwk.rs b/src/jwk.rs index 31f944d2..328fd78b 100644 --- a/src/jwk.rs +++ b/src/jwk.rs @@ -176,6 +176,8 @@ pub enum KeyAlgorithm { ES256, /// ECDSA using SHA-384 ES384, + /// ECDSA using SHA-512 + ES512, /// RSASSA-PKCS1-v1_5 using SHA-256 RS256, @@ -219,6 +221,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), @@ -553,6 +556,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..21c70c1d 100644 --- a/tests/ecdsa/mod.rs +++ b/tests/ecdsa/mod.rs @@ -191,3 +191,72 @@ fn ec_jwk_from_key() { .unwrap() ); } + +// ES512 Tests +#[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-----