diff --git a/Cargo.toml b/Cargo.toml index a9ea9ae0..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,8 +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"] } +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"] } @@ -65,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", "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/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/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 9aad882e..ec7b1626 100644 --- a/src/crypto/rust_crypto/ecdsa.rs +++ b/src/crypto/rust_crypto/ecdsa.rs @@ -1,6 +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}; @@ -11,7 +10,10 @@ use p256::ecdsa::{ use p384::ecdsa::{ Signature as Signature384, SigningKey as SigningKey384, VerifyingKey as VerifyingKey384, }; -use rsa::pkcs8::DecodePrivateKey; +use p521::ecdsa::{ + Signature as Signature521, SigningKey as SigningKey521, VerifyingKey as VerifyingKey521, +}; +use pkcs8::DecodePrivateKey; use signature::{Error, Signer, Verifier}; macro_rules! define_ecdsa_signer { @@ -85,3 +87,68 @@ 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 sign() instead of sign_recoverable() since P521 doesn't support it +pub struct Es512Signer(SigningKey521); + +impl Es512Signer { + pub(crate) fn new(encoding_key: &EncodingKey) -> Result { + if encoding_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)?, + )); + } + } + } + } + } + + Err(new_error(ErrorKind::InvalidKeyFormat)) + } +} + +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()) + } +} + +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 cd0c9bdc..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()), } } @@ -70,6 +112,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 +135,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 a6ab15cc..5073bd92 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, @@ -531,7 +535,7 @@ impl Jwk { } EllipticCurve::Ed25519 => { format!( - r#"{{"crv":{},"kty":{},"x":"{}"}}"#, + r#"{{crv:{},"kty":{},"x":"{}"}}"#, serde_json::to_string(&a.curve).unwrap(), serde_json::to_string(&a.key_type).unwrap(), a.x, @@ -613,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() { diff --git a/tests/ecdsa/mod.rs b/tests/ecdsa/mod.rs index 25da1228..4e84449e 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-----