diff --git a/Cargo.toml b/Cargo.toml index f62f3a5e..ea3d624c 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"] } +k256 = { version = "0.13.4", 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", "k256", "rand", "rsa", "sha2"] aws_lc_rs = ["aws-lc-rs"] [[bench]] diff --git a/examples/atproto.rs b/examples/atproto.rs new file mode 100644 index 00000000..4eb907cd --- /dev/null +++ b/examples/atproto.rs @@ -0,0 +1,32 @@ +use jsonwebtoken::jwk::Jwk; +use jsonwebtoken::{DecodingKey, Validation, decode, decode_header}; +use std::collections::HashMap; + +// These were generated from Node.js using the @atproto/crypto library: +const TOKEN: &str = "eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJFUzI1NksifQ.eyJzY29wZSI6ImNvbS5hdHByb3RvLmFjY2VzcyIsInN1YiI6ImRpZDpleGFtcGxlOmFsaWNlIiwiaWF0IjoxNzYyODA5ODk4LCJhdWQiOiJkaWQ6d2ViOmJza3kubmV0d29yayJ9.krVCmWVQ2lTdXzi7Gcu0vv-szONeYj7kSpevjGiGBJcJnY5NgweIhNEzsnqoi6ni9VONgIrYfCj6T7LhJr9isg"; +const JWK: &str = r#"{ "kty": "EC", "x": "elgF6kwpkD00J9SPmoXBtaueneZf-77LnzrGrB7Ic7A", "y": "BTKRlhfwemkSQdB560lxw-Sg4GNH1gjkXXrryU-7jNM", "crv": "secp256k1" }"#; + +fn main() -> Result<(), Box> { + let jwk: Jwk = serde_json::from_str(JWK).unwrap(); + let header = decode_header(TOKEN).unwrap(); + + println!("Header Algorithm: {:#?}", header.alg); + + let validation = { + let mut validation = Validation::new(header.alg); + validation.set_audience(&["did:web:bsky.network"]); + validation.set_required_spec_claims(&["sub", "scope"]); + validation.validate_exp = false; + validation + }; + + let decoded_token = decode::>( + TOKEN, + &DecodingKey::from_jwk(&jwk).unwrap(), + &validation, + )?; + + println!("{:#?}", decoded_token); + + Ok(()) +} diff --git a/src/algorithms.rs b/src/algorithms.rs index 94eb3637..c1756942 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::ES256K], Self::Ed => &[Algorithm::EdDSA], } } @@ -48,6 +48,9 @@ pub enum Algorithm { /// ECDSA using SHA-384 ES384, + /// ECDSA using secp256k1 + ES256K, + /// RSASSA-PKCS1-v1_5 using SHA-256 RS256, /// RSASSA-PKCS1-v1_5 using SHA-384 @@ -74,6 +77,7 @@ impl FromStr for Algorithm { "HS384" => Ok(Algorithm::HS384), "HS512" => Ok(Algorithm::HS512), "ES256" => Ok(Algorithm::ES256), + "ES256K" => Ok(Algorithm::ES256K), "ES384" => Ok(Algorithm::ES384), "RS256" => Ok(Algorithm::RS256), "RS384" => Ok(Algorithm::RS384), @@ -97,7 +101,7 @@ impl Algorithm { | Algorithm::PS256 | Algorithm::PS384 | Algorithm::PS512 => AlgorithmFamily::Rsa, - Algorithm::ES256 | Algorithm::ES384 => AlgorithmFamily::Ec, + Algorithm::ES256 | Algorithm::ES384 | Algorithm::ES256K => AlgorithmFamily::Ec, Algorithm::EdDSA => AlgorithmFamily::Ed, } } @@ -112,6 +116,7 @@ mod tests { #[test] #[wasm_bindgen_test] fn generate_algorithm_enum_from_str() { + assert!(Algorithm::from_str("ES256K").is_ok()); assert!(Algorithm::from_str("HS256").is_ok()); assert!(Algorithm::from_str("HS384").is_ok()); assert!(Algorithm::from_str("HS512").is_ok()); diff --git a/src/crypto/rust_crypto/ecdsa.rs b/src/crypto/rust_crypto/ecdsa.rs index f192adea..bfa15ef6 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 k256::ecdsa::{ + Signature as Signature256K, SigningKey as SigningKey256K, VerifyingKey as VerifyingKey256K +}; use rsa::pkcs8::DecodePrivateKey; use signature::{Error, Signer, Verifier}; @@ -82,6 +85,8 @@ macro_rules! define_ecdsa_verifier { define_ecdsa_signer!(Es256Signer, Algorithm::ES256, SigningKey256); define_ecdsa_signer!(Es384Signer, Algorithm::ES384, SigningKey384); +define_ecdsa_signer!(Es256KSigner, Algorithm::ES256K, SigningKey256K); define_ecdsa_verifier!(Es256Verifier, Algorithm::ES256, VerifyingKey256, Signature256); define_ecdsa_verifier!(Es384Verifier, Algorithm::ES384, VerifyingKey384, Signature384); +define_ecdsa_verifier!(Es256KVerifier, Algorithm::ES256K, VerifyingKey256K, Signature256K); diff --git a/src/decoding.rs b/src/decoding.rs index 51d793e7..6f80d9db 100644 --- a/src/decoding.rs +++ b/src/decoding.rs @@ -26,7 +26,7 @@ use crate::crypto::aws_lc::{ }; #[cfg(feature = "rust_crypto")] use crate::crypto::rust_crypto::{ - ecdsa::{Es256Verifier, Es384Verifier}, + ecdsa::{Es256Verifier, Es384Verifier, Es256KVerifier}, 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::ES256K => Box::new(Es256KVerifier::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..5099640e 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -26,7 +26,7 @@ use crate::crypto::aws_lc::{ }; #[cfg(feature = "rust_crypto")] use crate::crypto::rust_crypto::{ - ecdsa::{Es256Signer, Es384Signer}, + ecdsa::{Es256Signer, Es384Signer, Es256KSigner}, 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::ES256K => Box::new(Es256KSigner::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..a31e01d7 100644 --- a/src/jwk.rs +++ b/src/jwk.rs @@ -23,6 +23,8 @@ use p256::{ecdsa::SigningKey as P256SigningKey, pkcs8::DecodePrivateKey}; #[cfg(feature = "rust_crypto")] use p384::ecdsa::SigningKey as P384SigningKey; #[cfg(feature = "rust_crypto")] +use k256::ecdsa::SigningKey as K256SigningKey; +#[cfg(feature = "rust_crypto")] use rsa::{RsaPrivateKey, pkcs1::DecodeRsaPrivateKey, traits::PublicKeyParts}; #[cfg(feature = "rust_crypto")] use sha2::{Digest, Sha256, Sha384, Sha512}; @@ -177,6 +179,9 @@ pub enum KeyAlgorithm { /// ECDSA using SHA-384 ES384, + /// ECDSA using secp256k + ES256K, + /// RSASSA-PKCS1-v1_5 using SHA-256 RS256, /// RSASSA-PKCS1-v1_5 using SHA-384 @@ -219,6 +224,7 @@ impl FromStr for KeyAlgorithm { "HS512" => Ok(KeyAlgorithm::HS512), "ES256" => Ok(KeyAlgorithm::ES256), "ES384" => Ok(KeyAlgorithm::ES384), + "ES256K" => Ok(KeyAlgorithm::ES256K), "RS256" => Ok(KeyAlgorithm::RS256), "RS384" => Ok(KeyAlgorithm::RS384), "PS256" => Ok(KeyAlgorithm::PS256), @@ -319,6 +325,9 @@ pub enum EllipticCurve { /// P-521 curve -- unsupported by `ring`. #[serde(rename = "P-521")] P521, + /// K-256 curve + #[serde(rename = "secp256k1")] + Secp256k1, /// Ed25519 curve #[serde(rename = "Ed25519")] Ed25519, @@ -501,6 +510,18 @@ fn extract_ec_public_key_coordinates( _ => Err(ErrorKind::InvalidEcdsaKey.into()), } } + Algorithm::ES256K => { + let signing_key = K256SigningKey::from_pkcs8_der(key_content) + .map_err(|_| ErrorKind::InvalidEcdsaKey)?; + let public_key = signing_key.verifying_key(); + let encoded = public_key.to_encoded_point(false); + match encoded.coordinates() { + k256::elliptic_curve::sec1::Coordinates::Uncompressed { x, y } => { + Ok((EllipticCurve::Secp256k1, x.to_vec(), y.to_vec())) + } + _ => Err(ErrorKind::InvalidEcdsaKey.into()) + } + } Algorithm::ES384 => { let signing_key = P384SigningKey::from_pkcs8_der(key_content) .map_err(|_| ErrorKind::InvalidEcdsaKey)?; @@ -553,6 +574,7 @@ impl Jwk { Algorithm::HS512 => KeyAlgorithm::HS512, Algorithm::ES256 => KeyAlgorithm::ES256, Algorithm::ES384 => KeyAlgorithm::ES384, + Algorithm::ES256K => KeyAlgorithm::ES256K, Algorithm::RS256 => KeyAlgorithm::RS256, Algorithm::RS384 => KeyAlgorithm::RS384, Algorithm::RS512 => KeyAlgorithm::RS512, @@ -600,7 +622,7 @@ impl Jwk { pub fn thumbprint(&self, hash_function: ThumbprintHash) -> String { let pre = match &self.algorithm { AlgorithmParameters::EllipticCurve(a) => match a.curve { - EllipticCurve::P256 | EllipticCurve::P384 | EllipticCurve::P521 => { + EllipticCurve::P256 | EllipticCurve::P384 | EllipticCurve::P521 | EllipticCurve::Secp256k1 => { format!( r#"{{"crv":{},"kty":{},"x":"{}","y":"{}"}}"#, serde_json::to_string(&a.curve).unwrap(), @@ -627,7 +649,7 @@ impl Jwk { ) } AlgorithmParameters::OctetKeyPair(a) => match a.curve { - EllipticCurve::P256 | EllipticCurve::P384 | EllipticCurve::P521 => { + EllipticCurve::P256 | EllipticCurve::P384 | EllipticCurve::P521 | EllipticCurve::Secp256k1 => { panic!("OctetKeyPair can't contain this curve type") } EllipticCurve::Ed25519 => { diff --git a/tests/ecdsa/mod.rs b/tests/ecdsa/mod.rs index 25da1228..c48218b2 100644 --- a/tests/ecdsa/mod.rs +++ b/tests/ecdsa/mod.rs @@ -103,6 +103,65 @@ fn ec_x_y() { assert!(res.is_ok()); } +#[cfg(feature = "use_pem")] +#[test] +#[wasm_bindgen_test] +fn es256k_pem() { + let privkey = include_str!("private_es256k_key.pem"); + let pubkey = include_str!("public_es256k_key.pem"); + + let my_claims = Claims { + sub: "b@b.com".to_string(), + company: "ACME".to_string(), + exp: OffsetDateTime::now_utc().unix_timestamp() + 10000, + }; + + let encrypted = encode( + &Header::new(Algorithm::ES256K), + &my_claims, + &EncodingKey::from_ec_pem(privkey.as_ref()).unwrap(), + ) + .unwrap(); + + let res = decode::( + &encrypted, + &DecodingKey::from_ec_pem(pubkey.as_ref()).unwrap(), + &Validation::new(Algorithm::ES256K), + ); + assert!(res.is_ok()); +} + +#[cfg(feature = "use_pem")] +#[test] +#[wasm_bindgen_test] +fn es256k_jwk() { + use jsonwebtoken::jwk::Jwk; + + let privkey = include_str!("private_es256k_key.pem"); + let pubkey = include_str!("public_es256k_jwk.json"); + let jwk: Jwk = serde_json::from_str(pubkey).unwrap(); + + let my_claims = Claims { + sub: "b@b.com".to_string(), + company: "ACME".to_string(), + exp: OffsetDateTime::now_utc().unix_timestamp() + 10000, + }; + + let encrypted = encode( + &Header::new(Algorithm::ES256K), + &my_claims, + &EncodingKey::from_ec_pem(privkey.as_ref()).unwrap(), + ) + .unwrap(); + + let res = decode::( + &encrypted, + &DecodingKey::from_jwk(&jwk).unwrap(), + &Validation::new(Algorithm::ES256K), + ); + assert!(res.is_ok()); +} + #[cfg(feature = "use_pem")] #[test] #[wasm_bindgen_test] diff --git a/tests/ecdsa/private_es256k_key.pem b/tests/ecdsa/private_es256k_key.pem new file mode 100644 index 00000000..9de8a556 --- /dev/null +++ b/tests/ecdsa/private_es256k_key.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgmSIP8ezE7unJ624FYvIx +jCGK7n5abnzqVKjG9p63MO2hRANCAATd1slq/xOfdan3Oq9gMjCi7x25M7ubpM1H +tVBdj/NyNR9LHW9g6MI94yPvm2c3/fVczqL4lxuhJLGLWQGPYwjG +-----END PRIVATE KEY----- diff --git a/tests/ecdsa/public_es256k_jwk.json b/tests/ecdsa/public_es256k_jwk.json new file mode 100644 index 00000000..081c2de3 --- /dev/null +++ b/tests/ecdsa/public_es256k_jwk.json @@ -0,0 +1,7 @@ +{ + "kty": "EC", + "x": "3dbJav8Tn3Wp9zqvYDIwou8duTO7m6TNR7VQXY_zcjU", + "y": "H0sdb2Dowj3jI--bZzf99VzOoviXG6EksYtZAY9jCMY", + "crv": "secp256k1", + "d": "mSIP8ezE7unJ624FYvIxjCGK7n5abnzqVKjG9p63MO0" +} diff --git a/tests/ecdsa/public_es256k_key.pem b/tests/ecdsa/public_es256k_key.pem new file mode 100644 index 00000000..1f071be2 --- /dev/null +++ b/tests/ecdsa/public_es256k_key.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE3dbJav8Tn3Wp9zqvYDIwou8duTO7m6TN +R7VQXY/zcjUfSx1vYOjCPeMj75tnN/31XM6i+JcboSSxi1kBj2MIxg== +-----END PUBLIC KEY-----