Skip to content
Open
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
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -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]]
Expand Down
32 changes: 32 additions & 0 deletions examples/atproto.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
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::<HashMap<String, serde_json::Value>>(
TOKEN,
&DecodingKey::from_jwk(&jwk).unwrap(),
&validation,
)?;

println!("{:#?}", decoded_token);

Ok(())
}
9 changes: 7 additions & 2 deletions src/algorithms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
}
}
Expand All @@ -48,6 +48,9 @@ pub enum Algorithm {
/// ECDSA using SHA-384
ES384,

/// ECDSA using secp256k1
ES256K,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be gated by rust_crypto feature since it's only available there? As well as all JWK operations?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to make this rust_crypto gated (that's a little beyond my current knowledge of rust)

would it be something like wrapping in: };
#[cfg(feature = "rust_crypto")]?


/// RSASSA-PKCS1-v1_5 using SHA-256
RS256,
/// RSASSA-PKCS1-v1_5 using SHA-384
Expand All @@ -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),
Expand All @@ -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,
}
}
Expand All @@ -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());
Expand Down
5 changes: 5 additions & 0 deletions src/crypto/rust_crypto/ecdsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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);
3 changes: 2 additions & 1 deletion src/decoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -326,6 +326,7 @@ pub fn jwt_verifier_factory(
Algorithm::HS512 => Box::new(Hs512Verifier::new(key)?) as Box<dyn JwtVerifier>,
Algorithm::ES256 => Box::new(Es256Verifier::new(key)?) as Box<dyn JwtVerifier>,
Algorithm::ES384 => Box::new(Es384Verifier::new(key)?) as Box<dyn JwtVerifier>,
Algorithm::ES256K => Box::new(Es256KVerifier::new(key)?) as Box<dyn JwtVerifier>,
Algorithm::RS256 => Box::new(Rsa256Verifier::new(key)?) as Box<dyn JwtVerifier>,
Algorithm::RS384 => Box::new(Rsa384Verifier::new(key)?) as Box<dyn JwtVerifier>,
Algorithm::RS512 => Box::new(Rsa512Verifier::new(key)?) as Box<dyn JwtVerifier>,
Expand Down
3 changes: 2 additions & 1 deletion src/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -202,6 +202,7 @@ pub(crate) fn jwt_signer_factory(
Algorithm::HS512 => Box::new(Hs512Signer::new(key)?) as Box<dyn JwtSigner>,
Algorithm::ES256 => Box::new(Es256Signer::new(key)?) as Box<dyn JwtSigner>,
Algorithm::ES384 => Box::new(Es384Signer::new(key)?) as Box<dyn JwtSigner>,
Algorithm::ES256K => Box::new(Es256KSigner::new(key)?) as Box<dyn JwtSigner>,
Algorithm::RS256 => Box::new(Rsa256Signer::new(key)?) as Box<dyn JwtSigner>,
Algorithm::RS384 => Box::new(Rsa384Signer::new(key)?) as Box<dyn JwtSigner>,
Algorithm::RS512 => Box::new(Rsa512Signer::new(key)?) as Box<dyn JwtSigner>,
Expand Down
26 changes: 24 additions & 2 deletions src/jwk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
Expand All @@ -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 => {
Expand Down
59 changes: 59 additions & 0 deletions tests/ecdsa/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Claims>(
&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::<Claims>(
&encrypted,
&DecodingKey::from_jwk(&jwk).unwrap(),
&Validation::new(Algorithm::ES256K),
);
assert!(res.is_ok());
}

#[cfg(feature = "use_pem")]
#[test]
#[wasm_bindgen_test]
Expand Down
5 changes: 5 additions & 0 deletions tests/ecdsa/private_es256k_key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgmSIP8ezE7unJ624FYvIx
jCGK7n5abnzqVKjG9p63MO2hRANCAATd1slq/xOfdan3Oq9gMjCi7x25M7ubpM1H
tVBdj/NyNR9LHW9g6MI94yPvm2c3/fVczqL4lxuhJLGLWQGPYwjG
-----END PRIVATE KEY-----
7 changes: 7 additions & 0 deletions tests/ecdsa/public_es256k_jwk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"kty": "EC",
"x": "3dbJav8Tn3Wp9zqvYDIwou8duTO7m6TNR7VQXY_zcjU",
"y": "H0sdb2Dowj3jI--bZzf99VzOoviXG6EksYtZAY9jCMY",
"crv": "secp256k1",
"d": "mSIP8ezE7unJ624FYvIxjCGK7n5abnzqVKjG9p63MO0"
}
4 changes: 4 additions & 0 deletions tests/ecdsa/public_es256k_key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE3dbJav8Tn3Wp9zqvYDIwou8duTO7m6TN
R7VQXY/zcjUfSx1vYOjCPeMj75tnN/31XM6i+JcboSSxi1kBj2MIxg==
-----END PUBLIC KEY-----
Loading