From 6bac1bdbf0bb33b2b95d5c0430c99e1cb56ba417 Mon Sep 17 00:00:00 2001 From: Kellen Frodelius-Fujimoto Date: Mon, 1 Apr 2019 12:11:28 +0200 Subject: [PATCH 01/55] Add example of using `chrono::DateTime` in claims Using `chrono`'s `serde` feature uses ISO 8601 instead of a Unix timestamp as specified in RFC 7519 section 2, "NumericDate". This example uses custom de/serialize functions as shown in the [serde.rs example, "Custom Date Format"](https://serde.rs/custom-date-format.html). NOTE: Currently fractional values are not supported in the example, though they are in the spec. --- examples/custom_chrono.rs | 101 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 examples/custom_chrono.rs diff --git a/examples/custom_chrono.rs b/examples/custom_chrono.rs new file mode 100644 index 0000000..7fd7b3a --- /dev/null +++ b/examples/custom_chrono.rs @@ -0,0 +1,101 @@ +extern crate jsonwebtoken as jwt; +extern crate serde; +#[macro_use] +extern crate serde_derive; +extern crate chrono; + +use chrono::prelude::*; +use jwt::{Header, Validation}; + +const SECRET: &str = "some-secret"; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +struct Claims { + sub: String, + #[serde(with = "jwt_numeric_date")] + iat: DateTime, + #[serde(with = "jwt_numeric_date")] + exp: DateTime, +} + +mod jwt_numeric_date { + //! Custom serialization of DateTime to conform with the JWT spec (RFC 7519 section 2, "Numeric Date") + use chrono::{DateTime, TimeZone, Utc}; + use serde::{self, Deserialize, Deserializer, Serializer}; + + /// Serializes a DateTime to a Unix timestamp (milliseconds since 1970/1/1T00:00:00T) + pub fn serialize(date: &DateTime, serializer: S) -> Result + where + S: Serializer, + { + let timestamp = date.timestamp(); + serializer.serialize_i64(timestamp) + } + + /// Attempts to deserialize an i64 and use as a Unix timestamp + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Utc.timestamp_opt(i64::deserialize(deserializer)?, 0) + .single() // If there are multiple or no valid DateTimes from timestamp, return None + .ok_or_else(|| serde::de::Error::custom("invalid Unix timestamp value")) + } + + #[cfg(test)] + mod tests { + use super::*; + use jwt::{Header, Validation}; + + const EXPECTED_TOKEN: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJDdXN0b20gRGF0ZVRpbWUgc2VyL2RlIiwiaWF0IjowLCJleHAiOjMyNTAzNjgwMDAwfQ.RTgha0S53MjPC2pMA4e2oMzaBxSY3DMjiYR2qFfV55A"; + + use super::super::{Claims, SECRET}; + + #[test] + fn round_trip() { + let sub = "Custom DateTime ser/de".to_string(); + let iat = Utc.timestamp(0, 0); + let exp = Utc.timestamp(32503680000, 0); + + let claims = Claims { sub: sub.clone(), iat, exp }; + + let token = jwt::encode(&Header::default(), &claims, SECRET.as_ref()) + .expect("Failed to encode claims"); + + assert_eq!(&token, EXPECTED_TOKEN); + + let decoded = jwt::decode::(&token, SECRET.as_ref(), &Validation::default()) + .expect("Failed to decode token"); + + assert_eq!(decoded.claims, claims); + } + + #[test] + fn should_fail_on_invalid_timestamp() { + // A token with the expiry of i64::MAX + 1 + let overflow_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJDdXN0b20gRGF0ZVRpbWUgc2VyL2RlIiwiaWF0IjowLCJleHAiOjkyMjMzNzIwMzY4NTQ3NzYwMDB9.G2PKreA27U8_xOwuIeCYXacFYeR46f9FyENIZfCrvEc"; + + let decode_result = + jwt::decode::(&overflow_token, SECRET.as_ref(), &Validation::default()); + + assert!(decode_result.is_err()); + } + } +} + +fn main() -> Result<(), Box> { + let sub = "Custom DateTime ser/de".to_string(); + let iat = Utc::now(); + let exp = iat + chrono::Duration::days(1); + + let claims = Claims { sub: sub.clone(), iat, exp }; + + let token = jwt::encode(&Header::default(), &claims, SECRET.as_ref())?; + + println!("serialized token: {}", &token); + + let token_data = jwt::decode::(&token, SECRET.as_ref(), &Validation::default())?; + + println!("token data:\n{:#?}", &token_data); + Ok(()) +} From 5d01baea948de7ff2212d73989503bdbc364d9b2 Mon Sep 17 00:00:00 2001 From: Jake Shadle Date: Wed, 15 May 2019 16:16:49 +0200 Subject: [PATCH 02/55] Bump ring --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 581ce58..cb2d641 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ keywords = ["jwt", "web", "api", "token", "json"] serde_json = "1.0" serde_derive = "1.0" serde = "1.0" -ring = "0.14.4" +ring = "0.14.6" base64 = "0.10" untrusted = "0.6" chrono = "0.4" From 6cfb5c7c0e0ddfb48cf19679bfc3de0110db3f95 Mon Sep 17 00:00:00 2001 From: Jake Shadle Date: Wed, 15 May 2019 16:19:38 +0200 Subject: [PATCH 03/55] Add Key trait and the supported formats --- src/crypto.rs | 204 +++++++++++++++++++++++++++++++++++++++++++------- src/errors.rs | 2 + src/lib.rs | 4 +- 3 files changed, 183 insertions(+), 27 deletions(-) diff --git a/src/crypto.rs b/src/crypto.rs index 76dccd2..cab93e7 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -56,16 +56,32 @@ impl FromStr for Algorithm { } /// The actual HS signing + encoding -fn sign_hmac(alg: &'static digest::Algorithm, key: &[u8], signing_input: &str) -> Result { - let signing_key = hmac::SigningKey::new(alg, key); +fn sign_hmac( + alg: &'static digest::Algorithm, + key: K, + signing_input: &str, +) -> Result { + let signing_key = hmac::SigningKey::new(alg, key.as_ref()); let digest = hmac::sign(&signing_key, signing_input.as_bytes()); Ok(base64::encode_config::(&digest, base64::URL_SAFE_NO_PAD)) } /// The actual ECDSA signing + encoding -fn sign_ecdsa(alg: &'static signature::EcdsaSigningAlgorithm, key: &[u8], signing_input: &str) -> Result { - let signing_key = signature::EcdsaKeyPair::from_pkcs8(alg, untrusted::Input::from(key))?; +fn sign_ecdsa( + alg: &'static signature::EcdsaSigningAlgorithm, + key: K, + signing_input: &str, +) -> Result { + let signing_key = match key.format() { + KeyFormat::PKCS8 => { + signature::EcdsaKeyPair::from_pkcs8(alg, untrusted::Input::from(key.as_ref()))? + } + _ => { + return Err(ErrorKind::InvalidKeyFormat)?; + } + }; + let rng = rand::SystemRandom::new(); let sig = signing_key.sign(&rng, untrusted::Input::from(signing_input.as_bytes()))?; Ok(base64::encode_config(&sig, base64::URL_SAFE_NO_PAD)) @@ -73,11 +89,25 @@ fn sign_ecdsa(alg: &'static signature::EcdsaSigningAlgorithm, key: &[u8], signin /// The actual RSA signing + encoding /// Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html -fn sign_rsa(alg: &'static signature::RsaEncoding, key: &[u8], signing_input: &str) -> Result { - let key_pair = Arc::new( - signature::RsaKeyPair::from_der(untrusted::Input::from(key)) - .map_err(|_| ErrorKind::InvalidRsaKey)?, - ); +fn sign_rsa( + alg: &'static signature::RsaEncoding, + key: K, + signing_input: &str, +) -> Result { + let key_bytes = untrusted::Input::from(key.as_ref()); + let key_pair = match key.format() { + KeyFormat::DER => { + signature::RsaKeyPair::from_der(key_bytes).map_err(|_| ErrorKind::InvalidRsaKey)? + } + KeyFormat::PKCS8 => { + signature::RsaKeyPair::from_pkcs8(key_bytes).map_err(|_| ErrorKind::InvalidRsaKey)? + } + _ => { + return Err(ErrorKind::InvalidKeyFormat)?; + } + }; + + let key_pair = Arc::new(key_pair); let mut signature = vec![0; key_pair.public_modulus_len()]; let rng = rand::SystemRandom::new(); key_pair @@ -91,14 +121,18 @@ fn sign_rsa(alg: &'static signature::RsaEncoding, key: &[u8], signing_input: &st /// the base64 url safe encoded of the result. /// /// Only use this function if you want to do something other than JWT. -pub fn sign(signing_input: &str, key: &[u8], algorithm: Algorithm) -> Result { +pub fn sign(signing_input: &str, key: K, algorithm: Algorithm) -> Result { match algorithm { Algorithm::HS256 => sign_hmac(&digest::SHA256, key, signing_input), Algorithm::HS384 => sign_hmac(&digest::SHA384, key, signing_input), Algorithm::HS512 => sign_hmac(&digest::SHA512, key, signing_input), - Algorithm::ES256 => sign_ecdsa(&signature::ECDSA_P256_SHA256_FIXED_SIGNING, key, signing_input), - Algorithm::ES384 => sign_ecdsa(&signature::ECDSA_P384_SHA384_FIXED_SIGNING, key, signing_input), + Algorithm::ES256 => { + sign_ecdsa(&signature::ECDSA_P256_SHA256_FIXED_SIGNING, key, signing_input) + } + Algorithm::ES384 => { + sign_ecdsa(&signature::ECDSA_P384_SHA384_FIXED_SIGNING, key, signing_input) + } Algorithm::RS256 => sign_rsa(&signature::RSA_PKCS1_SHA256, key, signing_input), Algorithm::RS384 => sign_rsa(&signature::RSA_PKCS1_SHA384, key, signing_input), @@ -134,29 +168,149 @@ fn verify_ring( pub fn verify( signature: &str, signing_input: &str, - key: &[u8], + public_key: &[u8], algorithm: Algorithm, ) -> Result { match algorithm { Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => { // we just re-sign the data with the key and compare if they are equal - let signed = sign(signing_input, key, algorithm)?; + let signed = sign(signing_input, Hmac::from(&public_key), algorithm)?; Ok(verify_slices_are_equal(signature.as_ref(), signed.as_ref()).is_ok()) } Algorithm::ES256 => { - verify_ring(&signature::ECDSA_P256_SHA256_FIXED, signature, signing_input, key) + verify_ring(&signature::ECDSA_P256_SHA256_FIXED, signature, signing_input, public_key) } Algorithm::ES384 => { - verify_ring(&signature::ECDSA_P384_SHA384_FIXED, signature, signing_input, key) - } - Algorithm::RS256 => { - verify_ring(&signature::RSA_PKCS1_2048_8192_SHA256, signature, signing_input, key) - } - Algorithm::RS384 => { - verify_ring(&signature::RSA_PKCS1_2048_8192_SHA384, signature, signing_input, key) - } - Algorithm::RS512 => { - verify_ring(&signature::RSA_PKCS1_2048_8192_SHA512, signature, signing_input, key) + verify_ring(&signature::ECDSA_P384_SHA384_FIXED, signature, signing_input, public_key) } + Algorithm::RS256 => verify_ring( + &signature::RSA_PKCS1_2048_8192_SHA256, + signature, + signing_input, + public_key, + ), + Algorithm::RS384 => verify_ring( + &signature::RSA_PKCS1_2048_8192_SHA384, + signature, + signing_input, + public_key, + ), + Algorithm::RS512 => verify_ring( + &signature::RSA_PKCS1_2048_8192_SHA512, + signature, + signing_input, + public_key, + ), + } +} + +/// The supported RSA key formats, see the documentation for ring::signature::RsaKeyPair +/// for more information +pub enum KeyFormat { + /// An unencrypted PKCS#8-encoded key. Can be used with both ECDSA and RSA + /// algorithms when signing. See ring for information. + PKCS8, + /// A binary DER-encoded ASN.1 key. Can only be used with RSA algorithms + /// when signing. See ring for more information + DER, + /// This is not a key format, but provided for convenience since HMAC is + /// a supported signing algorithm. + HMAC, +} + +/// A tiny abstraction on top of raw key buffers to add key format +/// information +pub trait Key: AsRef<[u8]> { + /// The format of the key + fn format(&self) -> KeyFormat; +} + +/// This blanket implementation aligns with the key loading as of version 6.0.0 +// impl Key for T +// where +// T: AsRef<[u8]>, +// { +// fn format(&self) -> KeyFormat { +// KeyFormat::DER +// } +// } + +/// A convenience wrapper for a key buffer as an unencrypted PKCS#8-encoded, +/// see ring for more details +pub struct Pkcs8<'a> { + key_bytes: &'a [u8], +} + +impl<'a> Key for Pkcs8<'a> { + fn format(&self) -> KeyFormat { + KeyFormat::PKCS8 + } +} + +impl<'a> AsRef<[u8]> for Pkcs8<'a> { + fn as_ref(&self) -> &[u8] { + self.key_bytes + } +} + +impl<'a, T> From<&'a T> for Pkcs8<'a> +where + T: AsRef<[u8]>, +{ + fn from(key: &'a T) -> Self { + Self { key_bytes: key.as_ref() } + } +} + +/// A convenience wrapper for a key buffer as a binary DER-encoded ASN.1 key, +/// see ring for more details +pub struct Der<'a> { + key_bytes: &'a [u8], +} + +impl<'a> Key for Der<'a> { + fn format(&self) -> KeyFormat { + KeyFormat::DER + } +} + +impl<'a> AsRef<[u8]> for Der<'a> { + fn as_ref(&self) -> &[u8] { + self.key_bytes + } +} + +impl<'a, T> From<&'a T> for Der<'a> +where + T: AsRef<[u8]>, +{ + fn from(key: &'a T) -> Self { + Self { key_bytes: key.as_ref() } + } +} + +/// Convenience wrapper for an HMAC key +pub struct Hmac<'a> { + key_bytes: &'a [u8], +} + +impl<'a> Key for Hmac<'a> { + fn format(&self) -> KeyFormat { + KeyFormat::HMAC + } +} + +impl<'a> AsRef<[u8]> for Hmac<'a> { + fn as_ref(&self) -> &[u8] { + self.key_bytes + } +} + +impl<'a, T> From<&'a T> for Hmac<'a> +where + T: AsRef<[u8]>, +{ + fn from(key: &'a T) -> Self { + Self { key_bytes: key.as_ref() } } } diff --git a/src/errors.rs b/src/errors.rs index 8111596..d05a0ee 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -42,6 +42,8 @@ pub enum ErrorKind { InvalidRsaKey, /// When the algorithm from string doesn't match the one passed to `from_str` InvalidAlgorithmName, + /// When a key is provided with an invalid format + InvalidKeyFormat, // validation error /// When a token’s `exp` claim indicates that it has expired diff --git a/src/lib.rs b/src/lib.rs index 592e565..35217f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ mod header; mod serialization; mod validation; -pub use crypto::{sign, verify, Algorithm}; +pub use crypto::{sign, verify, Algorithm, Der, Hmac, Key, KeyFormat, Pkcs8}; pub use header::Header; pub use serialization::TokenData; pub use validation::Validation; @@ -54,7 +54,7 @@ use validation::validate; /// // This will create a JWT using HS256 as algorithm /// let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap(); /// ``` -pub fn encode(header: &Header, claims: &T, key: &[u8]) -> Result { +pub fn encode(header: &Header, claims: &T, key: K) -> Result { let encoded_header = to_jwt_part(&header)?; let encoded_claims = to_jwt_part(&claims)?; let signing_input = [encoded_header.as_ref(), encoded_claims.as_ref()].join("."); From bae7a12a4b804ac6aff3fc96675463e41dcff870 Mon Sep 17 00:00:00 2001 From: Jake Shadle Date: Wed, 15 May 2019 16:20:09 +0200 Subject: [PATCH 04/55] Fix examples --- examples/custom_header.rs | 4 ++-- examples/validation.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/custom_header.rs b/examples/custom_header.rs index 99d346d..d43ae50 100644 --- a/examples/custom_header.rs +++ b/examples/custom_header.rs @@ -15,13 +15,13 @@ struct Claims { fn main() { let my_claims = Claims { sub: "b@b.com".to_owned(), company: "ACME".to_owned(), exp: 10000000000 }; - let key = "secret"; + let key = b"secret"; let mut header = Header::default(); header.kid = Some("signing_key".to_owned()); header.alg = Algorithm::HS512; - let token = match encode(&header, &my_claims, key.as_ref()) { + let token = match encode(&header, &my_claims, jwt::Der::from(key)) { Ok(t) => t, Err(_) => panic!(), // in practice you would return the error }; diff --git a/examples/validation.rs b/examples/validation.rs index 31fb7eb..6b2435a 100644 --- a/examples/validation.rs +++ b/examples/validation.rs @@ -15,8 +15,8 @@ struct Claims { fn main() { let my_claims = Claims { sub: "b@b.com".to_owned(), company: "ACME".to_owned(), exp: 10000000000 }; - let key = "secret"; - let token = match encode(&Header::default(), &my_claims, key.as_ref()) { + let key = b"secret"; + let token = match encode(&Header::default(), &my_claims, jwt::Hmac::from(key)) { Ok(t) => t, Err(_) => panic!(), // in practice you would return the error }; From c5db9fbe32eefc1d9b8d7cb62bb40fc5d2ea5fe7 Mon Sep 17 00:00:00 2001 From: Jake Shadle Date: Wed, 15 May 2019 16:20:25 +0200 Subject: [PATCH 05/55] Fix tests --- tests/ecdsa.rs | 14 +++++++++++--- tests/lib.rs | 10 +++++----- tests/rsa.rs | 17 ++++++++++++----- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/tests/ecdsa.rs b/tests/ecdsa.rs index 72f13df..5c1b315 100644 --- a/tests/ecdsa.rs +++ b/tests/ecdsa.rs @@ -4,7 +4,7 @@ extern crate serde_derive; extern crate chrono; use chrono::Utc; -use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Header, Validation}; +use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Der, Header, Pkcs8, Validation}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] struct Claims { @@ -16,7 +16,7 @@ struct Claims { #[test] fn round_trip_sign_verification() { let privkey = include_bytes!("private_ecdsa_key.pk8"); - let encrypted = sign("hello world", privkey, Algorithm::ES256).unwrap(); + let encrypted = sign("hello world", Pkcs8::from(&&privkey[..]), Algorithm::ES256).unwrap(); let pubkey = include_bytes!("public_ecdsa_key.pk8"); let is_valid = verify(&encrypted, "hello world", pubkey, Algorithm::ES256).unwrap(); assert!(is_valid); @@ -30,9 +30,17 @@ fn round_trip_claim() { exp: Utc::now().timestamp() + 10000, }; let privkey = include_bytes!("private_ecdsa_key.pk8"); - let token = encode(&Header::new(Algorithm::ES256), &my_claims, privkey).unwrap(); + let token = + encode(&Header::new(Algorithm::ES256), &my_claims, Pkcs8::from(&&privkey[..])).unwrap(); let pubkey = include_bytes!("public_ecdsa_key.pk8"); let token_data = decode::(&token, pubkey, &Validation::new(Algorithm::ES256)).unwrap(); assert_eq!(my_claims, token_data.claims); assert!(token_data.header.kid.is_none()); } + +#[test] +#[should_panic(expected = "InvalidKeyFormat")] +fn fails_with_non_pkcs8_key_format() { + let privkey = include_bytes!("private_rsa_key.der"); + let _encrypted = sign("hello world", Der::from(&&privkey[..]), Algorithm::ES256).unwrap(); +} diff --git a/tests/lib.rs b/tests/lib.rs index 129ee3d..f489477 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -5,7 +5,7 @@ extern crate chrono; use chrono::Utc; use jsonwebtoken::{ - dangerous_unsafe_decode, decode, decode_header, encode, sign, verify, Algorithm, Header, + dangerous_unsafe_decode, decode, decode_header, encode, sign, verify, Algorithm, Header, Hmac, Validation, }; use std::str::FromStr; @@ -19,7 +19,7 @@ struct Claims { #[test] fn sign_hs256() { - let result = sign("hello world", b"secret", Algorithm::HS256).unwrap(); + let result = sign("hello world", Hmac::from(b"secret"), Algorithm::HS256).unwrap(); let expected = "c0zGLzKEFWj0VxWuufTXiRMk5tlI5MbGDAYhzaxIYjo"; assert_eq!(result, expected); } @@ -40,7 +40,7 @@ fn encode_with_custom_header() { }; let mut header = Header::default(); header.kid = Some("kid".to_string()); - let token = encode(&header, &my_claims, "secret".as_ref()).unwrap(); + let token = encode(&header, &my_claims, Hmac::from(b"secret")).unwrap(); let token_data = decode::(&token, "secret".as_ref(), &Validation::default()).unwrap(); assert_eq!(my_claims, token_data.claims); assert_eq!("kid", token_data.header.kid.unwrap()); @@ -53,7 +53,7 @@ fn round_trip_claim() { company: "ACME".to_string(), exp: Utc::now().timestamp() + 10000, }; - let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap(); + let token = encode(&Header::default(), &my_claims, Hmac::from(b"secret")).unwrap(); let token_data = decode::(&token, "secret".as_ref(), &Validation::default()).unwrap(); assert_eq!(my_claims, token_data.claims); assert!(token_data.header.kid.is_none()); @@ -144,7 +144,7 @@ fn does_validation_in_right_order() { company: "ACME".to_string(), exp: Utc::now().timestamp() + 10000, }; - let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap(); + let token = encode(&Header::default(), &my_claims, Hmac::from(b"secret")).unwrap(); let v = Validation { leeway: 5, validate_exp: true, diff --git a/tests/rsa.rs b/tests/rsa.rs index def1f9f..4f2111a 100644 --- a/tests/rsa.rs +++ b/tests/rsa.rs @@ -4,7 +4,7 @@ extern crate serde_derive; extern crate chrono; use chrono::Utc; -use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Header, Validation}; +use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Der, Header, Pkcs8, Validation}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] struct Claims { @@ -15,8 +15,8 @@ struct Claims { #[test] fn round_trip_sign_verification() { - let encrypted = - sign("hello world", include_bytes!("private_rsa_key.der"), Algorithm::RS256).unwrap(); + let privkey = include_bytes!("private_rsa_key.der"); + let encrypted = sign("hello world", Der::from(&&privkey[..]), Algorithm::RS256).unwrap(); let is_valid = verify(&encrypted, "hello world", include_bytes!("public_rsa_key.der"), Algorithm::RS256) .unwrap(); @@ -30,9 +30,9 @@ fn round_trip_claim() { company: "ACME".to_string(), exp: Utc::now().timestamp() + 10000, }; + let privkey = include_bytes!("private_rsa_key.der"); let token = - encode(&Header::new(Algorithm::RS256), &my_claims, include_bytes!("private_rsa_key.der")) - .unwrap(); + encode(&Header::new(Algorithm::RS256), &my_claims, Der::from(&&privkey[..])).unwrap(); let token_data = decode::( &token, include_bytes!("public_rsa_key.der"), @@ -42,3 +42,10 @@ fn round_trip_claim() { assert_eq!(my_claims, token_data.claims); assert!(token_data.header.kid.is_none()); } + +#[test] +#[should_panic(expected = "InvalidRsaKey")] +fn fails_with_different_key_format() { + let privkey = include_bytes!("private_rsa_key.der"); + sign("hello world", Pkcs8::from(&&privkey[..]), Algorithm::RS256).unwrap(); +} From d51a3f632db33f3c0111b40633db30309e95e973 Mon Sep 17 00:00:00 2001 From: Jake Shadle Date: Wed, 15 May 2019 16:20:32 +0200 Subject: [PATCH 06/55] Fix benches --- benches/jwt.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benches/jwt.rs b/benches/jwt.rs index dab789c..47ba571 100644 --- a/benches/jwt.rs +++ b/benches/jwt.rs @@ -4,7 +4,7 @@ extern crate test; #[macro_use] extern crate serde_derive; -use jwt::{decode, encode, Header, Validation}; +use jwt::{decode, encode, Header, Hmac, Validation}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] struct Claims { @@ -16,7 +16,7 @@ struct Claims { fn bench_encode(b: &mut test::Bencher) { let claim = Claims { sub: "b@b.com".to_owned(), company: "ACME".to_owned() }; - b.iter(|| encode(&Header::default(), &claim, "secret".as_ref())); + b.iter(|| encode(&Header::default(), &claim, Hmac::from(b"secret"))); } #[bench] From 920d6f6759c61f7da9ee21b56d6ce51d80ccc498 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sat, 25 May 2019 17:51:31 +0200 Subject: [PATCH 07/55] Start work on 7.0.0 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 929b36e..074c0bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +# 7.0.0 (unreleased) + ## 6.0.1 (2019-05-10) - Fix Algorithm mapping in FromStr for RSA From c26bdf7e063f6b8e65ad393285389507c9bc4080 Mon Sep 17 00:00:00 2001 From: Kan-Ru Chen Date: Sat, 8 Jun 2019 03:08:54 +0900 Subject: [PATCH 08/55] Support for RSASSA-PSS signing algorithm As specified in https://tools.ietf.org/html/rfc7518#section-3.5 - PS256 - RSASSA-PSS using SHA-256 hash algorithm - PS384 - RSASSA-PSS using SHA-384 hash algorithm - PS512 - RSASSA-PSS using SHA-512 hash algorithm --- README.md | 3 +++ src/crypto.rs | 43 +++++++++++++++++++++++++++++++++++++++---- src/errors.rs | 2 +- tests/lib.rs | 3 +++ tests/rsa.rs | 41 ++++++++++++++++++++++++----------------- 5 files changed, 70 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 269c1a4..597d7f3 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,9 @@ This library currently supports the following: - RS256 - RS384 - RS512 +- PS256 +- PS384 +- PS512 - ES256 - ES384 diff --git a/src/crypto.rs b/src/crypto.rs index 76dccd2..2ada40d 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -30,6 +30,13 @@ pub enum Algorithm { RS384, /// RSASSA-PKCS1-v1_5 using SHA-512 RS512, + + /// RSASSA-PSS using SHA-256 + PS256, + /// RSASSA-PSS using SHA-384 + PS384, + /// RSASSA-PSS using SHA-512 + PS512, } impl Default for Algorithm { @@ -50,6 +57,9 @@ impl FromStr for Algorithm { "RS256" => Ok(Algorithm::RS256), "RS384" => Ok(Algorithm::RS384), "RS512" => Ok(Algorithm::RS512), + "PS256" => Ok(Algorithm::PS256), + "PS384" => Ok(Algorithm::PS384), + "PS512" => Ok(Algorithm::PS512), _ => Err(new_error(ErrorKind::InvalidAlgorithmName)), } } @@ -64,7 +74,11 @@ fn sign_hmac(alg: &'static digest::Algorithm, key: &[u8], signing_input: &str) - } /// The actual ECDSA signing + encoding -fn sign_ecdsa(alg: &'static signature::EcdsaSigningAlgorithm, key: &[u8], signing_input: &str) -> Result { +fn sign_ecdsa( + alg: &'static signature::EcdsaSigningAlgorithm, + key: &[u8], + signing_input: &str, +) -> Result { let signing_key = signature::EcdsaKeyPair::from_pkcs8(alg, untrusted::Input::from(key))?; let rng = rand::SystemRandom::new(); let sig = signing_key.sign(&rng, untrusted::Input::from(signing_input.as_bytes()))?; @@ -73,7 +87,11 @@ fn sign_ecdsa(alg: &'static signature::EcdsaSigningAlgorithm, key: &[u8], signin /// The actual RSA signing + encoding /// Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html -fn sign_rsa(alg: &'static signature::RsaEncoding, key: &[u8], signing_input: &str) -> Result { +fn sign_rsa( + alg: &'static dyn signature::RsaEncoding, + key: &[u8], + signing_input: &str, +) -> Result { let key_pair = Arc::new( signature::RsaKeyPair::from_der(untrusted::Input::from(key)) .map_err(|_| ErrorKind::InvalidRsaKey)?, @@ -97,12 +115,20 @@ pub fn sign(signing_input: &str, key: &[u8], algorithm: Algorithm) -> Result sign_hmac(&digest::SHA384, key, signing_input), Algorithm::HS512 => sign_hmac(&digest::SHA512, key, signing_input), - Algorithm::ES256 => sign_ecdsa(&signature::ECDSA_P256_SHA256_FIXED_SIGNING, key, signing_input), - Algorithm::ES384 => sign_ecdsa(&signature::ECDSA_P384_SHA384_FIXED_SIGNING, key, signing_input), + Algorithm::ES256 => { + sign_ecdsa(&signature::ECDSA_P256_SHA256_FIXED_SIGNING, key, signing_input) + } + Algorithm::ES384 => { + sign_ecdsa(&signature::ECDSA_P384_SHA384_FIXED_SIGNING, key, signing_input) + } Algorithm::RS256 => sign_rsa(&signature::RSA_PKCS1_SHA256, key, signing_input), Algorithm::RS384 => sign_rsa(&signature::RSA_PKCS1_SHA384, key, signing_input), Algorithm::RS512 => sign_rsa(&signature::RSA_PKCS1_SHA512, key, signing_input), + + Algorithm::PS256 => sign_rsa(&signature::RSA_PSS_SHA256, key, signing_input), + Algorithm::PS384 => sign_rsa(&signature::RSA_PSS_SHA384, key, signing_input), + Algorithm::PS512 => sign_rsa(&signature::RSA_PSS_SHA512, key, signing_input), } } @@ -158,5 +184,14 @@ pub fn verify( Algorithm::RS512 => { verify_ring(&signature::RSA_PKCS1_2048_8192_SHA512, signature, signing_input, key) } + Algorithm::PS256 => { + verify_ring(&signature::RSA_PSS_2048_8192_SHA256, signature, signing_input, key) + } + Algorithm::PS384 => { + verify_ring(&signature::RSA_PSS_2048_8192_SHA384, signature, signing_input, key) + } + Algorithm::PS512 => { + verify_ring(&signature::RSA_PSS_2048_8192_SHA512, signature, signing_input, key) + } } } diff --git a/src/errors.rs b/src/errors.rs index 8111596..052e1ee 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -97,7 +97,7 @@ impl StdError for Error { } } - fn cause(&self) -> Option<&StdError> { + fn cause(&self) -> Option<&dyn StdError> { match *self.0 { ErrorKind::InvalidToken => None, ErrorKind::InvalidSignature => None, diff --git a/tests/lib.rs b/tests/lib.rs index 129ee3d..33f3cd1 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -166,5 +166,8 @@ fn generate_algorithm_enum_from_str() { assert!(Algorithm::from_str("RS256").is_ok()); assert!(Algorithm::from_str("RS384").is_ok()); assert!(Algorithm::from_str("RS512").is_ok()); + assert!(Algorithm::from_str("PS256").is_ok()); + assert!(Algorithm::from_str("PS384").is_ok()); + assert!(Algorithm::from_str("PS512").is_ok()); assert!(Algorithm::from_str("").is_err()); } diff --git a/tests/rsa.rs b/tests/rsa.rs index def1f9f..0ee80d2 100644 --- a/tests/rsa.rs +++ b/tests/rsa.rs @@ -6,6 +6,15 @@ extern crate chrono; use chrono::Utc; use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Header, Validation}; +const RSA_ALGORITHMS: &[Algorithm] = &[ + Algorithm::RS256, + Algorithm::RS384, + Algorithm::RS512, + Algorithm::PS256, + Algorithm::PS384, + Algorithm::PS512, +]; + #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] struct Claims { sub: String, @@ -15,12 +24,12 @@ struct Claims { #[test] fn round_trip_sign_verification() { - let encrypted = - sign("hello world", include_bytes!("private_rsa_key.der"), Algorithm::RS256).unwrap(); - let is_valid = - verify(&encrypted, "hello world", include_bytes!("public_rsa_key.der"), Algorithm::RS256) - .unwrap(); - assert!(is_valid); + for &alg in RSA_ALGORITHMS { + let encrypted = sign("hello world", include_bytes!("private_rsa_key.der"), alg).unwrap(); + let is_valid = + verify(&encrypted, "hello world", include_bytes!("public_rsa_key.der"), alg).unwrap(); + assert!(is_valid); + } } #[test] @@ -30,15 +39,13 @@ fn round_trip_claim() { company: "ACME".to_string(), exp: Utc::now().timestamp() + 10000, }; - let token = - encode(&Header::new(Algorithm::RS256), &my_claims, include_bytes!("private_rsa_key.der")) - .unwrap(); - let token_data = decode::( - &token, - include_bytes!("public_rsa_key.der"), - &Validation::new(Algorithm::RS256), - ) - .unwrap(); - assert_eq!(my_claims, token_data.claims); - assert!(token_data.header.kid.is_none()); + for &alg in RSA_ALGORITHMS { + let token = + encode(&Header::new(alg), &my_claims, include_bytes!("private_rsa_key.der")).unwrap(); + let token_data = + decode::(&token, include_bytes!("public_rsa_key.der"), &Validation::new(alg)) + .unwrap(); + assert_eq!(my_claims, token_data.claims); + assert!(token_data.header.kid.is_none()); + } } From 84ee604e882b77d406166be7de8c4463bc15eec2 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sun, 16 Jun 2019 17:51:43 +0200 Subject: [PATCH 09/55] trait -> enum --- README.md | 11 +- examples/custom_header.rs | 6 +- examples/validation.rs | 6 +- src/algorithms.rs | 48 +++++++ src/crypto.rs | 265 ++++++++++---------------------------- src/header.rs | 2 +- src/keys.rs | 13 ++ src/lib.rs | 17 ++- src/validation.rs | 3 +- tests/ecdsa.rs | 12 +- tests/lib.rs | 28 ++-- tests/rsa.rs | 12 +- 12 files changed, 175 insertions(+), 248 deletions(-) create mode 100644 src/algorithms.rs create mode 100644 src/keys.rs diff --git a/README.md b/README.md index 269c1a4..41aa215 100644 --- a/README.md +++ b/README.md @@ -8,20 +8,11 @@ Add the following to Cargo.toml: ```toml -jsonwebtoken = "6" +jsonwebtoken = "7" serde_derive = "1" serde = "1" ``` -## Help wanted for v7 - -v6 was released as a stopgap version to update Ring and add a couple of features like ES256/384. -The results are not very ergonomic once we factor in all the possible ways to load a RSA key for example. -A possible solution is to have decoder types as described in https://github.com/Keats/jsonwebtoken/issues/76 -but I currently do not have the time to implement it myself. -I will take any better idea as well of course! - - ## How to use Complete examples are available in the examples directory: a basic one and one with a custom header. diff --git a/examples/custom_header.rs b/examples/custom_header.rs index d43ae50..cdcf38b 100644 --- a/examples/custom_header.rs +++ b/examples/custom_header.rs @@ -3,7 +3,7 @@ extern crate jsonwebtoken as jwt; extern crate serde_derive; use jwt::errors::ErrorKind; -use jwt::{decode, encode, Algorithm, Header, Validation}; +use jwt::{decode, encode, Algorithm, Header, Validation, Key}; #[derive(Debug, Serialize, Deserialize)] struct Claims { @@ -21,14 +21,14 @@ fn main() { header.kid = Some("signing_key".to_owned()); header.alg = Algorithm::HS512; - let token = match encode(&header, &my_claims, jwt::Der::from(key)) { + let token = match encode(&header, &my_claims, Key::Hmac(key)) { Ok(t) => t, Err(_) => panic!(), // in practice you would return the error }; println!("{:?}", token); let token_data = - match decode::(&token, key.as_ref(), &Validation::new(Algorithm::HS512)) { + match decode::(&token, Key::Hmac(key), &Validation::new(Algorithm::HS512)) { Ok(c) => c, Err(err) => match *err.kind() { ErrorKind::InvalidToken => panic!(), // Example on how to handle a specific error diff --git a/examples/validation.rs b/examples/validation.rs index 6b2435a..71c95b5 100644 --- a/examples/validation.rs +++ b/examples/validation.rs @@ -3,7 +3,7 @@ extern crate jsonwebtoken as jwt; extern crate serde_derive; use jwt::errors::ErrorKind; -use jwt::{decode, encode, Header, Validation}; +use jwt::{decode, encode, Header, Validation, Key}; #[derive(Debug, Serialize, Deserialize)] struct Claims { @@ -16,13 +16,13 @@ fn main() { let my_claims = Claims { sub: "b@b.com".to_owned(), company: "ACME".to_owned(), exp: 10000000000 }; let key = b"secret"; - let token = match encode(&Header::default(), &my_claims, jwt::Hmac::from(key)) { + let token = match encode(&Header::default(), &my_claims, Key::Hmac(key)) { Ok(t) => t, Err(_) => panic!(), // in practice you would return the error }; let validation = Validation { sub: Some("b@b.com".to_string()), ..Validation::default() }; - let token_data = match decode::(&token, key.as_ref(), &validation) { + let token_data = match decode::(&token, Key::Hmac(key), &validation) { Ok(c) => c, Err(err) => match *err.kind() { ErrorKind::InvalidToken => panic!("Token is invalid"), // Example on how to handle a specific error diff --git a/src/algorithms.rs b/src/algorithms.rs new file mode 100644 index 0000000..0e172f3 --- /dev/null +++ b/src/algorithms.rs @@ -0,0 +1,48 @@ +use errors::{new_error, Error, ErrorKind, Result}; +use std::str::FromStr; + +/// The algorithms supported for signing/verifying +#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)] +pub enum Algorithm { + /// HMAC using SHA-256 + HS256, + /// HMAC using SHA-384 + HS384, + /// HMAC using SHA-512 + HS512, + + /// ECDSA using SHA-256 + ES256, + /// ECDSA using SHA-384 + ES384, + + /// RSASSA-PKCS1-v1_5 using SHA-256 + RS256, + /// RSASSA-PKCS1-v1_5 using SHA-384 + RS384, + /// RSASSA-PKCS1-v1_5 using SHA-512 + RS512, +} + +impl Default for Algorithm { + fn default() -> Self { + Algorithm::HS256 + } +} + +impl FromStr for Algorithm { + type Err = Error; + fn from_str(s: &str) -> Result { + match s { + "HS256" => Ok(Algorithm::HS256), + "HS384" => Ok(Algorithm::HS384), + "HS512" => Ok(Algorithm::HS512), + "ES256" => Ok(Algorithm::ES256), + "ES384" => Ok(Algorithm::ES384), + "RS256" => Ok(Algorithm::RS256), + "RS384" => Ok(Algorithm::RS384), + "RS512" => Ok(Algorithm::RS512), + _ => Err(new_error(ErrorKind::InvalidAlgorithmName)), + } + } +} diff --git a/src/crypto.rs b/src/crypto.rs index cab93e7..741585c 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -3,82 +3,35 @@ use std::sync::Arc; use base64; use ring::constant_time::verify_slices_are_equal; use ring::{digest, hmac, rand, signature}; -use std::str::FromStr; use untrusted; -use errors::{new_error, Error, ErrorKind, Result}; - -/// The algorithms supported for signing/verifying -#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)] -pub enum Algorithm { - /// HMAC using SHA-256 - HS256, - /// HMAC using SHA-384 - HS384, - /// HMAC using SHA-512 - HS512, - - /// ECDSA using SHA-256 - ES256, - - /// ECDSA using SHA-384 - ES384, - - /// RSASSA-PKCS1-v1_5 using SHA-256 - RS256, - /// RSASSA-PKCS1-v1_5 using SHA-384 - RS384, - /// RSASSA-PKCS1-v1_5 using SHA-512 - RS512, -} - -impl Default for Algorithm { - fn default() -> Self { - Algorithm::HS256 - } -} - -impl FromStr for Algorithm { - type Err = Error; - fn from_str(s: &str) -> Result { - match s { - "HS256" => Ok(Algorithm::HS256), - "HS384" => Ok(Algorithm::HS384), - "HS512" => Ok(Algorithm::HS512), - "ES256" => Ok(Algorithm::ES256), - "ES384" => Ok(Algorithm::ES384), - "RS256" => Ok(Algorithm::RS256), - "RS384" => Ok(Algorithm::RS384), - "RS512" => Ok(Algorithm::RS512), - _ => Err(new_error(ErrorKind::InvalidAlgorithmName)), - } - } -} +use algorithms::Algorithm; +use errors::{new_error, ErrorKind, Result}; +use keys::Key; /// The actual HS signing + encoding -fn sign_hmac( - alg: &'static digest::Algorithm, - key: K, - signing_input: &str, -) -> Result { - let signing_key = hmac::SigningKey::new(alg, key.as_ref()); +fn sign_hmac(alg: &'static digest::Algorithm, key: Key, signing_input: &str) -> Result { + let signing_key = match key { + Key::Hmac(bytes) => hmac::SigningKey::new(alg, bytes), + _ => return Err(ErrorKind::InvalidKeyFormat)?, + }; let digest = hmac::sign(&signing_key, signing_input.as_bytes()); Ok(base64::encode_config::(&digest, base64::URL_SAFE_NO_PAD)) } /// The actual ECDSA signing + encoding -fn sign_ecdsa( +fn sign_ecdsa( alg: &'static signature::EcdsaSigningAlgorithm, - key: K, + key: Key, signing_input: &str, ) -> Result { - let signing_key = match key.format() { - KeyFormat::PKCS8 => { - signature::EcdsaKeyPair::from_pkcs8(alg, untrusted::Input::from(key.as_ref()))? + let signing_key = match key { + Key::Pkcs8(bytes) => { + signature::EcdsaKeyPair::from_pkcs8(alg, untrusted::Input::from(bytes))? } _ => { - return Err(ErrorKind::InvalidKeyFormat)?; + return Err(new_error(ErrorKind::InvalidKeyFormat)); } }; @@ -89,19 +42,12 @@ fn sign_ecdsa( /// The actual RSA signing + encoding /// Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html -fn sign_rsa( - alg: &'static signature::RsaEncoding, - key: K, - signing_input: &str, -) -> Result { - let key_bytes = untrusted::Input::from(key.as_ref()); - let key_pair = match key.format() { - KeyFormat::DER => { - signature::RsaKeyPair::from_der(key_bytes).map_err(|_| ErrorKind::InvalidRsaKey)? - } - KeyFormat::PKCS8 => { - signature::RsaKeyPair::from_pkcs8(key_bytes).map_err(|_| ErrorKind::InvalidRsaKey)? - } +fn sign_rsa(alg: &'static signature::RsaEncoding, key: Key, signing_input: &str) -> Result { + let key_pair = match key { + Key::Der(bytes) => signature::RsaKeyPair::from_der(untrusted::Input::from(bytes)) + .map_err(|_| ErrorKind::InvalidRsaKey)?, + Key::Pkcs8(bytes) => signature::RsaKeyPair::from_pkcs8(untrusted::Input::from(bytes)) + .map_err(|_| ErrorKind::InvalidRsaKey)?, _ => { return Err(ErrorKind::InvalidKeyFormat)?; } @@ -121,7 +67,7 @@ fn sign_rsa( /// the base64 url safe encoded of the result. /// /// Only use this function if you want to do something other than JWT. -pub fn sign(signing_input: &str, key: K, algorithm: Algorithm) -> Result { +pub fn sign(signing_input: &str, key: Key, algorithm: Algorithm) -> Result { match algorithm { Algorithm::HS256 => sign_hmac(&digest::SHA256, key, signing_input), Algorithm::HS384 => sign_hmac(&digest::SHA384, key, signing_input), @@ -157,6 +103,36 @@ fn verify_ring( Ok(res.is_ok()) } +fn verify_ring_es( + alg: &dyn signature::VerificationAlgorithm, + signature: &str, + signing_input: &str, + key: Key, +) -> Result { + let bytes = match key { + Key::Pkcs8(bytes) => bytes, + _ => { + return Err(ErrorKind::InvalidKeyFormat)?; + } + }; + verify_ring(alg, signature, signing_input, bytes) +} + +fn verify_ring_rsa( + alg: &dyn signature::VerificationAlgorithm, + signature: &str, + signing_input: &str, + key: Key, +) -> Result { + let bytes = match key { + Key::Der(bytes) | Key::Pkcs8(bytes) => bytes, + _ => { + return Err(ErrorKind::InvalidKeyFormat)?; + } + }; + verify_ring(alg, signature, signing_input, bytes) +} + /// Compares the signature given with a re-computed signature for HMAC or using the public key /// for RSA. /// @@ -168,34 +144,40 @@ fn verify_ring( pub fn verify( signature: &str, signing_input: &str, - public_key: &[u8], + public_key: Key, algorithm: Algorithm, ) -> Result { match algorithm { Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => { // we just re-sign the data with the key and compare if they are equal - let signed = sign(signing_input, Hmac::from(&public_key), algorithm)?; + let signed = sign(signing_input, public_key, algorithm)?; Ok(verify_slices_are_equal(signature.as_ref(), signed.as_ref()).is_ok()) } - Algorithm::ES256 => { - verify_ring(&signature::ECDSA_P256_SHA256_FIXED, signature, signing_input, public_key) - } - Algorithm::ES384 => { - verify_ring(&signature::ECDSA_P384_SHA384_FIXED, signature, signing_input, public_key) - } - Algorithm::RS256 => verify_ring( + Algorithm::ES256 => verify_ring_es( + &signature::ECDSA_P256_SHA256_FIXED, + signature, + signing_input, + public_key, + ), + Algorithm::ES384 => verify_ring_es( + &signature::ECDSA_P384_SHA384_FIXED, + signature, + signing_input, + public_key, + ), + Algorithm::RS256 => verify_ring_rsa( &signature::RSA_PKCS1_2048_8192_SHA256, signature, signing_input, public_key, ), - Algorithm::RS384 => verify_ring( + Algorithm::RS384 => verify_ring_rsa( &signature::RSA_PKCS1_2048_8192_SHA384, signature, signing_input, public_key, ), - Algorithm::RS512 => verify_ring( + Algorithm::RS512 => verify_ring_rsa( &signature::RSA_PKCS1_2048_8192_SHA512, signature, signing_input, @@ -203,114 +185,3 @@ pub fn verify( ), } } - -/// The supported RSA key formats, see the documentation for ring::signature::RsaKeyPair -/// for more information -pub enum KeyFormat { - /// An unencrypted PKCS#8-encoded key. Can be used with both ECDSA and RSA - /// algorithms when signing. See ring for information. - PKCS8, - /// A binary DER-encoded ASN.1 key. Can only be used with RSA algorithms - /// when signing. See ring for more information - DER, - /// This is not a key format, but provided for convenience since HMAC is - /// a supported signing algorithm. - HMAC, -} - -/// A tiny abstraction on top of raw key buffers to add key format -/// information -pub trait Key: AsRef<[u8]> { - /// The format of the key - fn format(&self) -> KeyFormat; -} - -/// This blanket implementation aligns with the key loading as of version 6.0.0 -// impl Key for T -// where -// T: AsRef<[u8]>, -// { -// fn format(&self) -> KeyFormat { -// KeyFormat::DER -// } -// } - -/// A convenience wrapper for a key buffer as an unencrypted PKCS#8-encoded, -/// see ring for more details -pub struct Pkcs8<'a> { - key_bytes: &'a [u8], -} - -impl<'a> Key for Pkcs8<'a> { - fn format(&self) -> KeyFormat { - KeyFormat::PKCS8 - } -} - -impl<'a> AsRef<[u8]> for Pkcs8<'a> { - fn as_ref(&self) -> &[u8] { - self.key_bytes - } -} - -impl<'a, T> From<&'a T> for Pkcs8<'a> -where - T: AsRef<[u8]>, -{ - fn from(key: &'a T) -> Self { - Self { key_bytes: key.as_ref() } - } -} - -/// A convenience wrapper for a key buffer as a binary DER-encoded ASN.1 key, -/// see ring for more details -pub struct Der<'a> { - key_bytes: &'a [u8], -} - -impl<'a> Key for Der<'a> { - fn format(&self) -> KeyFormat { - KeyFormat::DER - } -} - -impl<'a> AsRef<[u8]> for Der<'a> { - fn as_ref(&self) -> &[u8] { - self.key_bytes - } -} - -impl<'a, T> From<&'a T> for Der<'a> -where - T: AsRef<[u8]>, -{ - fn from(key: &'a T) -> Self { - Self { key_bytes: key.as_ref() } - } -} - -/// Convenience wrapper for an HMAC key -pub struct Hmac<'a> { - key_bytes: &'a [u8], -} - -impl<'a> Key for Hmac<'a> { - fn format(&self) -> KeyFormat { - KeyFormat::HMAC - } -} - -impl<'a> AsRef<[u8]> for Hmac<'a> { - fn as_ref(&self) -> &[u8] { - self.key_bytes - } -} - -impl<'a, T> From<&'a T> for Hmac<'a> -where - T: AsRef<[u8]>, -{ - fn from(key: &'a T) -> Self { - Self { key_bytes: key.as_ref() } - } -} diff --git a/src/header.rs b/src/header.rs index 0db2614..2620519 100644 --- a/src/header.rs +++ b/src/header.rs @@ -1,4 +1,4 @@ -use crypto::Algorithm; +use algorithms::Algorithm; /// A basic JWT header, the alg defaults to HS256 and typ is automatically /// set to `JWT`. All the other fields are optional. diff --git a/src/keys.rs b/src/keys.rs new file mode 100644 index 0000000..7024daa --- /dev/null +++ b/src/keys.rs @@ -0,0 +1,13 @@ +/// The supported RSA key formats, see the documentation for ring::signature::RsaKeyPair +/// for more information +pub enum Key<'a> { + /// An unencrypted PKCS#8-encoded key. Can be used with both ECDSA and RSA + /// algorithms when signing. See ring for information. + Pkcs8(&'a [u8]), + /// A binary DER-encoded ASN.1 key. Can only be used with RSA algorithms + /// when signing. See ring for more information + Der(&'a [u8]), + /// This is not a key format, but provided for convenience since HMAC is + /// a supported signing algorithm. + Hmac(&'a [u8]), +} diff --git a/src/lib.rs b/src/lib.rs index 35217f8..b0a9073 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,6 @@ //! Create and parses JWT (JSON Web Tokens) //! //! Documentation: [stable](https://docs.rs/jsonwebtoken/) -#![recursion_limit = "300"] #![deny(missing_docs)] #[macro_use] @@ -13,15 +12,19 @@ extern crate serde; extern crate serde_json; extern crate untrusted; +mod algorithms; mod crypto; -/// All the errors, generated using error-chain +/// All the errors pub mod errors; mod header; +mod keys; mod serialization; mod validation; -pub use crypto::{sign, verify, Algorithm, Der, Hmac, Key, KeyFormat, Pkcs8}; +pub use algorithms::Algorithm; +pub use crypto::{sign, verify}; pub use header::Header; +pub use keys::Key; pub use serialization::TokenData; pub use validation::Validation; @@ -52,9 +55,9 @@ use validation::validate; /// /// // my_claims is a struct that implements Serialize /// // This will create a JWT using HS256 as algorithm -/// let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap(); +/// let token = encode(&Header::default(), &my_claims, Key::Hmac("secret".as_ref())).unwrap(); /// ``` -pub fn encode(header: &Header, claims: &T, key: K) -> Result { +pub fn encode(header: &Header, claims: &T, key: Key) -> Result { let encoded_header = to_jwt_part(&header)?; let encoded_claims = to_jwt_part(&claims)?; let signing_input = [encoded_header.as_ref(), encoded_claims.as_ref()].join("."); @@ -92,11 +95,11 @@ macro_rules! expect_two { /// /// let token = "a.jwt.token".to_string(); /// // Claims is a struct that implements Deserialize -/// let token_data = decode::(&token, "secret", &Validation::new(Algorithm::HS256)); +/// let token_data = decode::(&token, Key::Hmac("secret"), &Validation::new(Algorithm::HS256)); /// ``` pub fn decode( token: &str, - key: &[u8], + key: Key, validation: &Validation, ) -> Result> { let (signature, signing_input) = expect_two!(token.rsplitn(2, '.')); diff --git a/src/validation.rs b/src/validation.rs index 293147a..5798c01 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -3,7 +3,7 @@ use serde::ser::Serialize; use serde_json::map::Map; use serde_json::{from_value, to_value, Value}; -use crypto::Algorithm; +use algorithms::Algorithm; use errors::{new_error, ErrorKind, Result}; /// Contains the various validations that are applied after decoding a token. @@ -78,6 +78,7 @@ impl Validation { /// Since `aud` can be either a String or an array of String in the JWT spec, this method will take /// care of serializing the value. pub fn set_audience(&mut self, audience: &T) { + // TODO: check if the value is a string or an array and error if not self.aud = Some(to_value(audience).unwrap()); } } diff --git a/tests/ecdsa.rs b/tests/ecdsa.rs index 5c1b315..c11c0fd 100644 --- a/tests/ecdsa.rs +++ b/tests/ecdsa.rs @@ -4,7 +4,7 @@ extern crate serde_derive; extern crate chrono; use chrono::Utc; -use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Der, Header, Pkcs8, Validation}; +use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Key, Header, Validation}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] struct Claims { @@ -16,9 +16,9 @@ struct Claims { #[test] fn round_trip_sign_verification() { let privkey = include_bytes!("private_ecdsa_key.pk8"); - let encrypted = sign("hello world", Pkcs8::from(&&privkey[..]), Algorithm::ES256).unwrap(); + let encrypted = sign("hello world", Key::Pkcs8(&privkey[..]), Algorithm::ES256).unwrap(); let pubkey = include_bytes!("public_ecdsa_key.pk8"); - let is_valid = verify(&encrypted, "hello world", pubkey, Algorithm::ES256).unwrap(); + let is_valid = verify(&encrypted, "hello world", Key::Pkcs8(pubkey), Algorithm::ES256).unwrap(); assert!(is_valid); } @@ -31,9 +31,9 @@ fn round_trip_claim() { }; let privkey = include_bytes!("private_ecdsa_key.pk8"); let token = - encode(&Header::new(Algorithm::ES256), &my_claims, Pkcs8::from(&&privkey[..])).unwrap(); + encode(&Header::new(Algorithm::ES256), &my_claims, Key::Pkcs8(&privkey[..])).unwrap(); let pubkey = include_bytes!("public_ecdsa_key.pk8"); - let token_data = decode::(&token, pubkey, &Validation::new(Algorithm::ES256)).unwrap(); + let token_data = decode::(&token, Key::Pkcs8(pubkey), &Validation::new(Algorithm::ES256)).unwrap(); assert_eq!(my_claims, token_data.claims); assert!(token_data.header.kid.is_none()); } @@ -42,5 +42,5 @@ fn round_trip_claim() { #[should_panic(expected = "InvalidKeyFormat")] fn fails_with_non_pkcs8_key_format() { let privkey = include_bytes!("private_rsa_key.der"); - let _encrypted = sign("hello world", Der::from(&&privkey[..]), Algorithm::ES256).unwrap(); + let _encrypted = sign("hello world", Key::Der(&privkey[..]), Algorithm::ES256).unwrap(); } diff --git a/tests/lib.rs b/tests/lib.rs index f489477..6a4e328 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -5,7 +5,7 @@ extern crate chrono; use chrono::Utc; use jsonwebtoken::{ - dangerous_unsafe_decode, decode, decode_header, encode, sign, verify, Algorithm, Header, Hmac, + dangerous_unsafe_decode, decode, decode_header, encode, sign, verify, Algorithm, Header, Key, Validation, }; use std::str::FromStr; @@ -19,7 +19,7 @@ struct Claims { #[test] fn sign_hs256() { - let result = sign("hello world", Hmac::from(b"secret"), Algorithm::HS256).unwrap(); + let result = sign("hello world", Key::Hmac(b"secret"), Algorithm::HS256).unwrap(); let expected = "c0zGLzKEFWj0VxWuufTXiRMk5tlI5MbGDAYhzaxIYjo"; assert_eq!(result, expected); } @@ -27,7 +27,7 @@ fn sign_hs256() { #[test] fn verify_hs256() { let sig = "c0zGLzKEFWj0VxWuufTXiRMk5tlI5MbGDAYhzaxIYjo"; - let valid = verify(sig, "hello world", b"secret", Algorithm::HS256).unwrap(); + let valid = verify(sig, "hello world", Key::Hmac(b"secret"), Algorithm::HS256).unwrap(); assert!(valid); } @@ -40,8 +40,8 @@ fn encode_with_custom_header() { }; let mut header = Header::default(); header.kid = Some("kid".to_string()); - let token = encode(&header, &my_claims, Hmac::from(b"secret")).unwrap(); - let token_data = decode::(&token, "secret".as_ref(), &Validation::default()).unwrap(); + let token = encode(&header, &my_claims, Key::Hmac(b"secret")).unwrap(); + let token_data = decode::(&token, Key::Hmac(b"secret"), &Validation::default()).unwrap(); assert_eq!(my_claims, token_data.claims); assert_eq!("kid", token_data.header.kid.unwrap()); } @@ -53,8 +53,8 @@ fn round_trip_claim() { company: "ACME".to_string(), exp: Utc::now().timestamp() + 10000, }; - let token = encode(&Header::default(), &my_claims, Hmac::from(b"secret")).unwrap(); - let token_data = decode::(&token, "secret".as_ref(), &Validation::default()).unwrap(); + let token = encode(&Header::default(), &my_claims, Key::Hmac(b"secret")).unwrap(); + let token_data = decode::(&token, Key::Hmac(b"secret"), &Validation::default()).unwrap(); assert_eq!(my_claims, token_data.claims); assert!(token_data.header.kid.is_none()); } @@ -62,7 +62,7 @@ fn round_trip_claim() { #[test] fn decode_token() { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.9r56oF7ZliOBlOAyiOFperTGxBtPykRQiWNFxhDCW98"; - let claims = decode::(token, "secret".as_ref(), &Validation::default()); + let claims = decode::(token, Key::Hmac(b"secret"), &Validation::default()); println!("{:?}", claims); claims.unwrap(); } @@ -71,7 +71,7 @@ fn decode_token() { #[should_panic(expected = "InvalidToken")] fn decode_token_missing_parts() { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; - let claims = decode::(token, "secret".as_ref(), &Validation::default()); + let claims = decode::(token, Key::Hmac(b"secret"), &Validation::default()); claims.unwrap(); } @@ -80,7 +80,7 @@ fn decode_token_missing_parts() { fn decode_token_invalid_signature() { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.wrong"; - let claims = decode::(token, "secret".as_ref(), &Validation::default()); + let claims = decode::(token, Key::Hmac(b"secret"), &Validation::default()); claims.unwrap(); } @@ -88,14 +88,14 @@ fn decode_token_invalid_signature() { #[should_panic(expected = "InvalidAlgorithm")] fn decode_token_wrong_algorithm() { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.I1BvFoHe94AFf09O6tDbcSB8-jp8w6xZqmyHIwPeSdY"; - let claims = decode::(token, "secret".as_ref(), &Validation::new(Algorithm::RS512)); + let claims = decode::(token, Key::Hmac(b"secret"), &Validation::new(Algorithm::RS512)); claims.unwrap(); } #[test] fn decode_token_with_bytes_secret() { let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.Hm0yvKH25TavFPz7J_coST9lZFYH1hQo0tvhvImmaks"; - let claims = decode::(token, b"\x01\x02\x03", &Validation::default()); + let claims = decode::(token, Key::Hmac(b"\x01\x02\x03"), &Validation::default()); assert!(claims.is_ok()); } @@ -144,7 +144,7 @@ fn does_validation_in_right_order() { company: "ACME".to_string(), exp: Utc::now().timestamp() + 10000, }; - let token = encode(&Header::default(), &my_claims, Hmac::from(b"secret")).unwrap(); + let token = encode(&Header::default(), &my_claims, Key::Hmac(b"secret")).unwrap(); let v = Validation { leeway: 5, validate_exp: true, @@ -152,7 +152,7 @@ fn does_validation_in_right_order() { sub: Some("sub no check".to_string()), ..Validation::default() }; - let res = decode::(&token, "secret".as_ref(), &v); + let res = decode::(&token, Key::Hmac(b"secret"), &v); assert!(res.is_err()); println!("{:?}", res); //assert!(res.is_ok()); diff --git a/tests/rsa.rs b/tests/rsa.rs index 4f2111a..20ee7f7 100644 --- a/tests/rsa.rs +++ b/tests/rsa.rs @@ -4,7 +4,7 @@ extern crate serde_derive; extern crate chrono; use chrono::Utc; -use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Der, Header, Pkcs8, Validation}; +use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Key, Header, Validation}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] struct Claims { @@ -16,9 +16,9 @@ struct Claims { #[test] fn round_trip_sign_verification() { let privkey = include_bytes!("private_rsa_key.der"); - let encrypted = sign("hello world", Der::from(&&privkey[..]), Algorithm::RS256).unwrap(); + let encrypted = sign("hello world", Key::Der(&privkey[..]), Algorithm::RS256).unwrap(); let is_valid = - verify(&encrypted, "hello world", include_bytes!("public_rsa_key.der"), Algorithm::RS256) + verify(&encrypted, "hello world", Key::Der(include_bytes!("public_rsa_key.der")), Algorithm::RS256) .unwrap(); assert!(is_valid); } @@ -32,10 +32,10 @@ fn round_trip_claim() { }; let privkey = include_bytes!("private_rsa_key.der"); let token = - encode(&Header::new(Algorithm::RS256), &my_claims, Der::from(&&privkey[..])).unwrap(); + encode(&Header::new(Algorithm::RS256), &my_claims, Key::Der(&privkey[..])).unwrap(); let token_data = decode::( &token, - include_bytes!("public_rsa_key.der"), + Key::Der(include_bytes!("public_rsa_key.der")), &Validation::new(Algorithm::RS256), ) .unwrap(); @@ -47,5 +47,5 @@ fn round_trip_claim() { #[should_panic(expected = "InvalidRsaKey")] fn fails_with_different_key_format() { let privkey = include_bytes!("private_rsa_key.der"); - sign("hello world", Pkcs8::from(&&privkey[..]), Algorithm::RS256).unwrap(); + sign("hello world", Key::Pkcs8(&privkey[..]), Algorithm::RS256).unwrap(); } From 357eb4c53919751d28c4261f992120e7d3ee06e3 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sun, 16 Jun 2019 18:03:21 +0200 Subject: [PATCH 10/55] Update changelog --- CHANGELOG.md | 3 +++ Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 074c0bd..e2a497d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ # 7.0.0 (unreleased) +- Add support for PS256, PS384 and PS512 +- Change API for both sign/verify to take a `Key` enum rather than bytes + ## 6.0.1 (2019-05-10) - Fix Algorithm mapping in FromStr for RSA diff --git a/Cargo.toml b/Cargo.toml index cb2d641..1daf1a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jsonwebtoken" -version = "6.0.1" +version = "7.0.0" authors = ["Vincent Prouillet "] license = "MIT" readme = "README.md" From 3c62b98bd9d0e31f1f34bbf2d21103278fbfa0dc Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sun, 16 Jun 2019 18:15:14 +0200 Subject: [PATCH 11/55] Update example to v7 style --- examples/custom_chrono.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/examples/custom_chrono.rs b/examples/custom_chrono.rs index 7fd7b3a..2d3bcca 100644 --- a/examples/custom_chrono.rs +++ b/examples/custom_chrono.rs @@ -5,7 +5,7 @@ extern crate serde_derive; extern crate chrono; use chrono::prelude::*; -use jwt::{Header, Validation}; +use jwt::{Header, Key, Validation}; const SECRET: &str = "some-secret"; @@ -59,13 +59,14 @@ mod jwt_numeric_date { let claims = Claims { sub: sub.clone(), iat, exp }; - let token = jwt::encode(&Header::default(), &claims, SECRET.as_ref()) + let token = jwt::encode(&Header::default(), &claims, Key::Hmac(SECRET.as_ref())) .expect("Failed to encode claims"); assert_eq!(&token, EXPECTED_TOKEN); - let decoded = jwt::decode::(&token, SECRET.as_ref(), &Validation::default()) - .expect("Failed to decode token"); + let decoded = + jwt::decode::(&token, Key::Hmac(SECRET.as_ref()), &Validation::default()) + .expect("Failed to decode token"); assert_eq!(decoded.claims, claims); } @@ -90,11 +91,12 @@ fn main() -> Result<(), Box> { let claims = Claims { sub: sub.clone(), iat, exp }; - let token = jwt::encode(&Header::default(), &claims, SECRET.as_ref())?; + let token = jwt::encode(&Header::default(), &claims, Key::Hmac(SECRET.as_ref()))?; println!("serialized token: {}", &token); - let token_data = jwt::decode::(&token, SECRET.as_ref(), &Validation::default())?; + let token_data = + jwt::decode::(&token, Key::Hmac(SECRET.as_ref()), &Validation::default())?; println!("token data:\n{:#?}", &token_data); Ok(()) From 22cd4dbb621cad9078be432ca6b9956996a22e95 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sat, 6 Jul 2019 20:24:14 +0200 Subject: [PATCH 12/55] Remove unused test key --- tests/public_rsa_key_8.pem | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 tests/public_rsa_key_8.pem diff --git a/tests/public_rsa_key_8.pem b/tests/public_rsa_key_8.pem deleted file mode 100644 index 99e2a40..0000000 --- a/tests/public_rsa_key_8.pem +++ /dev/null @@ -1,8 +0,0 @@ ------BEGIN RSA PUBLIC KEY----- -MIIBCgKCAQEAyRE6rHuNR0QbHO3H3Kt2pOKGVhQqGZXInOduQNxXzuKlvQTLUTv4 -l4sggh5/CYYi/cvI+SXVT9kPWSKXxJXBXd/4LkvcPuUakBoAkfh+eiFVMh2VrUyW -yj3MFl0HTVF9KwRXLAcwkREiS3npThHRyIxuy0ZMeZfxVL5arMhw1SRELB8HoGfG -/AtH89BIE9jDBHZ9dLelK9a184zAf8LwoPLxvJb3Il5nncqPcSfKDDodMFBIMc4l -QzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xqi+yUod+j8MtvIj812dkS4QMiRVN/by2h -3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5TdQIDAQAB ------END RSA PUBLIC KEY----- From b8627260b2902a1ab4fdda83083be3e0b0fb9b7f Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sat, 6 Jul 2019 20:36:32 +0200 Subject: [PATCH 13/55] Update to edition 2018 --- CHANGELOG.md | 1 + Cargo.toml | 1 + src/algorithms.rs | 2 +- src/crypto.rs | 15 ++++++++------- src/header.rs | 2 +- src/lib.rs | 6 +++--- src/serialization.rs | 4 ++-- src/validation.rs | 6 +++--- tests/ecdsa.rs | 2 +- tests/lib.rs | 2 +- tests/rsa.rs | 5 ++++- 11 files changed, 26 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2a497d..dda7453 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Add support for PS256, PS384 and PS512 - Change API for both sign/verify to take a `Key` enum rather than bytes +- Update to 2018 edition ## 6.0.1 (2019-05-10) diff --git a/Cargo.toml b/Cargo.toml index 1daf1a1..41cf026 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ description = "Create and parse JWT in a strongly typed way." homepage = "https://github.com/Keats/rust-jwt" repository = "https://github.com/Keats/rust-jwt" keywords = ["jwt", "web", "api", "token", "json"] +edition = "2018" [dependencies] serde_json = "1.0" diff --git a/src/algorithms.rs b/src/algorithms.rs index a7f8589..34f8556 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -1,4 +1,4 @@ -use errors::{new_error, Error, ErrorKind, Result}; +use crate::errors::{new_error, Error, ErrorKind, Result}; use std::str::FromStr; /// The algorithms supported for signing/verifying diff --git a/src/crypto.rs b/src/crypto.rs index 588980a..5901b7e 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -1,13 +1,11 @@ -use std::sync::Arc; - use base64; use ring::constant_time::verify_slices_are_equal; use ring::{digest, hmac, rand, signature}; use untrusted; -use algorithms::Algorithm; -use errors::{new_error, ErrorKind, Result}; -use keys::Key; +use crate::algorithms::Algorithm; +use crate::errors::{new_error, ErrorKind, Result}; +use crate::keys::Key; /// The actual HS signing + encoding fn sign_hmac(alg: &'static digest::Algorithm, key: Key, signing_input: &str) -> Result { @@ -41,7 +39,11 @@ fn sign_ecdsa( /// The actual RSA signing + encoding /// Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html -fn sign_rsa(alg: &'static signature::RsaEncoding, key: Key, signing_input: &str) -> Result { +fn sign_rsa( + alg: &'static dyn signature::RsaEncoding, + key: Key, + signing_input: &str, +) -> Result { let key_pair = match key { Key::Der(bytes) => signature::RsaKeyPair::from_der(untrusted::Input::from(bytes)) .map_err(|_| ErrorKind::InvalidRsaKey)?, @@ -52,7 +54,6 @@ fn sign_rsa(alg: &'static signature::RsaEncoding, key: Key, signing_input: &str) } }; - let key_pair = Arc::new(key_pair); let mut signature = vec![0; key_pair.public_modulus_len()]; let rng = rand::SystemRandom::new(); key_pair diff --git a/src/header.rs b/src/header.rs index 2620519..2a17da9 100644 --- a/src/header.rs +++ b/src/header.rs @@ -1,4 +1,4 @@ -use algorithms::Algorithm; +use crate::algorithms::Algorithm; /// A basic JWT header, the alg defaults to HS256 and typ is automatically /// set to `JWT`. All the other fields are optional. diff --git a/src/lib.rs b/src/lib.rs index b0a9073..2f87d78 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,9 +31,9 @@ pub use validation::Validation; use serde::de::DeserializeOwned; use serde::ser::Serialize; -use errors::{new_error, ErrorKind, Result}; -use serialization::{from_jwt_part, from_jwt_part_claims, to_jwt_part}; -use validation::validate; +use crate::errors::{new_error, ErrorKind, Result}; +use crate::serialization::{from_jwt_part, from_jwt_part_claims, to_jwt_part}; +use crate::validation::validate; /// Encode the header and claims given and sign the payload using the algorithm from the header and the key /// diff --git a/src/serialization.rs b/src/serialization.rs index 995eee3..2a47688 100644 --- a/src/serialization.rs +++ b/src/serialization.rs @@ -4,8 +4,8 @@ use serde::ser::Serialize; use serde_json::map::Map; use serde_json::{from_str, to_string, Value}; -use errors::Result; -use header::Header; +use crate::errors::Result; +use crate::header::Header; /// The return type of a successful call to decode #[derive(Debug)] diff --git a/src/validation.rs b/src/validation.rs index 5798c01..19b3dc4 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -3,8 +3,8 @@ use serde::ser::Serialize; use serde_json::map::Map; use serde_json::{from_value, to_value, Value}; -use algorithms::Algorithm; -use errors::{new_error, ErrorKind, Result}; +use crate::algorithms::Algorithm; +use crate::errors::{new_error, ErrorKind, Result}; /// Contains the various validations that are applied after decoding a token. /// @@ -164,7 +164,7 @@ mod tests { use super::{validate, Validation}; - use errors::ErrorKind; + use crate::errors::ErrorKind; #[test] fn exp_in_future_ok() { diff --git a/tests/ecdsa.rs b/tests/ecdsa.rs index 4df1dab..12cf78d 100644 --- a/tests/ecdsa.rs +++ b/tests/ecdsa.rs @@ -7,7 +7,7 @@ use chrono::Utc; use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Header, Key, Validation}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -struct Claims { +pub struct Claims { sub: String, company: String, exp: i64, diff --git a/tests/lib.rs b/tests/lib.rs index b91f318..cce95a1 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -11,7 +11,7 @@ use jsonwebtoken::{ use std::str::FromStr; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -struct Claims { +pub struct Claims { sub: String, company: String, exp: i64, diff --git a/tests/rsa.rs b/tests/rsa.rs index b64f4c7..88432b4 100644 --- a/tests/rsa.rs +++ b/tests/rsa.rs @@ -16,7 +16,7 @@ const RSA_ALGORITHMS: &[Algorithm] = &[ ]; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -struct Claims { +pub struct Claims { sub: String, company: String, exp: i64, @@ -62,3 +62,6 @@ fn fails_with_different_key_format() { let privkey = include_bytes!("private_rsa_key.der"); sign("hello world", Key::Pkcs8(&privkey[..]), Algorithm::RS256).unwrap(); } + +#[test] +fn can_decode_jwt_io_example() {} From 10105af2fdffaf4f5406b7a84524ee8d0c840d95 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sat, 13 Jul 2019 17:43:44 +0200 Subject: [PATCH 14/55] Add support for modulus/exponent --- CHANGELOG.md | 1 + src/crypto.rs | 29 ++++++++++++++++++++++------- src/keys.rs | 2 ++ tests/rsa.rs | 35 ++++++++++++++++++++++++++++++++++- 4 files changed, 59 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dda7453..e0af583 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ # 7.0.0 (unreleased) - Add support for PS256, PS384 and PS512 +- Add support for verifying with modulus/exponent components for RSA - Change API for both sign/verify to take a `Key` enum rather than bytes - Update to 2018 edition diff --git a/src/crypto.rs b/src/crypto.rs index 5901b7e..7dfef01 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -123,18 +123,33 @@ fn verify_ring_es( } fn verify_ring_rsa( - alg: &dyn signature::VerificationAlgorithm, + alg: &signature::RsaParameters, signature: &str, signing_input: &str, key: Key, ) -> Result { - let bytes = match key { - Key::Der(bytes) | Key::Pkcs8(bytes) => bytes, - _ => { - return Err(ErrorKind::InvalidKeyFormat)?; + match key { + Key::Der(bytes) | Key::Pkcs8(bytes) => verify_ring(alg, signature, signing_input, bytes), + Key::ModulusExponent(n, e) => { + let signature_bytes = base64::decode_config(signature, base64::URL_SAFE_NO_PAD)?; + let message = untrusted::Input::from(signing_input.as_bytes()); + let modulus = untrusted::Input::from(n); + let exponent = untrusted::Input::from(e); + let expected_signature = untrusted::Input::from(signature_bytes.as_slice()); + + let res = signature::primitive::verify_rsa( + alg, + (modulus, exponent), + message, + expected_signature, + ); + + Ok(res.is_ok()) } - }; - verify_ring(alg, signature, signing_input, bytes) + _ => { + Err(ErrorKind::InvalidKeyFormat)? + } + } } /// Compares the signature given with a re-computed signature for HMAC or using the public key diff --git a/src/keys.rs b/src/keys.rs index 7024daa..cbabfa4 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -10,4 +10,6 @@ pub enum Key<'a> { /// This is not a key format, but provided for convenience since HMAC is /// a supported signing algorithm. Hmac(&'a [u8]), + /// A Modulus/exponent for a RSA public key + ModulusExponent(&'a [u8], &'a [u8]), } diff --git a/tests/rsa.rs b/tests/rsa.rs index 88432b4..3a32399 100644 --- a/tests/rsa.rs +++ b/tests/rsa.rs @@ -64,4 +64,37 @@ fn fails_with_different_key_format() { } #[test] -fn can_decode_jwt_io_example() {} +fn rsa_modulus_exponent() { + let modulus: Vec = vec![ + 0xc9, 0x11, 0x3a, 0xac, 0x7b, 0x8d, 0x47, 0x44, 0x1b, 0x1c, 0xed, 0xc7, 0xdc, 0xab, 0x76, + 0xa4, 0xe2, 0x86, 0x56, 0x14, 0x2a, 0x19, 0x95, 0xc8, 0x9c, 0xe7, 0x6e, 0x40, 0xdc, 0x57, + 0xce, 0xe2, 0xa5, 0xbd, 0x04, 0xcb, 0x51, 0x3b, 0xf8, 0x97, 0x8b, 0x20, 0x82, 0x1e, 0x7f, + 0x09, 0x86, 0x22, 0xfd, 0xcb, 0xc8, 0xf9, 0x25, 0xd5, 0x4f, 0xd9, 0x0f, 0x59, 0x22, 0x97, + 0xc4, 0x95, 0xc1, 0x5d, 0xdf, 0xf8, 0x2e, 0x4b, 0xdc, 0x3e, 0xe5, 0x1a, 0x90, 0x1a, 0x00, + 0x91, 0xf8, 0x7e, 0x7a, 0x21, 0x55, 0x32, 0x1d, 0x95, 0xad, 0x4c, 0x96, 0xca, 0x3d, 0xcc, + 0x16, 0x5d, 0x07, 0x4d, 0x51, 0x7d, 0x2b, 0x04, 0x57, 0x2c, 0x07, 0x30, 0x91, 0x11, 0x22, + 0x4b, 0x79, 0xe9, 0x4e, 0x11, 0xd1, 0xc8, 0x8c, 0x6e, 0xcb, 0x46, 0x4c, 0x79, 0x97, 0xf1, + 0x54, 0xbe, 0x5a, 0xac, 0xc8, 0x70, 0xd5, 0x24, 0x44, 0x2c, 0x1f, 0x07, 0xa0, 0x67, 0xc6, + 0xfc, 0x0b, 0x47, 0xf3, 0xd0, 0x48, 0x13, 0xd8, 0xc3, 0x04, 0x76, 0x7d, 0x74, 0xb7, 0xa5, + 0x2b, 0xd6, 0xb5, 0xf3, 0x8c, 0xc0, 0x7f, 0xc2, 0xf0, 0xa0, 0xf2, 0xf1, 0xbc, 0x96, 0xf7, + 0x22, 0x5e, 0x67, 0x9d, 0xca, 0x8f, 0x71, 0x27, 0xca, 0x0c, 0x3a, 0x1d, 0x30, 0x50, 0x48, + 0x31, 0xce, 0x25, 0x43, 0x30, 0xca, 0x2f, 0x98, 0x2f, 0x9a, 0x25, 0xcb, 0x5c, 0x1d, 0x40, + 0x18, 0xb9, 0xbc, 0x28, 0x18, 0xdf, 0x13, 0xcb, 0x37, 0x2f, 0x9c, 0x6a, 0x8b, 0xec, 0x94, + 0xa1, 0xdf, 0xa3, 0xf0, 0xcb, 0x6f, 0x22, 0x3f, 0x35, 0xd9, 0xd9, 0x12, 0xe1, 0x03, 0x22, + 0x45, 0x53, 0x7f, 0x6f, 0x2d, 0xa1, 0xdd, 0x96, 0x3c, 0x2d, 0x85, 0x46, 0xae, 0xa6, 0x57, + 0x65, 0x37, 0x20, 0x9f, 0x6b, 0xa3, 0x9f, 0xcb, 0x8a, 0x8d, 0x72, 0xd9, 0x54, 0x3e, 0x53, + 0x75, + ]; + let exponent: Vec = vec![0x01, 0x00, 0x01]; + let privkey = include_bytes!("private_rsa_key.der"); + + let encrypted = sign("hello world", Key::Der(&privkey[..]), Algorithm::RS256).unwrap(); + let is_valid = verify( + &encrypted, + "hello world", + Key::ModulusExponent(&modulus, &exponent), + Algorithm::RS256, + ) + .unwrap(); + assert!(is_valid); +} From f7423d075ad13fa114242c0a7e51ce51467e840a Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sat, 10 Aug 2019 11:38:14 -1000 Subject: [PATCH 15/55] Use *ring* 0.16.5. --- Cargo.toml | 3 +-- src/crypto.rs | 63 ++++++++++++++++++++------------------------------- src/lib.rs | 1 - 3 files changed, 26 insertions(+), 41 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 41cf026..c7a8393 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ edition = "2018" serde_json = "1.0" serde_derive = "1.0" serde = "1.0" -ring = "0.14.6" +ring = { version = "0.16.5", features = ["std"] } base64 = "0.10" -untrusted = "0.6" chrono = "0.4" diff --git a/src/crypto.rs b/src/crypto.rs index 7dfef01..3556fb8 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -1,21 +1,20 @@ use base64; use ring::constant_time::verify_slices_are_equal; -use ring::{digest, hmac, rand, signature}; -use untrusted; +use ring::{hmac, rand, signature}; use crate::algorithms::Algorithm; use crate::errors::{new_error, ErrorKind, Result}; use crate::keys::Key; /// The actual HS signing + encoding -fn sign_hmac(alg: &'static digest::Algorithm, key: Key, signing_input: &str) -> Result { +fn sign_hmac(alg: hmac::Algorithm, key: Key, signing_input: &str) -> Result { let signing_key = match key { - Key::Hmac(bytes) => hmac::SigningKey::new(alg, bytes), + Key::Hmac(bytes) => hmac::Key::new(alg, bytes), _ => return Err(ErrorKind::InvalidKeyFormat)?, }; let digest = hmac::sign(&signing_key, signing_input.as_bytes()); - Ok(base64::encode_config::(&digest, base64::URL_SAFE_NO_PAD)) + Ok(base64::encode_config::(&digest, base64::URL_SAFE_NO_PAD)) } /// The actual ECDSA signing + encoding @@ -25,15 +24,13 @@ fn sign_ecdsa( signing_input: &str, ) -> Result { let signing_key = match key { - Key::Pkcs8(bytes) => { - signature::EcdsaKeyPair::from_pkcs8(alg, untrusted::Input::from(bytes))? - } + Key::Pkcs8(bytes) => signature::EcdsaKeyPair::from_pkcs8(alg, bytes)?, _ => { return Err(new_error(ErrorKind::InvalidKeyFormat)); } }; let rng = rand::SystemRandom::new(); - let sig = signing_key.sign(&rng, untrusted::Input::from(signing_input.as_bytes()))?; + let sig = signing_key.sign(&rng, signing_input.as_bytes())?; Ok(base64::encode_config(&sig, base64::URL_SAFE_NO_PAD)) } @@ -45,10 +42,12 @@ fn sign_rsa( signing_input: &str, ) -> Result { let key_pair = match key { - Key::Der(bytes) => signature::RsaKeyPair::from_der(untrusted::Input::from(bytes)) - .map_err(|_| ErrorKind::InvalidRsaKey)?, - Key::Pkcs8(bytes) => signature::RsaKeyPair::from_pkcs8(untrusted::Input::from(bytes)) - .map_err(|_| ErrorKind::InvalidRsaKey)?, + Key::Der(bytes) => { + signature::RsaKeyPair::from_der(bytes).map_err(|_| ErrorKind::InvalidRsaKey)? + } + Key::Pkcs8(bytes) => { + signature::RsaKeyPair::from_pkcs8(bytes).map_err(|_| ErrorKind::InvalidRsaKey)? + } _ => { return Err(ErrorKind::InvalidKeyFormat)?; } @@ -69,9 +68,9 @@ fn sign_rsa( /// Only use this function if you want to do something other than JWT. pub fn sign(signing_input: &str, key: Key, algorithm: Algorithm) -> Result { match algorithm { - Algorithm::HS256 => sign_hmac(&digest::SHA256, key, signing_input), - Algorithm::HS384 => sign_hmac(&digest::SHA384, key, signing_input), - Algorithm::HS512 => sign_hmac(&digest::SHA512, key, signing_input), + Algorithm::HS256 => sign_hmac(hmac::HMAC_SHA256, key, signing_input), + Algorithm::HS384 => sign_hmac(hmac::HMAC_SHA384, key, signing_input), + Algorithm::HS512 => sign_hmac(hmac::HMAC_SHA512, key, signing_input), Algorithm::ES256 => { sign_ecdsa(&signature::ECDSA_P256_SHA256_FIXED_SIGNING, key, signing_input) @@ -92,23 +91,20 @@ pub fn sign(signing_input: &str, key: Key, algorithm: Algorithm) -> Result Result { let signature_bytes = base64::decode_config(signature, base64::URL_SAFE_NO_PAD)?; - let public_key_der = untrusted::Input::from(key); - let message = untrusted::Input::from(signing_input.as_bytes()); - let expected_signature = untrusted::Input::from(signature_bytes.as_slice()); - - let res = signature::verify(alg, public_key_der, message, expected_signature); + let public_key = signature::UnparsedPublicKey::new(alg, key); + let res = public_key.verify(signing_input.as_bytes(), &signature_bytes); Ok(res.is_ok()) } fn verify_ring_es( - alg: &dyn signature::VerificationAlgorithm, + alg: &'static dyn signature::VerificationAlgorithm, signature: &str, signing_input: &str, key: Key, @@ -123,7 +119,7 @@ fn verify_ring_es( } fn verify_ring_rsa( - alg: &signature::RsaParameters, + alg: &'static signature::RsaParameters, signature: &str, signing_input: &str, key: Key, @@ -131,24 +127,15 @@ fn verify_ring_rsa( match key { Key::Der(bytes) | Key::Pkcs8(bytes) => verify_ring(alg, signature, signing_input, bytes), Key::ModulusExponent(n, e) => { - let signature_bytes = base64::decode_config(signature, base64::URL_SAFE_NO_PAD)?; - let message = untrusted::Input::from(signing_input.as_bytes()); - let modulus = untrusted::Input::from(n); - let exponent = untrusted::Input::from(e); - let expected_signature = untrusted::Input::from(signature_bytes.as_slice()); + let public_key = signature::RsaPublicKeyComponents { n, e }; - let res = signature::primitive::verify_rsa( - alg, - (modulus, exponent), - message, - expected_signature, - ); + let signature_bytes = base64::decode_config(signature, base64::URL_SAFE_NO_PAD)?; + + let res = public_key.verify(alg, signing_input.as_bytes(), &signature_bytes); Ok(res.is_ok()) } - _ => { - Err(ErrorKind::InvalidKeyFormat)? - } + _ => Err(ErrorKind::InvalidKeyFormat)?, } } diff --git a/src/lib.rs b/src/lib.rs index 2f87d78..5061bd7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,6 @@ extern crate chrono; extern crate ring; extern crate serde; extern crate serde_json; -extern crate untrusted; mod algorithms; mod crypto; From 6e2f461a1bf5b7ba3e4f2b522834d6c326d1622d Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Wed, 23 Oct 2019 13:44:45 +0100 Subject: [PATCH 16/55] Fix readme wrt exp validation Closes #104 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b11ce26..ecdb723 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ let header = decode_header(&token)?; This does not perform any validation on the token. #### Validation -This library validates automatically the `exp` and `nbf` claims if present. You can also validate the `sub`, `iss` and `aud` but +This library validates automatically the `exp` claim. `nbf` is also validated if present. You can also validate the `sub`, `iss` and `aud` but those require setting the expected value in the `Validation` struct. Since validating time fields is always a bit tricky due to clock skew, From 0d7184a787e01746a990a8536d298a913f3e2ff0 Mon Sep 17 00:00:00 2001 From: dowwie Date: Sun, 27 Oct 2019 15:14:52 -0400 Subject: [PATCH 17/55] added update to aud type and aud validation --- CHANGELOG.md | 2 ++ src/validation.rs | 30 ++++++++++++++++++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0af583..eaf78e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ - Add support for verifying with modulus/exponent components for RSA - Change API for both sign/verify to take a `Key` enum rather than bytes - Update to 2018 edition +- Changed aud field type in Validation to Option>. Audience + validation now tests for "any-of-these" audience membership. ## 6.0.1 (2019-05-10) diff --git a/src/validation.rs b/src/validation.rs index 19b3dc4..e8a60f8 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use chrono::Utc; use serde::ser::Serialize; use serde_json::map::Map; @@ -49,7 +50,7 @@ pub struct Validation { /// the [set_audience](struct.Validation.html#method.set_audience) method to set it. /// /// Defaults to `None`. - pub aud: Option, + pub aud: Option>, /// If it contains a value, the validation will check that the `iss` field is the same as the /// one provided and will error otherwise. /// @@ -78,9 +79,29 @@ impl Validation { /// Since `aud` can be either a String or an array of String in the JWT spec, this method will take /// care of serializing the value. pub fn set_audience(&mut self, audience: &T) { - // TODO: check if the value is a string or an array and error if not - self.aud = Some(to_value(audience).unwrap()); + let aud = to_value(audience) + .unwrap_or_else(|_| panic!("Failed to_value within set_audience)")); + let aud = Validation::convert_aud(&aud) + .unwrap_or_else(|_| panic!("Failed convert_aud within set_audience")); + self.aud = Some(aud); } + + /// Converts a Value, representing a String or collection of Strings, to a + /// HashSet, required for audience membership testing + fn convert_aud(aud: &Value) -> Result> { + let aud_from_claim: Vec = match aud.is_array() { + true => from_value(aud.clone()).unwrap(), + false => { + let aud_str: String = match from_value(aud.clone()) { + Ok(val) => val, + Err(_) => return Err(new_error(ErrorKind::InvalidAudience)), + }; + vec![aud_str] + } + }; + + Ok(aud_from_claim.into_iter().collect()) + } } impl Default for Validation { @@ -145,7 +166,8 @@ pub fn validate(claims: &Map, options: &Validation) -> Result<()> if let Some(ref correct_aud) = options.aud { if let Some(aud) = claims.get("aud") { - if aud != correct_aud { + let converted_aud = Validation::convert_aud(aud)?; + if converted_aud.intersection(correct_aud).count() == 0 { return Err(new_error(ErrorKind::InvalidAudience)); } } else { From fe10accb6e5f6295f158b9c59d5954ddf6a582d8 Mon Sep 17 00:00:00 2001 From: dowwie Date: Sun, 27 Oct 2019 15:16:43 -0400 Subject: [PATCH 18/55] markedown edit to changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eaf78e1..bd1df1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - Add support for verifying with modulus/exponent components for RSA - Change API for both sign/verify to take a `Key` enum rather than bytes - Update to 2018 edition -- Changed aud field type in Validation to Option>. Audience +- Changed aud field type in Validation to `Option>`. Audience validation now tests for "any-of-these" audience membership. ## 6.0.1 (2019-05-10) From 68d6c84c8cc3b517ef8aeb8c5ed34d11aa6bc96a Mon Sep 17 00:00:00 2001 From: dowwie Date: Mon, 28 Oct 2019 11:49:02 -0400 Subject: [PATCH 19/55] revised set_audience, cleaned up validation, and cleared compiler warnings --- src/validation.rs | 61 ++++++++++++++--------------------------------- 1 file changed, 18 insertions(+), 43 deletions(-) diff --git a/src/validation.rs b/src/validation.rs index e8a60f8..f6497cc 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -1,11 +1,10 @@ use std::collections::HashSet; use chrono::Utc; -use serde::ser::Serialize; use serde_json::map::Map; -use serde_json::{from_value, to_value, Value}; +use serde_json::{from_value, Value}; -use crate::algorithms::Algorithm; -use crate::errors::{new_error, ErrorKind, Result}; +use crypto::Algorithm; +use errors::{new_error, ErrorKind, Result}; /// Contains the various validations that are applied after decoding a token. /// @@ -22,7 +21,7 @@ use crate::errors::{new_error, ErrorKind, Result}; /// /// // Setting audience /// let mut validation = Validation::default(); -/// validation.set_audience(&"Me"); // string +/// validation.set_audience(&["Me"]); // a single string /// validation.set_audience(&["Me", "You"]); // array of strings /// ``` #[derive(Debug, Clone, PartialEq)] @@ -44,10 +43,8 @@ pub struct Validation { /// /// Defaults to `false`. pub validate_nbf: bool, - /// If it contains a value, the validation will check that the `aud` field is the same as the - /// one provided and will error otherwise. - /// Since `aud` can be either a String or a Vec in the JWT spec, you will need to use - /// the [set_audience](struct.Validation.html#method.set_audience) method to set it. + /// If it contains a value, the validation will check that the `aud` field is a member of the + /// audience provided and will error otherwise. /// /// Defaults to `None`. pub aud: Option>, @@ -76,32 +73,10 @@ impl Validation { validation } - /// Since `aud` can be either a String or an array of String in the JWT spec, this method will take - /// care of serializing the value. - pub fn set_audience(&mut self, audience: &T) { - let aud = to_value(audience) - .unwrap_or_else(|_| panic!("Failed to_value within set_audience)")); - let aud = Validation::convert_aud(&aud) - .unwrap_or_else(|_| panic!("Failed convert_aud within set_audience")); - self.aud = Some(aud); + /// `aud` is a collection of one or more acceptable audience members + pub fn set_audience(&mut self, items: &[T]) { + self.aud = Some(items.iter().map(|x| x.to_string()).collect()) } - - /// Converts a Value, representing a String or collection of Strings, to a - /// HashSet, required for audience membership testing - fn convert_aud(aud: &Value) -> Result> { - let aud_from_claim: Vec = match aud.is_array() { - true => from_value(aud.clone()).unwrap(), - false => { - let aud_str: String = match from_value(aud.clone()) { - Ok(val) => val, - Err(_) => return Err(new_error(ErrorKind::InvalidAudience)), - }; - vec![aud_str] - } - }; - - Ok(aud_from_claim.into_iter().collect()) - } } impl Default for Validation { @@ -166,8 +141,8 @@ pub fn validate(claims: &Map, options: &Validation) -> Result<()> if let Some(ref correct_aud) = options.aud { if let Some(aud) = claims.get("aud") { - let converted_aud = Validation::convert_aud(aud)?; - if converted_aud.intersection(correct_aud).count() == 0 { + let provided_aud: HashSet = from_value(aud.clone())?; + if provided_aud.intersection(correct_aud).count() == 0 { return Err(new_error(ErrorKind::InvalidAudience)); } } else { @@ -186,7 +161,7 @@ mod tests { use super::{validate, Validation}; - use crate::errors::ErrorKind; + use errors::ErrorKind; #[test] fn exp_in_future_ok() { @@ -368,9 +343,9 @@ mod tests { #[test] fn aud_string_ok() { let mut claims = Map::new(); - claims.insert("aud".to_string(), to_value("Everyone").unwrap()); + claims.insert("aud".to_string(), to_value(["Everyone"]).unwrap()); let mut validation = Validation { validate_exp: false, ..Validation::default() }; - validation.set_audience(&"Everyone"); + validation.set_audience(&["Everyone"]); let res = validate(&claims, &validation); assert!(res.is_ok()); } @@ -388,7 +363,7 @@ mod tests { #[test] fn aud_type_mismatch_fails() { let mut claims = Map::new(); - claims.insert("aud".to_string(), to_value("Everyone").unwrap()); + claims.insert("aud".to_string(), to_value(["Everyone"]).unwrap()); let mut validation = Validation { validate_exp: false, ..Validation::default() }; validation.set_audience(&["UserA", "UserB"]); let res = validate(&claims, &validation); @@ -403,9 +378,9 @@ mod tests { #[test] fn aud_correct_type_not_matching_fails() { let mut claims = Map::new(); - claims.insert("aud".to_string(), to_value("Everyone").unwrap()); + claims.insert("aud".to_string(), to_value(["Everyone"]).unwrap()); let mut validation = Validation { validate_exp: false, ..Validation::default() }; - validation.set_audience(&"None"); + validation.set_audience(&["None"]); let res = validate(&claims, &validation); assert!(res.is_err()); @@ -419,7 +394,7 @@ mod tests { fn aud_missing_fails() { let claims = Map::new(); let mut validation = Validation { validate_exp: false, ..Validation::default() }; - validation.set_audience(&"None"); + validation.set_audience(&["None"]); let res = validate(&claims, &validation); assert!(res.is_err()); From 60a030874abe31364ad217d867f071c55e45d608 Mon Sep 17 00:00:00 2001 From: dowwie Date: Thu, 31 Oct 2019 14:12:08 -0400 Subject: [PATCH 20/55] updated imports in validation.rs --- src/validation.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/validation.rs b/src/validation.rs index f6497cc..8c7b2ca 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -3,8 +3,8 @@ use chrono::Utc; use serde_json::map::Map; use serde_json::{from_value, Value}; -use crypto::Algorithm; -use errors::{new_error, ErrorKind, Result}; +use crate::algorithms::Algorithm; +use crate::errors::{new_error, ErrorKind, Result}; /// Contains the various validations that are applied after decoding a token. /// @@ -161,7 +161,7 @@ mod tests { use super::{validate, Validation}; - use errors::ErrorKind; + use crate::errors::ErrorKind; #[test] fn exp_in_future_ok() { From 571898252f9acdf58b5332c45e2afd071e1e4004 Mon Sep 17 00:00:00 2001 From: Levi Date: Sun, 3 Nov 2019 06:22:51 -0600 Subject: [PATCH 21/55] Add PEM decoding support (#106) * Add PEM support with pem and simple_asn1. Documentation TODO * Make pkcs1 and pkcs8 versions of the RSA key, confirm they pass tests. * Add documentation, simplify * Update readme * Bump pem version * Remove extra print --- Cargo.toml | 2 + README.md | 30 ++- src/lib.rs | 30 +++ src/pem_decoder.rs | 198 ++++++++++++++++++ tests/ecdsa.rs | 15 +- tests/private_ecdsa_key.pem | 5 + ..._rsa_key.pem => private_rsa_key_pkcs1.pem} | 0 tests/private_rsa_key_pkcs8.pem | 28 +++ tests/public_ecdsa_key.pem | 4 + tests/public_rsa_key_pkcs1.pem | 8 + ...c_rsa_key.pem => public_rsa_key_pkcs8.pem} | 0 tests/rsa.rs | 32 ++- 12 files changed, 346 insertions(+), 6 deletions(-) create mode 100644 src/pem_decoder.rs create mode 100644 tests/private_ecdsa_key.pem rename tests/{private_rsa_key.pem => private_rsa_key_pkcs1.pem} (100%) create mode 100644 tests/private_rsa_key_pkcs8.pem create mode 100644 tests/public_ecdsa_key.pem create mode 100644 tests/public_rsa_key_pkcs1.pem rename tests/{public_rsa_key.pem => public_rsa_key_pkcs8.pem} (100%) diff --git a/Cargo.toml b/Cargo.toml index c7a8393..94d5a76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,5 @@ serde = "1.0" ring = { version = "0.16.5", features = ["std"] } base64 = "0.10" chrono = "0.4" +pem = "0.7" +simple_asn1 = "0.4.0" diff --git a/README.md b/README.md index ecdb723..2f5359d 100644 --- a/README.md +++ b/README.md @@ -113,8 +113,10 @@ This library currently supports the following: - ES384 ### RSA -`jsonwebtoken` can only read DER encoded keys currently. If you have openssl installed, -you can run the following commands to obtain the DER keys from PKCS#1 (ie with `BEGIN RSA PUBLIC KEY`) .pem. +`jsonwebtoken` can read DER and PEM encoded keys. + +#### DER Encoded +If you have openssl installed, you can run the following commands to obtain the DER keys from PKCS#1 (ie with `BEGIN RSA PUBLIC KEY`) .pem. If you have a PKCS#8 pem file (ie starting with `BEGIN PUBLIC KEY`), you will need to first convert it to PKCS#1: `openssl rsa -pubin -in -RSAPublicKey_out -out `. @@ -127,3 +129,27 @@ $ openssl rsa -in private_rsa_key.der -inform DER -RSAPublicKey_out -outform DER If you are getting an error with your public key, make sure you get it by using the command above to ensure it is in the right format. + +#### PEM Encoded +To generate a PKCS#1 RSA key, run `openssl genrsa -out private_rsa_key_pkcs1.pem 2048` +To convert a PKCS#1 RSA key to a PKCS#8 RSA key, run `openssl pkcs8 -topk8 -inform pem -in private_rsa_key_pkcs1.pem -outform pem -nocrypt -out private_rsa_key_pkcs8.pem` + +To use a PEM encoded private / public keys, a pem struct is returned by `decode_pem`. +This carries the lifetime of the data inside. Finally to use the key like any other +use the `.as_key(alg)` function on the pem struct. +``` +let privkey_pem = decode_pem(pem_string_here).unwrap(); +let privkey = privkey_pem.as_key(Algorithm::RS256).unwrap(); +``` + +### ECDSA +`jsonwebtoken` can read PKCS#8 DER encoded private keys and public keys, as well as PEM encoded keys. Like RSA, to read a PEM key, you must use the pem decoder. + +To generate an EC key, you can do the following. + +```bash +// private key +openssl ecparam -genkey -name prime256v1 | openssl ec -out private_key.pem +// public key +openssl ec -in private_key.pem -pubout -out public_key.pem +``` diff --git a/src/lib.rs b/src/lib.rs index 5061bd7..0243844 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ extern crate chrono; extern crate ring; extern crate serde; extern crate serde_json; +extern crate simple_asn1; mod algorithms; mod crypto; @@ -19,6 +20,7 @@ mod header; mod keys; mod serialization; mod validation; +mod pem_decoder; pub use algorithms::Algorithm; pub use crypto::{sign, verify}; @@ -26,6 +28,7 @@ pub use header::Header; pub use keys::Key; pub use serialization::TokenData; pub use validation::Validation; +pub use pem_decoder::PemEncodedKey; use serde::de::DeserializeOwned; use serde::ser::Serialize; @@ -164,3 +167,30 @@ pub fn decode_header(token: &str) -> Result
{ let (_, header) = expect_two!(signing_input.rsplitn(2, '.')); from_jwt_part(header) } + +/// Decode a PEM string to obtain its key +/// +/// This must be a tagged PEM encoded key, tags start with `-----BEGIN ..-----` +/// and end with a `-----END ..-----` +/// +/// ```rust +/// use jsonwebtoken::{decode_pem, sign, verify, Algorithm}; +/// +/// let pem_content = "-----BEGIN PRIVATE KEY----- +/// MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgWTFfCGljY6aw3Hrt +/// kHmPRiazukxPLb6ilpRAewjW8nihRANCAATDskChT+Altkm9X7MI69T3IUmrQU0L +/// 950IxEzvw/x5BMEINRMrXLBJhqzO9Bm+d6JbqA21YQmd1Kt4RzLJR1W+ +/// -----END PRIVATE KEY-----"; +/// +/// // First use decode_pem from jsonwebtoken +/// let privkey_pem = decode_pem(pem_content).unwrap(); +/// // If it decodes Ok, then you can start using it with a given algorithm +/// let privkey = privkey_pem.as_key().unwrap(); +/// +/// // When using the as_key function, you do not need to wrap in Key::Der or Key::Pkcs8 +/// // The same code can be used for public keys too. +/// let encrypted = sign("hello world", privkey, Algorithm::ES256).unwrap(); +/// ``` +pub fn decode_pem(content: &str) -> Result { + PemEncodedKey::read(content) +} diff --git a/src/pem_decoder.rs b/src/pem_decoder.rs new file mode 100644 index 0000000..32c9b7c --- /dev/null +++ b/src/pem_decoder.rs @@ -0,0 +1,198 @@ +use crate::keys::Key; +use crate::errors::{Result, ErrorKind}; + +extern crate pem; +extern crate simple_asn1; + +use simple_asn1::{OID, BigUint}; + +/// The return type of a successful PEM encoded key with `decode_pem` +/// +/// This struct gives a way to parse a string to a key for use in jsonwebtoken. +/// A struct is necessary as it provides the lifetime of the key +/// +/// PEM public private keys are encoded PKCS#1 or PKCS#8 +/// You will find that with PKCS#8 RSA keys that the PKCS#1 content +/// is embedded inside. This is what is provided to ring via `Key::Der` +/// For EC keys, they are always PKCS#8 on the outside but like RSA keys +/// EC keys contain a section within that ultimately has the configuration +/// that ring uses. +/// Documentation about these formats is at +/// PKCS#1: https://tools.ietf.org/html/rfc8017 +/// PKCS#8: https://tools.ietf.org/html/rfc5958 +pub struct PemEncodedKey { + content: Vec, + asn1: Vec, + pem_type: PemType, + encoded_with: PemEncodedWith, +} + +impl PemEncodedKey { + /// Read the PEM file for later key use + pub fn read(input: &str) -> Result { + match pem::parse(input) { + Ok(content) => { + let pem_contents = content.contents; + let asn1_content = match simple_asn1::from_der(pem_contents.as_slice()) { + Ok(asn1) => asn1, + Err(_) => return Err(ErrorKind::InvalidKeyFormat)?, + }; + + match content.tag.as_ref() { + // This handles a PKCS#1 RSA Private key + "RSA PRIVATE KEY" => Ok(PemEncodedKey { + content: pem_contents, + asn1: asn1_content, + pem_type: PemType::RSAPrivateKey, + encoded_with: PemEncodedWith::PKCS1, + }), + "RSA PUBLIC KEY" => Ok(PemEncodedKey { + content: pem_contents, + asn1: asn1_content, + pem_type: PemType::RSAPublicKey, + encoded_with: PemEncodedWith::PKCS1, + }), + + // No "EC PRIVATE KEY" + // https://security.stackexchange.com/questions/84327/converting-ecc-private-key-to-pkcs1-format + // "there is no such thing as a "PKCS#1 format" for elliptic curve (EC) keys" + + // This handles PKCS#8 private keys + "PRIVATE KEY" => { + match classify_pem(&asn1_content) { + Option::Some(Classification::EC) => Ok(PemEncodedKey { + content: pem_contents, + asn1: asn1_content, + pem_type: PemType::ECPrivateKey, + encoded_with: PemEncodedWith::PKCS8, + }), + Option::Some(Classification::RSA) => Ok(PemEncodedKey { + content: pem_contents, + asn1: asn1_content, + pem_type: PemType::RSAPrivateKey, + encoded_with: PemEncodedWith::PKCS8, + }), + _ => return Err(ErrorKind::InvalidKeyFormat)?, + } + } + + // This handles PKCS#8 public keys + "PUBLIC KEY" => { + match classify_pem(&asn1_content) { + Option::Some(Classification::EC) => Ok(PemEncodedKey { + content: pem_contents, + asn1: asn1_content, + pem_type: PemType::ECPublicKey, + encoded_with: PemEncodedWith::PKCS8, + }), + Option::Some(Classification::RSA) => Ok(PemEncodedKey { + content: pem_contents, + asn1: asn1_content, + pem_type: PemType::RSAPublicKey, + encoded_with: PemEncodedWith::PKCS8, + }), + _ => return Err(ErrorKind::InvalidKeyFormat)?, + } + } + + // Unknown type + _ => return Err(ErrorKind::InvalidKeyFormat)?, + } + }, + Err(_) => return Err(ErrorKind::InvalidKeyFormat)?, + } + } + + /// This will do the initial parsing of a PEM file. + /// Supported tagged pems include "RSA PRIVATE KEY", "RSA PUBLIC KEY", + /// "PRIVATE KEY", "PUBLIC KEY" + /// PEMs with multiple tagged portions are not supported + pub fn as_key(&self) -> Result> { + match self.encoded_with { + PemEncodedWith::PKCS1 => Ok(Key::Der(self.content.as_slice())), + PemEncodedWith::PKCS8 => { + match self.pem_type { + PemType::RSAPrivateKey => Ok(Key::Der(extract_first_bitstring(&self.asn1)?)), + PemType::RSAPublicKey => Ok(Key::Der(extract_first_bitstring(&self.asn1)?)), + PemType::ECPrivateKey => Ok(Key::Pkcs8(self.content.as_slice())), + PemType::ECPublicKey => Ok(Key::Pkcs8(extract_first_bitstring(&self.asn1)?)), + } + }, + } + } +} + +#[derive(Debug)] +#[derive(PartialEq)] +/// Supported PEM files for EC and RSA Public and Private Keys +enum PemType { + ECPublicKey, + ECPrivateKey, + RSAPublicKey, + RSAPrivateKey, +} + +#[derive(Debug)] +#[derive(PartialEq)] +enum PemEncodedWith { + PKCS1, + PKCS8, +} + +#[derive(Debug)] +#[derive(PartialEq)] +enum Classification { + EC, + RSA, +} + +// This really just finds and returns the first bitstring or octet string +// Which is the x coordinate for EC public keys +// And the DER contents of an RSA key +// Though PKCS#11 keys shouldn't have anything else. +// It will get confusing with certificates. +fn extract_first_bitstring(asn1: &Vec) -> Result<&[u8]> { + for asn1_entry in asn1.iter() { + match asn1_entry { + simple_asn1::ASN1Block::Sequence(_, entries) => { + if let Ok(result) = extract_first_bitstring(entries) { + return Ok(result); + } + } + simple_asn1::ASN1Block::BitString(_, _, value) => { + return Ok(value.as_ref()); + } + simple_asn1::ASN1Block::OctetString(_, value) => { + return Ok(value.as_ref()); + } + _ => () + } + } + return Err(ErrorKind::InvalidEcdsaKey)? +} + +fn classify_pem(asn1: &Vec) -> Option { + // These should be constant but the macro requires + // #![feature(const_vec_new)] + let ec_public_key_oid = simple_asn1::oid!(1,2,840,10045,2,1); + let rsa_public_key_oid = simple_asn1::oid!(1,2,840,113549,1,1,1); + + for asn1_entry in asn1.iter() { + match asn1_entry { + simple_asn1::ASN1Block::Sequence(_, entries) => { + if let Some(classification) = classify_pem(entries) { + return Some(classification); + } + } + simple_asn1::ASN1Block::ObjectIdentifier(_, oid) => { + if oid == ec_public_key_oid { + return Option::Some(Classification::EC); + } else if oid == rsa_public_key_oid { + return Option::Some(Classification::RSA); + } + } + _ => {} + } + } + return Option::default(); +} \ No newline at end of file diff --git a/tests/ecdsa.rs b/tests/ecdsa.rs index 12cf78d..e4ffa34 100644 --- a/tests/ecdsa.rs +++ b/tests/ecdsa.rs @@ -4,7 +4,7 @@ extern crate serde_derive; extern crate chrono; use chrono::Utc; -use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Header, Key, Validation}; +use jsonwebtoken::{decode, decode_pem, encode, sign, verify, Algorithm, Header, Key, Validation}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] pub struct Claims { @@ -14,7 +14,7 @@ pub struct Claims { } #[test] -fn round_trip_sign_verification() { +fn round_trip_sign_verification_pk8() { let privkey = include_bytes!("private_ecdsa_key.pk8"); let encrypted = sign("hello world", Key::Pkcs8(&privkey[..]), Algorithm::ES256).unwrap(); let pubkey = include_bytes!("public_ecdsa_key.pk8"); @@ -22,6 +22,17 @@ fn round_trip_sign_verification() { assert!(is_valid); } +#[test] +fn round_trip_sign_verification_pem() { + let privkey_pem = decode_pem(include_str!("private_ecdsa_key.pem")).unwrap(); + let privkey = privkey_pem.as_key().unwrap(); + let encrypted = sign("hello world", privkey, Algorithm::ES256).unwrap(); + let pubkey_pem = decode_pem(include_str!("public_ecdsa_key.pem")).unwrap(); + let pubkey = pubkey_pem.as_key().unwrap(); + let is_valid = verify(&encrypted, "hello world", pubkey, Algorithm::ES256).unwrap(); + assert!(is_valid); +} + #[test] fn round_trip_claim() { let my_claims = Claims { diff --git a/tests/private_ecdsa_key.pem b/tests/private_ecdsa_key.pem new file mode 100644 index 0000000..4613a0d --- /dev/null +++ b/tests/private_ecdsa_key.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgWTFfCGljY6aw3Hrt +kHmPRiazukxPLb6ilpRAewjW8nihRANCAATDskChT+Altkm9X7MI69T3IUmrQU0L +950IxEzvw/x5BMEINRMrXLBJhqzO9Bm+d6JbqA21YQmd1Kt4RzLJR1W+ +-----END PRIVATE KEY----- diff --git a/tests/private_rsa_key.pem b/tests/private_rsa_key_pkcs1.pem similarity index 100% rename from tests/private_rsa_key.pem rename to tests/private_rsa_key_pkcs1.pem diff --git a/tests/private_rsa_key_pkcs8.pem b/tests/private_rsa_key_pkcs8.pem new file mode 100644 index 0000000..2df7451 --- /dev/null +++ b/tests/private_rsa_key_pkcs8.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDJETqse41HRBsc +7cfcq3ak4oZWFCoZlcic525A3FfO4qW9BMtRO/iXiyCCHn8JhiL9y8j5JdVP2Q9Z +IpfElcFd3/guS9w+5RqQGgCR+H56IVUyHZWtTJbKPcwWXQdNUX0rBFcsBzCRESJL +eelOEdHIjG7LRkx5l/FUvlqsyHDVJEQsHwegZ8b8C0fz0EgT2MMEdn10t6Ur1rXz +jMB/wvCg8vG8lvciXmedyo9xJ8oMOh0wUEgxziVDMMovmC+aJctcHUAYubwoGN8T +yzcvnGqL7JSh36Pwy28iPzXZ2RLhAyJFU39vLaHdljwthUaupldlNyCfa6Ofy4qN +ctlUPlN1AgMBAAECggEAdESTQjQ70O8QIp1ZSkCYXeZjuhj081CK7jhhp/4ChK7J +GlFQZMwiBze7d6K84TwAtfQGZhQ7km25E1kOm+3hIDCoKdVSKch/oL54f/BK6sKl +qlIzQEAenho4DuKCm3I4yAw9gEc0DV70DuMTR0LEpYyXcNJY3KNBOTjN5EYQAR9s +2MeurpgK2MdJlIuZaIbzSGd+diiz2E6vkmcufJLtmYUT/k/ddWvEtz+1DnO6bRHh +xuuDMeJA/lGB/EYloSLtdyCF6sII6C6slJJtgfb0bPy7l8VtL5iDyz46IKyzdyzW +tKAn394dm7MYR1RlUBEfqFUyNK7C+pVMVoTwCC2V4QKBgQD64syfiQ2oeUlLYDm4 +CcKSP3RnES02bcTyEDFSuGyyS1jldI4A8GXHJ/lG5EYgiYa1RUivge4lJrlNfjyf +dV230xgKms7+JiXqag1FI+3mqjAgg4mYiNjaao8N8O3/PD59wMPeWYImsWXNyeHS +55rUKiHERtCcvdzKl4u35ZtTqQKBgQDNKnX2bVqOJ4WSqCgHRhOm386ugPHfy+8j +m6cicmUR46ND6ggBB03bCnEG9OtGisxTo/TuYVRu3WP4KjoJs2LD5fwdwJqpgtHl +yVsk45Y1Hfo+7M6lAuR8rzCi6kHHNb0HyBmZjysHWZsn79ZM+sQnLpgaYgQGRbKV +DZWlbw7g7QKBgQCl1u+98UGXAP1jFutwbPsx40IVszP4y5ypCe0gqgon3UiY/G+1 +zTLp79GGe/SjI2VpQ7AlW7TI2A0bXXvDSDi3/5Dfya9ULnFXv9yfvH1QwWToySpW +Kvd1gYSoiX84/WCtjZOr0e0HmLIb0vw0hqZA4szJSqoxQgvF22EfIWaIaQKBgQCf +34+OmMYw8fEvSCPxDxVvOwW2i7pvV14hFEDYIeZKW2W1HWBhVMzBfFB5SE8yaCQy +pRfOzj9aKOCm2FjjiErVNpkQoi6jGtLvScnhZAt/lr2TXTrl8OwVkPrIaN0bG/AS +aUYxmBPCpXu3UjhfQiWqFq/mFyzlqlgvuCc9g95HPQKBgAscKP8mLxdKwOgX8yFW +GcZ0izY/30012ajdHY+/QK5lsMoxTnn0skdS+spLxaS5ZEO4qvPVb8RAoCkWMMal +2pOhmquJQVDPDLuZHdrIiKiDM20dy9sMfHygWcZjQ4WSxf/J7T9canLZIXFhHAZT +3wc9h4G8BBCtWN2TN/LsGZdB +-----END PRIVATE KEY----- diff --git a/tests/public_ecdsa_key.pem b/tests/public_ecdsa_key.pem new file mode 100644 index 0000000..f8b9c8e --- /dev/null +++ b/tests/public_ecdsa_key.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEw7JAoU/gJbZJvV+zCOvU9yFJq0FN +C/edCMRM78P8eQTBCDUTK1ywSYaszvQZvneiW6gNtWEJndSreEcyyUdVvg== +-----END PUBLIC KEY----- diff --git a/tests/public_rsa_key_pkcs1.pem b/tests/public_rsa_key_pkcs1.pem new file mode 100644 index 0000000..99e2a40 --- /dev/null +++ b/tests/public_rsa_key_pkcs1.pem @@ -0,0 +1,8 @@ +-----BEGIN RSA PUBLIC KEY----- +MIIBCgKCAQEAyRE6rHuNR0QbHO3H3Kt2pOKGVhQqGZXInOduQNxXzuKlvQTLUTv4 +l4sggh5/CYYi/cvI+SXVT9kPWSKXxJXBXd/4LkvcPuUakBoAkfh+eiFVMh2VrUyW +yj3MFl0HTVF9KwRXLAcwkREiS3npThHRyIxuy0ZMeZfxVL5arMhw1SRELB8HoGfG +/AtH89BIE9jDBHZ9dLelK9a184zAf8LwoPLxvJb3Il5nncqPcSfKDDodMFBIMc4l +QzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xqi+yUod+j8MtvIj812dkS4QMiRVN/by2h +3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5TdQIDAQAB +-----END RSA PUBLIC KEY----- diff --git a/tests/public_rsa_key.pem b/tests/public_rsa_key_pkcs8.pem similarity index 100% rename from tests/public_rsa_key.pem rename to tests/public_rsa_key_pkcs8.pem diff --git a/tests/rsa.rs b/tests/rsa.rs index 3a32399..82e6cc4 100644 --- a/tests/rsa.rs +++ b/tests/rsa.rs @@ -4,7 +4,7 @@ extern crate serde_derive; extern crate chrono; use chrono::Utc; -use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Header, Key, Validation}; +use jsonwebtoken::{decode, decode_pem, encode, sign, verify, Algorithm, Header, Key, Validation}; const RSA_ALGORITHMS: &[Algorithm] = &[ Algorithm::RS256, @@ -23,7 +23,7 @@ pub struct Claims { } #[test] -fn round_trip_sign_verification() { +fn round_trip_sign_verification_der() { let privkey = include_bytes!("private_rsa_key.der"); for &alg in RSA_ALGORITHMS { let encrypted = sign("hello world", Key::Der(&privkey[..]), alg).unwrap(); @@ -34,6 +34,34 @@ fn round_trip_sign_verification() { } } +#[test] +fn round_trip_sign_verification_pem_pkcs1() { + let privkey_pem = decode_pem(include_str!("private_rsa_key_pkcs1.pem")).unwrap(); + let pubkey_pem = decode_pem(include_str!("public_rsa_key_pkcs1.pem")).unwrap(); + + for &alg in RSA_ALGORITHMS { + let privkey_key = privkey_pem.as_key().unwrap(); + let pubkey_key = pubkey_pem.as_key().unwrap(); + let encrypted = sign("hello world", privkey_key, alg).unwrap(); + let is_valid = verify(&encrypted, "hello world", pubkey_key, alg).unwrap(); + assert!(is_valid); + } +} + +#[test] +fn round_trip_sign_verification_pem_pkcs8() { + let privkey_pem = decode_pem(include_str!("private_rsa_key_pkcs8.pem")).unwrap(); + let pubkey_pem = decode_pem(include_str!("public_rsa_key_pkcs8.pem")).unwrap(); + + for &alg in RSA_ALGORITHMS { + let privkey_key = privkey_pem.as_key().unwrap(); + let pubkey_key = pubkey_pem.as_key().unwrap(); + let encrypted = sign("hello world", privkey_key, alg).unwrap(); + let is_valid = verify(&encrypted, "hello world", pubkey_key, alg).unwrap(); + assert!(is_valid); + } +} + #[test] fn round_trip_claim() { let my_claims = Claims { From b9a3e3086fa9b3f0399b333c9ddf136edd2e4a67 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sun, 3 Nov 2019 12:36:52 +0000 Subject: [PATCH 22/55] Fix Option handling --- src/pem_decoder.rs | 71 ++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/src/pem_decoder.rs b/src/pem_decoder.rs index 32c9b7c..e525eb9 100644 --- a/src/pem_decoder.rs +++ b/src/pem_decoder.rs @@ -6,11 +6,32 @@ extern crate simple_asn1; use simple_asn1::{OID, BigUint}; +/// Supported PEM files for EC and RSA Public and Private Keys +#[derive(Debug, PartialEq)] +enum PemType { + ECPublicKey, + ECPrivateKey, + RSAPublicKey, + RSAPrivateKey, +} + +#[derive(Debug, PartialEq)] +enum PemEncodedWith { + PKCS1, + PKCS8, +} + +#[derive(Debug, PartialEq)] +enum Classification { + EC, + RSA, +} + /// The return type of a successful PEM encoded key with `decode_pem` -/// +/// /// This struct gives a way to parse a string to a key for use in jsonwebtoken. /// A struct is necessary as it provides the lifetime of the key -/// +/// /// PEM public private keys are encoded PKCS#1 or PKCS#8 /// You will find that with PKCS#8 RSA keys that the PKCS#1 content /// is embedded inside. This is what is provided to ring via `Key::Der` @@ -37,7 +58,7 @@ impl PemEncodedKey { Ok(asn1) => asn1, Err(_) => return Err(ErrorKind::InvalidKeyFormat)?, }; - + match content.tag.as_ref() { // This handles a PKCS#1 RSA Private key "RSA PRIVATE KEY" => Ok(PemEncodedKey { @@ -52,7 +73,7 @@ impl PemEncodedKey { pem_type: PemType::RSAPublicKey, encoded_with: PemEncodedWith::PKCS1, }), - + // No "EC PRIVATE KEY" // https://security.stackexchange.com/questions/84327/converting-ecc-private-key-to-pkcs1-format // "there is no such thing as a "PKCS#1 format" for elliptic curve (EC) keys" @@ -60,13 +81,13 @@ impl PemEncodedKey { // This handles PKCS#8 private keys "PRIVATE KEY" => { match classify_pem(&asn1_content) { - Option::Some(Classification::EC) => Ok(PemEncodedKey { + Some(Classification::EC) => Ok(PemEncodedKey { content: pem_contents, asn1: asn1_content, pem_type: PemType::ECPrivateKey, encoded_with: PemEncodedWith::PKCS8, }), - Option::Some(Classification::RSA) => Ok(PemEncodedKey { + Some(Classification::RSA) => Ok(PemEncodedKey { content: pem_contents, asn1: asn1_content, pem_type: PemType::RSAPrivateKey, @@ -79,13 +100,13 @@ impl PemEncodedKey { // This handles PKCS#8 public keys "PUBLIC KEY" => { match classify_pem(&asn1_content) { - Option::Some(Classification::EC) => Ok(PemEncodedKey { + Some(Classification::EC) => Ok(PemEncodedKey { content: pem_contents, asn1: asn1_content, pem_type: PemType::ECPublicKey, encoded_with: PemEncodedWith::PKCS8, }), - Option::Some(Classification::RSA) => Ok(PemEncodedKey { + Some(Classification::RSA) => Ok(PemEncodedKey { content: pem_contents, asn1: asn1_content, pem_type: PemType::RSAPublicKey, @@ -122,34 +143,10 @@ impl PemEncodedKey { } } -#[derive(Debug)] -#[derive(PartialEq)] -/// Supported PEM files for EC and RSA Public and Private Keys -enum PemType { - ECPublicKey, - ECPrivateKey, - RSAPublicKey, - RSAPrivateKey, -} - -#[derive(Debug)] -#[derive(PartialEq)] -enum PemEncodedWith { - PKCS1, - PKCS8, -} - -#[derive(Debug)] -#[derive(PartialEq)] -enum Classification { - EC, - RSA, -} - // This really just finds and returns the first bitstring or octet string // Which is the x coordinate for EC public keys // And the DER contents of an RSA key -// Though PKCS#11 keys shouldn't have anything else. +// Though PKCS#11 keys shouldn't have anything else. // It will get confusing with certificates. fn extract_first_bitstring(asn1: &Vec) -> Result<&[u8]> { for asn1_entry in asn1.iter() { @@ -186,13 +183,13 @@ fn classify_pem(asn1: &Vec) -> Option { } simple_asn1::ASN1Block::ObjectIdentifier(_, oid) => { if oid == ec_public_key_oid { - return Option::Some(Classification::EC); + return Some(Classification::EC); } else if oid == rsa_public_key_oid { - return Option::Some(Classification::RSA); + return Some(Classification::RSA); } } _ => {} } } - return Option::default(); -} \ No newline at end of file + None +} From 06bebeaae3e621601d140109b7aabcc537f56ba4 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sun, 3 Nov 2019 12:55:36 +0000 Subject: [PATCH 23/55] cargo fmt --- src/lib.rs | 12 +-- src/pem_decoder.rs | 257 ++++++++++++++++++++++----------------------- src/validation.rs | 6 +- 3 files changed, 135 insertions(+), 140 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0243844..db07517 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,17 +18,17 @@ mod crypto; pub mod errors; mod header; mod keys; +mod pem_decoder; mod serialization; mod validation; -mod pem_decoder; pub use algorithms::Algorithm; pub use crypto::{sign, verify}; pub use header::Header; pub use keys::Key; +pub use pem_decoder::PemEncodedKey; pub use serialization::TokenData; pub use validation::Validation; -pub use pem_decoder::PemEncodedKey; use serde::de::DeserializeOwned; use serde::ser::Serialize; @@ -172,21 +172,21 @@ pub fn decode_header(token: &str) -> Result
{ /// /// This must be a tagged PEM encoded key, tags start with `-----BEGIN ..-----` /// and end with a `-----END ..-----` -/// +/// /// ```rust /// use jsonwebtoken::{decode_pem, sign, verify, Algorithm}; -/// +/// /// let pem_content = "-----BEGIN PRIVATE KEY----- /// MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgWTFfCGljY6aw3Hrt /// kHmPRiazukxPLb6ilpRAewjW8nihRANCAATDskChT+Altkm9X7MI69T3IUmrQU0L /// 950IxEzvw/x5BMEINRMrXLBJhqzO9Bm+d6JbqA21YQmd1Kt4RzLJR1W+ /// -----END PRIVATE KEY-----"; -/// +/// /// // First use decode_pem from jsonwebtoken /// let privkey_pem = decode_pem(pem_content).unwrap(); /// // If it decodes Ok, then you can start using it with a given algorithm /// let privkey = privkey_pem.as_key().unwrap(); -/// +/// /// // When using the as_key function, you do not need to wrap in Key::Der or Key::Pkcs8 /// // The same code can be used for public keys too. /// let encrypted = sign("hello world", privkey, Algorithm::ES256).unwrap(); diff --git a/src/pem_decoder.rs b/src/pem_decoder.rs index e525eb9..3ccaa6d 100644 --- a/src/pem_decoder.rs +++ b/src/pem_decoder.rs @@ -1,30 +1,30 @@ +use crate::errors::{ErrorKind, Result}; use crate::keys::Key; -use crate::errors::{Result, ErrorKind}; extern crate pem; extern crate simple_asn1; -use simple_asn1::{OID, BigUint}; +use simple_asn1::{BigUint, OID}; /// Supported PEM files for EC and RSA Public and Private Keys #[derive(Debug, PartialEq)] enum PemType { - ECPublicKey, - ECPrivateKey, - RSAPublicKey, - RSAPrivateKey, + ECPublicKey, + ECPrivateKey, + RSAPublicKey, + RSAPrivateKey, } #[derive(Debug, PartialEq)] enum PemEncodedWith { - PKCS1, - PKCS8, + PKCS1, + PKCS8, } #[derive(Debug, PartialEq)] enum Classification { - EC, - RSA, + EC, + RSA, } /// The return type of a successful PEM encoded key with `decode_pem` @@ -41,106 +41,101 @@ enum Classification { /// Documentation about these formats is at /// PKCS#1: https://tools.ietf.org/html/rfc8017 /// PKCS#8: https://tools.ietf.org/html/rfc5958 +#[derive(Debug)] pub struct PemEncodedKey { - content: Vec, - asn1: Vec, - pem_type: PemType, - encoded_with: PemEncodedWith, + content: Vec, + asn1: Vec, + pem_type: PemType, + encoded_with: PemEncodedWith, } impl PemEncodedKey { - /// Read the PEM file for later key use - pub fn read(input: &str) -> Result { - match pem::parse(input) { - Ok(content) => { - let pem_contents = content.contents; - let asn1_content = match simple_asn1::from_der(pem_contents.as_slice()) { - Ok(asn1) => asn1, - Err(_) => return Err(ErrorKind::InvalidKeyFormat)?, - }; + /// Read the PEM file for later key use + pub fn read(input: &str) -> Result { + match pem::parse(input) { + Ok(content) => { + let pem_contents = content.contents; + let asn1_content = match simple_asn1::from_der(pem_contents.as_slice()) { + Ok(asn1) => asn1, + Err(_) => return Err(ErrorKind::InvalidKeyFormat)?, + }; - match content.tag.as_ref() { - // This handles a PKCS#1 RSA Private key - "RSA PRIVATE KEY" => Ok(PemEncodedKey { - content: pem_contents, - asn1: asn1_content, - pem_type: PemType::RSAPrivateKey, - encoded_with: PemEncodedWith::PKCS1, - }), - "RSA PUBLIC KEY" => Ok(PemEncodedKey { - content: pem_contents, - asn1: asn1_content, - pem_type: PemType::RSAPublicKey, - encoded_with: PemEncodedWith::PKCS1, - }), + match content.tag.as_ref() { + // This handles a PKCS#1 RSA Private key + "RSA PRIVATE KEY" => Ok(PemEncodedKey { + content: pem_contents, + asn1: asn1_content, + pem_type: PemType::RSAPrivateKey, + encoded_with: PemEncodedWith::PKCS1, + }), + "RSA PUBLIC KEY" => Ok(PemEncodedKey { + content: pem_contents, + asn1: asn1_content, + pem_type: PemType::RSAPublicKey, + encoded_with: PemEncodedWith::PKCS1, + }), - // No "EC PRIVATE KEY" - // https://security.stackexchange.com/questions/84327/converting-ecc-private-key-to-pkcs1-format - // "there is no such thing as a "PKCS#1 format" for elliptic curve (EC) keys" + // No "EC PRIVATE KEY" + // https://security.stackexchange.com/questions/84327/converting-ecc-private-key-to-pkcs1-format + // "there is no such thing as a "PKCS#1 format" for elliptic curve (EC) keys" - // This handles PKCS#8 private keys - "PRIVATE KEY" => { - match classify_pem(&asn1_content) { - Some(Classification::EC) => Ok(PemEncodedKey { - content: pem_contents, - asn1: asn1_content, - pem_type: PemType::ECPrivateKey, - encoded_with: PemEncodedWith::PKCS8, - }), - Some(Classification::RSA) => Ok(PemEncodedKey { - content: pem_contents, - asn1: asn1_content, - pem_type: PemType::RSAPrivateKey, - encoded_with: PemEncodedWith::PKCS8, - }), - _ => return Err(ErrorKind::InvalidKeyFormat)?, + // This handles PKCS#8 private keys + "PRIVATE KEY" => match classify_pem(&asn1_content) { + Some(Classification::EC) => Ok(PemEncodedKey { + content: pem_contents, + asn1: asn1_content, + pem_type: PemType::ECPrivateKey, + encoded_with: PemEncodedWith::PKCS8, + }), + Some(Classification::RSA) => Ok(PemEncodedKey { + content: pem_contents, + asn1: asn1_content, + pem_type: PemType::RSAPrivateKey, + encoded_with: PemEncodedWith::PKCS8, + }), + _ => return Err(ErrorKind::InvalidKeyFormat)?, + }, + + // This handles PKCS#8 public keys + "PUBLIC KEY" => match classify_pem(&asn1_content) { + Some(Classification::EC) => Ok(PemEncodedKey { + content: pem_contents, + asn1: asn1_content, + pem_type: PemType::ECPublicKey, + encoded_with: PemEncodedWith::PKCS8, + }), + Some(Classification::RSA) => Ok(PemEncodedKey { + content: pem_contents, + asn1: asn1_content, + pem_type: PemType::RSAPublicKey, + encoded_with: PemEncodedWith::PKCS8, + }), + _ => return Err(ErrorKind::InvalidKeyFormat)?, + }, + + // Unknown type + _ => return Err(ErrorKind::InvalidKeyFormat)?, + } } - } - - // This handles PKCS#8 public keys - "PUBLIC KEY" => { - match classify_pem(&asn1_content) { - Some(Classification::EC) => Ok(PemEncodedKey { - content: pem_contents, - asn1: asn1_content, - pem_type: PemType::ECPublicKey, - encoded_with: PemEncodedWith::PKCS8, - }), - Some(Classification::RSA) => Ok(PemEncodedKey { - content: pem_contents, - asn1: asn1_content, - pem_type: PemType::RSAPublicKey, - encoded_with: PemEncodedWith::PKCS8, - }), - _ => return Err(ErrorKind::InvalidKeyFormat)?, - } - } - - // Unknown type - _ => return Err(ErrorKind::InvalidKeyFormat)?, + Err(_) => return Err(ErrorKind::InvalidKeyFormat)?, } - }, - Err(_) => return Err(ErrorKind::InvalidKeyFormat)?, } - } - /// This will do the initial parsing of a PEM file. - /// Supported tagged pems include "RSA PRIVATE KEY", "RSA PUBLIC KEY", - /// "PRIVATE KEY", "PUBLIC KEY" - /// PEMs with multiple tagged portions are not supported - pub fn as_key(&self) -> Result> { - match self.encoded_with { - PemEncodedWith::PKCS1 => Ok(Key::Der(self.content.as_slice())), - PemEncodedWith::PKCS8 => { - match self.pem_type { - PemType::RSAPrivateKey => Ok(Key::Der(extract_first_bitstring(&self.asn1)?)), - PemType::RSAPublicKey => Ok(Key::Der(extract_first_bitstring(&self.asn1)?)), - PemType::ECPrivateKey => Ok(Key::Pkcs8(self.content.as_slice())), - PemType::ECPublicKey => Ok(Key::Pkcs8(extract_first_bitstring(&self.asn1)?)), + /// This will do the initial parsing of a PEM file. + /// Supported tagged pems include "RSA PRIVATE KEY", "RSA PUBLIC KEY", + /// "PRIVATE KEY", "PUBLIC KEY" + /// PEMs with multiple tagged portions are not supported + pub fn as_key(&self) -> Result> { + match self.encoded_with { + PemEncodedWith::PKCS1 => Ok(Key::Der(self.content.as_slice())), + PemEncodedWith::PKCS8 => match self.pem_type { + PemType::RSAPrivateKey => Ok(Key::Der(extract_first_bitstring(&self.asn1)?)), + PemType::RSAPublicKey => Ok(Key::Der(extract_first_bitstring(&self.asn1)?)), + PemType::ECPrivateKey => Ok(Key::Pkcs8(self.content.as_slice())), + PemType::ECPublicKey => Ok(Key::Pkcs8(extract_first_bitstring(&self.asn1)?)), + }, } - }, } - } } // This really just finds and returns the first bitstring or octet string @@ -149,47 +144,47 @@ impl PemEncodedKey { // Though PKCS#11 keys shouldn't have anything else. // It will get confusing with certificates. fn extract_first_bitstring(asn1: &Vec) -> Result<&[u8]> { - for asn1_entry in asn1.iter() { - match asn1_entry { - simple_asn1::ASN1Block::Sequence(_, entries) => { - if let Ok(result) = extract_first_bitstring(entries) { - return Ok(result); + for asn1_entry in asn1.iter() { + match asn1_entry { + simple_asn1::ASN1Block::Sequence(_, entries) => { + if let Ok(result) = extract_first_bitstring(entries) { + return Ok(result); + } + } + simple_asn1::ASN1Block::BitString(_, _, value) => { + return Ok(value.as_ref()); + } + simple_asn1::ASN1Block::OctetString(_, value) => { + return Ok(value.as_ref()); + } + _ => (), } - } - simple_asn1::ASN1Block::BitString(_, _, value) => { - return Ok(value.as_ref()); - } - simple_asn1::ASN1Block::OctetString(_, value) => { - return Ok(value.as_ref()); - } - _ => () } - } - return Err(ErrorKind::InvalidEcdsaKey)? + return Err(ErrorKind::InvalidEcdsaKey)?; } fn classify_pem(asn1: &Vec) -> Option { - // These should be constant but the macro requires - // #![feature(const_vec_new)] - let ec_public_key_oid = simple_asn1::oid!(1,2,840,10045,2,1); - let rsa_public_key_oid = simple_asn1::oid!(1,2,840,113549,1,1,1); + // These should be constant but the macro requires + // #![feature(const_vec_new)] + let ec_public_key_oid = simple_asn1::oid!(1, 2, 840, 10045, 2, 1); + let rsa_public_key_oid = simple_asn1::oid!(1, 2, 840, 113549, 1, 1, 1); - for asn1_entry in asn1.iter() { - match asn1_entry { - simple_asn1::ASN1Block::Sequence(_, entries) => { - if let Some(classification) = classify_pem(entries) { - return Some(classification); + for asn1_entry in asn1.iter() { + match asn1_entry { + simple_asn1::ASN1Block::Sequence(_, entries) => { + if let Some(classification) = classify_pem(entries) { + return Some(classification); + } + } + simple_asn1::ASN1Block::ObjectIdentifier(_, oid) => { + if oid == ec_public_key_oid { + return Some(Classification::EC); + } else if oid == rsa_public_key_oid { + return Some(Classification::RSA); + } + } + _ => {} } - } - simple_asn1::ASN1Block::ObjectIdentifier(_, oid) => { - if oid == ec_public_key_oid { - return Some(Classification::EC); - } else if oid == rsa_public_key_oid { - return Some(Classification::RSA); - } - } - _ => {} } - } None } diff --git a/src/validation.rs b/src/validation.rs index 8c7b2ca..463249e 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -1,7 +1,7 @@ -use std::collections::HashSet; use chrono::Utc; use serde_json::map::Map; use serde_json::{from_value, Value}; +use std::collections::HashSet; use crate::algorithms::Algorithm; use crate::errors::{new_error, ErrorKind, Result}; @@ -141,8 +141,8 @@ pub fn validate(claims: &Map, options: &Validation) -> Result<()> if let Some(ref correct_aud) = options.aud { if let Some(aud) = claims.get("aud") { - let provided_aud: HashSet = from_value(aud.clone())?; - if provided_aud.intersection(correct_aud).count() == 0 { + let provided_aud: HashSet = from_value(aud.clone())?; + if provided_aud.intersection(correct_aud).count() == 0 { return Err(new_error(ErrorKind::InvalidAudience)); } } else { From caef740ad419bc575f6558ca61f7e354141ed5f1 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sun, 3 Nov 2019 13:17:04 +0000 Subject: [PATCH 24/55] Refactoring + more idiomatic enum names --- src/keys.rs | 1 + src/pem_decoder.rs | 114 ++++++++++++++++++++++----------------------- 2 files changed, 57 insertions(+), 58 deletions(-) diff --git a/src/keys.rs b/src/keys.rs index cbabfa4..0d2e6c4 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -1,5 +1,6 @@ /// The supported RSA key formats, see the documentation for ring::signature::RsaKeyPair /// for more information +#[derive(Debug, PartialEq)] pub enum Key<'a> { /// An unencrypted PKCS#8-encoded key. Can be used with both ECDSA and RSA /// algorithms when signing. See ring for information. diff --git a/src/pem_decoder.rs b/src/pem_decoder.rs index 3ccaa6d..0f08440 100644 --- a/src/pem_decoder.rs +++ b/src/pem_decoder.rs @@ -9,22 +9,22 @@ use simple_asn1::{BigUint, OID}; /// Supported PEM files for EC and RSA Public and Private Keys #[derive(Debug, PartialEq)] enum PemType { - ECPublicKey, - ECPrivateKey, - RSAPublicKey, - RSAPrivateKey, + EcPublicKey, + EcPrivateKey, + RsaPublicKey, + RsaPrivateKey, } #[derive(Debug, PartialEq)] -enum PemEncodedWith { - PKCS1, - PKCS8, +enum Standard { + Pkcs1, + Pkcs8, } #[derive(Debug, PartialEq)] enum Classification { - EC, - RSA, + Ec, + Rsa, } /// The return type of a successful PEM encoded key with `decode_pem` @@ -46,7 +46,7 @@ pub struct PemEncodedKey { content: Vec, asn1: Vec, pem_type: PemType, - encoded_with: PemEncodedWith, + standard: Standard, } impl PemEncodedKey { @@ -65,55 +65,51 @@ impl PemEncodedKey { "RSA PRIVATE KEY" => Ok(PemEncodedKey { content: pem_contents, asn1: asn1_content, - pem_type: PemType::RSAPrivateKey, - encoded_with: PemEncodedWith::PKCS1, + pem_type: PemType::RsaPrivateKey, + standard: Standard::Pkcs1, }), "RSA PUBLIC KEY" => Ok(PemEncodedKey { content: pem_contents, asn1: asn1_content, - pem_type: PemType::RSAPublicKey, - encoded_with: PemEncodedWith::PKCS1, + pem_type: PemType::RsaPublicKey, + standard: Standard::Pkcs1, }), // No "EC PRIVATE KEY" // https://security.stackexchange.com/questions/84327/converting-ecc-private-key-to-pkcs1-format // "there is no such thing as a "PKCS#1 format" for elliptic curve (EC) keys" - // This handles PKCS#8 private keys - "PRIVATE KEY" => match classify_pem(&asn1_content) { - Some(Classification::EC) => Ok(PemEncodedKey { - content: pem_contents, - asn1: asn1_content, - pem_type: PemType::ECPrivateKey, - encoded_with: PemEncodedWith::PKCS8, - }), - Some(Classification::RSA) => Ok(PemEncodedKey { - content: pem_contents, - asn1: asn1_content, - pem_type: PemType::RSAPrivateKey, - encoded_with: PemEncodedWith::PKCS8, - }), - _ => return Err(ErrorKind::InvalidKeyFormat)?, + // This handles PKCS#8 public & private keys + tag @ "PRIVATE KEY" | tag @ "PUBLIC KEY" => match classify_pem(&asn1_content) { + Some(c) => { + let is_private = tag == "PRIVATE KEY"; + let pem_type = match c { + Classification::Ec => { + if is_private { + PemType::EcPrivateKey + } else { + PemType::EcPublicKey + } + } + Classification::Rsa => { + if is_private { + PemType::RsaPrivateKey + } else { + PemType::RsaPublicKey + } + } + }; + Ok(PemEncodedKey { + content: pem_contents, + asn1: asn1_content, + pem_type, + standard: Standard::Pkcs8, + }) + } + None => return Err(ErrorKind::InvalidKeyFormat)?, }, - // This handles PKCS#8 public keys - "PUBLIC KEY" => match classify_pem(&asn1_content) { - Some(Classification::EC) => Ok(PemEncodedKey { - content: pem_contents, - asn1: asn1_content, - pem_type: PemType::ECPublicKey, - encoded_with: PemEncodedWith::PKCS8, - }), - Some(Classification::RSA) => Ok(PemEncodedKey { - content: pem_contents, - asn1: asn1_content, - pem_type: PemType::RSAPublicKey, - encoded_with: PemEncodedWith::PKCS8, - }), - _ => return Err(ErrorKind::InvalidKeyFormat)?, - }, - - // Unknown type + // Unknown/unsupported type _ => return Err(ErrorKind::InvalidKeyFormat)?, } } @@ -126,13 +122,13 @@ impl PemEncodedKey { /// "PRIVATE KEY", "PUBLIC KEY" /// PEMs with multiple tagged portions are not supported pub fn as_key(&self) -> Result> { - match self.encoded_with { - PemEncodedWith::PKCS1 => Ok(Key::Der(self.content.as_slice())), - PemEncodedWith::PKCS8 => match self.pem_type { - PemType::RSAPrivateKey => Ok(Key::Der(extract_first_bitstring(&self.asn1)?)), - PemType::RSAPublicKey => Ok(Key::Der(extract_first_bitstring(&self.asn1)?)), - PemType::ECPrivateKey => Ok(Key::Pkcs8(self.content.as_slice())), - PemType::ECPublicKey => Ok(Key::Pkcs8(extract_first_bitstring(&self.asn1)?)), + match self.standard { + Standard::Pkcs1 => Ok(Key::Der(self.content.as_slice())), + Standard::Pkcs8 => match self.pem_type { + PemType::RsaPrivateKey => Ok(Key::Der(extract_first_bitstring(&self.asn1)?)), + PemType::RsaPublicKey => Ok(Key::Der(extract_first_bitstring(&self.asn1)?)), + PemType::EcPrivateKey => Ok(Key::Pkcs8(self.content.as_slice())), + PemType::EcPublicKey => Ok(Key::Pkcs8(extract_first_bitstring(&self.asn1)?)), }, } } @@ -160,9 +156,10 @@ fn extract_first_bitstring(asn1: &Vec) -> Result<&[u8]> _ => (), } } - return Err(ErrorKind::InvalidEcdsaKey)?; + Err(ErrorKind::InvalidEcdsaKey)? } +/// Find whether this is EC or RSA fn classify_pem(asn1: &Vec) -> Option { // These should be constant but the macro requires // #![feature(const_vec_new)] @@ -178,9 +175,10 @@ fn classify_pem(asn1: &Vec) -> Option { } simple_asn1::ASN1Block::ObjectIdentifier(_, oid) => { if oid == ec_public_key_oid { - return Some(Classification::EC); - } else if oid == rsa_public_key_oid { - return Some(Classification::RSA); + return Some(Classification::Ec); + } + if oid == rsa_public_key_oid { + return Some(Classification::Rsa); } } _ => {} From 210e96063d69a8c269465c473178d8b56fcf9ae3 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sun, 3 Nov 2019 15:36:19 +0000 Subject: [PATCH 25/55] Reorganise tests --- CHANGELOG.md | 1 + Cargo.toml | 6 +- src/algorithms.rs | 19 +++ src/validation.rs | 24 +++ tests/{ecdsa.rs => ec/mod.rs} | 13 +- tests/{ => ec}/private_ecdsa_key.pem | 0 tests/{ => ec}/private_ecdsa_key.pk8 | Bin tests/{ => ec}/public_ecdsa_key.pem | 0 tests/{ => ec}/public_ecdsa_key.pk8 | 0 tests/hmac.rs | 134 ++++++++++++++++ tests/lib.rs | 177 +--------------------- tests/{rsa.rs => rsa/mod.rs} | 13 +- tests/{ => rsa}/private_rsa_key.der | Bin tests/{ => rsa}/private_rsa_key_pkcs1.pem | 0 tests/{ => rsa}/private_rsa_key_pkcs8.pem | 0 tests/{ => rsa}/public_rsa_key.der | Bin tests/{ => rsa}/public_rsa_key_pkcs1.pem | 0 tests/{ => rsa}/public_rsa_key_pkcs8.pem | 0 18 files changed, 193 insertions(+), 194 deletions(-) rename tests/{ecdsa.rs => ec/mod.rs} (83%) rename tests/{ => ec}/private_ecdsa_key.pem (100%) rename tests/{ => ec}/private_ecdsa_key.pk8 (100%) rename tests/{ => ec}/public_ecdsa_key.pem (100%) rename tests/{ => ec}/public_ecdsa_key.pk8 (100%) create mode 100644 tests/hmac.rs rename tests/{rsa.rs => rsa/mod.rs} (94%) rename tests/{ => rsa}/private_rsa_key.der (100%) rename tests/{ => rsa}/private_rsa_key_pkcs1.pem (100%) rename tests/{ => rsa}/private_rsa_key_pkcs8.pem (100%) rename tests/{ => rsa}/public_rsa_key.der (100%) rename tests/{ => rsa}/public_rsa_key_pkcs1.pem (100%) rename tests/{ => rsa}/public_rsa_key_pkcs8.pem (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd1df1f..a2aefd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Update to 2018 edition - Changed aud field type in Validation to `Option>`. Audience validation now tests for "any-of-these" audience membership. +- Add support for keys in PEM format ## 6.0.1 (2019-05-10) diff --git a/Cargo.toml b/Cargo.toml index 94d5a76..b294bd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,9 @@ serde_json = "1.0" serde_derive = "1.0" serde = "1.0" ring = { version = "0.16.5", features = ["std"] } -base64 = "0.10" +base64 = "0.11" +# For validation chrono = "0.4" +# For PEM decoding pem = "0.7" -simple_asn1 = "0.4.0" +simple_asn1 = "0.4" diff --git a/src/algorithms.rs b/src/algorithms.rs index 34f8556..6b05356 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -56,3 +56,22 @@ impl FromStr for Algorithm { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generate_algorithm_enum_from_str() { + assert!(Algorithm::from_str("HS256").is_ok()); + assert!(Algorithm::from_str("HS384").is_ok()); + assert!(Algorithm::from_str("HS512").is_ok()); + assert!(Algorithm::from_str("RS256").is_ok()); + assert!(Algorithm::from_str("RS384").is_ok()); + assert!(Algorithm::from_str("RS512").is_ok()); + assert!(Algorithm::from_str("PS256").is_ok()); + assert!(Algorithm::from_str("PS384").is_ok()); + assert!(Algorithm::from_str("PS512").is_ok()); + assert!(Algorithm::from_str("").is_err()); + } +} diff --git a/src/validation.rs b/src/validation.rs index 463249e..280fd72 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -403,4 +403,28 @@ mod tests { _ => assert!(false), }; } + + // https://github.com/Keats/jsonwebtoken/issues/51 + #[test] + fn does_validation_in_right_order() { + let mut claims = Map::new(); + claims.insert("exp".to_string(), to_value(Utc::now().timestamp() + 10000).unwrap()); + let v = Validation { + leeway: 5, + validate_exp: true, + iss: Some("iss no check".to_string()), + sub: Some("sub no check".to_string()), + ..Validation::default() + }; + let res = validate(&claims, &v); + // It errors because it needs to validate iss/sub which are missing + assert!(res.is_err()); + match res.unwrap_err().kind() { + &ErrorKind::InvalidIssuer => (), + t @ _ => { + println!("{:?}", t); + assert!(false) + } + }; + } } diff --git a/tests/ecdsa.rs b/tests/ec/mod.rs similarity index 83% rename from tests/ecdsa.rs rename to tests/ec/mod.rs index e4ffa34..8a15ecc 100644 --- a/tests/ecdsa.rs +++ b/tests/ec/mod.rs @@ -1,10 +1,6 @@ -extern crate jsonwebtoken; -#[macro_use] -extern crate serde_derive; -extern crate chrono; - use chrono::Utc; use jsonwebtoken::{decode, decode_pem, encode, sign, verify, Algorithm, Header, Key, Validation}; +use serde_derive::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] pub struct Claims { @@ -49,10 +45,3 @@ fn round_trip_claim() { assert_eq!(my_claims, token_data.claims); assert!(token_data.header.kid.is_none()); } - -#[test] -#[should_panic(expected = "InvalidKeyFormat")] -fn fails_with_non_pkcs8_key_format() { - let privkey = include_bytes!("private_rsa_key.der"); - let _encrypted = sign("hello world", Key::Der(&privkey[..]), Algorithm::ES256).unwrap(); -} diff --git a/tests/private_ecdsa_key.pem b/tests/ec/private_ecdsa_key.pem similarity index 100% rename from tests/private_ecdsa_key.pem rename to tests/ec/private_ecdsa_key.pem diff --git a/tests/private_ecdsa_key.pk8 b/tests/ec/private_ecdsa_key.pk8 similarity index 100% rename from tests/private_ecdsa_key.pk8 rename to tests/ec/private_ecdsa_key.pk8 diff --git a/tests/public_ecdsa_key.pem b/tests/ec/public_ecdsa_key.pem similarity index 100% rename from tests/public_ecdsa_key.pem rename to tests/ec/public_ecdsa_key.pem diff --git a/tests/public_ecdsa_key.pk8 b/tests/ec/public_ecdsa_key.pk8 similarity index 100% rename from tests/public_ecdsa_key.pk8 rename to tests/ec/public_ecdsa_key.pk8 diff --git a/tests/hmac.rs b/tests/hmac.rs new file mode 100644 index 0000000..3c3f284 --- /dev/null +++ b/tests/hmac.rs @@ -0,0 +1,134 @@ +use chrono::Utc; +use jsonwebtoken::{ + dangerous_unsafe_decode, decode, decode_header, encode, sign, verify, Algorithm, Header, Key, + Validation, +}; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct Claims { + sub: String, + company: String, + exp: i64, +} + +#[test] +fn sign_hs256() { + let result = sign("hello world", Key::Hmac(b"secret"), Algorithm::HS256).unwrap(); + let expected = "c0zGLzKEFWj0VxWuufTXiRMk5tlI5MbGDAYhzaxIYjo"; + assert_eq!(result, expected); +} + +#[test] +fn verify_hs256() { + let sig = "c0zGLzKEFWj0VxWuufTXiRMk5tlI5MbGDAYhzaxIYjo"; + let valid = verify(sig, "hello world", Key::Hmac(b"secret"), Algorithm::HS256).unwrap(); + assert!(valid); +} + +#[test] +fn encode_with_custom_header() { + let my_claims = Claims { + sub: "b@b.com".to_string(), + company: "ACME".to_string(), + exp: Utc::now().timestamp() + 10000, + }; + let mut header = Header::default(); + header.kid = Some("kid".to_string()); + let token = encode(&header, &my_claims, Key::Hmac(b"secret")).unwrap(); + let token_data = + decode::(&token, Key::Hmac(b"secret"), &Validation::default()).unwrap(); + assert_eq!(my_claims, token_data.claims); + assert_eq!("kid", token_data.header.kid.unwrap()); +} + +#[test] +fn round_trip_claim() { + let my_claims = Claims { + sub: "b@b.com".to_string(), + company: "ACME".to_string(), + exp: Utc::now().timestamp() + 10000, + }; + let token = encode(&Header::default(), &my_claims, Key::Hmac(b"secret")).unwrap(); + let token_data = + decode::(&token, Key::Hmac(b"secret"), &Validation::default()).unwrap(); + assert_eq!(my_claims, token_data.claims); + assert!(token_data.header.kid.is_none()); +} + +#[test] +fn decode_token() { + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.9r56oF7ZliOBlOAyiOFperTGxBtPykRQiWNFxhDCW98"; + let claims = decode::(token, Key::Hmac(b"secret"), &Validation::default()); + println!("{:?}", claims); + claims.unwrap(); +} + +#[test] +#[should_panic(expected = "InvalidToken")] +fn decode_token_missing_parts() { + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; + let claims = decode::(token, Key::Hmac(b"secret"), &Validation::default()); + claims.unwrap(); +} + +#[test] +#[should_panic(expected = "InvalidSignature")] +fn decode_token_invalid_signature() { + let token = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.wrong"; + let claims = decode::(token, Key::Hmac(b"secret"), &Validation::default()); + claims.unwrap(); +} + +#[test] +#[should_panic(expected = "InvalidAlgorithm")] +fn decode_token_wrong_algorithm() { + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.I1BvFoHe94AFf09O6tDbcSB8-jp8w6xZqmyHIwPeSdY"; + let claims = decode::(token, Key::Hmac(b"secret"), &Validation::new(Algorithm::RS512)); + claims.unwrap(); +} + +#[test] +fn decode_token_with_bytes_secret() { + let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.Hm0yvKH25TavFPz7J_coST9lZFYH1hQo0tvhvImmaks"; + let claims = decode::(token, Key::Hmac(b"\x01\x02\x03"), &Validation::default()); + assert!(claims.is_ok()); +} + +#[test] +fn decode_header_only() { + let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjb21wYW55IjoiMTIzNDU2Nzg5MCIsInN1YiI6IkpvaG4gRG9lIn0.S"; + let header = decode_header(token).unwrap(); + assert_eq!(header.alg, Algorithm::HS256); + assert_eq!(header.typ, Some("JWT".to_string())); +} + +#[test] +fn dangerous_unsafe_decode_token() { + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.9r56oF7ZliOBlOAyiOFperTGxBtPykRQiWNFxhDCW98"; + let claims = dangerous_unsafe_decode::(token); + claims.unwrap(); +} + +#[test] +#[should_panic(expected = "InvalidToken")] +fn dangerous_unsafe_decode_token_missing_parts() { + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; + let claims = dangerous_unsafe_decode::(token); + claims.unwrap(); +} + +#[test] +fn dangerous_unsafe_decode_token_invalid_signature() { + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.wrong"; + let claims = dangerous_unsafe_decode::(token); + claims.unwrap(); +} + +#[test] +fn dangerous_unsafe_decode_token_wrong_algorithm() { + let token = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.fLxey-hxAKX5rNHHIx1_Ch0KmrbiuoakDVbsJjLWrx8fbjKjrPuWMYEJzTU3SBnYgnZokC-wqSdqckXUOunC-g"; + let claims = dangerous_unsafe_decode::(token); + claims.unwrap(); +} diff --git a/tests/lib.rs b/tests/lib.rs index cce95a1..30241b4 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,175 +1,2 @@ -extern crate jsonwebtoken; -#[macro_use] -extern crate serde_derive; -extern crate chrono; - -use chrono::Utc; -use jsonwebtoken::{ - dangerous_unsafe_decode, decode, decode_header, encode, sign, verify, Algorithm, Header, Key, - Validation, -}; -use std::str::FromStr; - -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -pub struct Claims { - sub: String, - company: String, - exp: i64, -} - -#[test] -fn sign_hs256() { - let result = sign("hello world", Key::Hmac(b"secret"), Algorithm::HS256).unwrap(); - let expected = "c0zGLzKEFWj0VxWuufTXiRMk5tlI5MbGDAYhzaxIYjo"; - assert_eq!(result, expected); -} - -#[test] -fn verify_hs256() { - let sig = "c0zGLzKEFWj0VxWuufTXiRMk5tlI5MbGDAYhzaxIYjo"; - let valid = verify(sig, "hello world", Key::Hmac(b"secret"), Algorithm::HS256).unwrap(); - assert!(valid); -} - -#[test] -fn encode_with_custom_header() { - let my_claims = Claims { - sub: "b@b.com".to_string(), - company: "ACME".to_string(), - exp: Utc::now().timestamp() + 10000, - }; - let mut header = Header::default(); - header.kid = Some("kid".to_string()); - let token = encode(&header, &my_claims, Key::Hmac(b"secret")).unwrap(); - let token_data = - decode::(&token, Key::Hmac(b"secret"), &Validation::default()).unwrap(); - assert_eq!(my_claims, token_data.claims); - assert_eq!("kid", token_data.header.kid.unwrap()); -} - -#[test] -fn round_trip_claim() { - let my_claims = Claims { - sub: "b@b.com".to_string(), - company: "ACME".to_string(), - exp: Utc::now().timestamp() + 10000, - }; - let token = encode(&Header::default(), &my_claims, Key::Hmac(b"secret")).unwrap(); - let token_data = - decode::(&token, Key::Hmac(b"secret"), &Validation::default()).unwrap(); - assert_eq!(my_claims, token_data.claims); - assert!(token_data.header.kid.is_none()); -} - -#[test] -fn decode_token() { - let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.9r56oF7ZliOBlOAyiOFperTGxBtPykRQiWNFxhDCW98"; - let claims = decode::(token, Key::Hmac(b"secret"), &Validation::default()); - println!("{:?}", claims); - claims.unwrap(); -} - -#[test] -#[should_panic(expected = "InvalidToken")] -fn decode_token_missing_parts() { - let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; - let claims = decode::(token, Key::Hmac(b"secret"), &Validation::default()); - claims.unwrap(); -} - -#[test] -#[should_panic(expected = "InvalidSignature")] -fn decode_token_invalid_signature() { - let token = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.wrong"; - let claims = decode::(token, Key::Hmac(b"secret"), &Validation::default()); - claims.unwrap(); -} - -#[test] -#[should_panic(expected = "InvalidAlgorithm")] -fn decode_token_wrong_algorithm() { - let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.I1BvFoHe94AFf09O6tDbcSB8-jp8w6xZqmyHIwPeSdY"; - let claims = decode::(token, Key::Hmac(b"secret"), &Validation::new(Algorithm::RS512)); - claims.unwrap(); -} - -#[test] -fn decode_token_with_bytes_secret() { - let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.Hm0yvKH25TavFPz7J_coST9lZFYH1hQo0tvhvImmaks"; - let claims = decode::(token, Key::Hmac(b"\x01\x02\x03"), &Validation::default()); - assert!(claims.is_ok()); -} - -#[test] -fn decode_header_only() { - let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjb21wYW55IjoiMTIzNDU2Nzg5MCIsInN1YiI6IkpvaG4gRG9lIn0.S"; - let header = decode_header(token).unwrap(); - assert_eq!(header.alg, Algorithm::HS256); - assert_eq!(header.typ, Some("JWT".to_string())); -} - -#[test] -fn dangerous_unsafe_decode_token() { - let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.9r56oF7ZliOBlOAyiOFperTGxBtPykRQiWNFxhDCW98"; - let claims = dangerous_unsafe_decode::(token); - claims.unwrap(); -} - -#[test] -#[should_panic(expected = "InvalidToken")] -fn dangerous_unsafe_decode_token_missing_parts() { - let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; - let claims = dangerous_unsafe_decode::(token); - claims.unwrap(); -} - -#[test] -fn dangerous_unsafe_decode_token_invalid_signature() { - let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.wrong"; - let claims = dangerous_unsafe_decode::(token); - claims.unwrap(); -} - -#[test] -fn dangerous_unsafe_decode_token_wrong_algorithm() { - let token = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.fLxey-hxAKX5rNHHIx1_Ch0KmrbiuoakDVbsJjLWrx8fbjKjrPuWMYEJzTU3SBnYgnZokC-wqSdqckXUOunC-g"; - let claims = dangerous_unsafe_decode::(token); - claims.unwrap(); -} - -// https://github.com/Keats/jsonwebtoken/issues/51 -#[test] -fn does_validation_in_right_order() { - let my_claims = Claims { - sub: "b@b.com".to_string(), - company: "ACME".to_string(), - exp: Utc::now().timestamp() + 10000, - }; - let token = encode(&Header::default(), &my_claims, Key::Hmac(b"secret")).unwrap(); - let v = Validation { - leeway: 5, - validate_exp: true, - iss: Some("iss no check".to_string()), - sub: Some("sub no check".to_string()), - ..Validation::default() - }; - let res = decode::(&token, Key::Hmac(b"secret"), &v); - assert!(res.is_err()); - println!("{:?}", res); - //assert!(res.is_ok()); -} - -#[test] -fn generate_algorithm_enum_from_str() { - assert!(Algorithm::from_str("HS256").is_ok()); - assert!(Algorithm::from_str("HS384").is_ok()); - assert!(Algorithm::from_str("HS512").is_ok()); - assert!(Algorithm::from_str("RS256").is_ok()); - assert!(Algorithm::from_str("RS384").is_ok()); - assert!(Algorithm::from_str("RS512").is_ok()); - assert!(Algorithm::from_str("PS256").is_ok()); - assert!(Algorithm::from_str("PS384").is_ok()); - assert!(Algorithm::from_str("PS512").is_ok()); - assert!(Algorithm::from_str("").is_err()); -} +mod ec; +mod rsa; diff --git a/tests/rsa.rs b/tests/rsa/mod.rs similarity index 94% rename from tests/rsa.rs rename to tests/rsa/mod.rs index 82e6cc4..d453cfe 100644 --- a/tests/rsa.rs +++ b/tests/rsa/mod.rs @@ -1,10 +1,6 @@ -extern crate jsonwebtoken; -#[macro_use] -extern crate serde_derive; -extern crate chrono; - use chrono::Utc; use jsonwebtoken::{decode, decode_pem, encode, sign, verify, Algorithm, Header, Key, Validation}; +use serde_derive::{Deserialize, Serialize}; const RSA_ALGORITHMS: &[Algorithm] = &[ Algorithm::RS256, @@ -126,3 +122,10 @@ fn rsa_modulus_exponent() { .unwrap(); assert!(is_valid); } + +#[test] +#[should_panic(expected = "InvalidKeyFormat")] +fn fails_with_non_pkcs8_key_format() { + let privkey = include_bytes!("private_rsa_key.der"); + let _encrypted = sign("hello world", Key::Der(&privkey[..]), Algorithm::ES256).unwrap(); +} diff --git a/tests/private_rsa_key.der b/tests/rsa/private_rsa_key.der similarity index 100% rename from tests/private_rsa_key.der rename to tests/rsa/private_rsa_key.der diff --git a/tests/private_rsa_key_pkcs1.pem b/tests/rsa/private_rsa_key_pkcs1.pem similarity index 100% rename from tests/private_rsa_key_pkcs1.pem rename to tests/rsa/private_rsa_key_pkcs1.pem diff --git a/tests/private_rsa_key_pkcs8.pem b/tests/rsa/private_rsa_key_pkcs8.pem similarity index 100% rename from tests/private_rsa_key_pkcs8.pem rename to tests/rsa/private_rsa_key_pkcs8.pem diff --git a/tests/public_rsa_key.der b/tests/rsa/public_rsa_key.der similarity index 100% rename from tests/public_rsa_key.der rename to tests/rsa/public_rsa_key.der diff --git a/tests/public_rsa_key_pkcs1.pem b/tests/rsa/public_rsa_key_pkcs1.pem similarity index 100% rename from tests/public_rsa_key_pkcs1.pem rename to tests/rsa/public_rsa_key_pkcs1.pem diff --git a/tests/public_rsa_key_pkcs8.pem b/tests/rsa/public_rsa_key_pkcs8.pem similarity index 100% rename from tests/public_rsa_key_pkcs8.pem rename to tests/rsa/public_rsa_key_pkcs8.pem From 417e00780d333af58f3963d57999e06bcfd1030a Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sun, 3 Nov 2019 15:46:08 +0000 Subject: [PATCH 26/55] Use serde with derive feature --- Cargo.toml | 3 +-- examples/custom_chrono.rs | 22 +++++++--------------- examples/custom_header.rs | 8 +++----- examples/validation.rs | 9 +++------ src/algorithms.rs | 1 + src/header.rs | 1 + src/lib.rs | 9 --------- tests/ec/mod.rs | 2 +- tests/hmac.rs | 2 +- tests/rsa/mod.rs | 2 +- 10 files changed, 19 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b294bd8..75d2314 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,7 @@ edition = "2018" [dependencies] serde_json = "1.0" -serde_derive = "1.0" -serde = "1.0" +serde = {version = "1.0", features = ["derive"] } ring = { version = "0.16.5", features = ["std"] } base64 = "0.11" # For validation diff --git a/examples/custom_chrono.rs b/examples/custom_chrono.rs index 2d3bcca..fa6d868 100644 --- a/examples/custom_chrono.rs +++ b/examples/custom_chrono.rs @@ -1,11 +1,6 @@ -extern crate jsonwebtoken as jwt; -extern crate serde; -#[macro_use] -extern crate serde_derive; -extern crate chrono; - use chrono::prelude::*; -use jwt::{Header, Key, Validation}; +use jsonwebtoken::{Header, Key, Validation}; +use serde::{Deserialize, Serialize}; const SECRET: &str = "some-secret"; @@ -44,9 +39,6 @@ mod jwt_numeric_date { #[cfg(test)] mod tests { - use super::*; - use jwt::{Header, Validation}; - const EXPECTED_TOKEN: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJDdXN0b20gRGF0ZVRpbWUgc2VyL2RlIiwiaWF0IjowLCJleHAiOjMyNTAzNjgwMDAwfQ.RTgha0S53MjPC2pMA4e2oMzaBxSY3DMjiYR2qFfV55A"; use super::super::{Claims, SECRET}; @@ -59,13 +51,13 @@ mod jwt_numeric_date { let claims = Claims { sub: sub.clone(), iat, exp }; - let token = jwt::encode(&Header::default(), &claims, Key::Hmac(SECRET.as_ref())) + let token = encode(&Header::default(), &claims, Key::Hmac(SECRET.as_ref())) .expect("Failed to encode claims"); assert_eq!(&token, EXPECTED_TOKEN); let decoded = - jwt::decode::(&token, Key::Hmac(SECRET.as_ref()), &Validation::default()) + decode::(&token, Key::Hmac(SECRET.as_ref()), &Validation::default()) .expect("Failed to decode token"); assert_eq!(decoded.claims, claims); @@ -77,7 +69,7 @@ mod jwt_numeric_date { let overflow_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJDdXN0b20gRGF0ZVRpbWUgc2VyL2RlIiwiaWF0IjowLCJleHAiOjkyMjMzNzIwMzY4NTQ3NzYwMDB9.G2PKreA27U8_xOwuIeCYXacFYeR46f9FyENIZfCrvEc"; let decode_result = - jwt::decode::(&overflow_token, SECRET.as_ref(), &Validation::default()); + decode::(&overflow_token, SECRET.as_ref(), &Validation::default()); assert!(decode_result.is_err()); } @@ -91,12 +83,12 @@ fn main() -> Result<(), Box> { let claims = Claims { sub: sub.clone(), iat, exp }; - let token = jwt::encode(&Header::default(), &claims, Key::Hmac(SECRET.as_ref()))?; + let token = jsonwebtoken::encode(&Header::default(), &claims, Key::Hmac(SECRET.as_ref()))?; println!("serialized token: {}", &token); let token_data = - jwt::decode::(&token, Key::Hmac(SECRET.as_ref()), &Validation::default())?; + jsonwebtoken::decode::(&token, Key::Hmac(SECRET.as_ref()), &Validation::default())?; println!("token data:\n{:#?}", &token_data); Ok(()) diff --git a/examples/custom_header.rs b/examples/custom_header.rs index d4c94da..d4e5c58 100644 --- a/examples/custom_header.rs +++ b/examples/custom_header.rs @@ -1,9 +1,7 @@ -extern crate jsonwebtoken as jwt; -#[macro_use] -extern crate serde_derive; +use serde::{Deserialize, Serialize}; -use jwt::errors::ErrorKind; -use jwt::{decode, encode, Algorithm, Header, Key, Validation}; +use jsonwebtoken::errors::ErrorKind; +use jsonwebtoken::{decode, encode, Algorithm, Header, Key, Validation}; #[derive(Debug, Serialize, Deserialize)] struct Claims { diff --git a/examples/validation.rs b/examples/validation.rs index 552eef0..bfc6bcc 100644 --- a/examples/validation.rs +++ b/examples/validation.rs @@ -1,9 +1,6 @@ -extern crate jsonwebtoken as jwt; -#[macro_use] -extern crate serde_derive; - -use jwt::errors::ErrorKind; -use jwt::{decode, encode, Header, Key, Validation}; +use jsonwebtoken::errors::ErrorKind; +use jsonwebtoken::{decode, encode, Header, Key, Validation}; +use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] struct Claims { diff --git a/src/algorithms.rs b/src/algorithms.rs index 6b05356..09c8b17 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -1,4 +1,5 @@ use crate::errors::{new_error, Error, ErrorKind, Result}; +use serde::{Deserialize, Serialize}; use std::str::FromStr; /// The algorithms supported for signing/verifying diff --git a/src/header.rs b/src/header.rs index 2a17da9..66f38d8 100644 --- a/src/header.rs +++ b/src/header.rs @@ -1,4 +1,5 @@ use crate::algorithms::Algorithm; +use serde::{Deserialize, Serialize}; /// A basic JWT header, the alg defaults to HS256 and typ is automatically /// set to `JWT`. All the other fields are optional. diff --git a/src/lib.rs b/src/lib.rs index db07517..6f9b1fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,15 +3,6 @@ //! Documentation: [stable](https://docs.rs/jsonwebtoken/) #![deny(missing_docs)] -#[macro_use] -extern crate serde_derive; -extern crate base64; -extern crate chrono; -extern crate ring; -extern crate serde; -extern crate serde_json; -extern crate simple_asn1; - mod algorithms; mod crypto; /// All the errors diff --git a/tests/ec/mod.rs b/tests/ec/mod.rs index 8a15ecc..cc3321e 100644 --- a/tests/ec/mod.rs +++ b/tests/ec/mod.rs @@ -1,6 +1,6 @@ use chrono::Utc; use jsonwebtoken::{decode, decode_pem, encode, sign, verify, Algorithm, Header, Key, Validation}; -use serde_derive::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] pub struct Claims { diff --git a/tests/hmac.rs b/tests/hmac.rs index 3c3f284..89cfaf7 100644 --- a/tests/hmac.rs +++ b/tests/hmac.rs @@ -3,7 +3,7 @@ use jsonwebtoken::{ dangerous_unsafe_decode, decode, decode_header, encode, sign, verify, Algorithm, Header, Key, Validation, }; -use serde_derive::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] pub struct Claims { diff --git a/tests/rsa/mod.rs b/tests/rsa/mod.rs index d453cfe..8fde515 100644 --- a/tests/rsa/mod.rs +++ b/tests/rsa/mod.rs @@ -1,6 +1,6 @@ use chrono::Utc; use jsonwebtoken::{decode, decode_pem, encode, sign, verify, Algorithm, Header, Key, Validation}; -use serde_derive::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize}; const RSA_ALGORITHMS: &[Algorithm] = &[ Algorithm::RS256, From 73d96357c359633699f5edd2cfed3b726fb414b4 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sun, 3 Nov 2019 16:13:22 +0000 Subject: [PATCH 27/55] Simplify header decoding --- src/errors.rs | 26 ++++++++++++++------------ src/header.rs | 14 ++++++++++++-- src/lib.rs | 12 ++++++------ src/serialization.rs | 16 ++++------------ 4 files changed, 36 insertions(+), 32 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index 317a5cb..0c8b148 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -45,7 +45,7 @@ pub enum ErrorKind { /// When a key is provided with an invalid format InvalidKeyFormat, - // validation error + // Validation errors /// When a token’s `exp` claim indicates that it has expired ExpiredSignature, /// When a token’s `iss` claim does not match the expected issuer @@ -91,6 +91,7 @@ impl StdError for Error { ErrorKind::InvalidSubject => "invalid subject", ErrorKind::ImmatureSignature => "immature signature", ErrorKind::InvalidAlgorithm => "algorithms don't match", + ErrorKind::InvalidAlgorithmName => "not a known algorithm", ErrorKind::Base64(ref err) => err.description(), ErrorKind::Json(ref err) => err.description(), ErrorKind::Utf8(ref err) => err.description(), @@ -111,6 +112,7 @@ impl StdError for Error { ErrorKind::InvalidSubject => None, ErrorKind::ImmatureSignature => None, ErrorKind::InvalidAlgorithm => None, + ErrorKind::InvalidAlgorithmName => None, ErrorKind::Base64(ref err) => Some(err), ErrorKind::Json(ref err) => Some(err), ErrorKind::Utf8(ref err) => Some(err), @@ -123,17 +125,17 @@ impl StdError for Error { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self.0 { - ErrorKind::InvalidToken => write!(f, "invalid token"), - ErrorKind::InvalidSignature => write!(f, "invalid signature"), - ErrorKind::InvalidEcdsaKey => write!(f, "invalid ECDSA key"), - ErrorKind::InvalidRsaKey => write!(f, "invalid RSA key"), - ErrorKind::ExpiredSignature => write!(f, "expired signature"), - ErrorKind::InvalidIssuer => write!(f, "invalid issuer"), - ErrorKind::InvalidAudience => write!(f, "invalid audience"), - ErrorKind::InvalidSubject => write!(f, "invalid subject"), - ErrorKind::ImmatureSignature => write!(f, "immature signature"), - ErrorKind::InvalidAlgorithm => write!(f, "algorithms don't match"), - ErrorKind::Base64(ref err) => write!(f, "base64 error: {}", err), + ErrorKind::InvalidToken + | ErrorKind::InvalidSignature + | ErrorKind::InvalidEcdsaKey + | ErrorKind::InvalidRsaKey + | ErrorKind::ExpiredSignature + | ErrorKind::InvalidIssuer + | ErrorKind::InvalidAudience + | ErrorKind::InvalidSubject + | ErrorKind::ImmatureSignature + | ErrorKind::InvalidAlgorithm + | ErrorKind::InvalidAlgorithmName => write!(f, "{}", self.description()), ErrorKind::Json(ref err) => write!(f, "JSON error: {}", err), ErrorKind::Utf8(ref err) => write!(f, "UTF-8 error: {}", err), ErrorKind::Crypto(ref err) => write!(f, "Crypto error: {}", err), diff --git a/src/header.rs b/src/header.rs index 66f38d8..73a2a1d 100644 --- a/src/header.rs +++ b/src/header.rs @@ -1,6 +1,8 @@ -use crate::algorithms::Algorithm; use serde::{Deserialize, Serialize}; +use crate::algorithms::Algorithm; +use crate::errors::Result; + /// A basic JWT header, the alg defaults to HS256 and typ is automatically /// set to `JWT`. All the other fields are optional. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -43,7 +45,7 @@ pub struct Header { impl Header { /// Returns a JWT header with the algorithm given - pub fn new(algorithm: Algorithm) -> Header { + pub fn new(algorithm: Algorithm) -> Self { Header { typ: Some("JWT".to_string()), alg: algorithm, @@ -54,6 +56,14 @@ impl Header { x5t: None, } } + + /// Converts an encoded part into the Header struct if possible + pub(crate) fn from_encoded(encoded_part: &str) -> Result { + let decoded = base64::decode_config(encoded_part, base64::URL_SAFE_NO_PAD)?; + let s = String::from_utf8(decoded)?; + + Ok(serde_json::from_str(&s)?) + } } impl Default for Header { diff --git a/src/lib.rs b/src/lib.rs index 6f9b1fa..0dc6cbf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,7 @@ use serde::de::DeserializeOwned; use serde::ser::Serialize; use crate::errors::{new_error, ErrorKind, Result}; -use crate::serialization::{from_jwt_part, from_jwt_part_claims, to_jwt_part}; +use crate::serialization::{encode_part, from_jwt_part_claims}; use crate::validation::validate; /// Encode the header and claims given and sign the payload using the algorithm from the header and the key @@ -51,8 +51,8 @@ use crate::validation::validate; /// let token = encode(&Header::default(), &my_claims, Key::Hmac("secret".as_ref())).unwrap(); /// ``` pub fn encode(header: &Header, claims: &T, key: Key) -> Result { - let encoded_header = to_jwt_part(&header)?; - let encoded_claims = to_jwt_part(&claims)?; + let encoded_header = encode_part(&header)?; + let encoded_claims = encode_part(&claims)?; let signing_input = [encoded_header.as_ref(), encoded_claims.as_ref()].join("."); let signature = sign(&*signing_input, key, header.alg)?; @@ -97,7 +97,7 @@ pub fn decode( ) -> Result> { let (signature, signing_input) = expect_two!(token.rsplitn(2, '.')); let (claims, header) = expect_two!(signing_input.rsplitn(2, '.')); - let header: Header = from_jwt_part(header)?; + let header = Header::from_encoded(header)?; if !verify(signature, signing_input, key, header.alg)? { return Err(new_error(ErrorKind::InvalidSignature)); @@ -135,7 +135,7 @@ pub fn decode( pub fn dangerous_unsafe_decode(token: &str) -> Result> { let (_, signing_input) = expect_two!(token.rsplitn(2, '.')); let (claims, header) = expect_two!(signing_input.rsplitn(2, '.')); - let header: Header = from_jwt_part(header)?; + let header = Header::from_encoded(header)?; let (decoded_claims, _): (T, _) = from_jwt_part_claims(claims)?; @@ -156,7 +156,7 @@ pub fn dangerous_unsafe_decode(token: &str) -> Result Result
{ let (_, signing_input) = expect_two!(token.rsplitn(2, '.')); let (_, header) = expect_two!(signing_input.rsplitn(2, '.')); - from_jwt_part(header) + Header::from_encoded(header) } /// Decode a PEM string to obtain its key diff --git a/src/serialization.rs b/src/serialization.rs index 2a47688..d1aafeb 100644 --- a/src/serialization.rs +++ b/src/serialization.rs @@ -16,18 +16,10 @@ pub struct TokenData { pub claims: T, } -/// Serializes to JSON and encodes to base64 -pub fn to_jwt_part(input: &T) -> Result { - let encoded = to_string(input)?; - Ok(base64::encode_config(encoded.as_bytes(), base64::URL_SAFE_NO_PAD)) -} - -/// Decodes from base64 and deserializes from JSON to a struct -pub fn from_jwt_part, T: DeserializeOwned>(encoded: B) -> Result { - let decoded = base64::decode_config(encoded.as_ref(), base64::URL_SAFE_NO_PAD)?; - let s = String::from_utf8(decoded)?; - - Ok(from_str(&s)?) +/// Serializes a struct to JSON and encodes it in base64 +pub fn encode_part(input: &T) -> Result { + let json = to_string(input)?; + Ok(base64::encode_config(json.as_bytes(), base64::URL_SAFE_NO_PAD)) } /// Decodes from base64 and deserializes from JSON to a struct AND a hashmap From 53188e1f403cd46a84ccf7251372c4f14c07f31f Mon Sep 17 00:00:00 2001 From: Levi Date: Wed, 6 Nov 2019 07:58:49 -0600 Subject: [PATCH 28/55] Add functions to create pems and ders of the public keys (#108) --- src/lib.rs | 31 ++++++++++++++ src/pem_encoder.rs | 100 +++++++++++++++++++++++++++++++++++++++++++++ tests/ec/mod.rs | 30 +++++++++++++- tests/rsa/mod.rs | 47 ++++++++++++++++++++- 4 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 src/pem_encoder.rs diff --git a/src/lib.rs b/src/lib.rs index 0dc6cbf..6785890 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ pub mod errors; mod header; mod keys; mod pem_decoder; +mod pem_encoder; mod serialization; mod validation; @@ -185,3 +186,33 @@ pub fn decode_header(token: &str) -> Result
{ pub fn decode_pem(content: &str) -> Result { PemEncodedKey::read(content) } + +/// TODO +pub fn encode_rsa_public_pkcs1_pem(modulus: &[u8], exponent: &[u8]) -> Result { + pem_encoder::encode_rsa_public_pkcs1_pem(modulus, exponent) +} + +/// TODO +pub fn encode_rsa_public_pkcs1_der(modulus: &[u8], exponent: &[u8]) -> Result> { + pem_encoder::encode_rsa_public_pkcs1_der(modulus, exponent) +} + +/// TODO +pub fn encode_rsa_public_pkcs8_pem(modulus: &[u8], exponent: &[u8]) -> Result { + pem_encoder::encode_rsa_public_pkcs8_pem(modulus, exponent) +} + +/// TODO +pub fn encode_rsa_public_pkcs8_der(modulus: &[u8], exponent: &[u8]) -> Result> { + pem_encoder::encode_rsa_public_pkcs8_der(modulus, exponent) +} + +/// TODO +pub fn encode_ec_public_pem(x_coordinate: &[u8]) -> Result { + pem_encoder::encode_ec_public_pem(x_coordinate) +} + +/// TODO +pub fn encode_ec_public_der(x_coordinate: &[u8]) -> Result> { + pem_encoder::encode_ec_public_der(x_coordinate) +} \ No newline at end of file diff --git a/src/pem_encoder.rs b/src/pem_encoder.rs new file mode 100644 index 0000000..0149259 --- /dev/null +++ b/src/pem_encoder.rs @@ -0,0 +1,100 @@ +use crate::errors::{ErrorKind, Result}; +use simple_asn1::{ASN1Block, BigUint, BigInt, OID}; +use pem::{Pem}; + +extern crate base64; +extern crate pem; +extern crate simple_asn1; + +pub fn encode_rsa_public_pkcs1_pem(modulus: &[u8], exponent: &[u8]) -> Result { + Ok(pem::encode(&Pem { + contents: encode_rsa_public_pkcs1_der(modulus, exponent)?, + tag: "RSA PUBLIC KEY".to_string(), + })) +} + +pub fn encode_rsa_public_pkcs1_der(modulus: &[u8], exponent: &[u8]) -> Result> { + match simple_asn1::to_der(&encode_rsa_public_pksc1_asn1(modulus, exponent)) { + Ok(bytes) => Ok(bytes), + Err(_) => return Err(ErrorKind::InvalidRsaKey)?, + } +} + +pub fn encode_rsa_public_pkcs8_pem(modulus: &[u8], exponent: &[u8]) -> Result { + Ok(pem::encode(&Pem { + contents: encode_rsa_public_pkcs8_der(modulus, exponent)?, + tag: "PUBLIC KEY".to_string(), + })) +} + +pub fn encode_rsa_public_pkcs8_der(modulus: &[u8], exponent: &[u8]) -> Result> { + match simple_asn1::to_der(&encode_rsa_public_pksc8_asn1(modulus, exponent)?) { + Ok(bytes) => Ok(bytes), + Err(_) => return Err(ErrorKind::InvalidRsaKey)?, + } +} + +pub fn encode_ec_public_pem(x: &[u8]) -> Result { + Ok(pem::encode(&Pem { + contents: encode_ec_public_der(x)?, + tag: "PUBLIC KEY".to_string(), + })) +} + +pub fn encode_ec_public_der(x: &[u8]) -> Result> { + match simple_asn1::to_der(&encode_ec_public_asn1(x)) { + Ok(bytes) => Ok(bytes), + Err(_) => return Err(ErrorKind::InvalidEcdsaKey)?, + } +} + +fn encode_rsa_public_pksc8_asn1(modulus: &[u8], exponent: &[u8]) -> Result { + let pksc1 = match simple_asn1::to_der(&encode_rsa_public_pksc1_asn1(modulus, exponent)) { + Ok(bytes) => bytes, + Err(_) => return Err(ErrorKind::InvalidRsaKey)?, + }; + Ok(ASN1Block::Sequence( + 0, + vec![ + ASN1Block::Sequence( + 0, + vec![ + // rsaEncryption (PKCS #1) + ASN1Block::ObjectIdentifier(0, simple_asn1::oid!(1,2,840,113549,1,1,1)), + ASN1Block::Null(0) + ] + ), + // the second parameter takes the count of bits + ASN1Block::BitString(0, pksc1.len() * 8, pksc1) + ], + )) +} + +fn encode_rsa_public_pksc1_asn1(modulus: &[u8], exponent: &[u8]) -> ASN1Block { + ASN1Block::Sequence( + 0, + vec![ + ASN1Block::Integer(0, BigInt::from_signed_bytes_be(modulus)), + ASN1Block::Integer(0, BigInt::from_signed_bytes_be(exponent)), + ], + ) +} + +fn encode_ec_public_asn1(x: &[u8]) -> ASN1Block { + ASN1Block::Sequence( + 0, + vec![ + ASN1Block::Sequence( + 0, + vec![ + // ecPublicKey (ANSI X9.62 public key type) + ASN1Block::ObjectIdentifier(0, simple_asn1::oid!(1, 2, 840, 10045, 2, 1)), + // prime256v1 (ANSI X9.62 named elliptic curve) + ASN1Block::ObjectIdentifier(0, simple_asn1::oid!(1, 2, 840, 10045, 3, 1, 7)), + ], + ), + // the second parameter takes the count of bits + ASN1Block::BitString(0, x.len() * 8, x.to_vec()), + ], + ) +} diff --git a/tests/ec/mod.rs b/tests/ec/mod.rs index cc3321e..d4fb16d 100644 --- a/tests/ec/mod.rs +++ b/tests/ec/mod.rs @@ -1,6 +1,7 @@ use chrono::Utc; -use jsonwebtoken::{decode, decode_pem, encode, sign, verify, Algorithm, Header, Key, Validation}; +use jsonwebtoken::{decode, decode_pem, encode_ec_public_pem, encode_ec_public_der, encode, sign, verify, Algorithm, Header, Key, Validation}; use serde::{Deserialize, Serialize}; +use ring::{signature, signature::KeyPair}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] pub struct Claims { @@ -45,3 +46,30 @@ fn round_trip_claim() { assert_eq!(my_claims, token_data.claims); assert!(token_data.header.kid.is_none()); } + +#[test] +fn public_key_encoding() { + let privkey_pem = decode_pem(include_str!("private_ecdsa_key.pem")).unwrap(); + let privkey = privkey_pem.as_key().unwrap(); + let alg = &signature::ECDSA_P256_SHA256_FIXED_SIGNING; + let ring_key = signature::EcdsaKeyPair::from_pkcs8(alg, match privkey { + Key::Pkcs8(bytes) => bytes, + _ => panic!("Unexpected") + }).unwrap(); + + let public_key_pem = encode_ec_public_pem(ring_key.public_key().as_ref()).unwrap(); + assert_eq!(include_str!("public_ecdsa_key.pem").trim(), public_key_pem.replace('\r', "").trim()); + + let public_key_der = encode_ec_public_der(ring_key.public_key().as_ref()).unwrap(); + // The stored ".pk8" key is just the x coordinate of the EC key + // It's not truly a pkcs8 formatted DER + // To get around that, a prepended binary specifies the EC key, EC name, + // and X coordinate length. The length is unlikely to change.. in the + // event that it does, look at the pem file (convert base64 to hex) and find + // where 0x03, 0x42 don't match up. 0x42 is the length. + let mut stored_pk8_der = vec![0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A, 0x86, 0x48, + 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, + 0x07, 0x03, 0x42, 0x00]; + stored_pk8_der.extend(include_bytes!("public_ecdsa_key.pk8").to_vec()); + assert_eq!(stored_pk8_der, public_key_der); +} \ No newline at end of file diff --git a/tests/rsa/mod.rs b/tests/rsa/mod.rs index 8fde515..c1c3962 100644 --- a/tests/rsa/mod.rs +++ b/tests/rsa/mod.rs @@ -1,6 +1,7 @@ use chrono::Utc; -use jsonwebtoken::{decode, decode_pem, encode, sign, verify, Algorithm, Header, Key, Validation}; +use jsonwebtoken::{decode, decode_pem, encode_rsa_public_pkcs1_pem, encode_rsa_public_pkcs1_der, encode_rsa_public_pkcs8_pem, encode, sign, verify, Algorithm, Header, Key, Validation}; use serde::{Deserialize, Serialize}; +use ring::{signature, signature::KeyPair}; const RSA_ALGORITHMS: &[Algorithm] = &[ Algorithm::RS256, @@ -129,3 +130,47 @@ fn fails_with_non_pkcs8_key_format() { let privkey = include_bytes!("private_rsa_key.der"); let _encrypted = sign("hello world", Key::Der(&privkey[..]), Algorithm::ES256).unwrap(); } + +#[test] +fn public_key_encoding_pkcs1() { + let privkey_pem = decode_pem(include_str!("private_rsa_key_pkcs8.pem")).unwrap(); + let privkey = privkey_pem.as_key().unwrap(); + let ring_key = signature::RsaKeyPair::from_der(match privkey { + Key::Der(bytes) => bytes, + _ => panic!("Unexpected") + }).unwrap(); + let mut modulus = vec!(0); + modulus.extend(ring_key.public_key().modulus().big_endian_without_leading_zero()); + let exponent = ring_key.public_key().exponent(); + + let public_key_pkcs1_pem = encode_rsa_public_pkcs1_pem( + modulus.as_ref(), + exponent.big_endian_without_leading_zero() + ).unwrap(); + assert_eq!(include_str!("public_rsa_key_pkcs1.pem").trim(), public_key_pkcs1_pem.replace('\r', "").trim()); + + let public_key_pkcs1_der = encode_rsa_public_pkcs1_der( + modulus.as_ref(), + exponent.big_endian_without_leading_zero() + ).unwrap(); + assert_eq!(include_bytes!("public_rsa_key.der").to_vec(), public_key_pkcs1_der); +} + +#[test] +fn public_key_encoding_pkcs8() { + let privkey_pem = decode_pem(include_str!("private_rsa_key_pkcs8.pem")).unwrap(); + let privkey = privkey_pem.as_key().unwrap(); + let ring_key = signature::RsaKeyPair::from_der(match privkey { + Key::Der(bytes) => bytes, + _ => panic!("Unexpected") + }).unwrap(); + let mut modulus = vec!(0); + modulus.extend(ring_key.public_key().modulus().big_endian_without_leading_zero()); + let exponent = ring_key.public_key().exponent(); + + let public_key_pkcs8 = encode_rsa_public_pkcs8_pem( + modulus.as_ref(), + exponent.big_endian_without_leading_zero() + ).unwrap(); + assert_eq!(include_str!("public_rsa_key_pkcs8.pem").trim(), public_key_pkcs8.replace('\r', "").trim()); +} \ No newline at end of file From 382e4478cf4694f073c29e9ce570a6932887c9dc Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Wed, 6 Nov 2019 18:30:59 +0000 Subject: [PATCH 29/55] Move pem encoding tests --- src/lib.rs | 2 +- src/pem_encoder.rs | 120 +++++++++++++++++++++++++++++++++++++++++---- tests/ec/mod.rs | 30 +----------- tests/rsa/mod.rs | 47 +----------------- 4 files changed, 113 insertions(+), 86 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6785890..5a81b2f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -215,4 +215,4 @@ pub fn encode_ec_public_pem(x_coordinate: &[u8]) -> Result { /// TODO pub fn encode_ec_public_der(x_coordinate: &[u8]) -> Result> { pem_encoder::encode_ec_public_der(x_coordinate) -} \ No newline at end of file +} diff --git a/src/pem_encoder.rs b/src/pem_encoder.rs index 0149259..101cef7 100644 --- a/src/pem_encoder.rs +++ b/src/pem_encoder.rs @@ -1,6 +1,6 @@ use crate::errors::{ErrorKind, Result}; -use simple_asn1::{ASN1Block, BigUint, BigInt, OID}; -use pem::{Pem}; +use pem::Pem; +use simple_asn1::{ASN1Block, BigInt, BigUint, OID}; extern crate base64; extern crate pem; @@ -35,10 +35,7 @@ pub fn encode_rsa_public_pkcs8_der(modulus: &[u8], exponent: &[u8]) -> Result Result { - Ok(pem::encode(&Pem { - contents: encode_ec_public_der(x)?, - tag: "PUBLIC KEY".to_string(), - })) + Ok(pem::encode(&Pem { contents: encode_ec_public_der(x)?, tag: "PUBLIC KEY".to_string() })) } pub fn encode_ec_public_der(x: &[u8]) -> Result> { @@ -60,12 +57,12 @@ fn encode_rsa_public_pksc8_asn1(modulus: &[u8], exponent: &[u8]) -> Result ASN1Block { ], ) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::decode_pem; + use crate::keys::Key; + use ring::{signature, signature::KeyPair}; + + #[test] + fn public_key_encoding_pkcs1() { + let privkey_pem = + decode_pem(include_str!("../tests/rsa/private_rsa_key_pkcs8.pem")).unwrap(); + let privkey = privkey_pem.as_key().unwrap(); + let ring_key = signature::RsaKeyPair::from_der(match privkey { + Key::Der(bytes) => bytes, + _ => panic!("Unexpected"), + }) + .unwrap(); + let mut modulus = vec![0]; + modulus.extend(ring_key.public_key().modulus().big_endian_without_leading_zero()); + let exponent = ring_key.public_key().exponent(); + + let public_key_pkcs1_pem = encode_rsa_public_pkcs1_pem( + modulus.as_ref(), + exponent.big_endian_without_leading_zero(), + ) + .unwrap(); + assert_eq!( + include_str!("../tests/rsa/public_rsa_key_pkcs1.pem").trim(), + public_key_pkcs1_pem.replace('\r', "").trim() + ); + + let public_key_pkcs1_der = encode_rsa_public_pkcs1_der( + modulus.as_ref(), + exponent.big_endian_without_leading_zero(), + ) + .unwrap(); + assert_eq!( + include_bytes!("../tests/rsa/public_rsa_key.der").to_vec(), + public_key_pkcs1_der + ); + } + + #[test] + fn public_key_encoding_pkcs8() { + let privkey_pem = + decode_pem(include_str!("../tests/rsa/private_rsa_key_pkcs8.pem")).unwrap(); + let privkey = privkey_pem.as_key().unwrap(); + let ring_key = signature::RsaKeyPair::from_der(match privkey { + Key::Der(bytes) => bytes, + _ => panic!("Unexpected"), + }) + .unwrap(); + let mut modulus = vec![0]; + modulus.extend(ring_key.public_key().modulus().big_endian_without_leading_zero()); + let exponent = ring_key.public_key().exponent(); + + let public_key_pkcs8 = encode_rsa_public_pkcs8_pem( + modulus.as_ref(), + exponent.big_endian_without_leading_zero(), + ) + .unwrap(); + assert_eq!( + include_str!("../tests/rsa/public_rsa_key_pkcs8.pem").trim(), + public_key_pkcs8.replace('\r', "").trim() + ); + } + + #[test] + fn public_key_encoding() { + let privkey_pem = decode_pem(include_str!("../tests/ec/private_ecdsa_key.pem")).unwrap(); + let privkey = privkey_pem.as_key().unwrap(); + let alg = &signature::ECDSA_P256_SHA256_FIXED_SIGNING; + let ring_key = signature::EcdsaKeyPair::from_pkcs8( + alg, + match privkey { + Key::Pkcs8(bytes) => bytes, + _ => panic!("Unexpected"), + }, + ) + .unwrap(); + + let public_key_pem = encode_ec_public_pem(ring_key.public_key().as_ref()).unwrap(); + assert_eq!( + include_str!("../tests/ec/public_ecdsa_key.pem").trim(), + public_key_pem.replace('\r', "").trim() + ); + + let public_key_der = encode_ec_public_der(ring_key.public_key().as_ref()).unwrap(); + // The stored ".pk8" key is just the x coordinate of the EC key + // It's not truly a pkcs8 formatted DER + // To get around that, a prepended binary specifies the EC key, EC name, + // and X coordinate length. The length is unlikely to change.. in the + // event that it does, look at the pem file (convert base64 to hex) and find + // where 0x03, 0x42 don't match up. 0x42 is the length. + let mut stored_pk8_der = vec![ + 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, + 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, + ]; + stored_pk8_der.extend(include_bytes!("../tests/ec/public_ecdsa_key.pk8").to_vec()); + assert_eq!(stored_pk8_der, public_key_der); + } +} diff --git a/tests/ec/mod.rs b/tests/ec/mod.rs index d4fb16d..cc3321e 100644 --- a/tests/ec/mod.rs +++ b/tests/ec/mod.rs @@ -1,7 +1,6 @@ use chrono::Utc; -use jsonwebtoken::{decode, decode_pem, encode_ec_public_pem, encode_ec_public_der, encode, sign, verify, Algorithm, Header, Key, Validation}; +use jsonwebtoken::{decode, decode_pem, encode, sign, verify, Algorithm, Header, Key, Validation}; use serde::{Deserialize, Serialize}; -use ring::{signature, signature::KeyPair}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] pub struct Claims { @@ -46,30 +45,3 @@ fn round_trip_claim() { assert_eq!(my_claims, token_data.claims); assert!(token_data.header.kid.is_none()); } - -#[test] -fn public_key_encoding() { - let privkey_pem = decode_pem(include_str!("private_ecdsa_key.pem")).unwrap(); - let privkey = privkey_pem.as_key().unwrap(); - let alg = &signature::ECDSA_P256_SHA256_FIXED_SIGNING; - let ring_key = signature::EcdsaKeyPair::from_pkcs8(alg, match privkey { - Key::Pkcs8(bytes) => bytes, - _ => panic!("Unexpected") - }).unwrap(); - - let public_key_pem = encode_ec_public_pem(ring_key.public_key().as_ref()).unwrap(); - assert_eq!(include_str!("public_ecdsa_key.pem").trim(), public_key_pem.replace('\r', "").trim()); - - let public_key_der = encode_ec_public_der(ring_key.public_key().as_ref()).unwrap(); - // The stored ".pk8" key is just the x coordinate of the EC key - // It's not truly a pkcs8 formatted DER - // To get around that, a prepended binary specifies the EC key, EC name, - // and X coordinate length. The length is unlikely to change.. in the - // event that it does, look at the pem file (convert base64 to hex) and find - // where 0x03, 0x42 don't match up. 0x42 is the length. - let mut stored_pk8_der = vec![0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A, 0x86, 0x48, - 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, - 0x07, 0x03, 0x42, 0x00]; - stored_pk8_der.extend(include_bytes!("public_ecdsa_key.pk8").to_vec()); - assert_eq!(stored_pk8_der, public_key_der); -} \ No newline at end of file diff --git a/tests/rsa/mod.rs b/tests/rsa/mod.rs index c1c3962..8fde515 100644 --- a/tests/rsa/mod.rs +++ b/tests/rsa/mod.rs @@ -1,7 +1,6 @@ use chrono::Utc; -use jsonwebtoken::{decode, decode_pem, encode_rsa_public_pkcs1_pem, encode_rsa_public_pkcs1_der, encode_rsa_public_pkcs8_pem, encode, sign, verify, Algorithm, Header, Key, Validation}; +use jsonwebtoken::{decode, decode_pem, encode, sign, verify, Algorithm, Header, Key, Validation}; use serde::{Deserialize, Serialize}; -use ring::{signature, signature::KeyPair}; const RSA_ALGORITHMS: &[Algorithm] = &[ Algorithm::RS256, @@ -130,47 +129,3 @@ fn fails_with_non_pkcs8_key_format() { let privkey = include_bytes!("private_rsa_key.der"); let _encrypted = sign("hello world", Key::Der(&privkey[..]), Algorithm::ES256).unwrap(); } - -#[test] -fn public_key_encoding_pkcs1() { - let privkey_pem = decode_pem(include_str!("private_rsa_key_pkcs8.pem")).unwrap(); - let privkey = privkey_pem.as_key().unwrap(); - let ring_key = signature::RsaKeyPair::from_der(match privkey { - Key::Der(bytes) => bytes, - _ => panic!("Unexpected") - }).unwrap(); - let mut modulus = vec!(0); - modulus.extend(ring_key.public_key().modulus().big_endian_without_leading_zero()); - let exponent = ring_key.public_key().exponent(); - - let public_key_pkcs1_pem = encode_rsa_public_pkcs1_pem( - modulus.as_ref(), - exponent.big_endian_without_leading_zero() - ).unwrap(); - assert_eq!(include_str!("public_rsa_key_pkcs1.pem").trim(), public_key_pkcs1_pem.replace('\r', "").trim()); - - let public_key_pkcs1_der = encode_rsa_public_pkcs1_der( - modulus.as_ref(), - exponent.big_endian_without_leading_zero() - ).unwrap(); - assert_eq!(include_bytes!("public_rsa_key.der").to_vec(), public_key_pkcs1_der); -} - -#[test] -fn public_key_encoding_pkcs8() { - let privkey_pem = decode_pem(include_str!("private_rsa_key_pkcs8.pem")).unwrap(); - let privkey = privkey_pem.as_key().unwrap(); - let ring_key = signature::RsaKeyPair::from_der(match privkey { - Key::Der(bytes) => bytes, - _ => panic!("Unexpected") - }).unwrap(); - let mut modulus = vec!(0); - modulus.extend(ring_key.public_key().modulus().big_endian_without_leading_zero()); - let exponent = ring_key.public_key().exponent(); - - let public_key_pkcs8 = encode_rsa_public_pkcs8_pem( - modulus.as_ref(), - exponent.big_endian_without_leading_zero() - ).unwrap(); - assert_eq!(include_str!("public_rsa_key_pkcs8.pem").trim(), public_key_pkcs8.replace('\r', "").trim()); -} \ No newline at end of file From a6ea8c2c1a4bb6ad4778b6320f6c18a1ab4545f4 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Wed, 6 Nov 2019 18:41:51 +0000 Subject: [PATCH 30/55] clippy + fmt --- src/crypto.rs | 8 ++++---- src/pem_decoder.rs | 19 ++++++++++--------- src/pem_encoder.rs | 10 +++++----- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/crypto.rs b/src/crypto.rs index 3556fb8..804f082 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -10,7 +10,7 @@ use crate::keys::Key; fn sign_hmac(alg: hmac::Algorithm, key: Key, signing_input: &str) -> Result { let signing_key = match key { Key::Hmac(bytes) => hmac::Key::new(alg, bytes), - _ => return Err(ErrorKind::InvalidKeyFormat)?, + _ => return Err(ErrorKind::InvalidKeyFormat.into()), }; let digest = hmac::sign(&signing_key, signing_input.as_bytes()); @@ -49,7 +49,7 @@ fn sign_rsa( signature::RsaKeyPair::from_pkcs8(bytes).map_err(|_| ErrorKind::InvalidRsaKey)? } _ => { - return Err(ErrorKind::InvalidKeyFormat)?; + return Err(ErrorKind::InvalidKeyFormat.into()); } }; @@ -112,7 +112,7 @@ fn verify_ring_es( let bytes = match key { Key::Pkcs8(bytes) => bytes, _ => { - return Err(ErrorKind::InvalidKeyFormat)?; + return Err(ErrorKind::InvalidKeyFormat.into()); } }; verify_ring(alg, signature, signing_input, bytes) @@ -135,7 +135,7 @@ fn verify_ring_rsa( Ok(res.is_ok()) } - _ => Err(ErrorKind::InvalidKeyFormat)?, + _ => Err(ErrorKind::InvalidKeyFormat.into()), } } diff --git a/src/pem_decoder.rs b/src/pem_decoder.rs index 0f08440..c42b042 100644 --- a/src/pem_decoder.rs +++ b/src/pem_decoder.rs @@ -57,7 +57,7 @@ impl PemEncodedKey { let pem_contents = content.contents; let asn1_content = match simple_asn1::from_der(pem_contents.as_slice()) { Ok(asn1) => asn1, - Err(_) => return Err(ErrorKind::InvalidKeyFormat)?, + Err(_) => return Err(ErrorKind::InvalidKeyFormat.into()), }; match content.tag.as_ref() { @@ -106,14 +106,14 @@ impl PemEncodedKey { standard: Standard::Pkcs8, }) } - None => return Err(ErrorKind::InvalidKeyFormat)?, + None => Err(ErrorKind::InvalidKeyFormat.into()), }, // Unknown/unsupported type - _ => return Err(ErrorKind::InvalidKeyFormat)?, + _ => Err(ErrorKind::InvalidKeyFormat.into()), } } - Err(_) => return Err(ErrorKind::InvalidKeyFormat)?, + Err(_) => Err(ErrorKind::InvalidKeyFormat.into()), } } @@ -139,7 +139,7 @@ impl PemEncodedKey { // And the DER contents of an RSA key // Though PKCS#11 keys shouldn't have anything else. // It will get confusing with certificates. -fn extract_first_bitstring(asn1: &Vec) -> Result<&[u8]> { +fn extract_first_bitstring(asn1: &[simple_asn1::ASN1Block]) -> Result<&[u8]> { for asn1_entry in asn1.iter() { match asn1_entry { simple_asn1::ASN1Block::Sequence(_, entries) => { @@ -156,15 +156,16 @@ fn extract_first_bitstring(asn1: &Vec) -> Result<&[u8]> _ => (), } } - Err(ErrorKind::InvalidEcdsaKey)? + + Err(ErrorKind::InvalidEcdsaKey.into()) } /// Find whether this is EC or RSA -fn classify_pem(asn1: &Vec) -> Option { +fn classify_pem(asn1: &[simple_asn1::ASN1Block]) -> Option { // These should be constant but the macro requires // #![feature(const_vec_new)] - let ec_public_key_oid = simple_asn1::oid!(1, 2, 840, 10045, 2, 1); - let rsa_public_key_oid = simple_asn1::oid!(1, 2, 840, 113549, 1, 1, 1); + let ec_public_key_oid = simple_asn1::oid!(1, 2, 840, 10_045, 2, 1); + let rsa_public_key_oid = simple_asn1::oid!(1, 2, 840, 113_549, 1, 1, 1); for asn1_entry in asn1.iter() { match asn1_entry { diff --git a/src/pem_encoder.rs b/src/pem_encoder.rs index 101cef7..d719fc1 100644 --- a/src/pem_encoder.rs +++ b/src/pem_encoder.rs @@ -16,7 +16,7 @@ pub fn encode_rsa_public_pkcs1_pem(modulus: &[u8], exponent: &[u8]) -> Result Result> { match simple_asn1::to_der(&encode_rsa_public_pksc1_asn1(modulus, exponent)) { Ok(bytes) => Ok(bytes), - Err(_) => return Err(ErrorKind::InvalidRsaKey)?, + Err(_) => Err(ErrorKind::InvalidRsaKey.into()), } } @@ -30,7 +30,7 @@ pub fn encode_rsa_public_pkcs8_pem(modulus: &[u8], exponent: &[u8]) -> Result Result> { match simple_asn1::to_der(&encode_rsa_public_pksc8_asn1(modulus, exponent)?) { Ok(bytes) => Ok(bytes), - Err(_) => return Err(ErrorKind::InvalidRsaKey)?, + Err(_) => Err(ErrorKind::InvalidRsaKey.into()), } } @@ -41,14 +41,14 @@ pub fn encode_ec_public_pem(x: &[u8]) -> Result { pub fn encode_ec_public_der(x: &[u8]) -> Result> { match simple_asn1::to_der(&encode_ec_public_asn1(x)) { Ok(bytes) => Ok(bytes), - Err(_) => return Err(ErrorKind::InvalidEcdsaKey)?, + Err(_) => Err(ErrorKind::InvalidEcdsaKey.into()), } } fn encode_rsa_public_pksc8_asn1(modulus: &[u8], exponent: &[u8]) -> Result { let pksc1 = match simple_asn1::to_der(&encode_rsa_public_pksc1_asn1(modulus, exponent)) { Ok(bytes) => bytes, - Err(_) => return Err(ErrorKind::InvalidRsaKey)?, + Err(_) => return Err(ErrorKind::InvalidRsaKey.into()), }; Ok(ASN1Block::Sequence( 0, @@ -57,7 +57,7 @@ fn encode_rsa_public_pksc8_asn1(modulus: &[u8], exponent: &[u8]) -> Result Date: Wed, 6 Nov 2019 22:32:13 +0000 Subject: [PATCH 31/55] Remove Key approach in favour of &[u8] with pem --- examples/custom_chrono.rs | 13 ++- examples/custom_header.rs | 19 ++-- examples/validation.rs | 6 +- src/algorithms.rs | 4 +- src/crypto.rs | 71 ++++---------- src/errors.rs | 10 +- src/keys.rs | 16 ---- src/lib.rs | 36 +------- src/pem_decoder.rs | 63 ++++++++----- src/pem_encoder.rs | 190 +++++++++++++++++++------------------- src/serialization.rs | 3 +- tests/ec/mod.rs | 35 ++++--- tests/hmac.rs | 26 +++--- tests/rsa/mod.rs | 121 ++++++++++-------------- 14 files changed, 262 insertions(+), 351 deletions(-) delete mode 100644 src/keys.rs diff --git a/examples/custom_chrono.rs b/examples/custom_chrono.rs index fa6d868..e0b1185 100644 --- a/examples/custom_chrono.rs +++ b/examples/custom_chrono.rs @@ -1,5 +1,5 @@ use chrono::prelude::*; -use jsonwebtoken::{Header, Key, Validation}; +use jsonwebtoken::{Header, Validation}; use serde::{Deserialize, Serialize}; const SECRET: &str = "some-secret"; @@ -51,14 +51,13 @@ mod jwt_numeric_date { let claims = Claims { sub: sub.clone(), iat, exp }; - let token = encode(&Header::default(), &claims, Key::Hmac(SECRET.as_ref())) + let token = encode(&Header::default(), &claims, SECRET.as_ref()) .expect("Failed to encode claims"); assert_eq!(&token, EXPECTED_TOKEN); - let decoded = - decode::(&token, Key::Hmac(SECRET.as_ref()), &Validation::default()) - .expect("Failed to decode token"); + let decoded = decode::(&token, SECRET.as_ref(), &Validation::default()) + .expect("Failed to decode token"); assert_eq!(decoded.claims, claims); } @@ -83,12 +82,12 @@ fn main() -> Result<(), Box> { let claims = Claims { sub: sub.clone(), iat, exp }; - let token = jsonwebtoken::encode(&Header::default(), &claims, Key::Hmac(SECRET.as_ref()))?; + let token = jsonwebtoken::encode(&Header::default(), &claims, SECRET.as_ref())?; println!("serialized token: {}", &token); let token_data = - jsonwebtoken::decode::(&token, Key::Hmac(SECRET.as_ref()), &Validation::default())?; + jsonwebtoken::decode::(&token, SECRET.as_ref(), &Validation::default())?; println!("token data:\n{:#?}", &token_data); Ok(()) diff --git a/examples/custom_header.rs b/examples/custom_header.rs index d4e5c58..f298c8d 100644 --- a/examples/custom_header.rs +++ b/examples/custom_header.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use jsonwebtoken::errors::ErrorKind; -use jsonwebtoken::{decode, encode, Algorithm, Header, Key, Validation}; +use jsonwebtoken::{decode, encode, Algorithm, Header, Validation}; #[derive(Debug, Serialize, Deserialize)] struct Claims { @@ -19,20 +19,19 @@ fn main() { header.kid = Some("signing_key".to_owned()); header.alg = Algorithm::HS512; - let token = match encode(&header, &my_claims, Key::Hmac(key)) { + let token = match encode(&header, &my_claims, key) { Ok(t) => t, Err(_) => panic!(), // in practice you would return the error }; println!("{:?}", token); - let token_data = - match decode::(&token, Key::Hmac(key), &Validation::new(Algorithm::HS512)) { - Ok(c) => c, - Err(err) => match *err.kind() { - ErrorKind::InvalidToken => panic!(), // Example on how to handle a specific error - _ => panic!(), - }, - }; + let token_data = match decode::(&token, key, &Validation::new(Algorithm::HS512)) { + Ok(c) => c, + Err(err) => match *err.kind() { + ErrorKind::InvalidToken => panic!(), // Example on how to handle a specific error + _ => panic!(), + }, + }; println!("{:?}", token_data.claims); println!("{:?}", token_data.header); } diff --git a/examples/validation.rs b/examples/validation.rs index bfc6bcc..6c22d3f 100644 --- a/examples/validation.rs +++ b/examples/validation.rs @@ -1,5 +1,5 @@ use jsonwebtoken::errors::ErrorKind; -use jsonwebtoken::{decode, encode, Header, Key, Validation}; +use jsonwebtoken::{decode, encode, Header, Validation}; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] @@ -13,13 +13,13 @@ fn main() { let my_claims = Claims { sub: "b@b.com".to_owned(), company: "ACME".to_owned(), exp: 10000000000 }; let key = b"secret"; - let token = match encode(&Header::default(), &my_claims, Key::Hmac(key)) { + let token = match encode(&Header::default(), &my_claims, key) { Ok(t) => t, Err(_) => panic!(), // in practice you would return the error }; let validation = Validation { sub: Some("b@b.com".to_string()), ..Validation::default() }; - let token_data = match decode::(&token, Key::Hmac(key), &validation) { + let token_data = match decode::(&token, key, &validation) { Ok(c) => c, Err(err) => match *err.kind() { ErrorKind::InvalidToken => panic!("Token is invalid"), // Example on how to handle a specific error diff --git a/src/algorithms.rs b/src/algorithms.rs index 09c8b17..6054125 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -1,4 +1,4 @@ -use crate::errors::{new_error, Error, ErrorKind, Result}; +use crate::errors::{Error, ErrorKind, Result}; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -53,7 +53,7 @@ impl FromStr for Algorithm { "PS384" => Ok(Algorithm::PS384), "PS512" => Ok(Algorithm::PS512), "RS512" => Ok(Algorithm::RS512), - _ => Err(new_error(ErrorKind::InvalidAlgorithmName)), + _ => Err(ErrorKind::InvalidAlgorithmName.into()), } } } diff --git a/src/crypto.rs b/src/crypto.rs index 804f082..ad95dc1 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -3,32 +3,23 @@ use ring::constant_time::verify_slices_are_equal; use ring::{hmac, rand, signature}; use crate::algorithms::Algorithm; -use crate::errors::{new_error, ErrorKind, Result}; -use crate::keys::Key; +use crate::errors::{ErrorKind, Result}; +use crate::pem_decoder::PemEncodedKey; /// The actual HS signing + encoding -fn sign_hmac(alg: hmac::Algorithm, key: Key, signing_input: &str) -> Result { - let signing_key = match key { - Key::Hmac(bytes) => hmac::Key::new(alg, bytes), - _ => return Err(ErrorKind::InvalidKeyFormat.into()), - }; - let digest = hmac::sign(&signing_key, signing_input.as_bytes()); - +fn sign_hmac(alg: hmac::Algorithm, key: &[u8], signing_input: &str) -> Result { + let digest = hmac::sign(&hmac::Key::new(alg, key), signing_input.as_bytes()); Ok(base64::encode_config::(&digest, base64::URL_SAFE_NO_PAD)) } /// The actual ECDSA signing + encoding fn sign_ecdsa( alg: &'static signature::EcdsaSigningAlgorithm, - key: Key, + key: &[u8], signing_input: &str, ) -> Result { - let signing_key = match key { - Key::Pkcs8(bytes) => signature::EcdsaKeyPair::from_pkcs8(alg, bytes)?, - _ => { - return Err(new_error(ErrorKind::InvalidKeyFormat)); - } - }; + let pem_key = PemEncodedKey::new(key)?; + let signing_key = signature::EcdsaKeyPair::from_pkcs8(alg, pem_key.as_ec_private_key()?)?; let rng = rand::SystemRandom::new(); let sig = signing_key.sign(&rng, signing_input.as_bytes())?; Ok(base64::encode_config(&sig, base64::URL_SAFE_NO_PAD)) @@ -38,20 +29,12 @@ fn sign_ecdsa( /// Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html fn sign_rsa( alg: &'static dyn signature::RsaEncoding, - key: Key, + key: &[u8], signing_input: &str, ) -> Result { - let key_pair = match key { - Key::Der(bytes) => { - signature::RsaKeyPair::from_der(bytes).map_err(|_| ErrorKind::InvalidRsaKey)? - } - Key::Pkcs8(bytes) => { - signature::RsaKeyPair::from_pkcs8(bytes).map_err(|_| ErrorKind::InvalidRsaKey)? - } - _ => { - return Err(ErrorKind::InvalidKeyFormat.into()); - } - }; + let pem_key = PemEncodedKey::new(key)?; + let key_pair = signature::RsaKeyPair::from_der(pem_key.as_rsa_key()?) + .map_err(|_| ErrorKind::InvalidRsaKey)?; let mut signature = vec![0; key_pair.public_modulus_len()]; let rng = rand::SystemRandom::new(); @@ -66,7 +49,7 @@ fn sign_rsa( /// the base64 url safe encoded of the result. /// /// Only use this function if you want to do something other than JWT. -pub fn sign(signing_input: &str, key: Key, algorithm: Algorithm) -> Result { +pub fn sign(signing_input: &str, key: &[u8], algorithm: Algorithm) -> Result { match algorithm { Algorithm::HS256 => sign_hmac(hmac::HMAC_SHA256, key, signing_input), Algorithm::HS384 => sign_hmac(hmac::HMAC_SHA384, key, signing_input), @@ -107,36 +90,20 @@ fn verify_ring_es( alg: &'static dyn signature::VerificationAlgorithm, signature: &str, signing_input: &str, - key: Key, + key: &[u8], ) -> Result { - let bytes = match key { - Key::Pkcs8(bytes) => bytes, - _ => { - return Err(ErrorKind::InvalidKeyFormat.into()); - } - }; - verify_ring(alg, signature, signing_input, bytes) + let pem_key = PemEncodedKey::new(key)?; + verify_ring(alg, signature, signing_input, pem_key.as_ec_public_key()?) } fn verify_ring_rsa( alg: &'static signature::RsaParameters, signature: &str, signing_input: &str, - key: Key, + key: &[u8], ) -> Result { - match key { - Key::Der(bytes) | Key::Pkcs8(bytes) => verify_ring(alg, signature, signing_input, bytes), - Key::ModulusExponent(n, e) => { - let public_key = signature::RsaPublicKeyComponents { n, e }; - - let signature_bytes = base64::decode_config(signature, base64::URL_SAFE_NO_PAD)?; - - let res = public_key.verify(alg, signing_input.as_bytes(), &signature_bytes); - - Ok(res.is_ok()) - } - _ => Err(ErrorKind::InvalidKeyFormat.into()), - } + let pem_key = PemEncodedKey::new(key)?; + verify_ring(alg, signature, signing_input, pem_key.as_rsa_key()?) } /// Compares the signature given with a re-computed signature for HMAC or using the public key @@ -150,7 +117,7 @@ fn verify_ring_rsa( pub fn verify( signature: &str, signing_input: &str, - key: Key, + key: &[u8], algorithm: Algorithm, ) -> Result { match algorithm { diff --git a/src/errors.rs b/src/errors.rs index 0c8b148..3973139 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -92,11 +92,12 @@ impl StdError for Error { ErrorKind::ImmatureSignature => "immature signature", ErrorKind::InvalidAlgorithm => "algorithms don't match", ErrorKind::InvalidAlgorithmName => "not a known algorithm", + ErrorKind::InvalidKeyFormat => "invalid key format", + ErrorKind::__Nonexhaustive => "unknown error", ErrorKind::Base64(ref err) => err.description(), ErrorKind::Json(ref err) => err.description(), ErrorKind::Utf8(ref err) => err.description(), ErrorKind::Crypto(ref err) => err.description(), - _ => unreachable!(), } } @@ -113,11 +114,12 @@ impl StdError for Error { ErrorKind::ImmatureSignature => None, ErrorKind::InvalidAlgorithm => None, ErrorKind::InvalidAlgorithmName => None, + ErrorKind::InvalidKeyFormat => None, + ErrorKind::__Nonexhaustive => None, ErrorKind::Base64(ref err) => Some(err), ErrorKind::Json(ref err) => Some(err), ErrorKind::Utf8(ref err) => Some(err), ErrorKind::Crypto(ref err) => Some(err), - _ => unreachable!(), } } } @@ -135,11 +137,13 @@ impl fmt::Display for Error { | ErrorKind::InvalidSubject | ErrorKind::ImmatureSignature | ErrorKind::InvalidAlgorithm + | ErrorKind::InvalidKeyFormat | ErrorKind::InvalidAlgorithmName => write!(f, "{}", self.description()), ErrorKind::Json(ref err) => write!(f, "JSON error: {}", err), ErrorKind::Utf8(ref err) => write!(f, "UTF-8 error: {}", err), ErrorKind::Crypto(ref err) => write!(f, "Crypto error: {}", err), - _ => unreachable!(), + ErrorKind::Base64(ref err) => write!(f, "Base64 error: {}", err), + ErrorKind::__Nonexhaustive => write!(f, "Unknown error"), } } } diff --git a/src/keys.rs b/src/keys.rs deleted file mode 100644 index 0d2e6c4..0000000 --- a/src/keys.rs +++ /dev/null @@ -1,16 +0,0 @@ -/// The supported RSA key formats, see the documentation for ring::signature::RsaKeyPair -/// for more information -#[derive(Debug, PartialEq)] -pub enum Key<'a> { - /// An unencrypted PKCS#8-encoded key. Can be used with both ECDSA and RSA - /// algorithms when signing. See ring for information. - Pkcs8(&'a [u8]), - /// A binary DER-encoded ASN.1 key. Can only be used with RSA algorithms - /// when signing. See ring for more information - Der(&'a [u8]), - /// This is not a key format, but provided for convenience since HMAC is - /// a supported signing algorithm. - Hmac(&'a [u8]), - /// A Modulus/exponent for a RSA public key - ModulusExponent(&'a [u8], &'a [u8]), -} diff --git a/src/lib.rs b/src/lib.rs index 5a81b2f..c11b6a6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,6 @@ mod crypto; /// All the errors pub mod errors; mod header; -mod keys; mod pem_decoder; mod pem_encoder; mod serialization; @@ -17,8 +16,6 @@ mod validation; pub use algorithms::Algorithm; pub use crypto::{sign, verify}; pub use header::Header; -pub use keys::Key; -pub use pem_decoder::PemEncodedKey; pub use serialization::TokenData; pub use validation::Validation; @@ -49,9 +46,9 @@ use crate::validation::validate; /// /// // my_claims is a struct that implements Serialize /// // This will create a JWT using HS256 as algorithm -/// let token = encode(&Header::default(), &my_claims, Key::Hmac("secret".as_ref())).unwrap(); +/// let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap(); /// ``` -pub fn encode(header: &Header, claims: &T, key: Key) -> Result { +pub fn encode(header: &Header, claims: &T, key: &[u8]) -> Result { let encoded_header = encode_part(&header)?; let encoded_claims = encode_part(&claims)?; let signing_input = [encoded_header.as_ref(), encoded_claims.as_ref()].join("."); @@ -93,7 +90,7 @@ macro_rules! expect_two { /// ``` pub fn decode( token: &str, - key: Key, + key: &[u8], validation: &Validation, ) -> Result> { let (signature, signing_input) = expect_two!(token.rsplitn(2, '.')); @@ -160,33 +157,6 @@ pub fn decode_header(token: &str) -> Result
{ Header::from_encoded(header) } -/// Decode a PEM string to obtain its key -/// -/// This must be a tagged PEM encoded key, tags start with `-----BEGIN ..-----` -/// and end with a `-----END ..-----` -/// -/// ```rust -/// use jsonwebtoken::{decode_pem, sign, verify, Algorithm}; -/// -/// let pem_content = "-----BEGIN PRIVATE KEY----- -/// MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgWTFfCGljY6aw3Hrt -/// kHmPRiazukxPLb6ilpRAewjW8nihRANCAATDskChT+Altkm9X7MI69T3IUmrQU0L -/// 950IxEzvw/x5BMEINRMrXLBJhqzO9Bm+d6JbqA21YQmd1Kt4RzLJR1W+ -/// -----END PRIVATE KEY-----"; -/// -/// // First use decode_pem from jsonwebtoken -/// let privkey_pem = decode_pem(pem_content).unwrap(); -/// // If it decodes Ok, then you can start using it with a given algorithm -/// let privkey = privkey_pem.as_key().unwrap(); -/// -/// // When using the as_key function, you do not need to wrap in Key::Der or Key::Pkcs8 -/// // The same code can be used for public keys too. -/// let encrypted = sign("hello world", privkey, Algorithm::ES256).unwrap(); -/// ``` -pub fn decode_pem(content: &str) -> Result { - PemEncodedKey::read(content) -} - /// TODO pub fn encode_rsa_public_pkcs1_pem(modulus: &[u8], exponent: &[u8]) -> Result { pem_encoder::encode_rsa_public_pkcs1_pem(modulus, exponent) diff --git a/src/pem_decoder.rs b/src/pem_decoder.rs index c42b042..2c29f7d 100644 --- a/src/pem_decoder.rs +++ b/src/pem_decoder.rs @@ -1,5 +1,4 @@ use crate::errors::{ErrorKind, Result}; -use crate::keys::Key; extern crate pem; extern crate simple_asn1; @@ -9,10 +8,10 @@ use simple_asn1::{BigUint, OID}; /// Supported PEM files for EC and RSA Public and Private Keys #[derive(Debug, PartialEq)] enum PemType { - EcPublicKey, - EcPrivateKey, - RsaPublicKey, - RsaPrivateKey, + EcPublic, + EcPrivate, + RsaPublic, + RsaPrivate, } #[derive(Debug, PartialEq)] @@ -42,7 +41,7 @@ enum Classification { /// PKCS#1: https://tools.ietf.org/html/rfc8017 /// PKCS#8: https://tools.ietf.org/html/rfc5958 #[derive(Debug)] -pub struct PemEncodedKey { +pub(crate) struct PemEncodedKey { content: Vec, asn1: Vec, pem_type: PemType, @@ -51,7 +50,7 @@ pub struct PemEncodedKey { impl PemEncodedKey { /// Read the PEM file for later key use - pub fn read(input: &str) -> Result { + pub fn new(input: &[u8]) -> Result { match pem::parse(input) { Ok(content) => { let pem_contents = content.contents; @@ -65,13 +64,13 @@ impl PemEncodedKey { "RSA PRIVATE KEY" => Ok(PemEncodedKey { content: pem_contents, asn1: asn1_content, - pem_type: PemType::RsaPrivateKey, + pem_type: PemType::RsaPrivate, standard: Standard::Pkcs1, }), "RSA PUBLIC KEY" => Ok(PemEncodedKey { content: pem_contents, asn1: asn1_content, - pem_type: PemType::RsaPublicKey, + pem_type: PemType::RsaPublic, standard: Standard::Pkcs1, }), @@ -86,16 +85,16 @@ impl PemEncodedKey { let pem_type = match c { Classification::Ec => { if is_private { - PemType::EcPrivateKey + PemType::EcPrivate } else { - PemType::EcPublicKey + PemType::EcPublic } } Classification::Rsa => { if is_private { - PemType::RsaPrivateKey + PemType::RsaPrivate } else { - PemType::RsaPublicKey + PemType::RsaPublic } } }; @@ -117,18 +116,36 @@ impl PemEncodedKey { } } - /// This will do the initial parsing of a PEM file. - /// Supported tagged pems include "RSA PRIVATE KEY", "RSA PUBLIC KEY", - /// "PRIVATE KEY", "PUBLIC KEY" - /// PEMs with multiple tagged portions are not supported - pub fn as_key(&self) -> Result> { + /// Can only be PKCS8 + pub fn as_ec_private_key(&self) -> Result<&[u8]> { match self.standard { - Standard::Pkcs1 => Ok(Key::Der(self.content.as_slice())), + Standard::Pkcs1 => Err(ErrorKind::InvalidKeyFormat.into()), Standard::Pkcs8 => match self.pem_type { - PemType::RsaPrivateKey => Ok(Key::Der(extract_first_bitstring(&self.asn1)?)), - PemType::RsaPublicKey => Ok(Key::Der(extract_first_bitstring(&self.asn1)?)), - PemType::EcPrivateKey => Ok(Key::Pkcs8(self.content.as_slice())), - PemType::EcPublicKey => Ok(Key::Pkcs8(extract_first_bitstring(&self.asn1)?)), + PemType::EcPrivate => Ok(self.content.as_slice()), + _ => Err(ErrorKind::InvalidKeyFormat.into()), + }, + } + } + + /// Can only be PKCS8 + pub fn as_ec_public_key(&self) -> Result<&[u8]> { + match self.standard { + Standard::Pkcs1 => Err(ErrorKind::InvalidKeyFormat.into()), + Standard::Pkcs8 => match self.pem_type { + PemType::EcPublic => extract_first_bitstring(&self.asn1), + _ => Err(ErrorKind::InvalidKeyFormat.into()), + }, + } + } + + /// Can be PKCS1 or PKCS8 + pub fn as_rsa_key(&self) -> Result<&[u8]> { + match self.standard { + Standard::Pkcs1 => Ok(self.content.as_slice()), + Standard::Pkcs8 => match self.pem_type { + PemType::RsaPrivate => extract_first_bitstring(&self.asn1), + PemType::RsaPublic => extract_first_bitstring(&self.asn1), + _ => Err(ErrorKind::InvalidKeyFormat.into()), }, } } diff --git a/src/pem_encoder.rs b/src/pem_encoder.rs index d719fc1..ef7ba9d 100644 --- a/src/pem_encoder.rs +++ b/src/pem_encoder.rs @@ -99,102 +99,100 @@ fn encode_ec_public_asn1(x: &[u8]) -> ASN1Block { #[cfg(test)] mod tests { use super::*; - use crate::decode_pem; - use crate::keys::Key; use ring::{signature, signature::KeyPair}; - #[test] - fn public_key_encoding_pkcs1() { - let privkey_pem = - decode_pem(include_str!("../tests/rsa/private_rsa_key_pkcs8.pem")).unwrap(); - let privkey = privkey_pem.as_key().unwrap(); - let ring_key = signature::RsaKeyPair::from_der(match privkey { - Key::Der(bytes) => bytes, - _ => panic!("Unexpected"), - }) - .unwrap(); - let mut modulus = vec![0]; - modulus.extend(ring_key.public_key().modulus().big_endian_without_leading_zero()); - let exponent = ring_key.public_key().exponent(); - - let public_key_pkcs1_pem = encode_rsa_public_pkcs1_pem( - modulus.as_ref(), - exponent.big_endian_without_leading_zero(), - ) - .unwrap(); - assert_eq!( - include_str!("../tests/rsa/public_rsa_key_pkcs1.pem").trim(), - public_key_pkcs1_pem.replace('\r', "").trim() - ); - - let public_key_pkcs1_der = encode_rsa_public_pkcs1_der( - modulus.as_ref(), - exponent.big_endian_without_leading_zero(), - ) - .unwrap(); - assert_eq!( - include_bytes!("../tests/rsa/public_rsa_key.der").to_vec(), - public_key_pkcs1_der - ); - } - - #[test] - fn public_key_encoding_pkcs8() { - let privkey_pem = - decode_pem(include_str!("../tests/rsa/private_rsa_key_pkcs8.pem")).unwrap(); - let privkey = privkey_pem.as_key().unwrap(); - let ring_key = signature::RsaKeyPair::from_der(match privkey { - Key::Der(bytes) => bytes, - _ => panic!("Unexpected"), - }) - .unwrap(); - let mut modulus = vec![0]; - modulus.extend(ring_key.public_key().modulus().big_endian_without_leading_zero()); - let exponent = ring_key.public_key().exponent(); - - let public_key_pkcs8 = encode_rsa_public_pkcs8_pem( - modulus.as_ref(), - exponent.big_endian_without_leading_zero(), - ) - .unwrap(); - assert_eq!( - include_str!("../tests/rsa/public_rsa_key_pkcs8.pem").trim(), - public_key_pkcs8.replace('\r', "").trim() - ); - } - - #[test] - fn public_key_encoding() { - let privkey_pem = decode_pem(include_str!("../tests/ec/private_ecdsa_key.pem")).unwrap(); - let privkey = privkey_pem.as_key().unwrap(); - let alg = &signature::ECDSA_P256_SHA256_FIXED_SIGNING; - let ring_key = signature::EcdsaKeyPair::from_pkcs8( - alg, - match privkey { - Key::Pkcs8(bytes) => bytes, - _ => panic!("Unexpected"), - }, - ) - .unwrap(); - - let public_key_pem = encode_ec_public_pem(ring_key.public_key().as_ref()).unwrap(); - assert_eq!( - include_str!("../tests/ec/public_ecdsa_key.pem").trim(), - public_key_pem.replace('\r', "").trim() - ); - - let public_key_der = encode_ec_public_der(ring_key.public_key().as_ref()).unwrap(); - // The stored ".pk8" key is just the x coordinate of the EC key - // It's not truly a pkcs8 formatted DER - // To get around that, a prepended binary specifies the EC key, EC name, - // and X coordinate length. The length is unlikely to change.. in the - // event that it does, look at the pem file (convert base64 to hex) and find - // where 0x03, 0x42 don't match up. 0x42 is the length. - let mut stored_pk8_der = vec![ - 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, - 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, - ]; - stored_pk8_der.extend(include_bytes!("../tests/ec/public_ecdsa_key.pk8").to_vec()); - assert_eq!(stored_pk8_der, public_key_der); - } + // #[test] + // fn public_key_encoding_pkcs1() { + // let privkey_pem = + // decode_pem(include_str!("../tests/rsa/private_rsa_key_pkcs8.pem")).unwrap(); + // let privkey = privkey_pem.as_key().unwrap(); + // let ring_key = signature::RsaKeyPair::from_der(match privkey { + // Key::Der(bytes) => bytes, + // _ => panic!("Unexpected"), + // }) + // .unwrap(); + // let mut modulus = vec![0]; + // modulus.extend(ring_key.public_key().modulus().big_endian_without_leading_zero()); + // let exponent = ring_key.public_key().exponent(); + // + // let public_key_pkcs1_pem = encode_rsa_public_pkcs1_pem( + // modulus.as_ref(), + // exponent.big_endian_without_leading_zero(), + // ) + // .unwrap(); + // assert_eq!( + // include_str!("../tests/rsa/public_rsa_key_pkcs1.pem").trim(), + // public_key_pkcs1_pem.replace('\r', "").trim() + // ); + // + // let public_key_pkcs1_der = encode_rsa_public_pkcs1_der( + // modulus.as_ref(), + // exponent.big_endian_without_leading_zero(), + // ) + // .unwrap(); + // assert_eq!( + // include_bytes!("../tests/rsa/public_rsa_key.der").to_vec(), + // public_key_pkcs1_der + // ); + // } + // + // #[test] + // fn public_key_encoding_pkcs8() { + // let privkey_pem = + // decode_pem(include_str!("../tests/rsa/private_rsa_key_pkcs8.pem")).unwrap(); + // let privkey = privkey_pem.as_key().unwrap(); + // let ring_key = signature::RsaKeyPair::from_der(match privkey { + // Key::Der(bytes) => bytes, + // _ => panic!("Unexpected"), + // }) + // .unwrap(); + // let mut modulus = vec![0]; + // modulus.extend(ring_key.public_key().modulus().big_endian_without_leading_zero()); + // let exponent = ring_key.public_key().exponent(); + // + // let public_key_pkcs8 = encode_rsa_public_pkcs8_pem( + // modulus.as_ref(), + // exponent.big_endian_without_leading_zero(), + // ) + // .unwrap(); + // assert_eq!( + // include_str!("../tests/rsa/public_rsa_key_pkcs8.pem").trim(), + // public_key_pkcs8.replace('\r', "").trim() + // ); + // } + // + // #[test] + // fn public_key_encoding() { + // let privkey_pem = decode_pem(include_str!("../tests/ec/private_ecdsa_key.pem")).unwrap(); + // let privkey = privkey_pem.as_key().unwrap(); + // let alg = &signature::ECDSA_P256_SHA256_FIXED_SIGNING; + // let ring_key = signature::EcdsaKeyPair::from_pkcs8( + // alg, + // match privkey { + // Key::Pkcs8(bytes) => bytes, + // _ => panic!("Unexpected"), + // }, + // ) + // .unwrap(); + // + // let public_key_pem = encode_ec_public_pem(ring_key.public_key().as_ref()).unwrap(); + // assert_eq!( + // include_str!("../tests/ec/public_ecdsa_key.pem").trim(), + // public_key_pem.replace('\r', "").trim() + // ); + // + // let public_key_der = encode_ec_public_der(ring_key.public_key().as_ref()).unwrap(); + // // The stored ".pk8" key is just the x coordinate of the EC key + // // It's not truly a pkcs8 formatted DER + // // To get around that, a prepended binary specifies the EC key, EC name, + // // and X coordinate length. The length is unlikely to change.. in the + // // event that it does, look at the pem file (convert base64 to hex) and find + // // where 0x03, 0x42 don't match up. 0x42 is the length. + // let mut stored_pk8_der = vec![ + // 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, + // 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, + // ]; + // stored_pk8_der.extend(include_bytes!("../tests/ec/public_ecdsa_key.pk8").to_vec()); + // assert_eq!(stored_pk8_der, public_key_der); + // } } diff --git a/src/serialization.rs b/src/serialization.rs index d1aafeb..6c6df1f 100644 --- a/src/serialization.rs +++ b/src/serialization.rs @@ -22,7 +22,8 @@ pub fn encode_part(input: &T) -> Result { Ok(base64::encode_config(json.as_bytes(), base64::URL_SAFE_NO_PAD)) } -/// Decodes from base64 and deserializes from JSON to a struct AND a hashmap +/// Decodes from base64 and deserializes from JSON to a struct AND a hashmap of Value so we can +/// run validation on it pub fn from_jwt_part_claims, T: DeserializeOwned>( encoded: B, ) -> Result<(T, Map)> { diff --git a/tests/ec/mod.rs b/tests/ec/mod.rs index cc3321e..b02721c 100644 --- a/tests/ec/mod.rs +++ b/tests/ec/mod.rs @@ -1,5 +1,5 @@ use chrono::Utc; -use jsonwebtoken::{decode, decode_pem, encode, sign, verify, Algorithm, Header, Key, Validation}; +use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Header, Validation}; use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] @@ -9,22 +9,21 @@ pub struct Claims { exp: i64, } -#[test] -fn round_trip_sign_verification_pk8() { - let privkey = include_bytes!("private_ecdsa_key.pk8"); - let encrypted = sign("hello world", Key::Pkcs8(&privkey[..]), Algorithm::ES256).unwrap(); - let pubkey = include_bytes!("public_ecdsa_key.pk8"); - let is_valid = verify(&encrypted, "hello world", Key::Pkcs8(pubkey), Algorithm::ES256).unwrap(); - assert!(is_valid); -} +// TODO: remove completely? +//#[test] +//fn round_trip_sign_verification_pk8() { +// let privkey = include_bytes!("private_ecdsa_key.pk8"); +// let encrypted = sign("hello world", privkey, Algorithm::ES256).unwrap(); +// let pubkey = include_bytes!("public_ecdsa_key.pk8"); +// let is_valid = verify(&encrypted, "hello world", pubkey, Algorithm::ES256).unwrap(); +// assert!(is_valid); +//} #[test] fn round_trip_sign_verification_pem() { - let privkey_pem = decode_pem(include_str!("private_ecdsa_key.pem")).unwrap(); - let privkey = privkey_pem.as_key().unwrap(); + let privkey = include_bytes!("private_ecdsa_key.pem"); let encrypted = sign("hello world", privkey, Algorithm::ES256).unwrap(); - let pubkey_pem = decode_pem(include_str!("public_ecdsa_key.pem")).unwrap(); - let pubkey = pubkey_pem.as_key().unwrap(); + let pubkey = include_bytes!("public_ecdsa_key.pem"); let is_valid = verify(&encrypted, "hello world", pubkey, Algorithm::ES256).unwrap(); assert!(is_valid); } @@ -36,12 +35,10 @@ fn round_trip_claim() { company: "ACME".to_string(), exp: Utc::now().timestamp() + 10000, }; - let privkey = include_bytes!("private_ecdsa_key.pk8"); - let token = - encode(&Header::new(Algorithm::ES256), &my_claims, Key::Pkcs8(&privkey[..])).unwrap(); - let pubkey = include_bytes!("public_ecdsa_key.pk8"); - let token_data = - decode::(&token, Key::Pkcs8(pubkey), &Validation::new(Algorithm::ES256)).unwrap(); + let privkey = include_bytes!("private_ecdsa_key.pem"); + let token = encode(&Header::new(Algorithm::ES256), &my_claims, privkey).unwrap(); + let pubkey = include_bytes!("public_ecdsa_key.pem"); + let token_data = decode::(&token, pubkey, &Validation::new(Algorithm::ES256)).unwrap(); assert_eq!(my_claims, token_data.claims); assert!(token_data.header.kid.is_none()); } diff --git a/tests/hmac.rs b/tests/hmac.rs index 89cfaf7..0a245fc 100644 --- a/tests/hmac.rs +++ b/tests/hmac.rs @@ -1,6 +1,6 @@ use chrono::Utc; use jsonwebtoken::{ - dangerous_unsafe_decode, decode, decode_header, encode, sign, verify, Algorithm, Header, Key, + dangerous_unsafe_decode, decode, decode_header, encode, sign, verify, Algorithm, Header, Validation, }; use serde::{Deserialize, Serialize}; @@ -14,7 +14,7 @@ pub struct Claims { #[test] fn sign_hs256() { - let result = sign("hello world", Key::Hmac(b"secret"), Algorithm::HS256).unwrap(); + let result = sign("hello world", b"secret", Algorithm::HS256).unwrap(); let expected = "c0zGLzKEFWj0VxWuufTXiRMk5tlI5MbGDAYhzaxIYjo"; assert_eq!(result, expected); } @@ -22,7 +22,7 @@ fn sign_hs256() { #[test] fn verify_hs256() { let sig = "c0zGLzKEFWj0VxWuufTXiRMk5tlI5MbGDAYhzaxIYjo"; - let valid = verify(sig, "hello world", Key::Hmac(b"secret"), Algorithm::HS256).unwrap(); + let valid = verify(sig, "hello world", b"secret", Algorithm::HS256).unwrap(); assert!(valid); } @@ -35,9 +35,8 @@ fn encode_with_custom_header() { }; let mut header = Header::default(); header.kid = Some("kid".to_string()); - let token = encode(&header, &my_claims, Key::Hmac(b"secret")).unwrap(); - let token_data = - decode::(&token, Key::Hmac(b"secret"), &Validation::default()).unwrap(); + let token = encode(&header, &my_claims, b"secret").unwrap(); + let token_data = decode::(&token, b"secret", &Validation::default()).unwrap(); assert_eq!(my_claims, token_data.claims); assert_eq!("kid", token_data.header.kid.unwrap()); } @@ -49,9 +48,8 @@ fn round_trip_claim() { company: "ACME".to_string(), exp: Utc::now().timestamp() + 10000, }; - let token = encode(&Header::default(), &my_claims, Key::Hmac(b"secret")).unwrap(); - let token_data = - decode::(&token, Key::Hmac(b"secret"), &Validation::default()).unwrap(); + let token = encode(&Header::default(), &my_claims, b"secret").unwrap(); + let token_data = decode::(&token, b"secret", &Validation::default()).unwrap(); assert_eq!(my_claims, token_data.claims); assert!(token_data.header.kid.is_none()); } @@ -59,7 +57,7 @@ fn round_trip_claim() { #[test] fn decode_token() { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.9r56oF7ZliOBlOAyiOFperTGxBtPykRQiWNFxhDCW98"; - let claims = decode::(token, Key::Hmac(b"secret"), &Validation::default()); + let claims = decode::(token, b"secret", &Validation::default()); println!("{:?}", claims); claims.unwrap(); } @@ -68,7 +66,7 @@ fn decode_token() { #[should_panic(expected = "InvalidToken")] fn decode_token_missing_parts() { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; - let claims = decode::(token, Key::Hmac(b"secret"), &Validation::default()); + let claims = decode::(token, b"secret", &Validation::default()); claims.unwrap(); } @@ -77,7 +75,7 @@ fn decode_token_missing_parts() { fn decode_token_invalid_signature() { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.wrong"; - let claims = decode::(token, Key::Hmac(b"secret"), &Validation::default()); + let claims = decode::(token, b"secret", &Validation::default()); claims.unwrap(); } @@ -85,14 +83,14 @@ fn decode_token_invalid_signature() { #[should_panic(expected = "InvalidAlgorithm")] fn decode_token_wrong_algorithm() { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.I1BvFoHe94AFf09O6tDbcSB8-jp8w6xZqmyHIwPeSdY"; - let claims = decode::(token, Key::Hmac(b"secret"), &Validation::new(Algorithm::RS512)); + let claims = decode::(token, b"secret", &Validation::new(Algorithm::RS512)); claims.unwrap(); } #[test] fn decode_token_with_bytes_secret() { let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.Hm0yvKH25TavFPz7J_coST9lZFYH1hQo0tvhvImmaks"; - let claims = decode::(token, Key::Hmac(b"\x01\x02\x03"), &Validation::default()); + let claims = decode::(token, b"\x01\x02\x03", &Validation::default()); assert!(claims.is_ok()); } diff --git a/tests/rsa/mod.rs b/tests/rsa/mod.rs index 8fde515..83c4fde 100644 --- a/tests/rsa/mod.rs +++ b/tests/rsa/mod.rs @@ -1,5 +1,5 @@ use chrono::Utc; -use jsonwebtoken::{decode, decode_pem, encode, sign, verify, Algorithm, Header, Key, Validation}; +use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Header, Validation}; use serde::{Deserialize, Serialize}; const RSA_ALGORITHMS: &[Algorithm] = &[ @@ -18,42 +18,26 @@ pub struct Claims { exp: i64, } -#[test] -fn round_trip_sign_verification_der() { - let privkey = include_bytes!("private_rsa_key.der"); - for &alg in RSA_ALGORITHMS { - let encrypted = sign("hello world", Key::Der(&privkey[..]), alg).unwrap(); - let is_valid = - verify(&encrypted, "hello world", Key::Der(include_bytes!("public_rsa_key.der")), alg) - .unwrap(); - assert!(is_valid); - } -} - #[test] fn round_trip_sign_verification_pem_pkcs1() { - let privkey_pem = decode_pem(include_str!("private_rsa_key_pkcs1.pem")).unwrap(); - let pubkey_pem = decode_pem(include_str!("public_rsa_key_pkcs1.pem")).unwrap(); + let privkey_pem = include_bytes!("private_rsa_key_pkcs1.pem"); + let pubkey_pem = include_bytes!("public_rsa_key_pkcs1.pem"); for &alg in RSA_ALGORITHMS { - let privkey_key = privkey_pem.as_key().unwrap(); - let pubkey_key = pubkey_pem.as_key().unwrap(); - let encrypted = sign("hello world", privkey_key, alg).unwrap(); - let is_valid = verify(&encrypted, "hello world", pubkey_key, alg).unwrap(); + let encrypted = sign("hello world", privkey_pem, alg).unwrap(); + let is_valid = verify(&encrypted, "hello world", pubkey_pem, alg).unwrap(); assert!(is_valid); } } #[test] fn round_trip_sign_verification_pem_pkcs8() { - let privkey_pem = decode_pem(include_str!("private_rsa_key_pkcs8.pem")).unwrap(); - let pubkey_pem = decode_pem(include_str!("public_rsa_key_pkcs8.pem")).unwrap(); + let privkey_pem = include_bytes!("private_rsa_key_pkcs8.pem"); + let pubkey_pem = include_bytes!("public_rsa_key_pkcs8.pem"); for &alg in RSA_ALGORITHMS { - let privkey_key = privkey_pem.as_key().unwrap(); - let pubkey_key = pubkey_pem.as_key().unwrap(); - let encrypted = sign("hello world", privkey_key, alg).unwrap(); - let is_valid = verify(&encrypted, "hello world", pubkey_key, alg).unwrap(); + let encrypted = sign("hello world", privkey_pem, alg).unwrap(); + let is_valid = verify(&encrypted, "hello world", pubkey_pem, alg).unwrap(); assert!(is_valid); } } @@ -65,13 +49,13 @@ fn round_trip_claim() { company: "ACME".to_string(), exp: Utc::now().timestamp() + 10000, }; - let privkey = include_bytes!("private_rsa_key.der"); + let privkey = include_bytes!("private_rsa_key_pkcs1.pem"); for &alg in RSA_ALGORITHMS { - let token = encode(&Header::new(alg), &my_claims, Key::Der(&privkey[..])).unwrap(); + let token = encode(&Header::new(alg), &my_claims, privkey).unwrap(); let token_data = decode::( &token, - Key::Der(include_bytes!("public_rsa_key.der")), + include_bytes!("public_rsa_key_pkcs1.pem"), &Validation::new(alg), ) .unwrap(); @@ -80,52 +64,45 @@ fn round_trip_claim() { } } -#[test] -#[should_panic(expected = "InvalidRsaKey")] -fn fails_with_different_key_format() { - let privkey = include_bytes!("private_rsa_key.der"); - sign("hello world", Key::Pkcs8(&privkey[..]), Algorithm::RS256).unwrap(); -} - -#[test] -fn rsa_modulus_exponent() { - let modulus: Vec = vec![ - 0xc9, 0x11, 0x3a, 0xac, 0x7b, 0x8d, 0x47, 0x44, 0x1b, 0x1c, 0xed, 0xc7, 0xdc, 0xab, 0x76, - 0xa4, 0xe2, 0x86, 0x56, 0x14, 0x2a, 0x19, 0x95, 0xc8, 0x9c, 0xe7, 0x6e, 0x40, 0xdc, 0x57, - 0xce, 0xe2, 0xa5, 0xbd, 0x04, 0xcb, 0x51, 0x3b, 0xf8, 0x97, 0x8b, 0x20, 0x82, 0x1e, 0x7f, - 0x09, 0x86, 0x22, 0xfd, 0xcb, 0xc8, 0xf9, 0x25, 0xd5, 0x4f, 0xd9, 0x0f, 0x59, 0x22, 0x97, - 0xc4, 0x95, 0xc1, 0x5d, 0xdf, 0xf8, 0x2e, 0x4b, 0xdc, 0x3e, 0xe5, 0x1a, 0x90, 0x1a, 0x00, - 0x91, 0xf8, 0x7e, 0x7a, 0x21, 0x55, 0x32, 0x1d, 0x95, 0xad, 0x4c, 0x96, 0xca, 0x3d, 0xcc, - 0x16, 0x5d, 0x07, 0x4d, 0x51, 0x7d, 0x2b, 0x04, 0x57, 0x2c, 0x07, 0x30, 0x91, 0x11, 0x22, - 0x4b, 0x79, 0xe9, 0x4e, 0x11, 0xd1, 0xc8, 0x8c, 0x6e, 0xcb, 0x46, 0x4c, 0x79, 0x97, 0xf1, - 0x54, 0xbe, 0x5a, 0xac, 0xc8, 0x70, 0xd5, 0x24, 0x44, 0x2c, 0x1f, 0x07, 0xa0, 0x67, 0xc6, - 0xfc, 0x0b, 0x47, 0xf3, 0xd0, 0x48, 0x13, 0xd8, 0xc3, 0x04, 0x76, 0x7d, 0x74, 0xb7, 0xa5, - 0x2b, 0xd6, 0xb5, 0xf3, 0x8c, 0xc0, 0x7f, 0xc2, 0xf0, 0xa0, 0xf2, 0xf1, 0xbc, 0x96, 0xf7, - 0x22, 0x5e, 0x67, 0x9d, 0xca, 0x8f, 0x71, 0x27, 0xca, 0x0c, 0x3a, 0x1d, 0x30, 0x50, 0x48, - 0x31, 0xce, 0x25, 0x43, 0x30, 0xca, 0x2f, 0x98, 0x2f, 0x9a, 0x25, 0xcb, 0x5c, 0x1d, 0x40, - 0x18, 0xb9, 0xbc, 0x28, 0x18, 0xdf, 0x13, 0xcb, 0x37, 0x2f, 0x9c, 0x6a, 0x8b, 0xec, 0x94, - 0xa1, 0xdf, 0xa3, 0xf0, 0xcb, 0x6f, 0x22, 0x3f, 0x35, 0xd9, 0xd9, 0x12, 0xe1, 0x03, 0x22, - 0x45, 0x53, 0x7f, 0x6f, 0x2d, 0xa1, 0xdd, 0x96, 0x3c, 0x2d, 0x85, 0x46, 0xae, 0xa6, 0x57, - 0x65, 0x37, 0x20, 0x9f, 0x6b, 0xa3, 0x9f, 0xcb, 0x8a, 0x8d, 0x72, 0xd9, 0x54, 0x3e, 0x53, - 0x75, - ]; - let exponent: Vec = vec![0x01, 0x00, 0x01]; - let privkey = include_bytes!("private_rsa_key.der"); - - let encrypted = sign("hello world", Key::Der(&privkey[..]), Algorithm::RS256).unwrap(); - let is_valid = verify( - &encrypted, - "hello world", - Key::ModulusExponent(&modulus, &exponent), - Algorithm::RS256, - ) - .unwrap(); - assert!(is_valid); -} +//#[test] +//fn rsa_modulus_exponent() { +// let modulus: Vec = vec![ +// 0xc9, 0x11, 0x3a, 0xac, 0x7b, 0x8d, 0x47, 0x44, 0x1b, 0x1c, 0xed, 0xc7, 0xdc, 0xab, 0x76, +// 0xa4, 0xe2, 0x86, 0x56, 0x14, 0x2a, 0x19, 0x95, 0xc8, 0x9c, 0xe7, 0x6e, 0x40, 0xdc, 0x57, +// 0xce, 0xe2, 0xa5, 0xbd, 0x04, 0xcb, 0x51, 0x3b, 0xf8, 0x97, 0x8b, 0x20, 0x82, 0x1e, 0x7f, +// 0x09, 0x86, 0x22, 0xfd, 0xcb, 0xc8, 0xf9, 0x25, 0xd5, 0x4f, 0xd9, 0x0f, 0x59, 0x22, 0x97, +// 0xc4, 0x95, 0xc1, 0x5d, 0xdf, 0xf8, 0x2e, 0x4b, 0xdc, 0x3e, 0xe5, 0x1a, 0x90, 0x1a, 0x00, +// 0x91, 0xf8, 0x7e, 0x7a, 0x21, 0x55, 0x32, 0x1d, 0x95, 0xad, 0x4c, 0x96, 0xca, 0x3d, 0xcc, +// 0x16, 0x5d, 0x07, 0x4d, 0x51, 0x7d, 0x2b, 0x04, 0x57, 0x2c, 0x07, 0x30, 0x91, 0x11, 0x22, +// 0x4b, 0x79, 0xe9, 0x4e, 0x11, 0xd1, 0xc8, 0x8c, 0x6e, 0xcb, 0x46, 0x4c, 0x79, 0x97, 0xf1, +// 0x54, 0xbe, 0x5a, 0xac, 0xc8, 0x70, 0xd5, 0x24, 0x44, 0x2c, 0x1f, 0x07, 0xa0, 0x67, 0xc6, +// 0xfc, 0x0b, 0x47, 0xf3, 0xd0, 0x48, 0x13, 0xd8, 0xc3, 0x04, 0x76, 0x7d, 0x74, 0xb7, 0xa5, +// 0x2b, 0xd6, 0xb5, 0xf3, 0x8c, 0xc0, 0x7f, 0xc2, 0xf0, 0xa0, 0xf2, 0xf1, 0xbc, 0x96, 0xf7, +// 0x22, 0x5e, 0x67, 0x9d, 0xca, 0x8f, 0x71, 0x27, 0xca, 0x0c, 0x3a, 0x1d, 0x30, 0x50, 0x48, +// 0x31, 0xce, 0x25, 0x43, 0x30, 0xca, 0x2f, 0x98, 0x2f, 0x9a, 0x25, 0xcb, 0x5c, 0x1d, 0x40, +// 0x18, 0xb9, 0xbc, 0x28, 0x18, 0xdf, 0x13, 0xcb, 0x37, 0x2f, 0x9c, 0x6a, 0x8b, 0xec, 0x94, +// 0xa1, 0xdf, 0xa3, 0xf0, 0xcb, 0x6f, 0x22, 0x3f, 0x35, 0xd9, 0xd9, 0x12, 0xe1, 0x03, 0x22, +// 0x45, 0x53, 0x7f, 0x6f, 0x2d, 0xa1, 0xdd, 0x96, 0x3c, 0x2d, 0x85, 0x46, 0xae, 0xa6, 0x57, +// 0x65, 0x37, 0x20, 0x9f, 0x6b, 0xa3, 0x9f, 0xcb, 0x8a, 0x8d, 0x72, 0xd9, 0x54, 0x3e, 0x53, +// 0x75, +// ]; +// let exponent: Vec = vec![0x01, 0x00, 0x01]; +// let privkey = include_bytes!("private_rsa_key.der"); +// +// let encrypted = sign("hello world", Key::Der(&privkey[..]), Algorithm::RS256).unwrap(); +// let is_valid = verify( +// &encrypted, +// "hello world", +// Key::ModulusExponent(&modulus, &exponent), +// Algorithm::RS256, +// ) +// .unwrap(); +// assert!(is_valid); +//} #[test] #[should_panic(expected = "InvalidKeyFormat")] fn fails_with_non_pkcs8_key_format() { - let privkey = include_bytes!("private_rsa_key.der"); - let _encrypted = sign("hello world", Key::Der(&privkey[..]), Algorithm::ES256).unwrap(); + let _encrypted = + sign("hello world", include_bytes!("private_rsa_key_pkcs1.pem"), Algorithm::ES256).unwrap(); } From 34ea1941791ec2f01f8ac36268f8e016dfa6c679 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Fri, 8 Nov 2019 19:00:19 +0000 Subject: [PATCH 32/55] Move crypto to a dir --- src/crypto/ecdsa.rs | 18 +++ src/{crypto.rs => crypto/mod.rs} | 132 +++++++++------------- src/crypto/rsa.rs | 25 ++++ src/header.rs | 4 +- src/jwk.rs | 31 +++++ src/lib.rs | 1 + src/serialization.rs | 18 ++- tests/{ec => ecdsa}/mod.rs | 0 tests/{ec => ecdsa}/private_ecdsa_key.pem | 0 tests/{ec => ecdsa}/private_ecdsa_key.pk8 | Bin tests/{ec => ecdsa}/public_ecdsa_key.pem | 0 tests/{ec => ecdsa}/public_ecdsa_key.pk8 | 0 tests/lib.rs | 2 +- 13 files changed, 144 insertions(+), 87 deletions(-) create mode 100644 src/crypto/ecdsa.rs rename src/{crypto.rs => crypto/mod.rs} (62%) create mode 100644 src/crypto/rsa.rs create mode 100644 src/jwk.rs rename tests/{ec => ecdsa}/mod.rs (100%) rename tests/{ec => ecdsa}/private_ecdsa_key.pem (100%) rename tests/{ec => ecdsa}/private_ecdsa_key.pk8 (100%) rename tests/{ec => ecdsa}/public_ecdsa_key.pem (100%) rename tests/{ec => ecdsa}/public_ecdsa_key.pk8 (100%) diff --git a/src/crypto/ecdsa.rs b/src/crypto/ecdsa.rs new file mode 100644 index 0000000..b1f00c9 --- /dev/null +++ b/src/crypto/ecdsa.rs @@ -0,0 +1,18 @@ +use ring::{rand, signature}; + +use crate::errors::{Result}; +use crate::pem_decoder::PemEncodedKey; +use crate::serialization::encode; + +/// The actual ECDSA signing + encoding +pub fn sign( + alg: &'static signature::EcdsaSigningAlgorithm, + key: &[u8], + signing_input: &str, +) -> Result { + let pem_key = PemEncodedKey::new(key)?; + let signing_key = signature::EcdsaKeyPair::from_pkcs8(alg, pem_key.as_ec_private_key()?)?; + let rng = rand::SystemRandom::new(); + let out = signing_key.sign(&rng, signing_input.as_bytes())?; + Ok(encode(out.as_ref())) +} diff --git a/src/crypto.rs b/src/crypto/mod.rs similarity index 62% rename from src/crypto.rs rename to src/crypto/mod.rs index ad95dc1..3ab788a 100644 --- a/src/crypto.rs +++ b/src/crypto/mod.rs @@ -1,54 +1,26 @@ -use base64; use ring::constant_time::verify_slices_are_equal; -use ring::{hmac, rand, signature}; +use ring::{hmac, signature}; use crate::algorithms::Algorithm; -use crate::errors::{ErrorKind, Result}; +use crate::errors::{Result}; use crate::pem_decoder::PemEncodedKey; +use crate::serialization::{encode, decode}; + +pub(crate) mod rsa; +pub(crate) mod ecdsa; /// The actual HS signing + encoding -fn sign_hmac(alg: hmac::Algorithm, key: &[u8], signing_input: &str) -> Result { +pub(crate) fn sign_hmac(alg: hmac::Algorithm, key: &[u8], signing_input: &str) -> Result { let digest = hmac::sign(&hmac::Key::new(alg, key), signing_input.as_bytes()); - Ok(base64::encode_config::(&digest, base64::URL_SAFE_NO_PAD)) + Ok(encode(digest.as_ref())) } -/// The actual ECDSA signing + encoding -fn sign_ecdsa( - alg: &'static signature::EcdsaSigningAlgorithm, - key: &[u8], - signing_input: &str, -) -> Result { - let pem_key = PemEncodedKey::new(key)?; - let signing_key = signature::EcdsaKeyPair::from_pkcs8(alg, pem_key.as_ec_private_key()?)?; - let rng = rand::SystemRandom::new(); - let sig = signing_key.sign(&rng, signing_input.as_bytes())?; - Ok(base64::encode_config(&sig, base64::URL_SAFE_NO_PAD)) -} - -/// The actual RSA signing + encoding -/// Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html -fn sign_rsa( - alg: &'static dyn signature::RsaEncoding, - key: &[u8], - signing_input: &str, -) -> Result { - let pem_key = PemEncodedKey::new(key)?; - let key_pair = signature::RsaKeyPair::from_der(pem_key.as_rsa_key()?) - .map_err(|_| ErrorKind::InvalidRsaKey)?; - - let mut signature = vec![0; key_pair.public_modulus_len()]; - let rng = rand::SystemRandom::new(); - key_pair - .sign(alg, &rng, signing_input.as_bytes(), &mut signature) - .map_err(|_| ErrorKind::InvalidRsaKey)?; - - Ok(base64::encode_config::<[u8]>(&signature, base64::URL_SAFE_NO_PAD)) -} /// Take the payload of a JWT, sign it using the algorithm given and return /// the base64 url safe encoded of the result. /// /// Only use this function if you want to do something other than JWT. +/// `key` is the secret for HMAC and a pem encoded string otherwise pub fn sign(signing_input: &str, key: &[u8], algorithm: Algorithm) -> Result { match algorithm { Algorithm::HS256 => sign_hmac(hmac::HMAC_SHA256, key, signing_input), @@ -56,56 +28,22 @@ pub fn sign(signing_input: &str, key: &[u8], algorithm: Algorithm) -> Result sign_hmac(hmac::HMAC_SHA512, key, signing_input), Algorithm::ES256 => { - sign_ecdsa(&signature::ECDSA_P256_SHA256_FIXED_SIGNING, key, signing_input) + ecdsa::sign(&signature::ECDSA_P256_SHA256_FIXED_SIGNING, key, signing_input) } Algorithm::ES384 => { - sign_ecdsa(&signature::ECDSA_P384_SHA384_FIXED_SIGNING, key, signing_input) + ecdsa::sign(&signature::ECDSA_P384_SHA384_FIXED_SIGNING, key, signing_input) } - Algorithm::RS256 => sign_rsa(&signature::RSA_PKCS1_SHA256, key, signing_input), - Algorithm::RS384 => sign_rsa(&signature::RSA_PKCS1_SHA384, key, signing_input), - Algorithm::RS512 => sign_rsa(&signature::RSA_PKCS1_SHA512, key, signing_input), + Algorithm::RS256 => rsa::sign(&signature::RSA_PKCS1_SHA256, key, signing_input), + Algorithm::RS384 => rsa::sign(&signature::RSA_PKCS1_SHA384, key, signing_input), + Algorithm::RS512 => rsa::sign(&signature::RSA_PKCS1_SHA512, key, signing_input), - Algorithm::PS256 => sign_rsa(&signature::RSA_PSS_SHA256, key, signing_input), - Algorithm::PS384 => sign_rsa(&signature::RSA_PSS_SHA384, key, signing_input), - Algorithm::PS512 => sign_rsa(&signature::RSA_PSS_SHA512, key, signing_input), + Algorithm::PS256 => rsa::sign(&signature::RSA_PSS_SHA256, key, signing_input), + Algorithm::PS384 => rsa::sign(&signature::RSA_PSS_SHA384, key, signing_input), + Algorithm::PS512 => rsa::sign(&signature::RSA_PSS_SHA512, key, signing_input), } } -/// See Ring docs for more details -fn verify_ring( - alg: &'static dyn signature::VerificationAlgorithm, - signature: &str, - signing_input: &str, - key: &[u8], -) -> Result { - let signature_bytes = base64::decode_config(signature, base64::URL_SAFE_NO_PAD)?; - let public_key = signature::UnparsedPublicKey::new(alg, key); - let res = public_key.verify(signing_input.as_bytes(), &signature_bytes); - - Ok(res.is_ok()) -} - -fn verify_ring_es( - alg: &'static dyn signature::VerificationAlgorithm, - signature: &str, - signing_input: &str, - key: &[u8], -) -> Result { - let pem_key = PemEncodedKey::new(key)?; - verify_ring(alg, signature, signing_input, pem_key.as_ec_public_key()?) -} - -fn verify_ring_rsa( - alg: &'static signature::RsaParameters, - signature: &str, - signing_input: &str, - key: &[u8], -) -> Result { - let pem_key = PemEncodedKey::new(key)?; - verify_ring(alg, signature, signing_input, pem_key.as_rsa_key()?) -} - /// Compares the signature given with a re-computed signature for HMAC or using the public key /// for RSA. /// @@ -152,3 +90,39 @@ pub fn verify( } } } + +// TODO: see if we can remove stuff? + +/// See Ring docs for more details +fn verify_ring( + alg: &'static dyn signature::VerificationAlgorithm, + signature: &str, + signing_input: &str, + key: &[u8], +) -> Result { + let signature_bytes = decode(signature)?; + let public_key = signature::UnparsedPublicKey::new(alg, key); + let res = public_key.verify(signing_input.as_bytes(), &signature_bytes); + + Ok(res.is_ok()) +} + +fn verify_ring_es( + alg: &'static dyn signature::VerificationAlgorithm, + signature: &str, + signing_input: &str, + key: &[u8], +) -> Result { + let pem_key = PemEncodedKey::new(key)?; + verify_ring(alg, signature, signing_input, pem_key.as_ec_public_key()?) +} + +fn verify_ring_rsa( + alg: &'static signature::RsaParameters, + signature: &str, + signing_input: &str, + key: &[u8], +) -> Result { + let pem_key = PemEncodedKey::new(key)?; + verify_ring(alg, signature, signing_input, pem_key.as_rsa_key()?) +} diff --git a/src/crypto/rsa.rs b/src/crypto/rsa.rs new file mode 100644 index 0000000..bc4a78c --- /dev/null +++ b/src/crypto/rsa.rs @@ -0,0 +1,25 @@ +use ring::{rand, signature}; + +use crate::errors::{ErrorKind, Result}; +use crate::pem_decoder::PemEncodedKey; +use crate::serialization::encode; + +/// The actual RSA signing + encoding +/// Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html +pub fn sign( + alg: &'static dyn signature::RsaEncoding, + key: &[u8], + signing_input: &str, +) -> Result { + let pem_key = PemEncodedKey::new(key)?; + let key_pair = signature::RsaKeyPair::from_der(pem_key.as_rsa_key()?) + .map_err(|_| ErrorKind::InvalidRsaKey)?; + + let mut signature = vec![0; key_pair.public_modulus_len()]; + let rng = rand::SystemRandom::new(); + key_pair + .sign(alg, &rng, signing_input.as_bytes(), &mut signature) + .map_err(|_| ErrorKind::InvalidRsaKey)?; + + Ok(encode(&signature)) +} diff --git a/src/header.rs b/src/header.rs index 73a2a1d..553dbb3 100644 --- a/src/header.rs +++ b/src/header.rs @@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize}; use crate::algorithms::Algorithm; use crate::errors::Result; +use crate::serialization::decode; + /// A basic JWT header, the alg defaults to HS256 and typ is automatically /// set to `JWT`. All the other fields are optional. @@ -59,7 +61,7 @@ impl Header { /// Converts an encoded part into the Header struct if possible pub(crate) fn from_encoded(encoded_part: &str) -> Result { - let decoded = base64::decode_config(encoded_part, base64::URL_SAFE_NO_PAD)?; + let decoded = decode(encoded_part)?; let s = String::from_utf8(decoded)?; Ok(serde_json::from_str(&s)?) diff --git a/src/jwk.rs b/src/jwk.rs new file mode 100644 index 0000000..bc331f9 --- /dev/null +++ b/src/jwk.rs @@ -0,0 +1,31 @@ +use serde::de::DeserializeOwned; + +use crate::validation::{Validation, validate}; +use crate::header::Header; +use crate::errors::{new_error, ErrorKind, Result}; +use crate::serialization::{from_jwt_part_claims, TokenData}; + + +pub fn decode_rsa_jwk( + token: &str, + modulus: &[u8], + exponent: &[u8], + validation: &Validation, +) -> Result> { + let (signature, signing_input) = expect_two!(token.rsplitn(2, '.')); + let (claims, header) = expect_two!(signing_input.rsplitn(2, '.')); + let header = Header::from_encoded(header)?; + + if !verify(signature, signing_input, key, header.alg)? { + return Err(new_error(ErrorKind::InvalidSignature)); + } + + if !validation.algorithms.contains(&header.alg) { + return Err(new_error(ErrorKind::InvalidAlgorithm)); + } + + let (decoded_claims, claims_map): (T, _) = from_jwt_part_claims(claims)?; + validate(&claims_map, validation)?; + + Ok(TokenData { header, claims: decoded_claims }) +} diff --git a/src/lib.rs b/src/lib.rs index c11b6a6..604f6fc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ mod pem_decoder; mod pem_encoder; mod serialization; mod validation; +// mod jwk; pub use algorithms::Algorithm; pub use crypto::{sign, verify}; diff --git a/src/serialization.rs b/src/serialization.rs index 6c6df1f..0f814d4 100644 --- a/src/serialization.rs +++ b/src/serialization.rs @@ -1,4 +1,3 @@ -use base64; use serde::de::DeserializeOwned; use serde::ser::Serialize; use serde_json::map::Map; @@ -16,19 +15,26 @@ pub struct TokenData { pub claims: T, } +pub(crate) fn encode(input: &[u8]) -> String { + base64::encode_config(input, base64::URL_SAFE_NO_PAD) +} + +pub(crate) fn decode(input: &str) -> Result> { + base64::decode_config(input, base64::URL_SAFE_NO_PAD).map_err(|e| e.into()) +} + /// Serializes a struct to JSON and encodes it in base64 -pub fn encode_part(input: &T) -> Result { +pub(crate) fn encode_part(input: &T) -> Result { let json = to_string(input)?; - Ok(base64::encode_config(json.as_bytes(), base64::URL_SAFE_NO_PAD)) + Ok(encode(json.as_bytes())) } /// Decodes from base64 and deserializes from JSON to a struct AND a hashmap of Value so we can /// run validation on it -pub fn from_jwt_part_claims, T: DeserializeOwned>( +pub(crate) fn from_jwt_part_claims, T: DeserializeOwned>( encoded: B, ) -> Result<(T, Map)> { - let decoded = base64::decode_config(encoded.as_ref(), base64::URL_SAFE_NO_PAD)?; - let s = String::from_utf8(decoded)?; + let s = String::from_utf8(decode(encoded.as_ref())?)?; let claims: T = from_str(&s)?; let map: Map<_, _> = from_str(&s)?; diff --git a/tests/ec/mod.rs b/tests/ecdsa/mod.rs similarity index 100% rename from tests/ec/mod.rs rename to tests/ecdsa/mod.rs diff --git a/tests/ec/private_ecdsa_key.pem b/tests/ecdsa/private_ecdsa_key.pem similarity index 100% rename from tests/ec/private_ecdsa_key.pem rename to tests/ecdsa/private_ecdsa_key.pem diff --git a/tests/ec/private_ecdsa_key.pk8 b/tests/ecdsa/private_ecdsa_key.pk8 similarity index 100% rename from tests/ec/private_ecdsa_key.pk8 rename to tests/ecdsa/private_ecdsa_key.pk8 diff --git a/tests/ec/public_ecdsa_key.pem b/tests/ecdsa/public_ecdsa_key.pem similarity index 100% rename from tests/ec/public_ecdsa_key.pem rename to tests/ecdsa/public_ecdsa_key.pem diff --git a/tests/ec/public_ecdsa_key.pk8 b/tests/ecdsa/public_ecdsa_key.pk8 similarity index 100% rename from tests/ec/public_ecdsa_key.pk8 rename to tests/ecdsa/public_ecdsa_key.pk8 diff --git a/tests/lib.rs b/tests/lib.rs index 30241b4..3760c89 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,2 +1,2 @@ -mod ec; +mod ecdsa; mod rsa; From b27981549f93a0f667e064cd3c7e2956a6a97277 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sat, 9 Nov 2019 11:42:40 +0000 Subject: [PATCH 33/55] Not working yet jwk decoding --- Cargo.toml | 2 +- src/crypto/ecdsa.rs | 2 +- src/crypto/mod.rs | 112 +++++++++++++++++------------------ src/decoding.rs | 136 +++++++++++++++++++++++++++++++++++++++++++ src/header.rs | 1 - src/jwk.rs | 31 ---------- src/lib.rs | 122 ++++---------------------------------- src/serialization.rs | 4 +- tests/rsa/mod.rs | 52 +++++------------ 9 files changed, 222 insertions(+), 240 deletions(-) create mode 100644 src/decoding.rs delete mode 100644 src/jwk.rs diff --git a/Cargo.toml b/Cargo.toml index 75d2314..de7847b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "jsonwebtoken" version = "7.0.0" -authors = ["Vincent Prouillet "] +authors = ["Vincent Prouillet "] license = "MIT" readme = "README.md" description = "Create and parse JWT in a strongly typed way." diff --git a/src/crypto/ecdsa.rs b/src/crypto/ecdsa.rs index b1f00c9..28974e2 100644 --- a/src/crypto/ecdsa.rs +++ b/src/crypto/ecdsa.rs @@ -1,6 +1,6 @@ use ring::{rand, signature}; -use crate::errors::{Result}; +use crate::errors::Result; use crate::pem_decoder::PemEncodedKey; use crate::serialization::encode; diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 3ab788a..6ec34fc 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -1,46 +1,54 @@ use ring::constant_time::verify_slices_are_equal; use ring::{hmac, signature}; +use simple_asn1::BigUint; use crate::algorithms::Algorithm; -use crate::errors::{Result}; +use crate::errors::Result; use crate::pem_decoder::PemEncodedKey; -use crate::serialization::{encode, decode}; +use crate::serialization::{decode, encode}; -pub(crate) mod rsa; pub(crate) mod ecdsa; +pub(crate) mod rsa; /// The actual HS signing + encoding -pub(crate) fn sign_hmac(alg: hmac::Algorithm, key: &[u8], signing_input: &str) -> Result { - let digest = hmac::sign(&hmac::Key::new(alg, key), signing_input.as_bytes()); +pub(crate) fn sign_hmac(alg: hmac::Algorithm, key: &[u8], message: &str) -> Result { + let digest = hmac::sign(&hmac::Key::new(alg, key), message.as_bytes()); Ok(encode(digest.as_ref())) } - /// Take the payload of a JWT, sign it using the algorithm given and return /// the base64 url safe encoded of the result. /// /// Only use this function if you want to do something other than JWT. /// `key` is the secret for HMAC and a pem encoded string otherwise -pub fn sign(signing_input: &str, key: &[u8], algorithm: Algorithm) -> Result { +pub fn sign(message: &str, key: &[u8], algorithm: Algorithm) -> Result { match algorithm { - Algorithm::HS256 => sign_hmac(hmac::HMAC_SHA256, key, signing_input), - Algorithm::HS384 => sign_hmac(hmac::HMAC_SHA384, key, signing_input), - Algorithm::HS512 => sign_hmac(hmac::HMAC_SHA512, key, signing_input), + Algorithm::HS256 => sign_hmac(hmac::HMAC_SHA256, key, message), + Algorithm::HS384 => sign_hmac(hmac::HMAC_SHA384, key, message), + Algorithm::HS512 => sign_hmac(hmac::HMAC_SHA512, key, message), - Algorithm::ES256 => { - ecdsa::sign(&signature::ECDSA_P256_SHA256_FIXED_SIGNING, key, signing_input) - } - Algorithm::ES384 => { - ecdsa::sign(&signature::ECDSA_P384_SHA384_FIXED_SIGNING, key, signing_input) - } + Algorithm::ES256 => ecdsa::sign(&signature::ECDSA_P256_SHA256_FIXED_SIGNING, key, message), + Algorithm::ES384 => ecdsa::sign(&signature::ECDSA_P384_SHA384_FIXED_SIGNING, key, message), - Algorithm::RS256 => rsa::sign(&signature::RSA_PKCS1_SHA256, key, signing_input), - Algorithm::RS384 => rsa::sign(&signature::RSA_PKCS1_SHA384, key, signing_input), - Algorithm::RS512 => rsa::sign(&signature::RSA_PKCS1_SHA512, key, signing_input), + Algorithm::RS256 => rsa::sign(&signature::RSA_PKCS1_SHA256, key, message), + Algorithm::RS384 => rsa::sign(&signature::RSA_PKCS1_SHA384, key, message), + Algorithm::RS512 => rsa::sign(&signature::RSA_PKCS1_SHA512, key, message), - Algorithm::PS256 => rsa::sign(&signature::RSA_PSS_SHA256, key, signing_input), - Algorithm::PS384 => rsa::sign(&signature::RSA_PSS_SHA384, key, signing_input), - Algorithm::PS512 => rsa::sign(&signature::RSA_PSS_SHA512, key, signing_input), + Algorithm::PS256 => rsa::sign(&signature::RSA_PSS_SHA256, key, message), + Algorithm::PS384 => rsa::sign(&signature::RSA_PSS_SHA384, key, message), + Algorithm::PS512 => rsa::sign(&signature::RSA_PSS_SHA512, key, message), + } +} + +fn rsa_alg_to_rsa_parameters(alg: Algorithm) -> &'static signature::RsaParameters { + match alg { + Algorithm::RS256 => &signature::RSA_PKCS1_2048_8192_SHA256, + Algorithm::RS384 => &signature::RSA_PKCS1_2048_8192_SHA384, + Algorithm::RS512 => &signature::RSA_PKCS1_2048_8192_SHA512, + Algorithm::PS256 => &signature::RSA_PSS_2048_8192_SHA256, + Algorithm::PS384 => &signature::RSA_PSS_2048_8192_SHA384, + Algorithm::PS512 => &signature::RSA_PSS_2048_8192_SHA512, + _ => unreachable!("Tried to get RSA signature for a non-rsa algorithm"), } } @@ -52,42 +60,21 @@ pub fn sign(signing_input: &str, key: &[u8], algorithm: Algorithm) -> Result Result { +/// For ECDSA/RSS, the `key` is the pem public key +pub fn verify(signature: &str, message: &str, key: &[u8], algorithm: Algorithm) -> Result { match algorithm { Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => { - // we just re-sign the data with the key and compare if they are equal - let signed = sign(signing_input, key, algorithm)?; + // we just re-sign the message with the key and compare if they are equal + let signed = sign(message, key, algorithm)?; Ok(verify_slices_are_equal(signature.as_ref(), signed.as_ref()).is_ok()) } Algorithm::ES256 => { - verify_ring_es(&signature::ECDSA_P256_SHA256_FIXED, signature, signing_input, key) + verify_ring_es(&signature::ECDSA_P256_SHA256_FIXED, signature, message, key) } Algorithm::ES384 => { - verify_ring_es(&signature::ECDSA_P384_SHA384_FIXED, signature, signing_input, key) - } - Algorithm::RS256 => { - verify_ring_rsa(&signature::RSA_PKCS1_2048_8192_SHA256, signature, signing_input, key) - } - Algorithm::RS384 => { - verify_ring_rsa(&signature::RSA_PKCS1_2048_8192_SHA384, signature, signing_input, key) - } - Algorithm::RS512 => { - verify_ring_rsa(&signature::RSA_PKCS1_2048_8192_SHA512, signature, signing_input, key) - } - Algorithm::PS256 => { - verify_ring_rsa(&signature::RSA_PSS_2048_8192_SHA256, signature, signing_input, key) - } - Algorithm::PS384 => { - verify_ring_rsa(&signature::RSA_PSS_2048_8192_SHA384, signature, signing_input, key) - } - Algorithm::PS512 => { - verify_ring_rsa(&signature::RSA_PSS_2048_8192_SHA512, signature, signing_input, key) + verify_ring_es(&signature::ECDSA_P384_SHA384_FIXED, signature, message, key) } + _ => verify_ring_rsa(rsa_alg_to_rsa_parameters(algorithm), signature, message, key), } } @@ -97,12 +84,12 @@ pub fn verify( fn verify_ring( alg: &'static dyn signature::VerificationAlgorithm, signature: &str, - signing_input: &str, + message: &str, key: &[u8], ) -> Result { let signature_bytes = decode(signature)?; let public_key = signature::UnparsedPublicKey::new(alg, key); - let res = public_key.verify(signing_input.as_bytes(), &signature_bytes); + let res = public_key.verify(message.as_bytes(), &signature_bytes); Ok(res.is_ok()) } @@ -110,19 +97,32 @@ fn verify_ring( fn verify_ring_es( alg: &'static dyn signature::VerificationAlgorithm, signature: &str, - signing_input: &str, + message: &str, key: &[u8], ) -> Result { let pem_key = PemEncodedKey::new(key)?; - verify_ring(alg, signature, signing_input, pem_key.as_ec_public_key()?) + verify_ring(alg, signature, message, pem_key.as_ec_public_key()?) } fn verify_ring_rsa( alg: &'static signature::RsaParameters, signature: &str, - signing_input: &str, + message: &str, key: &[u8], ) -> Result { let pem_key = PemEncodedKey::new(key)?; - verify_ring(alg, signature, signing_input, pem_key.as_rsa_key()?) + verify_ring(alg, signature, message, pem_key.as_rsa_key()?) +} + +pub fn verify_rsa_modulus_exponent( + alg: Algorithm, + signature: &str, + message: &str, + components: (&str, &str), +) -> Result { + let n = BigUint::from_bytes_be(&decode(components.0)?).to_bytes_be(); + let e = BigUint::from_bytes_be(&decode(components.1)?).to_bytes_be(); + let pubkey = signature::RsaPublicKeyComponents { n, e }; + let res = pubkey.verify(rsa_alg_to_rsa_parameters(alg), message.as_ref(), signature.as_ref()); + Ok(res.is_ok()) } diff --git a/src/decoding.rs b/src/decoding.rs new file mode 100644 index 0000000..0be1c4c --- /dev/null +++ b/src/decoding.rs @@ -0,0 +1,136 @@ +use serde::de::DeserializeOwned; + +use crate::crypto::{verify, verify_rsa_modulus_exponent}; +use crate::errors::{new_error, ErrorKind, Result}; +use crate::header::Header; +use crate::serialization::{from_jwt_part_claims, TokenData}; +use crate::validation::{validate, Validation}; + +/// Takes the result of a rsplit and ensure we only get 2 parts +/// Errors if we don't +macro_rules! expect_two { + ($iter:expr) => {{ + let mut i = $iter; + match (i.next(), i.next(), i.next()) { + (Some(first), Some(second), None) => (first, second), + _ => return Err(new_error(ErrorKind::InvalidToken)), + } + }}; +} + +/// Internal way to differentiate between public key types +enum DecodingKey<'a> { + SecretOrPem(&'a [u8]), + RsaModulusExponent { n: &'a str, e: &'a str }, +} + +fn _decode( + token: &str, + key: DecodingKey, + validation: &Validation, +) -> Result> { + let (signature, message) = expect_two!(token.rsplitn(2, '.')); + let (claims, header) = expect_two!(message.rsplitn(2, '.')); + let header = Header::from_encoded(header)?; + + if !validation.algorithms.contains(&header.alg) { + return Err(new_error(ErrorKind::InvalidAlgorithm)); + } + + let is_valid = match key { + DecodingKey::SecretOrPem(k) => verify(signature, message, k, header.alg), + DecodingKey::RsaModulusExponent { n, e } => { + verify_rsa_modulus_exponent(header.alg, signature, message, (n, e)) + } + }?; + + if !is_valid { + return Err(new_error(ErrorKind::InvalidSignature)); + } + + let (decoded_claims, claims_map): (T, _) = from_jwt_part_claims(claims)?; + validate(&claims_map, validation)?; + + Ok(TokenData { header, claims: decoded_claims }) +} + +/// Decode a token into a struct containing 2 fields: `claims` and `header`. +/// +/// If the token or its signature is invalid or the claims fail validation, it will return an error. +/// +/// ```rust +/// use serde::{Deserialize, Serialize}; +/// use jsonwebtoken::{decode, Validation, Algorithm}; +/// +/// #[derive(Debug, Serialize, Deserialize)] +/// struct Claims { +/// sub: String, +/// company: String +/// } +/// +/// let token = "a.jwt.token".to_string(); +/// // Claims is a struct that implements Deserialize +/// let token_message = decode::(&token, "secret".as_ref(), &Validation::new(Algorithm::HS256)); +/// ``` +pub fn decode( + token: &str, + key: &[u8], + validation: &Validation, +) -> Result> { + _decode(token, DecodingKey::SecretOrPem(key), validation) +} + +/// TO TEST +pub fn decode_rsa_jwk( + token: &str, + modulus: &str, + exponent: &str, + validation: &Validation, +) -> Result> { + _decode(token, DecodingKey::RsaModulusExponent { n: modulus, e: exponent }, validation) +} + +/// Decode a token without any signature validation into a struct containing 2 fields: `claims` and `header`. +/// +/// NOTE: Do not use this unless you know what you are doing! If the token's signature is invalid, it will *not* return an error. +/// +/// ```rust +/// use serde::{Deserialize, Serialize}; +/// use jsonwebtoken::{dangerous_unsafe_decode, Validation, Algorithm}; +/// +/// #[derive(Debug, Serialize, Deserialize)] +/// struct Claims { +/// sub: String, +/// company: String +/// } +/// +/// let token = "a.jwt.token".to_string(); +/// // Claims is a struct that implements Deserialize +/// let token_message = dangerous_unsafe_decode::(&token); +/// ``` +pub fn dangerous_unsafe_decode(token: &str) -> Result> { + let (_, message) = expect_two!(token.rsplitn(2, '.')); + let (claims, header) = expect_two!(message.rsplitn(2, '.')); + let header = Header::from_encoded(header)?; + + let (decoded_claims, _): (T, _) = from_jwt_part_claims(claims)?; + + Ok(TokenData { header, claims: decoded_claims }) +} + +/// Decode a token and return the Header. This is not doing any kind of validation: it is meant to be +/// used when you don't know which `alg` the token is using and want to find out. +/// +/// If the token has an invalid format, it will return an error. +/// +/// ```rust +/// use jsonwebtoken::decode_header; +/// +/// let token = "a.jwt.token".to_string(); +/// let header = decode_header(&token); +/// ``` +pub fn decode_header(token: &str) -> Result
{ + let (_, message) = expect_two!(token.rsplitn(2, '.')); + let (_, header) = expect_two!(message.rsplitn(2, '.')); + Header::from_encoded(header) +} diff --git a/src/header.rs b/src/header.rs index 553dbb3..0ced490 100644 --- a/src/header.rs +++ b/src/header.rs @@ -4,7 +4,6 @@ use crate::algorithms::Algorithm; use crate::errors::Result; use crate::serialization::decode; - /// A basic JWT header, the alg defaults to HS256 and typ is automatically /// set to `JWT`. All the other fields are optional. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/src/jwk.rs b/src/jwk.rs deleted file mode 100644 index bc331f9..0000000 --- a/src/jwk.rs +++ /dev/null @@ -1,31 +0,0 @@ -use serde::de::DeserializeOwned; - -use crate::validation::{Validation, validate}; -use crate::header::Header; -use crate::errors::{new_error, ErrorKind, Result}; -use crate::serialization::{from_jwt_part_claims, TokenData}; - - -pub fn decode_rsa_jwk( - token: &str, - modulus: &[u8], - exponent: &[u8], - validation: &Validation, -) -> Result> { - let (signature, signing_input) = expect_two!(token.rsplitn(2, '.')); - let (claims, header) = expect_two!(signing_input.rsplitn(2, '.')); - let header = Header::from_encoded(header)?; - - if !verify(signature, signing_input, key, header.alg)? { - return Err(new_error(ErrorKind::InvalidSignature)); - } - - if !validation.algorithms.contains(&header.alg) { - return Err(new_error(ErrorKind::InvalidAlgorithm)); - } - - let (decoded_claims, claims_map): (T, _) = from_jwt_part_claims(claims)?; - validate(&claims_map, validation)?; - - Ok(TokenData { header, claims: decoded_claims }) -} diff --git a/src/lib.rs b/src/lib.rs index 604f6fc..fa963b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ mod algorithms; mod crypto; +mod decoding; /// All the errors pub mod errors; mod header; @@ -12,29 +13,26 @@ mod pem_decoder; mod pem_encoder; mod serialization; mod validation; -// mod jwk; pub use algorithms::Algorithm; pub use crypto::{sign, verify}; +pub use decoding::{dangerous_unsafe_decode, decode, decode_header, decode_rsa_jwk}; pub use header::Header; pub use serialization::TokenData; pub use validation::Validation; -use serde::de::DeserializeOwned; use serde::ser::Serialize; -use crate::errors::{new_error, ErrorKind, Result}; -use crate::serialization::{encode_part, from_jwt_part_claims}; -use crate::validation::validate; +use crate::errors::Result; +use crate::serialization::encode_part; /// Encode the header and claims given and sign the payload using the algorithm from the header and the key /// -/// ```rust,ignore -/// #[macro_use] -/// extern crate serde_derive; +/// ```rust +/// use serde::{Deserialize, Serialize}; /// use jsonwebtoken::{encode, Algorithm, Header}; /// -/// /// #[derive(Debug, Serialize, Deserialize)] +/// #[derive(Debug, Serialize, Deserialize)] /// struct Claims { /// sub: String, /// company: String @@ -52,110 +50,10 @@ use crate::validation::validate; pub fn encode(header: &Header, claims: &T, key: &[u8]) -> Result { let encoded_header = encode_part(&header)?; let encoded_claims = encode_part(&claims)?; - let signing_input = [encoded_header.as_ref(), encoded_claims.as_ref()].join("."); - let signature = sign(&*signing_input, key, header.alg)?; + let message = [encoded_header.as_ref(), encoded_claims.as_ref()].join("."); + let signature = sign(&*message, key, header.alg)?; - Ok([signing_input, signature].join(".")) -} - -/// Used in decode: takes the result of a rsplit and ensure we only get 2 parts -/// Errors if we don't -macro_rules! expect_two { - ($iter:expr) => {{ - let mut i = $iter; - match (i.next(), i.next(), i.next()) { - (Some(first), Some(second), None) => (first, second), - _ => return Err(new_error(ErrorKind::InvalidToken)), - } - }}; -} - -/// Decode a token into a struct containing 2 fields: `claims` and `header`. -/// -/// If the token or its signature is invalid or the claims fail validation, it will return an error. -/// -/// ```rust,ignore -/// #[macro_use] -/// extern crate serde_derive; -/// use jsonwebtoken::{decode, Validation, Algorithm}; -/// -/// #[derive(Debug, Serialize, Deserialize)] -/// struct Claims { -/// sub: String, -/// company: String -/// } -/// -/// let token = "a.jwt.token".to_string(); -/// // Claims is a struct that implements Deserialize -/// let token_data = decode::(&token, Key::Hmac("secret"), &Validation::new(Algorithm::HS256)); -/// ``` -pub fn decode( - token: &str, - key: &[u8], - validation: &Validation, -) -> Result> { - let (signature, signing_input) = expect_two!(token.rsplitn(2, '.')); - let (claims, header) = expect_two!(signing_input.rsplitn(2, '.')); - let header = Header::from_encoded(header)?; - - if !verify(signature, signing_input, key, header.alg)? { - return Err(new_error(ErrorKind::InvalidSignature)); - } - - if !validation.algorithms.contains(&header.alg) { - return Err(new_error(ErrorKind::InvalidAlgorithm)); - } - - let (decoded_claims, claims_map): (T, _) = from_jwt_part_claims(claims)?; - validate(&claims_map, validation)?; - - Ok(TokenData { header, claims: decoded_claims }) -} - -/// Decode a token without any signature validation into a struct containing 2 fields: `claims` and `header`. -/// -/// NOTE: Do not use this unless you know what you are doing! If the token's signature is invalid, it will *not* return an error. -/// -/// ```rust,ignore -/// #[macro_use] -/// extern crate serde_derive; -/// use jsonwebtoken::{dangerous_unsafe_decode, Validation, Algorithm}; -/// -/// #[derive(Debug, Serialize, Deserialize)] -/// struct Claims { -/// sub: String, -/// company: String -/// } -/// -/// let token = "a.jwt.token".to_string(); -/// // Claims is a struct that implements Deserialize -/// let token_data = dangerous_unsafe_decode::(&token, &Validation::new(Algorithm::HS256)); -/// ``` -pub fn dangerous_unsafe_decode(token: &str) -> Result> { - let (_, signing_input) = expect_two!(token.rsplitn(2, '.')); - let (claims, header) = expect_two!(signing_input.rsplitn(2, '.')); - let header = Header::from_encoded(header)?; - - let (decoded_claims, _): (T, _) = from_jwt_part_claims(claims)?; - - Ok(TokenData { header, claims: decoded_claims }) -} - -/// Decode a token and return the Header. This is not doing any kind of validation: it is meant to be -/// used when you don't know which `alg` the token is using and want to find out. -/// -/// If the token has an invalid format, it will return an error. -/// -/// ```rust,ignore -/// use jsonwebtoken::decode_header; -/// -/// let token = "a.jwt.token".to_string(); -/// let header = decode_header(&token); -/// ``` -pub fn decode_header(token: &str) -> Result
{ - let (_, signing_input) = expect_two!(token.rsplitn(2, '.')); - let (_, header) = expect_two!(signing_input.rsplitn(2, '.')); - Header::from_encoded(header) + Ok([message, signature].join(".")) } /// TODO diff --git a/src/serialization.rs b/src/serialization.rs index 0f814d4..a3a7d4d 100644 --- a/src/serialization.rs +++ b/src/serialization.rs @@ -37,6 +37,6 @@ pub(crate) fn from_jwt_part_claims, T: DeserializeOwned>( let s = String::from_utf8(decode(encoded.as_ref())?)?; let claims: T = from_str(&s)?; - let map: Map<_, _> = from_str(&s)?; - Ok((claims, map)) + let validation_map: Map<_, _> = from_str(&s)?; + Ok((claims, validation_map)) } diff --git a/tests/rsa/mod.rs b/tests/rsa/mod.rs index 83c4fde..6cdb98c 100644 --- a/tests/rsa/mod.rs +++ b/tests/rsa/mod.rs @@ -1,5 +1,5 @@ use chrono::Utc; -use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Header, Validation}; +use jsonwebtoken::{decode, decode_rsa_jwk, encode, sign, verify, Algorithm, Header, Validation}; use serde::{Deserialize, Serialize}; const RSA_ALGORITHMS: &[Algorithm] = &[ @@ -64,41 +64,21 @@ fn round_trip_claim() { } } -//#[test] -//fn rsa_modulus_exponent() { -// let modulus: Vec = vec![ -// 0xc9, 0x11, 0x3a, 0xac, 0x7b, 0x8d, 0x47, 0x44, 0x1b, 0x1c, 0xed, 0xc7, 0xdc, 0xab, 0x76, -// 0xa4, 0xe2, 0x86, 0x56, 0x14, 0x2a, 0x19, 0x95, 0xc8, 0x9c, 0xe7, 0x6e, 0x40, 0xdc, 0x57, -// 0xce, 0xe2, 0xa5, 0xbd, 0x04, 0xcb, 0x51, 0x3b, 0xf8, 0x97, 0x8b, 0x20, 0x82, 0x1e, 0x7f, -// 0x09, 0x86, 0x22, 0xfd, 0xcb, 0xc8, 0xf9, 0x25, 0xd5, 0x4f, 0xd9, 0x0f, 0x59, 0x22, 0x97, -// 0xc4, 0x95, 0xc1, 0x5d, 0xdf, 0xf8, 0x2e, 0x4b, 0xdc, 0x3e, 0xe5, 0x1a, 0x90, 0x1a, 0x00, -// 0x91, 0xf8, 0x7e, 0x7a, 0x21, 0x55, 0x32, 0x1d, 0x95, 0xad, 0x4c, 0x96, 0xca, 0x3d, 0xcc, -// 0x16, 0x5d, 0x07, 0x4d, 0x51, 0x7d, 0x2b, 0x04, 0x57, 0x2c, 0x07, 0x30, 0x91, 0x11, 0x22, -// 0x4b, 0x79, 0xe9, 0x4e, 0x11, 0xd1, 0xc8, 0x8c, 0x6e, 0xcb, 0x46, 0x4c, 0x79, 0x97, 0xf1, -// 0x54, 0xbe, 0x5a, 0xac, 0xc8, 0x70, 0xd5, 0x24, 0x44, 0x2c, 0x1f, 0x07, 0xa0, 0x67, 0xc6, -// 0xfc, 0x0b, 0x47, 0xf3, 0xd0, 0x48, 0x13, 0xd8, 0xc3, 0x04, 0x76, 0x7d, 0x74, 0xb7, 0xa5, -// 0x2b, 0xd6, 0xb5, 0xf3, 0x8c, 0xc0, 0x7f, 0xc2, 0xf0, 0xa0, 0xf2, 0xf1, 0xbc, 0x96, 0xf7, -// 0x22, 0x5e, 0x67, 0x9d, 0xca, 0x8f, 0x71, 0x27, 0xca, 0x0c, 0x3a, 0x1d, 0x30, 0x50, 0x48, -// 0x31, 0xce, 0x25, 0x43, 0x30, 0xca, 0x2f, 0x98, 0x2f, 0x9a, 0x25, 0xcb, 0x5c, 0x1d, 0x40, -// 0x18, 0xb9, 0xbc, 0x28, 0x18, 0xdf, 0x13, 0xcb, 0x37, 0x2f, 0x9c, 0x6a, 0x8b, 0xec, 0x94, -// 0xa1, 0xdf, 0xa3, 0xf0, 0xcb, 0x6f, 0x22, 0x3f, 0x35, 0xd9, 0xd9, 0x12, 0xe1, 0x03, 0x22, -// 0x45, 0x53, 0x7f, 0x6f, 0x2d, 0xa1, 0xdd, 0x96, 0x3c, 0x2d, 0x85, 0x46, 0xae, 0xa6, 0x57, -// 0x65, 0x37, 0x20, 0x9f, 0x6b, 0xa3, 0x9f, 0xcb, 0x8a, 0x8d, 0x72, 0xd9, 0x54, 0x3e, 0x53, -// 0x75, -// ]; -// let exponent: Vec = vec![0x01, 0x00, 0x01]; -// let privkey = include_bytes!("private_rsa_key.der"); -// -// let encrypted = sign("hello world", Key::Der(&privkey[..]), Algorithm::RS256).unwrap(); -// let is_valid = verify( -// &encrypted, -// "hello world", -// Key::ModulusExponent(&modulus, &exponent), -// Algorithm::RS256, -// ) -// .unwrap(); -// assert!(is_valid); -//} +#[test] +fn rsa_modulus_exponent() { + let privkey = include_str!("private_rsa_key_pkcs1.pem"); + let my_claims = Claims { + sub: "b@b.com".to_string(), + company: "ACME".to_string(), + exp: Utc::now().timestamp() + 10000, + }; + let n = "yRE6rHuNR0QbHO3H3Kt2pOKGVhQqGZXInOduQNxXzuKlvQTLUTv4l4sggh5_CYYi_cvI-SXVT9kPWSKXxJXBXd_4LkvcPuUakBoAkfh-eiFVMh2VrUyWyj3MFl0HTVF9KwRXLAcwkREiS3npThHRyIxuy0ZMeZfxVL5arMhw1SRELB8HoGfG_AtH89BIE9jDBHZ9dLelK9a184zAf8LwoPLxvJb3Il5nncqPcSfKDDodMFBIMc4lQzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xqi-yUod-j8MtvIj812dkS4QMiRVN_by2h3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5TdQ"; + let e = "AQAB"; + + let encrypted = encode(&Header::new(Algorithm::RS256), &my_claims, privkey.as_ref()).unwrap(); + let res = decode_rsa_jwk::(&encrypted, n, e, &Validation::new(Algorithm::RS256)); + assert!(res.is_ok()); +} #[test] #[should_panic(expected = "InvalidKeyFormat")] From 614f3610a72b752d9241b06bcaf7c1f27910400a Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Mon, 11 Nov 2019 12:08:11 +0100 Subject: [PATCH 34/55] Fix stupid bug --- src/crypto/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 6ec34fc..8f624aa 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -120,9 +120,10 @@ pub fn verify_rsa_modulus_exponent( message: &str, components: (&str, &str), ) -> Result { + let signature_bytes = decode(signature)?; let n = BigUint::from_bytes_be(&decode(components.0)?).to_bytes_be(); let e = BigUint::from_bytes_be(&decode(components.1)?).to_bytes_be(); let pubkey = signature::RsaPublicKeyComponents { n, e }; - let res = pubkey.verify(rsa_alg_to_rsa_parameters(alg), message.as_ref(), signature.as_ref()); + let res = pubkey.verify(rsa_alg_to_rsa_parameters(alg), message.as_ref(), &signature_bytes); Ok(res.is_ok()) } From 8169ee3d9f15295202f683daf762522a6c093e2e Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Mon, 11 Nov 2019 19:47:35 +0100 Subject: [PATCH 35/55] Remove chrono from deps --- CHANGELOG.md | 1 - Cargo.toml | 14 ++++++++------ src/validation.rs | 37 +++++++++++++++++++++---------------- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2aefd9..acd53a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,6 @@ - Add support for PS256, PS384 and PS512 - Add support for verifying with modulus/exponent components for RSA -- Change API for both sign/verify to take a `Key` enum rather than bytes - Update to 2018 edition - Changed aud field type in Validation to `Option>`. Audience validation now tests for "any-of-these" audience membership. diff --git a/Cargo.toml b/Cargo.toml index de7847b..3ed0158 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,10 +4,10 @@ version = "7.0.0" authors = ["Vincent Prouillet "] license = "MIT" readme = "README.md" -description = "Create and parse JWT in a strongly typed way." -homepage = "https://github.com/Keats/rust-jwt" -repository = "https://github.com/Keats/rust-jwt" -keywords = ["jwt", "web", "api", "token", "json"] +description = "Create and decode JWTs in a strongly typed way." +homepage = "https://github.com/Keats/jsonwebtoken" +repository = "https://github.com/Keats/jsonwebtoken" +keywords = ["jwt", "web", "api", "token", "json", "jwk"] edition = "2018" [dependencies] @@ -15,8 +15,10 @@ serde_json = "1.0" serde = {version = "1.0", features = ["derive"] } ring = { version = "0.16.5", features = ["std"] } base64 = "0.11" -# For validation -chrono = "0.4" # For PEM decoding pem = "0.7" simple_asn1 = "0.4" + +[dev-dependencies] +# For the custom chrono example +chrono = "0.4" diff --git a/src/validation.rs b/src/validation.rs index 280fd72..8488634 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -1,14 +1,15 @@ -use chrono::Utc; +use std::time::{SystemTime, UNIX_EPOCH}; +use std::collections::HashSet; + use serde_json::map::Map; use serde_json::{from_value, Value}; -use std::collections::HashSet; use crate::algorithms::Algorithm; use crate::errors::{new_error, ErrorKind, Result}; /// Contains the various validations that are applied after decoding a token. /// -/// All time validation happen on UTC timestamps. +/// All time validation happen on UTC timestamps as seconds. /// /// ```rust /// use jsonwebtoken::Validation; @@ -30,7 +31,7 @@ pub struct Validation { /// account for clock skew. /// /// Defaults to `0`. - pub leeway: i64, + pub leeway: u64, /// Whether to validate the `exp` field. /// /// It will return an error if the time in the `exp` field is past. @@ -96,12 +97,17 @@ impl Default for Validation { } } +fn get_current_timestamp() -> u64 { + let start = SystemTime::now(); + start.duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs() +} + pub fn validate(claims: &Map, options: &Validation) -> Result<()> { - let now = Utc::now().timestamp(); + let now = get_current_timestamp(); if options.validate_exp { if let Some(exp) = claims.get("exp") { - if from_value::(exp.clone())? < now - options.leeway { + if from_value::(exp.clone())? < now - options.leeway { return Err(new_error(ErrorKind::ExpiredSignature)); } } else { @@ -111,7 +117,7 @@ pub fn validate(claims: &Map, options: &Validation) -> Result<()> if options.validate_nbf { if let Some(nbf) = claims.get("nbf") { - if from_value::(nbf.clone())? > now + options.leeway { + if from_value::(nbf.clone())? > now + options.leeway { return Err(new_error(ErrorKind::ImmatureSignature)); } } else { @@ -155,18 +161,17 @@ pub fn validate(claims: &Map, options: &Validation) -> Result<()> #[cfg(test)] mod tests { - use chrono::Utc; use serde_json::map::Map; use serde_json::to_value; - use super::{validate, Validation}; + use super::{validate, Validation, get_current_timestamp}; use crate::errors::ErrorKind; #[test] fn exp_in_future_ok() { let mut claims = Map::new(); - claims.insert("exp".to_string(), to_value(Utc::now().timestamp() + 10000).unwrap()); + claims.insert("exp".to_string(), to_value(get_current_timestamp() + 10000).unwrap()); let res = validate(&claims, &Validation::default()); assert!(res.is_ok()); } @@ -174,7 +179,7 @@ mod tests { #[test] fn exp_in_past_fails() { let mut claims = Map::new(); - claims.insert("exp".to_string(), to_value(Utc::now().timestamp() - 100000).unwrap()); + claims.insert("exp".to_string(), to_value(get_current_timestamp() - 100000).unwrap()); let res = validate(&claims, &Validation::default()); assert!(res.is_err()); @@ -187,7 +192,7 @@ mod tests { #[test] fn exp_in_past_but_in_leeway_ok() { let mut claims = Map::new(); - claims.insert("exp".to_string(), to_value(Utc::now().timestamp() - 500).unwrap()); + claims.insert("exp".to_string(), to_value(get_current_timestamp() - 500).unwrap()); let validation = Validation { leeway: 1000 * 60, ..Default::default() }; let res = validate(&claims, &validation); assert!(res.is_ok()); @@ -208,7 +213,7 @@ mod tests { #[test] fn nbf_in_past_ok() { let mut claims = Map::new(); - claims.insert("nbf".to_string(), to_value(Utc::now().timestamp() - 10000).unwrap()); + claims.insert("nbf".to_string(), to_value(get_current_timestamp() - 10000).unwrap()); let validation = Validation { validate_exp: false, validate_nbf: true, ..Validation::default() }; let res = validate(&claims, &validation); @@ -218,7 +223,7 @@ mod tests { #[test] fn nbf_in_future_fails() { let mut claims = Map::new(); - claims.insert("nbf".to_string(), to_value(Utc::now().timestamp() + 100000).unwrap()); + claims.insert("nbf".to_string(), to_value(get_current_timestamp() + 100000).unwrap()); let validation = Validation { validate_exp: false, validate_nbf: true, ..Validation::default() }; let res = validate(&claims, &validation); @@ -233,7 +238,7 @@ mod tests { #[test] fn nbf_in_future_but_in_leeway_ok() { let mut claims = Map::new(); - claims.insert("nbf".to_string(), to_value(Utc::now().timestamp() + 500).unwrap()); + claims.insert("nbf".to_string(), to_value(get_current_timestamp() + 500).unwrap()); let validation = Validation { leeway: 1000 * 60, validate_nbf: true, @@ -408,7 +413,7 @@ mod tests { #[test] fn does_validation_in_right_order() { let mut claims = Map::new(); - claims.insert("exp".to_string(), to_value(Utc::now().timestamp() + 10000).unwrap()); + claims.insert("exp".to_string(), to_value(get_current_timestamp() + 10000).unwrap()); let v = Validation { leeway: 5, validate_exp: true, From 1f6d0ffb2c6afafdc765b934444ec80bb5f1bfcd Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Mon, 11 Nov 2019 20:16:34 +0100 Subject: [PATCH 36/55] Refactor decoding --- Cargo.toml | 3 ++ src/crypto/ecdsa.rs | 19 ++++++++--- src/crypto/mod.rs | 79 ++++++++++++++------------------------------ src/crypto/rsa.rs | 39 +++++++++++++++++++--- src/decoding.rs | 4 +-- src/header.rs | 4 +-- src/lib.rs | 6 ++-- src/serialization.rs | 10 +++--- 8 files changed, 89 insertions(+), 75 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3ed0158..aa66e7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,6 @@ simple_asn1 = "0.4" [dev-dependencies] # For the custom chrono example chrono = "0.4" + +[badges] +maintenance = { status = "passively-developed" } diff --git a/src/crypto/ecdsa.rs b/src/crypto/ecdsa.rs index 28974e2..eb7aa05 100644 --- a/src/crypto/ecdsa.rs +++ b/src/crypto/ecdsa.rs @@ -2,17 +2,28 @@ use ring::{rand, signature}; use crate::errors::Result; use crate::pem_decoder::PemEncodedKey; -use crate::serialization::encode; +use crate::serialization::b64_encode; +use crate::algorithms::Algorithm; + + +/// Only used internally when validating EC, to map from our enum to the Ring EcdsaVerificationAlgorithm structs. +pub(crate) fn alg_to_ec_verification(alg: Algorithm) -> &'static signature::EcdsaVerificationAlgorithm { + match alg { + Algorithm::ES256 => &signature::ECDSA_P256_SHA256_FIXED, + Algorithm::ES384 => &signature::ECDSA_P384_SHA384_FIXED, + _ => unreachable!("Tried to get EC signature for a non-EC algorithm"), + } +} /// The actual ECDSA signing + encoding pub fn sign( alg: &'static signature::EcdsaSigningAlgorithm, key: &[u8], - signing_input: &str, + message: &str, ) -> Result { let pem_key = PemEncodedKey::new(key)?; let signing_key = signature::EcdsaKeyPair::from_pkcs8(alg, pem_key.as_ec_private_key()?)?; let rng = rand::SystemRandom::new(); - let out = signing_key.sign(&rng, signing_input.as_bytes())?; - Ok(encode(out.as_ref())) + let out = signing_key.sign(&rng, message.as_bytes())?; + Ok(b64_encode(out.as_ref())) } diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 8f624aa..4c1132d 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -1,19 +1,19 @@ use ring::constant_time::verify_slices_are_equal; use ring::{hmac, signature}; -use simple_asn1::BigUint; use crate::algorithms::Algorithm; use crate::errors::Result; use crate::pem_decoder::PemEncodedKey; -use crate::serialization::{decode, encode}; +use crate::serialization::{b64_decode, b64_encode}; pub(crate) mod ecdsa; pub(crate) mod rsa; /// The actual HS signing + encoding +/// Could be in its own file to match RSA/EC but it's 2 lines... pub(crate) fn sign_hmac(alg: hmac::Algorithm, key: &[u8], message: &str) -> Result { let digest = hmac::sign(&hmac::Key::new(alg, key), message.as_bytes()); - Ok(encode(digest.as_ref())) + Ok(b64_encode(digest.as_ref())) } /// Take the payload of a JWT, sign it using the algorithm given and return @@ -40,27 +40,16 @@ pub fn sign(message: &str, key: &[u8], algorithm: Algorithm) -> Result { } } -fn rsa_alg_to_rsa_parameters(alg: Algorithm) -> &'static signature::RsaParameters { - match alg { - Algorithm::RS256 => &signature::RSA_PKCS1_2048_8192_SHA256, - Algorithm::RS384 => &signature::RSA_PKCS1_2048_8192_SHA384, - Algorithm::RS512 => &signature::RSA_PKCS1_2048_8192_SHA512, - Algorithm::PS256 => &signature::RSA_PSS_2048_8192_SHA256, - Algorithm::PS384 => &signature::RSA_PSS_2048_8192_SHA384, - Algorithm::PS512 => &signature::RSA_PSS_2048_8192_SHA512, - _ => unreachable!("Tried to get RSA signature for a non-rsa algorithm"), - } -} - /// Compares the signature given with a re-computed signature for HMAC or using the public key -/// for RSA. +/// for RSA/EC. /// /// Only use this function if you want to do something other than JWT. /// /// `signature` is the signature part of a jwt (text after the second '.') /// -/// `signing_input` is base64(header) + "." + base64(claims) -/// For ECDSA/RSS, the `key` is the pem public key +/// `message` is base64(header) + "." + base64(claims) +/// For ECDSA/RSA, the `key` is the pem public key. If you want to verify using the public key +/// components (modulus/exponent), use `verify_rsa_components` instead. pub fn verify(signature: &str, message: &str, key: &[u8], algorithm: Algorithm) -> Result { match algorithm { Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => { @@ -68,17 +57,18 @@ pub fn verify(signature: &str, message: &str, key: &[u8], algorithm: Algorithm) let signed = sign(message, key, algorithm)?; Ok(verify_slices_are_equal(signature.as_ref(), signed.as_ref()).is_ok()) } - Algorithm::ES256 => { - verify_ring_es(&signature::ECDSA_P256_SHA256_FIXED, signature, message, key) + Algorithm::ES256 | Algorithm::ES384 => { + let pem_key = PemEncodedKey::new(key)?; + verify_ring(ecdsa::alg_to_ec_verification(algorithm), signature, message, pem_key.as_ec_public_key()?) } - Algorithm::ES384 => { - verify_ring_es(&signature::ECDSA_P384_SHA384_FIXED, signature, message, key) - } - _ => verify_ring_rsa(rsa_alg_to_rsa_parameters(algorithm), signature, message, key), + // only RSAs left + _ => { + let pem_key = PemEncodedKey::new(key)?; + verify_ring(rsa::alg_to_rsa_parameters(algorithm), signature, message, pem_key.as_rsa_key()?) + }, } } -// TODO: see if we can remove stuff? /// See Ring docs for more details fn verify_ring( @@ -87,43 +77,24 @@ fn verify_ring( message: &str, key: &[u8], ) -> Result { - let signature_bytes = decode(signature)?; + let signature_bytes = b64_decode(signature)?; let public_key = signature::UnparsedPublicKey::new(alg, key); let res = public_key.verify(message.as_bytes(), &signature_bytes); Ok(res.is_ok()) } -fn verify_ring_es( - alg: &'static dyn signature::VerificationAlgorithm, - signature: &str, - message: &str, - key: &[u8], -) -> Result { - let pem_key = PemEncodedKey::new(key)?; - verify_ring(alg, signature, message, pem_key.as_ec_public_key()?) -} - -fn verify_ring_rsa( - alg: &'static signature::RsaParameters, - signature: &str, - message: &str, - key: &[u8], -) -> Result { - let pem_key = PemEncodedKey::new(key)?; - verify_ring(alg, signature, message, pem_key.as_rsa_key()?) -} - -pub fn verify_rsa_modulus_exponent( - alg: Algorithm, +/// Verify the signature given using the (n, e) components of a RSA public key. +/// +/// `signature` is the signature part of a jwt (text after the second '.') +/// +/// `message` is base64(header) + "." + base64(claims) +pub fn verify_rsa_components( signature: &str, message: &str, components: (&str, &str), + alg: Algorithm, ) -> Result { - let signature_bytes = decode(signature)?; - let n = BigUint::from_bytes_be(&decode(components.0)?).to_bytes_be(); - let e = BigUint::from_bytes_be(&decode(components.1)?).to_bytes_be(); - let pubkey = signature::RsaPublicKeyComponents { n, e }; - let res = pubkey.verify(rsa_alg_to_rsa_parameters(alg), message.as_ref(), &signature_bytes); - Ok(res.is_ok()) + let signature_bytes = b64_decode(signature)?; + rsa::verify_from_components(&signature_bytes, message, components, alg) } diff --git a/src/crypto/rsa.rs b/src/crypto/rsa.rs index bc4a78c..a7241a2 100644 --- a/src/crypto/rsa.rs +++ b/src/crypto/rsa.rs @@ -1,15 +1,32 @@ use ring::{rand, signature}; +use simple_asn1::BigUint; use crate::errors::{ErrorKind, Result}; use crate::pem_decoder::PemEncodedKey; -use crate::serialization::encode; +use crate::serialization::{b64_encode, b64_decode}; +use crate::algorithms::Algorithm; + + +/// Only used internally when validating RSA, to map from our enum to the Ring param structs. +pub(crate) fn alg_to_rsa_parameters(alg: Algorithm) -> &'static signature::RsaParameters { + match alg { + Algorithm::RS256 => &signature::RSA_PKCS1_2048_8192_SHA256, + Algorithm::RS384 => &signature::RSA_PKCS1_2048_8192_SHA384, + Algorithm::RS512 => &signature::RSA_PKCS1_2048_8192_SHA512, + Algorithm::PS256 => &signature::RSA_PSS_2048_8192_SHA256, + Algorithm::PS384 => &signature::RSA_PSS_2048_8192_SHA384, + Algorithm::PS512 => &signature::RSA_PSS_2048_8192_SHA512, + _ => unreachable!("Tried to get RSA signature for a non-rsa algorithm"), + } +} + /// The actual RSA signing + encoding /// Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html -pub fn sign( +pub(crate) fn sign( alg: &'static dyn signature::RsaEncoding, key: &[u8], - signing_input: &str, + message: &str, ) -> Result { let pem_key = PemEncodedKey::new(key)?; let key_pair = signature::RsaKeyPair::from_der(pem_key.as_rsa_key()?) @@ -18,8 +35,20 @@ pub fn sign( let mut signature = vec![0; key_pair.public_modulus_len()]; let rng = rand::SystemRandom::new(); key_pair - .sign(alg, &rng, signing_input.as_bytes(), &mut signature) + .sign(alg, &rng, message.as_bytes(), &mut signature) .map_err(|_| ErrorKind::InvalidRsaKey)?; - Ok(encode(&signature)) + Ok(b64_encode(&signature)) +} + +pub(crate) fn verify_from_components( + signature_bytes: &[u8], + message: &str, + components: (&str, &str), + alg: Algorithm) -> Result { + let n = BigUint::from_bytes_be(&b64_decode(components.0)?).to_bytes_be(); + let e = BigUint::from_bytes_be(&b64_decode(components.1)?).to_bytes_be(); + let pubkey = signature::RsaPublicKeyComponents { n, e }; + let res = pubkey.verify(alg_to_rsa_parameters(alg), message.as_ref(), &signature_bytes); + Ok(res.is_ok()) } diff --git a/src/decoding.rs b/src/decoding.rs index 0be1c4c..c2934cc 100644 --- a/src/decoding.rs +++ b/src/decoding.rs @@ -1,6 +1,6 @@ use serde::de::DeserializeOwned; -use crate::crypto::{verify, verify_rsa_modulus_exponent}; +use crate::crypto::{verify, verify_rsa_components}; use crate::errors::{new_error, ErrorKind, Result}; use crate::header::Header; use crate::serialization::{from_jwt_part_claims, TokenData}; @@ -40,7 +40,7 @@ fn _decode( let is_valid = match key { DecodingKey::SecretOrPem(k) => verify(signature, message, k, header.alg), DecodingKey::RsaModulusExponent { n, e } => { - verify_rsa_modulus_exponent(header.alg, signature, message, (n, e)) + verify_rsa_components(signature, message, (n, e), header.alg) } }?; diff --git a/src/header.rs b/src/header.rs index 0ced490..c6ac473 100644 --- a/src/header.rs +++ b/src/header.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::algorithms::Algorithm; use crate::errors::Result; -use crate::serialization::decode; +use crate::serialization::b64_decode; /// A basic JWT header, the alg defaults to HS256 and typ is automatically /// set to `JWT`. All the other fields are optional. @@ -60,7 +60,7 @@ impl Header { /// Converts an encoded part into the Header struct if possible pub(crate) fn from_encoded(encoded_part: &str) -> Result { - let decoded = decode(encoded_part)?; + let decoded = b64_decode(encoded_part)?; let s = String::from_utf8(decoded)?; Ok(serde_json::from_str(&s)?) diff --git a/src/lib.rs b/src/lib.rs index fa963b5..4e067f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,7 @@ pub use validation::Validation; use serde::ser::Serialize; use crate::errors::Result; -use crate::serialization::encode_part; +use crate::serialization::b64_encode_part; /// Encode the header and claims given and sign the payload using the algorithm from the header and the key /// @@ -48,8 +48,8 @@ use crate::serialization::encode_part; /// let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap(); /// ``` pub fn encode(header: &Header, claims: &T, key: &[u8]) -> Result { - let encoded_header = encode_part(&header)?; - let encoded_claims = encode_part(&claims)?; + let encoded_header = b64_encode_part(&header)?; + let encoded_claims = b64_encode_part(&claims)?; let message = [encoded_header.as_ref(), encoded_claims.as_ref()].join("."); let signature = sign(&*message, key, header.alg)?; diff --git a/src/serialization.rs b/src/serialization.rs index a3a7d4d..22f83f4 100644 --- a/src/serialization.rs +++ b/src/serialization.rs @@ -15,18 +15,18 @@ pub struct TokenData { pub claims: T, } -pub(crate) fn encode(input: &[u8]) -> String { +pub(crate) fn b64_encode(input: &[u8]) -> String { base64::encode_config(input, base64::URL_SAFE_NO_PAD) } -pub(crate) fn decode(input: &str) -> Result> { +pub(crate) fn b64_decode(input: &str) -> Result> { base64::decode_config(input, base64::URL_SAFE_NO_PAD).map_err(|e| e.into()) } /// Serializes a struct to JSON and encodes it in base64 -pub(crate) fn encode_part(input: &T) -> Result { +pub(crate) fn b64_encode_part(input: &T) -> Result { let json = to_string(input)?; - Ok(encode(json.as_bytes())) + Ok(b64_encode(json.as_bytes())) } /// Decodes from base64 and deserializes from JSON to a struct AND a hashmap of Value so we can @@ -34,7 +34,7 @@ pub(crate) fn encode_part(input: &T) -> Result { pub(crate) fn from_jwt_part_claims, T: DeserializeOwned>( encoded: B, ) -> Result<(T, Map)> { - let s = String::from_utf8(decode(encoded.as_ref())?)?; + let s = String::from_utf8(b64_decode(encoded.as_ref())?)?; let claims: T = from_str(&s)?; let validation_map: Map<_, _> = from_str(&s)?; From 8e4757cb1dace5b616eff07d7b4274880a13d387 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Mon, 11 Nov 2019 20:29:57 +0100 Subject: [PATCH 37/55] More refactoring in the crypto mod --- src/crypto/ecdsa.rs | 18 ++++++++--- src/crypto/mod.rs | 73 ++++++++++++++++++++++++++------------------- src/crypto/rsa.rs | 22 ++++++++++---- src/decoding.rs | 4 +-- src/validation.rs | 4 +-- 5 files changed, 78 insertions(+), 43 deletions(-) diff --git a/src/crypto/ecdsa.rs b/src/crypto/ecdsa.rs index eb7aa05..ba2f0ff 100644 --- a/src/crypto/ecdsa.rs +++ b/src/crypto/ecdsa.rs @@ -1,17 +1,27 @@ use ring::{rand, signature}; +use crate::algorithms::Algorithm; use crate::errors::Result; use crate::pem_decoder::PemEncodedKey; use crate::serialization::b64_encode; -use crate::algorithms::Algorithm; - /// Only used internally when validating EC, to map from our enum to the Ring EcdsaVerificationAlgorithm structs. -pub(crate) fn alg_to_ec_verification(alg: Algorithm) -> &'static signature::EcdsaVerificationAlgorithm { +pub(crate) fn alg_to_ec_verification( + alg: Algorithm, +) -> &'static signature::EcdsaVerificationAlgorithm { match alg { Algorithm::ES256 => &signature::ECDSA_P256_SHA256_FIXED, Algorithm::ES384 => &signature::ECDSA_P384_SHA384_FIXED, - _ => unreachable!("Tried to get EC signature for a non-EC algorithm"), + _ => unreachable!("Tried to get EC alg for a non-EC algorithm"), + } +} + +/// Only used internally when signing EC, to map from our enum to the Ring EcdsaVerificationAlgorithm structs. +pub(crate) fn alg_to_ec_signing(alg: Algorithm) -> &'static signature::EcdsaSigningAlgorithm { + match alg { + Algorithm::ES256 => &signature::ECDSA_P256_SHA256_FIXED_SIGNING, + Algorithm::ES384 => &signature::ECDSA_P384_SHA384_FIXED_SIGNING, + _ => unreachable!("Tried to get EC alg for a non-EC algorithm"), } } diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 4c1132d..6667ee4 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -27,19 +27,33 @@ pub fn sign(message: &str, key: &[u8], algorithm: Algorithm) -> Result { Algorithm::HS384 => sign_hmac(hmac::HMAC_SHA384, key, message), Algorithm::HS512 => sign_hmac(hmac::HMAC_SHA512, key, message), - Algorithm::ES256 => ecdsa::sign(&signature::ECDSA_P256_SHA256_FIXED_SIGNING, key, message), - Algorithm::ES384 => ecdsa::sign(&signature::ECDSA_P384_SHA384_FIXED_SIGNING, key, message), + Algorithm::ES256 | Algorithm::ES384 => { + ecdsa::sign(ecdsa::alg_to_ec_signing(algorithm), key, message) + } - Algorithm::RS256 => rsa::sign(&signature::RSA_PKCS1_SHA256, key, message), - Algorithm::RS384 => rsa::sign(&signature::RSA_PKCS1_SHA384, key, message), - Algorithm::RS512 => rsa::sign(&signature::RSA_PKCS1_SHA512, key, message), - - Algorithm::PS256 => rsa::sign(&signature::RSA_PSS_SHA256, key, message), - Algorithm::PS384 => rsa::sign(&signature::RSA_PSS_SHA384, key, message), - Algorithm::PS512 => rsa::sign(&signature::RSA_PSS_SHA512, key, message), + Algorithm::RS256 + | Algorithm::RS384 + | Algorithm::RS512 + | Algorithm::PS256 + | Algorithm::PS384 + | Algorithm::PS512 => rsa::sign(rsa::alg_to_rsa_signing(algorithm), key, message), } } +/// See Ring docs for more details +fn verify_ring( + alg: &'static dyn signature::VerificationAlgorithm, + signature: &str, + message: &str, + key: &[u8], +) -> Result { + let signature_bytes = b64_decode(signature)?; + let public_key = signature::UnparsedPublicKey::new(alg, key); + let res = public_key.verify(message.as_bytes(), &signature_bytes); + + Ok(res.is_ok()) +} + /// Compares the signature given with a re-computed signature for HMAC or using the public key /// for RSA/EC. /// @@ -59,31 +73,30 @@ pub fn verify(signature: &str, message: &str, key: &[u8], algorithm: Algorithm) } Algorithm::ES256 | Algorithm::ES384 => { let pem_key = PemEncodedKey::new(key)?; - verify_ring(ecdsa::alg_to_ec_verification(algorithm), signature, message, pem_key.as_ec_public_key()?) + verify_ring( + ecdsa::alg_to_ec_verification(algorithm), + signature, + message, + pem_key.as_ec_public_key()?, + ) } - // only RSAs left - _ => { + Algorithm::RS256 + | Algorithm::RS384 + | Algorithm::RS512 + | Algorithm::PS256 + | Algorithm::PS384 + | Algorithm::PS512 => { let pem_key = PemEncodedKey::new(key)?; - verify_ring(rsa::alg_to_rsa_parameters(algorithm), signature, message, pem_key.as_rsa_key()?) - }, + verify_ring( + rsa::alg_to_rsa_parameters(algorithm), + signature, + message, + pem_key.as_rsa_key()?, + ) + } } } - -/// See Ring docs for more details -fn verify_ring( - alg: &'static dyn signature::VerificationAlgorithm, - signature: &str, - message: &str, - key: &[u8], -) -> Result { - let signature_bytes = b64_decode(signature)?; - let public_key = signature::UnparsedPublicKey::new(alg, key); - let res = public_key.verify(message.as_bytes(), &signature_bytes); - - Ok(res.is_ok()) -} - /// Verify the signature given using the (n, e) components of a RSA public key. /// /// `signature` is the signature part of a jwt (text after the second '.') @@ -96,5 +109,5 @@ pub fn verify_rsa_components( alg: Algorithm, ) -> Result { let signature_bytes = b64_decode(signature)?; - rsa::verify_from_components(&signature_bytes, message, components, alg) + rsa::verify_from_components(rsa::alg_to_rsa_parameters(alg), &signature_bytes, message, components) } diff --git a/src/crypto/rsa.rs b/src/crypto/rsa.rs index a7241a2..95e946b 100644 --- a/src/crypto/rsa.rs +++ b/src/crypto/rsa.rs @@ -1,11 +1,10 @@ use ring::{rand, signature}; use simple_asn1::BigUint; +use crate::algorithms::Algorithm; use crate::errors::{ErrorKind, Result}; use crate::pem_decoder::PemEncodedKey; -use crate::serialization::{b64_encode, b64_decode}; -use crate::algorithms::Algorithm; - +use crate::serialization::{b64_decode, b64_encode}; /// Only used internally when validating RSA, to map from our enum to the Ring param structs. pub(crate) fn alg_to_rsa_parameters(alg: Algorithm) -> &'static signature::RsaParameters { @@ -20,6 +19,18 @@ pub(crate) fn alg_to_rsa_parameters(alg: Algorithm) -> &'static signature::RsaPa } } +/// Only used internally when signing with RSA, to map from our enum to the Ring signing structs. +pub(crate) fn alg_to_rsa_signing(alg: Algorithm) -> &'static dyn signature::RsaEncoding { + match alg { + Algorithm::RS256 => &signature::RSA_PKCS1_SHA256, + Algorithm::RS384 => &signature::RSA_PKCS1_SHA384, + Algorithm::RS512 => &signature::RSA_PKCS1_SHA512, + Algorithm::PS256 => &signature::RSA_PSS_SHA256, + Algorithm::PS384 => &signature::RSA_PSS_SHA384, + Algorithm::PS512 => &signature::RSA_PSS_SHA512, + _ => unreachable!("Tried to get RSA signature for a non-rsa algorithm"), + } +} /// The actual RSA signing + encoding /// Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html @@ -42,13 +53,14 @@ pub(crate) fn sign( } pub(crate) fn verify_from_components( + alg: &'static signature::RsaParameters, signature_bytes: &[u8], message: &str, components: (&str, &str), - alg: Algorithm) -> Result { +) -> Result { let n = BigUint::from_bytes_be(&b64_decode(components.0)?).to_bytes_be(); let e = BigUint::from_bytes_be(&b64_decode(components.1)?).to_bytes_be(); let pubkey = signature::RsaPublicKeyComponents { n, e }; - let res = pubkey.verify(alg_to_rsa_parameters(alg), message.as_ref(), &signature_bytes); + let res = pubkey.verify(alg, message.as_ref(), &signature_bytes); Ok(res.is_ok()) } diff --git a/src/decoding.rs b/src/decoding.rs index c2934cc..e79847d 100644 --- a/src/decoding.rs +++ b/src/decoding.rs @@ -118,8 +118,8 @@ pub fn dangerous_unsafe_decode(token: &str) -> Result Date: Thu, 14 Nov 2019 19:43:43 +0100 Subject: [PATCH 38/55] Update docs --- README.md | 116 +++++++++++++------------ benches/jwt.rs | 8 +- src/algorithms.rs | 2 +- src/crypto/ecdsa.rs | 2 +- src/crypto/mod.rs | 14 ++- src/crypto/rsa.rs | 2 +- src/decoding.rs | 43 +++++++-- src/errors.rs | 4 +- src/lib.rs | 49 +++-------- src/{pem_decoder.rs => pem/decoder.rs} | 3 - src/{pem_encoder.rs => pem/encoder.rs} | 16 ++-- src/pem/mod.rs | 34 ++++++++ src/serialization.rs | 10 --- src/validation.rs | 2 +- tests/ecdsa/mod.rs | 5 +- tests/hmac.rs | 4 +- tests/rsa/mod.rs | 7 +- 17 files changed, 175 insertions(+), 146 deletions(-) rename src/{pem_decoder.rs => pem/decoder.rs} (99%) rename src/{pem_encoder.rs => pem/encoder.rs} (93%) create mode 100644 src/pem/mod.rs diff --git a/README.md b/README.md index 2f5359d..9834cc3 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,7 @@ Add the following to Cargo.toml: ```toml jsonwebtoken = "7" -serde_derive = "1" -serde = "1" +serde = {version = "1.0", features = ["derive"] } ``` ## How to use @@ -18,11 +17,8 @@ Complete examples are available in the examples directory: a basic one and one w In terms of imports and structs: ```rust -extern crate jsonwebtoken as jwt; -#[macro_use] -extern crate serde_derive; - -use jwt::{encode, decode, Header, Algorithm, Validation}; +use serde::{Serialize, Deserialize}; +use jsonwebtoken::{encode, decode, Header, Algorithm, Validation}; /// Our claims struct, it needs to derive `Serialize` and/or `Deserialize` #[derive(Debug, Serialize, Deserialize)] @@ -33,7 +29,7 @@ struct Claims { } ``` -### Encoding +### Header The default algorithm is HS256. ```rust @@ -45,38 +41,84 @@ All the parameters from the RFC are supported but the default header only has `t If you want to set the `kid` parameter or change the algorithm for example: ```rust -let mut header = Header::default(); +let mut header = Header::new(Algorithm::HS512); header.kid = Some("blabla".to_owned()); -header.alg = Algorithm::HS512; let token = encode(&header, &my_claims, "secret".as_ref())?; ``` Look at `examples/custom_header.rs` for a full working example. +### Encoding + +```rust +// HS256 +let token = encode(&Header::default(), &my_claims, "secret".as_ref())?; +// RSA +let token = encode(&Header::new(Algorithm::RS256), &my_claims, include_str!("privkey.pem"))?; +``` +Encoding a JWT takes 3 parameters: + +- a header: the `Header` struct +- some claims: your own struct +- a key + +When using HS256, HS2384 or HS512, the key is always a shared secret like in the example above. When using +RSA/EC, the key should always be the content of the private key in the PEM format. + ### Decoding + ```rust let token = decode::(&token, "secret".as_ref(), &Validation::default())?; -// token is a struct with 2 params: header and claims +// token is a struct with 2 fields: `header` and `claims` and `claims` is your own struct. ``` `decode` can error for a variety of reasons: - the token or its signature is invalid -- error while decoding base64 or the result of decoding base64 is not valid UTF-8 +- the token had invalid base64 - validation of at least one reserved claim failed -In some cases, for example if you don't know the algorithm used, you will want to only decode the header: +As with encoding, when using HS256, HS2384 or HS512, the key is always a shared secret like in the example above. When using +RSA/EC, the key should always be the content of the public key in the PEM format. + +In some cases, for example if you don't know the algorithm used or need to grab the `kid`, you can decode only the header: ```rust let header = decode_header(&token)?; ``` -This does not perform any validation on the token. +This does not perform any signature verification/validations on the token so it could have been tampered with. + +You can also decode a token using the public key components of a RSA key in base64 format. +The main use-case is for JWK where your public key is a JSON format like so: + +```json +{ + "kty":"RSA", + "e":"AQAB", + "kid":"6a7a119f-0876-4f7e-8d0f-bf3ea1391dd8", + "n":"yRE6rHuNR0QbHO3H3Kt2pOKGVhQqGZXInOduQNxXzuKlvQTLUTv4l4sggh5_CYYi_cvI-SXVT9kPWSKXxJXBXd_4LkvcPuUakBoAkfh-eiFVMh2VrUyWyj3MFl0HTVF9KwRXLAcwkREiS3npThHRyIxuy0ZMeZfxVL5arMhw1SRELB8HoGfG_AtH89BIE9jDBHZ9dLelK9a184zAf8LwoPLxvJb3Il5nncqPcSfKDDodMFBIMc4lQzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xqi-yUod-j8MtvIj812dkS4QMiRVN_by2h3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5TdQ" +} +``` + +```rust +let token = decode_rsa_components::(&token, jwk["n"], jwk["e"], &Validation::new(Algorithm::RS256))?; +// token is a struct with 2 fields: `header` and `claims` and `claims` is your own struct. +``` + +### Convertion .der to .pem + +You can use openssl for that: + +```bash +openssl rsa -inform DER -outform PEM -in mykey.der -out mykey.pem +``` + #### Validation This library validates automatically the `exp` claim. `nbf` is also validated if present. You can also validate the `sub`, `iss` and `aud` but those require setting the expected value in the `Validation` struct. Since validating time fields is always a bit tricky due to clock skew, -you can add some leeway to the `iat`, `exp` and `nbf` validation by setting a `leeway` parameter. +you can add some leeway to the `iat`, `exp` and `nbf` validation by setting the `leeway` field. Last but not least, you will need to set the algorithm(s) allowed for this token if you are not using `HS256`. @@ -112,44 +154,6 @@ This library currently supports the following: - ES256 - ES384 -### RSA -`jsonwebtoken` can read DER and PEM encoded keys. - -#### DER Encoded -If you have openssl installed, you can run the following commands to obtain the DER keys from PKCS#1 (ie with `BEGIN RSA PUBLIC KEY`) .pem. -If you have a PKCS#8 pem file (ie starting with `BEGIN PUBLIC KEY`), you will need to first convert it to PKCS#1: -`openssl rsa -pubin -in -RSAPublicKey_out -out `. - -```bash -// private key -$ openssl rsa -in private_rsa_key.pem -outform DER -out private_rsa_key.der -// public key -$ openssl rsa -in private_rsa_key.der -inform DER -RSAPublicKey_out -outform DER -out public_key.der -``` - -If you are getting an error with your public key, make sure you get it by using the command above to ensure -it is in the right format. - -#### PEM Encoded -To generate a PKCS#1 RSA key, run `openssl genrsa -out private_rsa_key_pkcs1.pem 2048` -To convert a PKCS#1 RSA key to a PKCS#8 RSA key, run `openssl pkcs8 -topk8 -inform pem -in private_rsa_key_pkcs1.pem -outform pem -nocrypt -out private_rsa_key_pkcs8.pem` - -To use a PEM encoded private / public keys, a pem struct is returned by `decode_pem`. -This carries the lifetime of the data inside. Finally to use the key like any other -use the `.as_key(alg)` function on the pem struct. -``` -let privkey_pem = decode_pem(pem_string_here).unwrap(); -let privkey = privkey_pem.as_key(Algorithm::RS256).unwrap(); -``` - -### ECDSA -`jsonwebtoken` can read PKCS#8 DER encoded private keys and public keys, as well as PEM encoded keys. Like RSA, to read a PEM key, you must use the pem decoder. - -To generate an EC key, you can do the following. - -```bash -// private key -openssl ecparam -genkey -name prime256v1 | openssl ec -out private_key.pem -// public key -openssl ec -in private_key.pem -pubout -out public_key.pem -``` +### RSA & ECDSA +By default, the `encode`/`decode` functions takes the PEM format since it is the most common. +RSA can also use the public key components modulus/exponent in base64 format for decoding. diff --git a/benches/jwt.rs b/benches/jwt.rs index 47ba571..9efe6e1 100644 --- a/benches/jwt.rs +++ b/benches/jwt.rs @@ -1,10 +1,8 @@ #![feature(test)] -extern crate jsonwebtoken as jwt; extern crate test; -#[macro_use] -extern crate serde_derive; -use jwt::{decode, encode, Header, Hmac, Validation}; +use jsonwebtoken::{decode, encode, Header, Validation}; +use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] struct Claims { @@ -16,7 +14,7 @@ struct Claims { fn bench_encode(b: &mut test::Bencher) { let claim = Claims { sub: "b@b.com".to_owned(), company: "ACME".to_owned() }; - b.iter(|| encode(&Header::default(), &claim, Hmac::from(b"secret"))); + b.iter(|| encode(&Header::default(), &claim, "secret".as_ref())); } #[bench] diff --git a/src/algorithms.rs b/src/algorithms.rs index 6054125..c9ff848 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -2,7 +2,7 @@ use crate::errors::{Error, ErrorKind, Result}; use serde::{Deserialize, Serialize}; use std::str::FromStr; -/// The algorithms supported for signing/verifying +/// The algorithms supported for signing/verifying JWTs #[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)] pub enum Algorithm { /// HMAC using SHA-256 diff --git a/src/crypto/ecdsa.rs b/src/crypto/ecdsa.rs index ba2f0ff..25099a7 100644 --- a/src/crypto/ecdsa.rs +++ b/src/crypto/ecdsa.rs @@ -2,7 +2,7 @@ use ring::{rand, signature}; use crate::algorithms::Algorithm; use crate::errors::Result; -use crate::pem_decoder::PemEncodedKey; +use crate::pem::decoder::PemEncodedKey; use crate::serialization::b64_encode; /// Only used internally when validating EC, to map from our enum to the Ring EcdsaVerificationAlgorithm structs. diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 6667ee4..98dc440 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -3,7 +3,7 @@ use ring::{hmac, signature}; use crate::algorithms::Algorithm; use crate::errors::Result; -use crate::pem_decoder::PemEncodedKey; +use crate::pem::decoder::PemEncodedKey; use crate::serialization::{b64_decode, b64_encode}; pub(crate) mod ecdsa; @@ -19,7 +19,8 @@ pub(crate) fn sign_hmac(alg: hmac::Algorithm, key: &[u8], message: &str) -> Resu /// Take the payload of a JWT, sign it using the algorithm given and return /// the base64 url safe encoded of the result. /// -/// Only use this function if you want to do something other than JWT. +/// If you just want to encode a JWT, use `encode` instead. +/// /// `key` is the secret for HMAC and a pem encoded string otherwise pub fn sign(message: &str, key: &[u8], algorithm: Algorithm) -> Result { match algorithm { @@ -57,7 +58,7 @@ fn verify_ring( /// Compares the signature given with a re-computed signature for HMAC or using the public key /// for RSA/EC. /// -/// Only use this function if you want to do something other than JWT. +/// If you just want to decode a JWT, use `decode` instead. /// /// `signature` is the signature part of a jwt (text after the second '.') /// @@ -109,5 +110,10 @@ pub fn verify_rsa_components( alg: Algorithm, ) -> Result { let signature_bytes = b64_decode(signature)?; - rsa::verify_from_components(rsa::alg_to_rsa_parameters(alg), &signature_bytes, message, components) + rsa::verify_from_components( + rsa::alg_to_rsa_parameters(alg), + &signature_bytes, + message, + components, + ) } diff --git a/src/crypto/rsa.rs b/src/crypto/rsa.rs index 95e946b..89da359 100644 --- a/src/crypto/rsa.rs +++ b/src/crypto/rsa.rs @@ -3,7 +3,7 @@ use simple_asn1::BigUint; use crate::algorithms::Algorithm; use crate::errors::{ErrorKind, Result}; -use crate::pem_decoder::PemEncodedKey; +use crate::pem::decoder::PemEncodedKey; use crate::serialization::{b64_decode, b64_encode}; /// Only used internally when validating RSA, to map from our enum to the Ring param structs. diff --git a/src/decoding.rs b/src/decoding.rs index e79847d..d740052 100644 --- a/src/decoding.rs +++ b/src/decoding.rs @@ -3,9 +3,18 @@ use serde::de::DeserializeOwned; use crate::crypto::{verify, verify_rsa_components}; use crate::errors::{new_error, ErrorKind, Result}; use crate::header::Header; -use crate::serialization::{from_jwt_part_claims, TokenData}; +use crate::serialization::from_jwt_part_claims; use crate::validation::{validate, Validation}; +/// The return type of a successful call to [decode](fn.decode.html). +#[derive(Debug)] +pub struct TokenData { + /// The decoded JWT header + pub header: Header, + /// The decoded JWT claims + pub claims: T, +} + /// Takes the result of a rsplit and ensure we only get 2 parts /// Errors if we don't macro_rules! expect_two { @@ -54,7 +63,7 @@ fn _decode( Ok(TokenData { header, claims: decoded_claims }) } -/// Decode a token into a struct containing 2 fields: `claims` and `header`. +/// Decode and validate a JWT using a secret for HS and a public PEM format for RSA/EC /// /// If the token or its signature is invalid or the claims fail validation, it will return an error. /// @@ -80,8 +89,27 @@ pub fn decode( _decode(token, DecodingKey::SecretOrPem(key), validation) } -/// TO TEST -pub fn decode_rsa_jwk( +/// Decode and validate a JWT using (n, e) base64 encoded public key components for RSA +/// +/// If the token or its signature is invalid or the claims fail validation, it will return an error. +/// +/// ```rust +/// use serde::{Deserialize, Serialize}; +/// use jsonwebtoken::{decode_rsa_components, Validation, Algorithm}; +/// +/// #[derive(Debug, Serialize, Deserialize)] +/// struct Claims { +/// sub: String, +/// company: String +/// } +/// +/// let modulus = "some-base64-data"; +/// let exponent = "some-base64-data"; +/// let token = "a.jwt.token".to_string(); +/// // Claims is a struct that implements Deserialize +/// let token_message = decode_rsa_components::(&token, &modulus, &exponent, &Validation::new(Algorithm::HS256)); +/// ``` +pub fn decode_rsa_components( token: &str, modulus: &str, exponent: &str, @@ -90,7 +118,7 @@ pub fn decode_rsa_jwk( _decode(token, DecodingKey::RsaModulusExponent { n: modulus, e: exponent }, validation) } -/// Decode a token without any signature validation into a struct containing 2 fields: `claims` and `header`. +/// Decode a JWT without any signature verification/validations. /// /// NOTE: Do not use this unless you know what you are doing! If the token's signature is invalid, it will *not* return an error. /// @@ -118,10 +146,9 @@ pub fn dangerous_unsafe_decode(token: &str) -> Result "algorithms don't match", ErrorKind::InvalidAlgorithmName => "not a known algorithm", ErrorKind::InvalidKeyFormat => "invalid key format", - ErrorKind::__Nonexhaustive => "unknown error", ErrorKind::Base64(ref err) => err.description(), ErrorKind::Json(ref err) => err.description(), ErrorKind::Utf8(ref err) => err.description(), ErrorKind::Crypto(ref err) => err.description(), + ErrorKind::__Nonexhaustive => "unknown error", } } @@ -115,11 +115,11 @@ impl StdError for Error { ErrorKind::InvalidAlgorithm => None, ErrorKind::InvalidAlgorithmName => None, ErrorKind::InvalidKeyFormat => None, - ErrorKind::__Nonexhaustive => None, ErrorKind::Base64(ref err) => Some(err), ErrorKind::Json(ref err) => Some(err), ErrorKind::Utf8(ref err) => Some(err), ErrorKind::Crypto(ref err) => Some(err), + ErrorKind::__Nonexhaustive => None, } } } diff --git a/src/lib.rs b/src/lib.rs index 4e067f1..77d34ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,21 +4,21 @@ #![deny(missing_docs)] mod algorithms; -mod crypto; +/// Lower level functions, if you want to do something other than JWTs +pub mod crypto; mod decoding; -/// All the errors +/// All the errors that can be encountered while encoding/decoding JWTs pub mod errors; mod header; -mod pem_decoder; -mod pem_encoder; +mod pem; mod serialization; mod validation; pub use algorithms::Algorithm; -pub use crypto::{sign, verify}; -pub use decoding::{dangerous_unsafe_decode, decode, decode_header, decode_rsa_jwk}; +pub use decoding::{ + dangerous_unsafe_decode, decode, decode_header, decode_rsa_components, TokenData, +}; pub use header::Header; -pub use serialization::TokenData; pub use validation::Validation; use serde::ser::Serialize; @@ -26,7 +26,8 @@ use serde::ser::Serialize; use crate::errors::Result; use crate::serialization::b64_encode_part; -/// Encode the header and claims given and sign the payload using the algorithm from the header and the key +/// Encode the header and claims given and sign the payload using the algorithm from the header and the key. +/// If the algorithm given is RSA or EC, the key needs to be in the PEM format. /// /// ```rust /// use serde::{Deserialize, Serialize}; @@ -51,37 +52,7 @@ pub fn encode(header: &Header, claims: &T, key: &[u8]) -> Result Result { - pem_encoder::encode_rsa_public_pkcs1_pem(modulus, exponent) -} - -/// TODO -pub fn encode_rsa_public_pkcs1_der(modulus: &[u8], exponent: &[u8]) -> Result> { - pem_encoder::encode_rsa_public_pkcs1_der(modulus, exponent) -} - -/// TODO -pub fn encode_rsa_public_pkcs8_pem(modulus: &[u8], exponent: &[u8]) -> Result { - pem_encoder::encode_rsa_public_pkcs8_pem(modulus, exponent) -} - -/// TODO -pub fn encode_rsa_public_pkcs8_der(modulus: &[u8], exponent: &[u8]) -> Result> { - pem_encoder::encode_rsa_public_pkcs8_der(modulus, exponent) -} - -/// TODO -pub fn encode_ec_public_pem(x_coordinate: &[u8]) -> Result { - pem_encoder::encode_ec_public_pem(x_coordinate) -} - -/// TODO -pub fn encode_ec_public_der(x_coordinate: &[u8]) -> Result> { - pem_encoder::encode_ec_public_der(x_coordinate) -} diff --git a/src/pem_decoder.rs b/src/pem/decoder.rs similarity index 99% rename from src/pem_decoder.rs rename to src/pem/decoder.rs index 2c29f7d..db41156 100644 --- a/src/pem_decoder.rs +++ b/src/pem/decoder.rs @@ -1,8 +1,5 @@ use crate::errors::{ErrorKind, Result}; -extern crate pem; -extern crate simple_asn1; - use simple_asn1::{BigUint, OID}; /// Supported PEM files for EC and RSA Public and Private Keys diff --git a/src/pem_encoder.rs b/src/pem/encoder.rs similarity index 93% rename from src/pem_encoder.rs rename to src/pem/encoder.rs index ef7ba9d..f2c2159 100644 --- a/src/pem_encoder.rs +++ b/src/pem/encoder.rs @@ -2,43 +2,39 @@ use crate::errors::{ErrorKind, Result}; use pem::Pem; use simple_asn1::{ASN1Block, BigInt, BigUint, OID}; -extern crate base64; -extern crate pem; -extern crate simple_asn1; - -pub fn encode_rsa_public_pkcs1_pem(modulus: &[u8], exponent: &[u8]) -> Result { +pub(crate) fn encode_rsa_public_pkcs1_pem(modulus: &[u8], exponent: &[u8]) -> Result { Ok(pem::encode(&Pem { contents: encode_rsa_public_pkcs1_der(modulus, exponent)?, tag: "RSA PUBLIC KEY".to_string(), })) } -pub fn encode_rsa_public_pkcs1_der(modulus: &[u8], exponent: &[u8]) -> Result> { +pub(crate) fn encode_rsa_public_pkcs1_der(modulus: &[u8], exponent: &[u8]) -> Result> { match simple_asn1::to_der(&encode_rsa_public_pksc1_asn1(modulus, exponent)) { Ok(bytes) => Ok(bytes), Err(_) => Err(ErrorKind::InvalidRsaKey.into()), } } -pub fn encode_rsa_public_pkcs8_pem(modulus: &[u8], exponent: &[u8]) -> Result { +pub(crate) fn encode_rsa_public_pkcs8_pem(modulus: &[u8], exponent: &[u8]) -> Result { Ok(pem::encode(&Pem { contents: encode_rsa_public_pkcs8_der(modulus, exponent)?, tag: "PUBLIC KEY".to_string(), })) } -pub fn encode_rsa_public_pkcs8_der(modulus: &[u8], exponent: &[u8]) -> Result> { +pub(crate) fn encode_rsa_public_pkcs8_der(modulus: &[u8], exponent: &[u8]) -> Result> { match simple_asn1::to_der(&encode_rsa_public_pksc8_asn1(modulus, exponent)?) { Ok(bytes) => Ok(bytes), Err(_) => Err(ErrorKind::InvalidRsaKey.into()), } } -pub fn encode_ec_public_pem(x: &[u8]) -> Result { +pub(crate) fn encode_ec_public_pem(x: &[u8]) -> Result { Ok(pem::encode(&Pem { contents: encode_ec_public_der(x)?, tag: "PUBLIC KEY".to_string() })) } -pub fn encode_ec_public_der(x: &[u8]) -> Result> { +pub(crate) fn encode_ec_public_der(x: &[u8]) -> Result> { match simple_asn1::to_der(&encode_ec_public_asn1(x)) { Ok(bytes) => Ok(bytes), Err(_) => Err(ErrorKind::InvalidEcdsaKey.into()), diff --git a/src/pem/mod.rs b/src/pem/mod.rs new file mode 100644 index 0000000..c79f281 --- /dev/null +++ b/src/pem/mod.rs @@ -0,0 +1,34 @@ +pub(crate) mod decoder; +mod encoder; + +use crate::errors::Result; + +/// Encode (n, e) components into the public PKCS1 PEM format +pub fn encode_rsa_public_pkcs1_pem(modulus: &[u8], exponent: &[u8]) -> Result { + encoder::encode_rsa_public_pkcs1_pem(modulus, exponent) +} + +/// Encode (n, e) components into the public PKCS1 PEM format +pub fn encode_rsa_public_pkcs1_der(modulus: &[u8], exponent: &[u8]) -> Result> { + encoder::encode_rsa_public_pkcs1_der(modulus, exponent) +} + +/// TODO +pub fn encode_rsa_public_pkcs8_pem(modulus: &[u8], exponent: &[u8]) -> Result { + encoder::encode_rsa_public_pkcs8_pem(modulus, exponent) +} + +/// TODO +pub fn encode_rsa_public_pkcs8_der(modulus: &[u8], exponent: &[u8]) -> Result> { + encoder::encode_rsa_public_pkcs8_der(modulus, exponent) +} + +/// TODO +pub fn encode_ec_public_pem(x_coordinate: &[u8]) -> Result { + encoder::encode_ec_public_pem(x_coordinate) +} + +/// TODO +pub fn encode_ec_public_der(x_coordinate: &[u8]) -> Result> { + encoder::encode_ec_public_der(x_coordinate) +} diff --git a/src/serialization.rs b/src/serialization.rs index 22f83f4..23d0883 100644 --- a/src/serialization.rs +++ b/src/serialization.rs @@ -4,16 +4,6 @@ use serde_json::map::Map; use serde_json::{from_str, to_string, Value}; use crate::errors::Result; -use crate::header::Header; - -/// The return type of a successful call to decode -#[derive(Debug)] -pub struct TokenData { - /// The decoded JWT header - pub header: Header, - /// The decoded JWT claims - pub claims: T, -} pub(crate) fn b64_encode(input: &[u8]) -> String { base64::encode_config(input, base64::URL_SAFE_NO_PAD) diff --git a/src/validation.rs b/src/validation.rs index 9afbf2a..48d79c7 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -7,7 +7,7 @@ use serde_json::{from_value, Value}; use crate::algorithms::Algorithm; use crate::errors::{new_error, ErrorKind, Result}; -/// Contains the various validations that are applied after decoding a token. +/// Contains the various validations that are applied after decoding a JWT. /// /// All time validation happen on UTC timestamps as seconds. /// diff --git a/tests/ecdsa/mod.rs b/tests/ecdsa/mod.rs index b02721c..b9754c1 100644 --- a/tests/ecdsa/mod.rs +++ b/tests/ecdsa/mod.rs @@ -1,5 +1,8 @@ use chrono::Utc; -use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Header, Validation}; +use jsonwebtoken::{ + crypto::{sign, verify}, + decode, encode, Algorithm, Header, Validation, +}; use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] diff --git a/tests/hmac.rs b/tests/hmac.rs index 0a245fc..98e2d56 100644 --- a/tests/hmac.rs +++ b/tests/hmac.rs @@ -1,7 +1,7 @@ use chrono::Utc; use jsonwebtoken::{ - dangerous_unsafe_decode, decode, decode_header, encode, sign, verify, Algorithm, Header, - Validation, + crypto::{sign, verify}, + dangerous_unsafe_decode, decode, decode_header, encode, Algorithm, Header, Validation, }; use serde::{Deserialize, Serialize}; diff --git a/tests/rsa/mod.rs b/tests/rsa/mod.rs index 6cdb98c..f93ed96 100644 --- a/tests/rsa/mod.rs +++ b/tests/rsa/mod.rs @@ -1,5 +1,8 @@ use chrono::Utc; -use jsonwebtoken::{decode, decode_rsa_jwk, encode, sign, verify, Algorithm, Header, Validation}; +use jsonwebtoken::{ + crypto::{sign, verify}, + decode, decode_rsa_components, encode, Algorithm, Header, Validation, +}; use serde::{Deserialize, Serialize}; const RSA_ALGORITHMS: &[Algorithm] = &[ @@ -76,7 +79,7 @@ fn rsa_modulus_exponent() { let e = "AQAB"; let encrypted = encode(&Header::new(Algorithm::RS256), &my_claims, privkey.as_ref()).unwrap(); - let res = decode_rsa_jwk::(&encrypted, n, e, &Validation::new(Algorithm::RS256)); + let res = decode_rsa_components::(&encrypted, n, e, &Validation::new(Algorithm::RS256)); assert!(res.is_ok()); } From 6e8d4a4be6aa16fdd23f5c5786c55d45dcc7975a Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Thu, 14 Nov 2019 19:48:38 +0100 Subject: [PATCH 39/55] Remove pem encoding for now --- src/pem/encoder.rs | 194 --------------------------------------------- src/pem/mod.rs | 33 -------- 2 files changed, 227 deletions(-) delete mode 100644 src/pem/encoder.rs diff --git a/src/pem/encoder.rs b/src/pem/encoder.rs deleted file mode 100644 index f2c2159..0000000 --- a/src/pem/encoder.rs +++ /dev/null @@ -1,194 +0,0 @@ -use crate::errors::{ErrorKind, Result}; -use pem::Pem; -use simple_asn1::{ASN1Block, BigInt, BigUint, OID}; - -pub(crate) fn encode_rsa_public_pkcs1_pem(modulus: &[u8], exponent: &[u8]) -> Result { - Ok(pem::encode(&Pem { - contents: encode_rsa_public_pkcs1_der(modulus, exponent)?, - tag: "RSA PUBLIC KEY".to_string(), - })) -} - -pub(crate) fn encode_rsa_public_pkcs1_der(modulus: &[u8], exponent: &[u8]) -> Result> { - match simple_asn1::to_der(&encode_rsa_public_pksc1_asn1(modulus, exponent)) { - Ok(bytes) => Ok(bytes), - Err(_) => Err(ErrorKind::InvalidRsaKey.into()), - } -} - -pub(crate) fn encode_rsa_public_pkcs8_pem(modulus: &[u8], exponent: &[u8]) -> Result { - Ok(pem::encode(&Pem { - contents: encode_rsa_public_pkcs8_der(modulus, exponent)?, - tag: "PUBLIC KEY".to_string(), - })) -} - -pub(crate) fn encode_rsa_public_pkcs8_der(modulus: &[u8], exponent: &[u8]) -> Result> { - match simple_asn1::to_der(&encode_rsa_public_pksc8_asn1(modulus, exponent)?) { - Ok(bytes) => Ok(bytes), - Err(_) => Err(ErrorKind::InvalidRsaKey.into()), - } -} - -pub(crate) fn encode_ec_public_pem(x: &[u8]) -> Result { - Ok(pem::encode(&Pem { contents: encode_ec_public_der(x)?, tag: "PUBLIC KEY".to_string() })) -} - -pub(crate) fn encode_ec_public_der(x: &[u8]) -> Result> { - match simple_asn1::to_der(&encode_ec_public_asn1(x)) { - Ok(bytes) => Ok(bytes), - Err(_) => Err(ErrorKind::InvalidEcdsaKey.into()), - } -} - -fn encode_rsa_public_pksc8_asn1(modulus: &[u8], exponent: &[u8]) -> Result { - let pksc1 = match simple_asn1::to_der(&encode_rsa_public_pksc1_asn1(modulus, exponent)) { - Ok(bytes) => bytes, - Err(_) => return Err(ErrorKind::InvalidRsaKey.into()), - }; - Ok(ASN1Block::Sequence( - 0, - vec![ - ASN1Block::Sequence( - 0, - vec![ - // rsaEncryption (PKCS #1) - ASN1Block::ObjectIdentifier(0, simple_asn1::oid!(1, 2, 840, 113_549, 1, 1, 1)), - ASN1Block::Null(0), - ], - ), - // the second parameter takes the count of bits - ASN1Block::BitString(0, pksc1.len() * 8, pksc1), - ], - )) -} - -fn encode_rsa_public_pksc1_asn1(modulus: &[u8], exponent: &[u8]) -> ASN1Block { - ASN1Block::Sequence( - 0, - vec![ - ASN1Block::Integer(0, BigInt::from_signed_bytes_be(modulus)), - ASN1Block::Integer(0, BigInt::from_signed_bytes_be(exponent)), - ], - ) -} - -fn encode_ec_public_asn1(x: &[u8]) -> ASN1Block { - ASN1Block::Sequence( - 0, - vec![ - ASN1Block::Sequence( - 0, - vec![ - // ecPublicKey (ANSI X9.62 public key type) - ASN1Block::ObjectIdentifier(0, simple_asn1::oid!(1, 2, 840, 10045, 2, 1)), - // prime256v1 (ANSI X9.62 named elliptic curve) - ASN1Block::ObjectIdentifier(0, simple_asn1::oid!(1, 2, 840, 10045, 3, 1, 7)), - ], - ), - // the second parameter takes the count of bits - ASN1Block::BitString(0, x.len() * 8, x.to_vec()), - ], - ) -} - -#[cfg(test)] -mod tests { - use super::*; - use ring::{signature, signature::KeyPair}; - - // #[test] - // fn public_key_encoding_pkcs1() { - // let privkey_pem = - // decode_pem(include_str!("../tests/rsa/private_rsa_key_pkcs8.pem")).unwrap(); - // let privkey = privkey_pem.as_key().unwrap(); - // let ring_key = signature::RsaKeyPair::from_der(match privkey { - // Key::Der(bytes) => bytes, - // _ => panic!("Unexpected"), - // }) - // .unwrap(); - // let mut modulus = vec![0]; - // modulus.extend(ring_key.public_key().modulus().big_endian_without_leading_zero()); - // let exponent = ring_key.public_key().exponent(); - // - // let public_key_pkcs1_pem = encode_rsa_public_pkcs1_pem( - // modulus.as_ref(), - // exponent.big_endian_without_leading_zero(), - // ) - // .unwrap(); - // assert_eq!( - // include_str!("../tests/rsa/public_rsa_key_pkcs1.pem").trim(), - // public_key_pkcs1_pem.replace('\r', "").trim() - // ); - // - // let public_key_pkcs1_der = encode_rsa_public_pkcs1_der( - // modulus.as_ref(), - // exponent.big_endian_without_leading_zero(), - // ) - // .unwrap(); - // assert_eq!( - // include_bytes!("../tests/rsa/public_rsa_key.der").to_vec(), - // public_key_pkcs1_der - // ); - // } - // - // #[test] - // fn public_key_encoding_pkcs8() { - // let privkey_pem = - // decode_pem(include_str!("../tests/rsa/private_rsa_key_pkcs8.pem")).unwrap(); - // let privkey = privkey_pem.as_key().unwrap(); - // let ring_key = signature::RsaKeyPair::from_der(match privkey { - // Key::Der(bytes) => bytes, - // _ => panic!("Unexpected"), - // }) - // .unwrap(); - // let mut modulus = vec![0]; - // modulus.extend(ring_key.public_key().modulus().big_endian_without_leading_zero()); - // let exponent = ring_key.public_key().exponent(); - // - // let public_key_pkcs8 = encode_rsa_public_pkcs8_pem( - // modulus.as_ref(), - // exponent.big_endian_without_leading_zero(), - // ) - // .unwrap(); - // assert_eq!( - // include_str!("../tests/rsa/public_rsa_key_pkcs8.pem").trim(), - // public_key_pkcs8.replace('\r', "").trim() - // ); - // } - // - // #[test] - // fn public_key_encoding() { - // let privkey_pem = decode_pem(include_str!("../tests/ec/private_ecdsa_key.pem")).unwrap(); - // let privkey = privkey_pem.as_key().unwrap(); - // let alg = &signature::ECDSA_P256_SHA256_FIXED_SIGNING; - // let ring_key = signature::EcdsaKeyPair::from_pkcs8( - // alg, - // match privkey { - // Key::Pkcs8(bytes) => bytes, - // _ => panic!("Unexpected"), - // }, - // ) - // .unwrap(); - // - // let public_key_pem = encode_ec_public_pem(ring_key.public_key().as_ref()).unwrap(); - // assert_eq!( - // include_str!("../tests/ec/public_ecdsa_key.pem").trim(), - // public_key_pem.replace('\r', "").trim() - // ); - // - // let public_key_der = encode_ec_public_der(ring_key.public_key().as_ref()).unwrap(); - // // The stored ".pk8" key is just the x coordinate of the EC key - // // It's not truly a pkcs8 formatted DER - // // To get around that, a prepended binary specifies the EC key, EC name, - // // and X coordinate length. The length is unlikely to change.. in the - // // event that it does, look at the pem file (convert base64 to hex) and find - // // where 0x03, 0x42 don't match up. 0x42 is the length. - // let mut stored_pk8_der = vec![ - // 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, - // 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, - // ]; - // stored_pk8_der.extend(include_bytes!("../tests/ec/public_ecdsa_key.pk8").to_vec()); - // assert_eq!(stored_pk8_der, public_key_der); - // } -} diff --git a/src/pem/mod.rs b/src/pem/mod.rs index c79f281..99753de 100644 --- a/src/pem/mod.rs +++ b/src/pem/mod.rs @@ -1,34 +1 @@ pub(crate) mod decoder; -mod encoder; - -use crate::errors::Result; - -/// Encode (n, e) components into the public PKCS1 PEM format -pub fn encode_rsa_public_pkcs1_pem(modulus: &[u8], exponent: &[u8]) -> Result { - encoder::encode_rsa_public_pkcs1_pem(modulus, exponent) -} - -/// Encode (n, e) components into the public PKCS1 PEM format -pub fn encode_rsa_public_pkcs1_der(modulus: &[u8], exponent: &[u8]) -> Result> { - encoder::encode_rsa_public_pkcs1_der(modulus, exponent) -} - -/// TODO -pub fn encode_rsa_public_pkcs8_pem(modulus: &[u8], exponent: &[u8]) -> Result { - encoder::encode_rsa_public_pkcs8_pem(modulus, exponent) -} - -/// TODO -pub fn encode_rsa_public_pkcs8_der(modulus: &[u8], exponent: &[u8]) -> Result> { - encoder::encode_rsa_public_pkcs8_der(modulus, exponent) -} - -/// TODO -pub fn encode_ec_public_pem(x_coordinate: &[u8]) -> Result { - encoder::encode_ec_public_pem(x_coordinate) -} - -/// TODO -pub fn encode_ec_public_der(x_coordinate: &[u8]) -> Result> { - encoder::encode_ec_public_der(x_coordinate) -} From dd642bed1dd9eba3f82e86fb87fcb3d86baa4560 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Thu, 14 Nov 2019 20:00:47 +0100 Subject: [PATCH 40/55] Use GH actions --- .github/workflows/ci.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8b7c4fe --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: ci +on: [push, pull_request] + +jobs: + tests: + name: Tests + runs-on: ${{ matrix.os }} + strategy: + matrix: + build: [pinned, stable, nightly] + include: + - build: pinned + os: ubuntu-18.04 + rust: 1.34.0 + - build: stable + os: ubuntu-18.04 + rust: stable + - build: nightly + os: ubuntu-18.04 + rust: nightly + steps: + - uses: actions/checkout@v1 + - name: Install Rust + uses: hecrj/setup-rust-action@v1 + with: + rust-version: ${{ matrix.rust }} + + - name: Build System Info + run: rustc --version + + - name: Run tests + run: cargo test From 3fe0bc1f83d06413ea9f8ced046c08ec6124f8a6 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Thu, 14 Nov 2019 20:24:38 +0100 Subject: [PATCH 41/55] Update README --- README.md | 59 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 9834cc3..2e0f694 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ [API documentation on docs.rs](https://docs.rs/jsonwebtoken/) +See [JSON Web Tokens](https://en.wikipedia.org/wiki/JSON_Web_Token) for more information on what are JSON Web Tokens. + ## Installation Add the following to Cargo.toml: @@ -12,6 +14,24 @@ jsonwebtoken = "7" serde = {version = "1.0", features = ["derive"] } ``` +The minimum required Rust version is 1.34. + +## Algorithms +This library currently supports the following: + +- HS256 +- HS384 +- HS512 +- RS256 +- RS384 +- RS512 +- PS256 +- PS384 +- PS512 +- ES256 +- ES384 + + ## How to use Complete examples are available in the examples directory: a basic one and one with a custom header. @@ -30,7 +50,7 @@ struct Claims { ``` ### Header -The default algorithm is HS256. +The default algorithm is HS256, which uses a shared secret. ```rust let token = encode(&Header::default(), &my_claims, "secret".as_ref())?; @@ -59,7 +79,7 @@ Encoding a JWT takes 3 parameters: - a header: the `Header` struct - some claims: your own struct -- a key +- a key/secret When using HS256, HS2384 or HS512, the key is always a shared secret like in the example above. When using RSA/EC, the key should always be the content of the private key in the PEM format. @@ -67,8 +87,8 @@ RSA/EC, the key should always be the content of the private key in the PEM forma ### Decoding ```rust +// `token` is a struct with 2 fields: `header` and `claims` where `claims` is your own struct. let token = decode::(&token, "secret".as_ref(), &Validation::default())?; -// token is a struct with 2 fields: `header` and `claims` and `claims` is your own struct. ``` `decode` can error for a variety of reasons: @@ -79,16 +99,16 @@ let token = decode::(&token, "secret".as_ref(), &Validation::default())? As with encoding, when using HS256, HS2384 or HS512, the key is always a shared secret like in the example above. When using RSA/EC, the key should always be the content of the public key in the PEM format. -In some cases, for example if you don't know the algorithm used or need to grab the `kid`, you can decode only the header: +In some cases, for example if you don't know the algorithm used or need to grab the `kid`, you can choose to decode only the header: ```rust let header = decode_header(&token)?; ``` -This does not perform any signature verification/validations on the token so it could have been tampered with. +This does not perform any signature verification or validate the token claims. You can also decode a token using the public key components of a RSA key in base64 format. -The main use-case is for JWK where your public key is a JSON format like so: +The main use-case is for JWK where your public key is in a JSON format like so: ```json { @@ -100,11 +120,11 @@ The main use-case is for JWK where your public key is a JSON format like so: ``` ```rust +// `token` is a struct with 2 fields: `header` and `claims` where `claims` is your own struct. let token = decode_rsa_components::(&token, jwk["n"], jwk["e"], &Validation::new(Algorithm::RS256))?; -// token is a struct with 2 fields: `header` and `claims` and `claims` is your own struct. ``` -### Convertion .der to .pem +### Converting .der to .pem You can use openssl for that: @@ -113,8 +133,8 @@ openssl rsa -inform DER -outform PEM -in mykey.der -out mykey.pem ``` -#### Validation -This library validates automatically the `exp` claim. `nbf` is also validated if present. You can also validate the `sub`, `iss` and `aud` but +## Validation +This library validates automatically the `exp` claim and `nbf` is validated if present. You can also validate the `sub`, `iss` and `aud` but those require setting the expected value in the `Validation` struct. Since validating time fields is always a bit tricky due to clock skew, @@ -138,22 +158,3 @@ let mut validation = Validation::default(); validation.set_audience(&"Me"); // string validation.set_audience(&["Me", "You"]); // array of strings ``` - -## Algorithms -This library currently supports the following: - -- HS256 -- HS384 -- HS512 -- RS256 -- RS384 -- RS512 -- PS256 -- PS384 -- PS512 -- ES256 -- ES384 - -### RSA & ECDSA -By default, the `encode`/`decode` functions takes the PEM format since it is the most common. -RSA can also use the public key components modulus/exponent in base64 format for decoding. From b35719b55540f611c71c069a3c7986c18b23a9da Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Thu, 14 Nov 2019 20:32:03 +0100 Subject: [PATCH 42/55] Point to validation example --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 2e0f694..2d26f9b 100644 --- a/README.md +++ b/README.md @@ -158,3 +158,5 @@ let mut validation = Validation::default(); validation.set_audience(&"Me"); // string validation.set_audience(&["Me", "You"]); // array of strings ``` + +Look at `examples/validation.rs` for a full working example. From 6121db3d077102a6da6a92d6cd023c4db4f2b0c6 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Thu, 14 Nov 2019 20:35:20 +0100 Subject: [PATCH 43/55] Min version is 1.36? --- .github/workflows/ci.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b7c4fe..83bdfe8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: include: - build: pinned os: ubuntu-18.04 - rust: 1.34.0 + rust: 1.36.0 - build: stable os: ubuntu-18.04 rust: stable diff --git a/README.md b/README.md index 2d26f9b..cd78b9d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ jsonwebtoken = "7" serde = {version = "1.0", features = ["derive"] } ``` -The minimum required Rust version is 1.34. +The minimum required Rust version is 1.36. ## Algorithms This library currently supports the following: From d550c5f3188e726dcd212565fe4ed96db68b11c9 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Fri, 15 Nov 2019 20:16:38 +0100 Subject: [PATCH 44/55] Add more tests and document SEC1 lack of support --- README.md | 8 ++++++++ src/pem/decoder.rs | 2 ++ tests/ecdsa/mod.rs | 23 +++++++++++++++++++---- tests/ecdsa/private_jwtio.pem | 6 ++++++ tests/ecdsa/private_jwtio_pkcs8.pem | 6 ++++++ tests/ecdsa/public_jwtio.pem | 5 +++++ tests/rsa/mod.rs | 19 +++++++++++++++++++ tests/rsa/private_jwtio.pem | 27 +++++++++++++++++++++++++++ tests/rsa/public_jwtio.pem | 9 +++++++++ 9 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 tests/ecdsa/private_jwtio.pem create mode 100644 tests/ecdsa/private_jwtio_pkcs8.pem create mode 100644 tests/ecdsa/public_jwtio.pem create mode 100644 tests/rsa/private_jwtio.pem create mode 100644 tests/rsa/public_jwtio.pem diff --git a/README.md b/README.md index cd78b9d..a1cbce6 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,14 @@ You can use openssl for that: openssl rsa -inform DER -outform PEM -in mykey.der -out mykey.pem ``` +### Convert SEC1 private key to PKCS8 +`jsonwebtoken` currently only supports PKCS8 format for private EC keys. If your key has `BEGIN EC PRIVATE KEY` at the top, +this is a SEC1 type and can be converted to PKCS8 like so: + +```bash +openssl pkcs8 -topk8 -nocrypt -in sec1.pem -out pkcs8.pem +``` + ## Validation This library validates automatically the `exp` claim and `nbf` is validated if present. You can also validate the `sub`, `iss` and `aud` but diff --git a/src/pem/decoder.rs b/src/pem/decoder.rs index db41156..ded5ac5 100644 --- a/src/pem/decoder.rs +++ b/src/pem/decoder.rs @@ -13,7 +13,9 @@ enum PemType { #[derive(Debug, PartialEq)] enum Standard { + // Only for RSA Pkcs1, + // RSA/EC Pkcs8, } diff --git a/tests/ecdsa/mod.rs b/tests/ecdsa/mod.rs index b9754c1..d90242e 100644 --- a/tests/ecdsa/mod.rs +++ b/tests/ecdsa/mod.rs @@ -25,23 +25,38 @@ pub struct Claims { #[test] fn round_trip_sign_verification_pem() { let privkey = include_bytes!("private_ecdsa_key.pem"); - let encrypted = sign("hello world", privkey, Algorithm::ES256).unwrap(); let pubkey = include_bytes!("public_ecdsa_key.pem"); + let encrypted = sign("hello world", privkey, Algorithm::ES256).unwrap(); let is_valid = verify(&encrypted, "hello world", pubkey, Algorithm::ES256).unwrap(); assert!(is_valid); } #[test] fn round_trip_claim() { + let privkey = include_bytes!("private_ecdsa_key.pem"); + let pubkey = include_bytes!("public_ecdsa_key.pem"); let my_claims = Claims { sub: "b@b.com".to_string(), company: "ACME".to_string(), exp: Utc::now().timestamp() + 10000, }; - let privkey = include_bytes!("private_ecdsa_key.pem"); let token = encode(&Header::new(Algorithm::ES256), &my_claims, privkey).unwrap(); - let pubkey = include_bytes!("public_ecdsa_key.pem"); let token_data = decode::(&token, pubkey, &Validation::new(Algorithm::ES256)).unwrap(); assert_eq!(my_claims, token_data.claims); - assert!(token_data.header.kid.is_none()); +} + +// https://jwt.io/ is often used for examples so ensure their example works with jsonwebtoken +#[test] +fn roundtrip_with_jwtio_example() { + // We currently do not support SEC1 so we use the converted PKCS8 formatted + let privkey = include_bytes!("private_jwtio_pkcs8.pem"); + let pubkey = include_bytes!("public_jwtio.pem"); + let my_claims = Claims { + sub: "b@b.com".to_string(), + company: "ACME".to_string(), + exp: Utc::now().timestamp() + 10000, + }; + let token = encode(&Header::new(Algorithm::ES384), &my_claims, privkey).unwrap(); + let token_data = decode::(&token, pubkey, &Validation::new(Algorithm::ES384)).unwrap(); + assert_eq!(my_claims, token_data.claims); } diff --git a/tests/ecdsa/private_jwtio.pem b/tests/ecdsa/private_jwtio.pem new file mode 100644 index 0000000..6e8b0cc --- /dev/null +++ b/tests/ecdsa/private_jwtio.pem @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDCAHpFQ62QnGCEvYh/pE9QmR1C9aLcDItRbslbmhen/h1tt8AyMhske +enT+rAyyPhGgBwYFK4EEACKhZANiAAQLW5ZJePZzMIPAxMtZXkEWbDF0zo9f2n4+ +T1h/2sh/fviblc/VTyrv10GEtIi5qiOy85Pf1RRw8lE5IPUWpgu553SteKigiKLU +PeNpbqmYZUkWGh3MLfVzLmx85ii2vMU= +-----END EC PRIVATE KEY----- diff --git a/tests/ecdsa/private_jwtio_pkcs8.pem b/tests/ecdsa/private_jwtio_pkcs8.pem new file mode 100644 index 0000000..893d58e --- /dev/null +++ b/tests/ecdsa/private_jwtio_pkcs8.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCAHpFQ62QnGCEvYh/p +E9QmR1C9aLcDItRbslbmhen/h1tt8AyMhskeenT+rAyyPhGhZANiAAQLW5ZJePZz +MIPAxMtZXkEWbDF0zo9f2n4+T1h/2sh/fviblc/VTyrv10GEtIi5qiOy85Pf1RRw +8lE5IPUWpgu553SteKigiKLUPeNpbqmYZUkWGh3MLfVzLmx85ii2vMU= +-----END PRIVATE KEY----- diff --git a/tests/ecdsa/public_jwtio.pem b/tests/ecdsa/public_jwtio.pem new file mode 100644 index 0000000..81eac4e --- /dev/null +++ b/tests/ecdsa/public_jwtio.pem @@ -0,0 +1,5 @@ +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEC1uWSXj2czCDwMTLWV5BFmwxdM6PX9p+ +Pk9Yf9rIf374m5XP1U8q79dBhLSIuaojsvOT39UUcPJROSD1FqYLued0rXiooIii +1D3jaW6pmGVJFhodzC31cy5sfOYotrzF +-----END PUBLIC KEY----- diff --git a/tests/rsa/mod.rs b/tests/rsa/mod.rs index f93ed96..9b3301e 100644 --- a/tests/rsa/mod.rs +++ b/tests/rsa/mod.rs @@ -89,3 +89,22 @@ fn fails_with_non_pkcs8_key_format() { let _encrypted = sign("hello world", include_bytes!("private_rsa_key_pkcs1.pem"), Algorithm::ES256).unwrap(); } + +// https://jwt.io/ is often used for examples so ensure their example works with jsonwebtoken +#[test] +fn roundtrip_with_jwtio_example_jey() { + let privkey_pem = include_bytes!("private_jwtio.pem"); + let pubkey_pem = include_bytes!("public_jwtio.pem"); + + let my_claims = Claims { + sub: "b@b.com".to_string(), + company: "ACME".to_string(), + exp: Utc::now().timestamp() + 10000, + }; + + for &alg in RSA_ALGORITHMS { + let token = encode(&Header::new(alg), &my_claims, privkey_pem).unwrap(); + let token_data = decode::(&token, pubkey_pem, &Validation::new(alg)).unwrap(); + assert_eq!(my_claims, token_data.claims); + } +} diff --git a/tests/rsa/private_jwtio.pem b/tests/rsa/private_jwtio.pem new file mode 100644 index 0000000..61056a5 --- /dev/null +++ b/tests/rsa/private_jwtio.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw +kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr +m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi +NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV +3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2 +QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs +kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go +amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM ++bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9 +D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC +0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y +lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+ +hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp +bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X ++jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B +BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC +2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx +QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz +5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9 +Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0 +NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j +8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma +3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K +y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB +jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE= +-----END RSA PRIVATE KEY----- diff --git a/tests/rsa/public_jwtio.pem b/tests/rsa/public_jwtio.pem new file mode 100644 index 0000000..12301e0 --- /dev/null +++ b/tests/rsa/public_jwtio.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv +vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc +aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy +tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 +e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb +V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9 +MwIDAQAB +-----END PUBLIC KEY----- From 8d678086eb3ef08840a0da6eb2c2f1fd329033aa Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Fri, 15 Nov 2019 20:33:49 +0100 Subject: [PATCH 45/55] v7 alpha 1 --- Cargo.toml | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aa66e7a..927b64d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "jsonwebtoken" -version = "7.0.0" +version = "7.0.0-alpha.1" authors = ["Vincent Prouillet "] license = "MIT" readme = "README.md" description = "Create and decode JWTs in a strongly typed way." homepage = "https://github.com/Keats/jsonwebtoken" repository = "https://github.com/Keats/jsonwebtoken" -keywords = ["jwt", "web", "api", "token", "json", "jwk"] +keywords = ["jwt", "web", "api", "token", "jwk"] edition = "2018" [dependencies] diff --git a/README.md b/README.md index a1cbce6..ca53cae 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [API documentation on docs.rs](https://docs.rs/jsonwebtoken/) -See [JSON Web Tokens](https://en.wikipedia.org/wiki/JSON_Web_Token) for more information on what are JSON Web Tokens. +See [JSON Web Tokens](https://en.wikipedia.org/wiki/JSON_Web_Token) for more information on what JSON Web Tokens are. ## Installation Add the following to Cargo.toml: From 499b439cb09a03ac08a96700f5eef18b782396ef Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Fri, 15 Nov 2019 20:39:19 +0100 Subject: [PATCH 46/55] Wrong badge --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 927b64d..37f8b32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,4 +24,4 @@ simple_asn1 = "0.4" chrono = "0.4" [badges] -maintenance = { status = "passively-developed" } +maintenance = { status = "passively-maintained" } From bfcfc1d3411805b1ab64bbcc85bc3dcf91a5eabb Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Thu, 28 Nov 2019 19:27:08 +0100 Subject: [PATCH 47/55] Handle aud not being a sequence Closes #110 --- Cargo.toml | 2 +- src/validation.rs | 38 ++++++++++++++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 37f8b32..d6f4a2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jsonwebtoken" -version = "7.0.0-alpha.1" +version = "7.0.0-alpha.2" authors = ["Vincent Prouillet "] license = "MIT" readme = "README.md" diff --git a/src/validation.rs b/src/validation.rs index 48d79c7..b8106ef 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -147,10 +147,20 @@ pub fn validate(claims: &Map, options: &Validation) -> Result<()> if let Some(ref correct_aud) = options.aud { if let Some(aud) = claims.get("aud") { - let provided_aud: HashSet = from_value(aud.clone())?; - if provided_aud.intersection(correct_aud).count() == 0 { - return Err(new_error(ErrorKind::InvalidAudience)); - } + match aud { + Value::String(aud_found) => { + if !correct_aud.contains(aud_found) { + return Err(new_error(ErrorKind::InvalidAudience)); + } + } + Value::Array(_) => { + let provided_aud: HashSet = from_value(aud.clone())?; + if provided_aud.intersection(correct_aud).count() == 0 { + return Err(new_error(ErrorKind::InvalidAudience)); + } + } + _ => return Err(new_error(ErrorKind::InvalidAudience)) + }; } else { return Err(new_error(ErrorKind::InvalidAudience)); } @@ -432,4 +442,24 @@ mod tests { } }; } + + // https://github.com/Keats/jsonwebtoken/issues/110 + #[test] + fn aud_use_validation_struct() { + let mut claims = Map::new(); + claims.insert("aud".to_string(), to_value("my-googleclientid1234.apps.googleusercontent.com").unwrap()); + + let aud = "my-googleclientid1234.apps.googleusercontent.com".to_string(); + let mut aud_hashset = std::collections::HashSet::new(); + aud_hashset.insert(aud); + + let validation = Validation { + aud: Some(aud_hashset), + validate_exp: false, + ..Validation::default() + }; + let res = validate(&claims, &validation); + println!("{:?}", res); + assert!(res.is_ok()); + } } From 771f955690e5e7537a0bb4cc92f9f1a1369ad6ba Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Thu, 28 Nov 2019 19:28:59 +0100 Subject: [PATCH 48/55] Better GH workflow --- .github/workflows/ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83bdfe8..6bcf8e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,10 @@ name: ci -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: + jobs: tests: From 0abeeac25fda44b87d8b6ecbe55905e3b105ca67 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sun, 29 Dec 2019 18:42:35 +0100 Subject: [PATCH 49/55] Add EncodingKey --- README.md | 8 ++-- examples/custom_chrono.rs | 13 +++++-- examples/custom_header.rs | 4 +- examples/validation.rs | 6 +-- src/crypto/ecdsa.rs | 5 +-- src/crypto/mod.rs | 17 ++++----- src/crypto/rsa.rs | 6 +-- src/encoding.rs | 79 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 38 +------------------ src/validation.rs | 14 +++---- tests/ecdsa/mod.rs | 19 ++++++++-- tests/hmac.rs | 11 ++++-- tests/rsa/mod.rs | 30 ++++++++------- 13 files changed, 157 insertions(+), 93 deletions(-) create mode 100644 src/encoding.rs diff --git a/README.md b/README.md index ca53cae..bb3d9c5 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Complete examples are available in the examples directory: a basic one and one w In terms of imports and structs: ```rust use serde::{Serialize, Deserialize}; -use jsonwebtoken::{encode, decode, Header, Algorithm, Validation}; +use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey}; /// Our claims struct, it needs to derive `Serialize` and/or `Deserialize` #[derive(Debug, Serialize, Deserialize)] @@ -53,7 +53,7 @@ struct Claims { The default algorithm is HS256, which uses a shared secret. ```rust -let token = encode(&Header::default(), &my_claims, "secret".as_ref())?; +let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret("secret".as_ref()))?; ``` #### Custom headers & changing algorithm @@ -63,7 +63,7 @@ If you want to set the `kid` parameter or change the algorithm for example: ```rust let mut header = Header::new(Algorithm::HS512); header.kid = Some("blabla".to_owned()); -let token = encode(&header, &my_claims, "secret".as_ref())?; +let token = encode(&header, &my_claims, &EncodingKey::from_secret("secret".as_ref()))?; ``` Look at `examples/custom_header.rs` for a full working example. @@ -71,7 +71,7 @@ Look at `examples/custom_header.rs` for a full working example. ```rust // HS256 -let token = encode(&Header::default(), &my_claims, "secret".as_ref())?; +let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret("secret".as_ref()))?; // RSA let token = encode(&Header::new(Algorithm::RS256), &my_claims, include_str!("privkey.pem"))?; ``` diff --git a/examples/custom_chrono.rs b/examples/custom_chrono.rs index e0b1185..facab56 100644 --- a/examples/custom_chrono.rs +++ b/examples/custom_chrono.rs @@ -1,5 +1,5 @@ use chrono::prelude::*; -use jsonwebtoken::{Header, Validation}; +use jsonwebtoken::{EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; const SECRET: &str = "some-secret"; @@ -51,8 +51,9 @@ mod jwt_numeric_date { let claims = Claims { sub: sub.clone(), iat, exp }; - let token = encode(&Header::default(), &claims, SECRET.as_ref()) - .expect("Failed to encode claims"); + let token = + encode(&Header::default(), &claims, &EncodingKey::from_secret(SECRET.as_ref())) + .expect("Failed to encode claims"); assert_eq!(&token, EXPECTED_TOKEN); @@ -82,7 +83,11 @@ fn main() -> Result<(), Box> { let claims = Claims { sub: sub.clone(), iat, exp }; - let token = jsonwebtoken::encode(&Header::default(), &claims, SECRET.as_ref())?; + let token = jsonwebtoken::encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(SECRET.as_ref()), + )?; println!("serialized token: {}", &token); diff --git a/examples/custom_header.rs b/examples/custom_header.rs index f298c8d..adaf89c 100644 --- a/examples/custom_header.rs +++ b/examples/custom_header.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use jsonwebtoken::errors::ErrorKind; -use jsonwebtoken::{decode, encode, Algorithm, Header, Validation}; +use jsonwebtoken::{decode, encode, Algorithm, EncodingKey, Header, Validation}; #[derive(Debug, Serialize, Deserialize)] struct Claims { @@ -19,7 +19,7 @@ fn main() { header.kid = Some("signing_key".to_owned()); header.alg = Algorithm::HS512; - let token = match encode(&header, &my_claims, key) { + let token = match encode(&header, &my_claims, &EncodingKey::from_secret(key)) { Ok(t) => t, Err(_) => panic!(), // in practice you would return the error }; diff --git a/examples/validation.rs b/examples/validation.rs index 6c22d3f..88f9da3 100644 --- a/examples/validation.rs +++ b/examples/validation.rs @@ -1,5 +1,5 @@ use jsonwebtoken::errors::ErrorKind; -use jsonwebtoken::{decode, encode, Header, Validation}; +use jsonwebtoken::{decode, encode, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] @@ -10,10 +10,10 @@ struct Claims { } fn main() { + let key = b"secret"; let my_claims = Claims { sub: "b@b.com".to_owned(), company: "ACME".to_owned(), exp: 10000000000 }; - let key = b"secret"; - let token = match encode(&Header::default(), &my_claims, key) { + let token = match encode(&Header::default(), &my_claims, &EncodingKey::from_secret(key)) { Ok(t) => t, Err(_) => panic!(), // in practice you would return the error }; diff --git a/src/crypto/ecdsa.rs b/src/crypto/ecdsa.rs index 25099a7..afa0f79 100644 --- a/src/crypto/ecdsa.rs +++ b/src/crypto/ecdsa.rs @@ -2,7 +2,6 @@ use ring::{rand, signature}; use crate::algorithms::Algorithm; use crate::errors::Result; -use crate::pem::decoder::PemEncodedKey; use crate::serialization::b64_encode; /// Only used internally when validating EC, to map from our enum to the Ring EcdsaVerificationAlgorithm structs. @@ -26,13 +25,13 @@ pub(crate) fn alg_to_ec_signing(alg: Algorithm) -> &'static signature::EcdsaSign } /// The actual ECDSA signing + encoding +/// The key needs to be in PKCS8 format pub fn sign( alg: &'static signature::EcdsaSigningAlgorithm, key: &[u8], message: &str, ) -> Result { - let pem_key = PemEncodedKey::new(key)?; - let signing_key = signature::EcdsaKeyPair::from_pkcs8(alg, pem_key.as_ec_private_key()?)?; + let signing_key = signature::EcdsaKeyPair::from_pkcs8(alg, key)?; let rng = rand::SystemRandom::new(); let out = signing_key.sign(&rng, message.as_bytes())?; Ok(b64_encode(out.as_ref())) diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 98dc440..7390286 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -2,6 +2,7 @@ use ring::constant_time::verify_slices_are_equal; use ring::{hmac, signature}; use crate::algorithms::Algorithm; +use crate::encoding::EncodingKey; use crate::errors::Result; use crate::pem::decoder::PemEncodedKey; use crate::serialization::{b64_decode, b64_encode}; @@ -20,16 +21,14 @@ pub(crate) fn sign_hmac(alg: hmac::Algorithm, key: &[u8], message: &str) -> Resu /// the base64 url safe encoded of the result. /// /// If you just want to encode a JWT, use `encode` instead. -/// -/// `key` is the secret for HMAC and a pem encoded string otherwise -pub fn sign(message: &str, key: &[u8], algorithm: Algorithm) -> Result { +pub fn sign(message: &str, key: &EncodingKey, algorithm: Algorithm) -> Result { match algorithm { - Algorithm::HS256 => sign_hmac(hmac::HMAC_SHA256, key, message), - Algorithm::HS384 => sign_hmac(hmac::HMAC_SHA384, key, message), - Algorithm::HS512 => sign_hmac(hmac::HMAC_SHA512, key, message), + Algorithm::HS256 => sign_hmac(hmac::HMAC_SHA256, key.inner(), message), + Algorithm::HS384 => sign_hmac(hmac::HMAC_SHA384, key.inner(), message), + Algorithm::HS512 => sign_hmac(hmac::HMAC_SHA512, key.inner(), message), Algorithm::ES256 | Algorithm::ES384 => { - ecdsa::sign(ecdsa::alg_to_ec_signing(algorithm), key, message) + ecdsa::sign(ecdsa::alg_to_ec_signing(algorithm), key.inner(), message) } Algorithm::RS256 @@ -37,7 +36,7 @@ pub fn sign(message: &str, key: &[u8], algorithm: Algorithm) -> Result { | Algorithm::RS512 | Algorithm::PS256 | Algorithm::PS384 - | Algorithm::PS512 => rsa::sign(rsa::alg_to_rsa_signing(algorithm), key, message), + | Algorithm::PS512 => rsa::sign(rsa::alg_to_rsa_signing(algorithm), key.inner(), message), } } @@ -69,7 +68,7 @@ pub fn verify(signature: &str, message: &str, key: &[u8], algorithm: Algorithm) match algorithm { Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => { // we just re-sign the message with the key and compare if they are equal - let signed = sign(message, key, algorithm)?; + let signed = sign(message, &EncodingKey::from_secret(key), algorithm)?; Ok(verify_slices_are_equal(signature.as_ref(), signed.as_ref()).is_ok()) } Algorithm::ES256 | Algorithm::ES384 => { diff --git a/src/crypto/rsa.rs b/src/crypto/rsa.rs index 89da359..ec69da5 100644 --- a/src/crypto/rsa.rs +++ b/src/crypto/rsa.rs @@ -3,7 +3,6 @@ use simple_asn1::BigUint; use crate::algorithms::Algorithm; use crate::errors::{ErrorKind, Result}; -use crate::pem::decoder::PemEncodedKey; use crate::serialization::{b64_decode, b64_encode}; /// Only used internally when validating RSA, to map from our enum to the Ring param structs. @@ -33,15 +32,14 @@ pub(crate) fn alg_to_rsa_signing(alg: Algorithm) -> &'static dyn signature::RsaE } /// The actual RSA signing + encoding +/// The key needs to be in PKCS8 format /// Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html pub(crate) fn sign( alg: &'static dyn signature::RsaEncoding, key: &[u8], message: &str, ) -> Result { - let pem_key = PemEncodedKey::new(key)?; - let key_pair = signature::RsaKeyPair::from_der(pem_key.as_rsa_key()?) - .map_err(|_| ErrorKind::InvalidRsaKey)?; + let key_pair = signature::RsaKeyPair::from_der(key).map_err(|_| ErrorKind::InvalidRsaKey)?; let mut signature = vec![0; key_pair.public_modulus_len()]; let rng = rand::SystemRandom::new(); diff --git a/src/encoding.rs b/src/encoding.rs new file mode 100644 index 0000000..8df4776 --- /dev/null +++ b/src/encoding.rs @@ -0,0 +1,79 @@ +use std::borrow::Cow; + +use serde::ser::Serialize; + +use crate::crypto; +use crate::errors::Result; +use crate::header::Header; +use crate::pem::decoder::PemEncodedKey; +use crate::serialization::b64_encode_part; + +/// A key to encode a JWT with. Can be a secret, a PEM-encoded key or a DER-encoded key. +#[derive(Debug, Clone, PartialEq)] +pub struct EncodingKey<'a> { + content: Cow<'a, [u8]>, +} + +impl<'a> EncodingKey<'a> { + /// If you're using HMAC, use that. + pub fn from_secret(secret: &'a [u8]) -> Self { + EncodingKey { content: Cow::Borrowed(secret) } + } + + /// If you are loading a RSA key from a .pem file + /// This errors if the key is not a valid RSA key + pub fn from_rsa_pem(key: &'a [u8]) -> Result { + let pem_key = PemEncodedKey::new(key)?; + let content = pem_key.as_rsa_key()?; + Ok(EncodingKey { content: Cow::Owned(content.to_vec()) }) + } + + /// If you are loading a ECDSA key from a .pem file + /// This errors if the key is not a valid private EC key + pub fn from_ec_pem(key: &'a [u8]) -> Result { + let pem_key = PemEncodedKey::new(key)?; + let content = pem_key.as_ec_private_key()?; + Ok(EncodingKey { content: Cow::Owned(content.to_vec()) }) + } + + /// If you know what you're doing and have the DER-encoded key, for RSA or ECDSA + pub fn from_der(der: &'a [u8]) -> Self { + EncodingKey { content: Cow::Borrowed(der) } + } + + /// Access the key, normal users do not need to use that. + pub fn inner(&'a self) -> &'a [u8] { + &self.content + } +} + +/// Encode the header and claims given and sign the payload using the algorithm from the header and the key. +/// If the algorithm given is RSA or EC, the key needs to be in the PEM format. +/// +/// ```rust +/// use serde::{Deserialize, Serialize}; +/// use jsonwebtoken::{encode, Algorithm, Header, EncodingKey}; +/// +/// #[derive(Debug, Serialize, Deserialize)] +/// struct Claims { +/// sub: String, +/// company: String +/// } +/// +/// let my_claims = Claims { +/// sub: "b@b.com".to_owned(), +/// company: "ACME".to_owned() +/// }; +/// +/// // my_claims is a struct that implements Serialize +/// // This will create a JWT using HS256 as algorithm +/// let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret("secret".as_ref())).unwrap(); +/// ``` +pub fn encode(header: &Header, claims: &T, key: &EncodingKey) -> Result { + let encoded_header = b64_encode_part(&header)?; + let encoded_claims = b64_encode_part(&claims)?; + let message = [encoded_header.as_ref(), encoded_claims.as_ref()].join("."); + let signature = crypto::sign(&*message, key, header.alg)?; + + Ok([message, signature].join(".")) +} diff --git a/src/lib.rs b/src/lib.rs index 77d34ca..7b3d7b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ mod algorithms; /// Lower level functions, if you want to do something other than JWTs pub mod crypto; mod decoding; +mod encoding; /// All the errors that can be encountered while encoding/decoding JWTs pub mod errors; mod header; @@ -18,41 +19,6 @@ pub use algorithms::Algorithm; pub use decoding::{ dangerous_unsafe_decode, decode, decode_header, decode_rsa_components, TokenData, }; +pub use encoding::{encode, EncodingKey}; pub use header::Header; pub use validation::Validation; - -use serde::ser::Serialize; - -use crate::errors::Result; -use crate::serialization::b64_encode_part; - -/// Encode the header and claims given and sign the payload using the algorithm from the header and the key. -/// If the algorithm given is RSA or EC, the key needs to be in the PEM format. -/// -/// ```rust -/// use serde::{Deserialize, Serialize}; -/// use jsonwebtoken::{encode, Algorithm, Header}; -/// -/// #[derive(Debug, Serialize, Deserialize)] -/// struct Claims { -/// sub: String, -/// company: String -/// } -/// -/// let my_claims = Claims { -/// sub: "b@b.com".to_owned(), -/// company: "ACME".to_owned() -/// }; -/// -/// // my_claims is a struct that implements Serialize -/// // This will create a JWT using HS256 as algorithm -/// let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap(); -/// ``` -pub fn encode(header: &Header, claims: &T, key: &[u8]) -> Result { - let encoded_header = b64_encode_part(&header)?; - let encoded_claims = b64_encode_part(&claims)?; - let message = [encoded_header.as_ref(), encoded_claims.as_ref()].join("."); - let signature = crypto::sign(&*message, key, header.alg)?; - - Ok([message, signature].join(".")) -} diff --git a/src/validation.rs b/src/validation.rs index b8106ef..b6805aa 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -159,7 +159,7 @@ pub fn validate(claims: &Map, options: &Validation) -> Result<()> return Err(new_error(ErrorKind::InvalidAudience)); } } - _ => return Err(new_error(ErrorKind::InvalidAudience)) + _ => return Err(new_error(ErrorKind::InvalidAudience)), }; } else { return Err(new_error(ErrorKind::InvalidAudience)); @@ -447,17 +447,17 @@ mod tests { #[test] fn aud_use_validation_struct() { let mut claims = Map::new(); - claims.insert("aud".to_string(), to_value("my-googleclientid1234.apps.googleusercontent.com").unwrap()); + claims.insert( + "aud".to_string(), + to_value("my-googleclientid1234.apps.googleusercontent.com").unwrap(), + ); let aud = "my-googleclientid1234.apps.googleusercontent.com".to_string(); let mut aud_hashset = std::collections::HashSet::new(); aud_hashset.insert(aud); - let validation = Validation { - aud: Some(aud_hashset), - validate_exp: false, - ..Validation::default() - }; + let validation = + Validation { aud: Some(aud_hashset), validate_exp: false, ..Validation::default() }; let res = validate(&claims, &validation); println!("{:?}", res); assert!(res.is_ok()); diff --git a/tests/ecdsa/mod.rs b/tests/ecdsa/mod.rs index d90242e..2ccec50 100644 --- a/tests/ecdsa/mod.rs +++ b/tests/ecdsa/mod.rs @@ -1,7 +1,7 @@ use chrono::Utc; use jsonwebtoken::{ crypto::{sign, verify}, - decode, encode, Algorithm, Header, Validation, + decode, encode, Algorithm, EncodingKey, Header, Validation, }; use serde::{Deserialize, Serialize}; @@ -26,7 +26,8 @@ pub struct Claims { fn round_trip_sign_verification_pem() { let privkey = include_bytes!("private_ecdsa_key.pem"); let pubkey = include_bytes!("public_ecdsa_key.pem"); - let encrypted = sign("hello world", privkey, Algorithm::ES256).unwrap(); + let encrypted = + sign("hello world", &EncodingKey::from_ec_pem(privkey).unwrap(), Algorithm::ES256).unwrap(); let is_valid = verify(&encrypted, "hello world", pubkey, Algorithm::ES256).unwrap(); assert!(is_valid); } @@ -40,7 +41,12 @@ fn round_trip_claim() { company: "ACME".to_string(), exp: Utc::now().timestamp() + 10000, }; - let token = encode(&Header::new(Algorithm::ES256), &my_claims, privkey).unwrap(); + let token = encode( + &Header::new(Algorithm::ES256), + &my_claims, + &EncodingKey::from_ec_pem(privkey).unwrap(), + ) + .unwrap(); let token_data = decode::(&token, pubkey, &Validation::new(Algorithm::ES256)).unwrap(); assert_eq!(my_claims, token_data.claims); } @@ -56,7 +62,12 @@ fn roundtrip_with_jwtio_example() { company: "ACME".to_string(), exp: Utc::now().timestamp() + 10000, }; - let token = encode(&Header::new(Algorithm::ES384), &my_claims, privkey).unwrap(); + let token = encode( + &Header::new(Algorithm::ES384), + &my_claims, + &EncodingKey::from_ec_pem(privkey).unwrap(), + ) + .unwrap(); let token_data = decode::(&token, pubkey, &Validation::new(Algorithm::ES384)).unwrap(); assert_eq!(my_claims, token_data.claims); } diff --git a/tests/hmac.rs b/tests/hmac.rs index 98e2d56..d77f8bc 100644 --- a/tests/hmac.rs +++ b/tests/hmac.rs @@ -1,7 +1,8 @@ use chrono::Utc; use jsonwebtoken::{ crypto::{sign, verify}, - dangerous_unsafe_decode, decode, decode_header, encode, Algorithm, Header, Validation, + dangerous_unsafe_decode, decode, decode_header, encode, Algorithm, EncodingKey, Header, + Validation, }; use serde::{Deserialize, Serialize}; @@ -14,7 +15,8 @@ pub struct Claims { #[test] fn sign_hs256() { - let result = sign("hello world", b"secret", Algorithm::HS256).unwrap(); + let result = + sign("hello world", &EncodingKey::from_secret(b"secret"), Algorithm::HS256).unwrap(); let expected = "c0zGLzKEFWj0VxWuufTXiRMk5tlI5MbGDAYhzaxIYjo"; assert_eq!(result, expected); } @@ -35,7 +37,7 @@ fn encode_with_custom_header() { }; let mut header = Header::default(); header.kid = Some("kid".to_string()); - let token = encode(&header, &my_claims, b"secret").unwrap(); + let token = encode(&header, &my_claims, &EncodingKey::from_secret(b"secret")).unwrap(); let token_data = decode::(&token, b"secret", &Validation::default()).unwrap(); assert_eq!(my_claims, token_data.claims); assert_eq!("kid", token_data.header.kid.unwrap()); @@ -48,7 +50,8 @@ fn round_trip_claim() { company: "ACME".to_string(), exp: Utc::now().timestamp() + 10000, }; - let token = encode(&Header::default(), &my_claims, b"secret").unwrap(); + let token = + encode(&Header::default(), &my_claims, &EncodingKey::from_secret(b"secret")).unwrap(); let token_data = decode::(&token, b"secret", &Validation::default()).unwrap(); assert_eq!(my_claims, token_data.claims); assert!(token_data.header.kid.is_none()); diff --git a/tests/rsa/mod.rs b/tests/rsa/mod.rs index 9b3301e..814c645 100644 --- a/tests/rsa/mod.rs +++ b/tests/rsa/mod.rs @@ -1,7 +1,7 @@ use chrono::Utc; use jsonwebtoken::{ crypto::{sign, verify}, - decode, decode_rsa_components, encode, Algorithm, Header, Validation, + decode, decode_rsa_components, encode, Algorithm, EncodingKey, Header, Validation, }; use serde::{Deserialize, Serialize}; @@ -27,7 +27,8 @@ fn round_trip_sign_verification_pem_pkcs1() { let pubkey_pem = include_bytes!("public_rsa_key_pkcs1.pem"); for &alg in RSA_ALGORITHMS { - let encrypted = sign("hello world", privkey_pem, alg).unwrap(); + let encrypted = + sign("hello world", &EncodingKey::from_rsa_pem(privkey_pem).unwrap(), alg).unwrap(); let is_valid = verify(&encrypted, "hello world", pubkey_pem, alg).unwrap(); assert!(is_valid); } @@ -39,7 +40,8 @@ fn round_trip_sign_verification_pem_pkcs8() { let pubkey_pem = include_bytes!("public_rsa_key_pkcs8.pem"); for &alg in RSA_ALGORITHMS { - let encrypted = sign("hello world", privkey_pem, alg).unwrap(); + let encrypted = + sign("hello world", &EncodingKey::from_rsa_pem(privkey_pem).unwrap(), alg).unwrap(); let is_valid = verify(&encrypted, "hello world", pubkey_pem, alg).unwrap(); assert!(is_valid); } @@ -55,7 +57,9 @@ fn round_trip_claim() { let privkey = include_bytes!("private_rsa_key_pkcs1.pem"); for &alg in RSA_ALGORITHMS { - let token = encode(&Header::new(alg), &my_claims, privkey).unwrap(); + let token = + encode(&Header::new(alg), &my_claims, &EncodingKey::from_rsa_pem(privkey).unwrap()) + .unwrap(); let token_data = decode::( &token, include_bytes!("public_rsa_key_pkcs1.pem"), @@ -78,18 +82,16 @@ fn rsa_modulus_exponent() { let n = "yRE6rHuNR0QbHO3H3Kt2pOKGVhQqGZXInOduQNxXzuKlvQTLUTv4l4sggh5_CYYi_cvI-SXVT9kPWSKXxJXBXd_4LkvcPuUakBoAkfh-eiFVMh2VrUyWyj3MFl0HTVF9KwRXLAcwkREiS3npThHRyIxuy0ZMeZfxVL5arMhw1SRELB8HoGfG_AtH89BIE9jDBHZ9dLelK9a184zAf8LwoPLxvJb3Il5nncqPcSfKDDodMFBIMc4lQzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xqi-yUod-j8MtvIj812dkS4QMiRVN_by2h3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5TdQ"; let e = "AQAB"; - let encrypted = encode(&Header::new(Algorithm::RS256), &my_claims, privkey.as_ref()).unwrap(); + let encrypted = encode( + &Header::new(Algorithm::RS256), + &my_claims, + &EncodingKey::from_rsa_pem(privkey.as_ref()).unwrap(), + ) + .unwrap(); let res = decode_rsa_components::(&encrypted, n, e, &Validation::new(Algorithm::RS256)); assert!(res.is_ok()); } -#[test] -#[should_panic(expected = "InvalidKeyFormat")] -fn fails_with_non_pkcs8_key_format() { - let _encrypted = - sign("hello world", include_bytes!("private_rsa_key_pkcs1.pem"), Algorithm::ES256).unwrap(); -} - // https://jwt.io/ is often used for examples so ensure their example works with jsonwebtoken #[test] fn roundtrip_with_jwtio_example_jey() { @@ -103,7 +105,9 @@ fn roundtrip_with_jwtio_example_jey() { }; for &alg in RSA_ALGORITHMS { - let token = encode(&Header::new(alg), &my_claims, privkey_pem).unwrap(); + let token = + encode(&Header::new(alg), &my_claims, &EncodingKey::from_rsa_pem(privkey_pem).unwrap()) + .unwrap(); let token_data = decode::(&token, pubkey_pem, &Validation::new(alg)).unwrap(); assert_eq!(my_claims, token_data.claims); } From 77ae0effc86960c1af935d155edbca9a1dadd266 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sun, 29 Dec 2019 21:50:06 +0100 Subject: [PATCH 50/55] Add DecodingKey --- Cargo.toml | 2 +- README.md | 22 ++---- benches/jwt.rs | 9 ++- examples/custom_chrono.rs | 17 +++-- examples/custom_header.rs | 8 ++- examples/validation.rs | 4 +- src/crypto/mod.rs | 62 ++++++----------- src/crypto/rsa.rs | 4 +- src/decoding.rs | 142 ++++++++++++++++++++------------------ src/encoding.rs | 7 +- src/lib.rs | 4 +- tests/ecdsa/mod.rs | 61 ++++++++++------ tests/hmac.rs | 33 ++++++--- tests/rsa/mod.rs | 45 +++++++++--- 14 files changed, 235 insertions(+), 185 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d6f4a2a..02a49ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jsonwebtoken" -version = "7.0.0-alpha.2" +version = "7.0.0-alpha.3" authors = ["Vincent Prouillet "] license = "MIT" readme = "README.md" diff --git a/README.md b/README.md index bb3d9c5..15dda9d 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Complete examples are available in the examples directory: a basic one and one w In terms of imports and structs: ```rust use serde::{Serialize, Deserialize}; -use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey}; +use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey, DecodingKey}; /// Our claims struct, it needs to derive `Serialize` and/or `Deserialize` #[derive(Debug, Serialize, Deserialize)] @@ -73,7 +73,7 @@ Look at `examples/custom_header.rs` for a full working example. // HS256 let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret("secret".as_ref()))?; // RSA -let token = encode(&Header::new(Algorithm::RS256), &my_claims, include_str!("privkey.pem"))?; +let token = encode(&Header::new(Algorithm::RS256), &my_claims, &EncodingKey::from_rsa_pem(include_bytes!("privkey.pem"))?)?; ``` Encoding a JWT takes 3 parameters: @@ -82,13 +82,13 @@ Encoding a JWT takes 3 parameters: - a key/secret When using HS256, HS2384 or HS512, the key is always a shared secret like in the example above. When using -RSA/EC, the key should always be the content of the private key in the PEM format. +RSA/EC, the key should always be the content of the private key in the PEM or DER format. ### Decoding ```rust // `token` is a struct with 2 fields: `header` and `claims` where `claims` is your own struct. -let token = decode::(&token, "secret".as_ref(), &Validation::default())?; +let token = decode::(&token, &DecodingKey::from_secret("secret".as_ref()), &Validation::default())?; ``` `decode` can error for a variety of reasons: @@ -97,7 +97,7 @@ let token = decode::(&token, "secret".as_ref(), &Validation::default())? - validation of at least one reserved claim failed As with encoding, when using HS256, HS2384 or HS512, the key is always a shared secret like in the example above. When using -RSA/EC, the key should always be the content of the public key in the PEM format. +RSA/EC, the key should always be the content of the public key in the PEM or DER format. In some cases, for example if you don't know the algorithm used or need to grab the `kid`, you can choose to decode only the header: @@ -121,15 +121,7 @@ The main use-case is for JWK where your public key is in a JSON format like so: ```rust // `token` is a struct with 2 fields: `header` and `claims` where `claims` is your own struct. -let token = decode_rsa_components::(&token, jwk["n"], jwk["e"], &Validation::new(Algorithm::RS256))?; -``` - -### Converting .der to .pem - -You can use openssl for that: - -```bash -openssl rsa -inform DER -outform PEM -in mykey.der -out mykey.pem +let token = decode::(&token, &EncodingKey::from_rsa_components(jwk["n"], jwk["e"]), &Validation::new(Algorithm::RS256))?; ``` ### Convert SEC1 private key to PKCS8 @@ -145,7 +137,7 @@ openssl pkcs8 -topk8 -nocrypt -in sec1.pem -out pkcs8.pem This library validates automatically the `exp` claim and `nbf` is validated if present. You can also validate the `sub`, `iss` and `aud` but those require setting the expected value in the `Validation` struct. -Since validating time fields is always a bit tricky due to clock skew, +Since validating time fields is always a bit tricky due to clock skew, you can add some leeway to the `iat`, `exp` and `nbf` validation by setting the `leeway` field. Last but not least, you will need to set the algorithm(s) allowed for this token if you are not using `HS256`. diff --git a/benches/jwt.rs b/benches/jwt.rs index 9efe6e1..41a4fc9 100644 --- a/benches/jwt.rs +++ b/benches/jwt.rs @@ -1,7 +1,7 @@ #![feature(test)] extern crate test; -use jsonwebtoken::{decode, encode, Header, Validation}; +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] @@ -13,12 +13,15 @@ struct Claims { #[bench] fn bench_encode(b: &mut test::Bencher) { let claim = Claims { sub: "b@b.com".to_owned(), company: "ACME".to_owned() }; + let key = EncodingKey::from_secret("secret".as_ref()); - b.iter(|| encode(&Header::default(), &claim, "secret".as_ref())); + b.iter(|| encode(&Header::default(), &claim, &key)); } #[bench] fn bench_decode(b: &mut test::Bencher) { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; - b.iter(|| decode::(token, "secret".as_ref(), &Validation::default())); + let key = DecodingKey::from_secret("secret".as_ref()); + + b.iter(|| decode::(token, &key, &Validation::default())); } diff --git a/examples/custom_chrono.rs b/examples/custom_chrono.rs index facab56..561ad10 100644 --- a/examples/custom_chrono.rs +++ b/examples/custom_chrono.rs @@ -1,5 +1,5 @@ use chrono::prelude::*; -use jsonwebtoken::{EncodingKey, Header, Validation}; +use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; const SECRET: &str = "some-secret"; @@ -57,8 +57,12 @@ mod jwt_numeric_date { assert_eq!(&token, EXPECTED_TOKEN); - let decoded = decode::(&token, SECRET.as_ref(), &Validation::default()) - .expect("Failed to decode token"); + let decoded = decode::( + &token, + &DecodingKey::from_secret(SECRET.as_ref()), + &Validation::default(), + ) + .expect("Failed to decode token"); assert_eq!(decoded.claims, claims); } @@ -91,8 +95,11 @@ fn main() -> Result<(), Box> { println!("serialized token: {}", &token); - let token_data = - jsonwebtoken::decode::(&token, SECRET.as_ref(), &Validation::default())?; + let token_data = jsonwebtoken::decode::( + &token, + &DecodingKey::from_secret(SECRET.as_ref()), + &Validation::default(), + )?; println!("token data:\n{:#?}", &token_data); Ok(()) diff --git a/examples/custom_header.rs b/examples/custom_header.rs index adaf89c..3ea556a 100644 --- a/examples/custom_header.rs +++ b/examples/custom_header.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use jsonwebtoken::errors::ErrorKind; -use jsonwebtoken::{decode, encode, Algorithm, EncodingKey, Header, Validation}; +use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; #[derive(Debug, Serialize, Deserialize)] struct Claims { @@ -25,7 +25,11 @@ fn main() { }; println!("{:?}", token); - let token_data = match decode::(&token, key, &Validation::new(Algorithm::HS512)) { + let token_data = match decode::( + &token, + &DecodingKey::from_secret(key), + &Validation::new(Algorithm::HS512), + ) { Ok(c) => c, Err(err) => match *err.kind() { ErrorKind::InvalidToken => panic!(), // Example on how to handle a specific error diff --git a/examples/validation.rs b/examples/validation.rs index 88f9da3..928770c 100644 --- a/examples/validation.rs +++ b/examples/validation.rs @@ -1,5 +1,5 @@ use jsonwebtoken::errors::ErrorKind; -use jsonwebtoken::{decode, encode, EncodingKey, Header, Validation}; +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] @@ -19,7 +19,7 @@ fn main() { }; let validation = Validation { sub: Some("b@b.com".to_string()), ..Validation::default() }; - let token_data = match decode::(&token, key, &validation) { + let token_data = match decode::(&token, &DecodingKey::from_secret(key), &validation) { Ok(c) => c, Err(err) => match *err.kind() { ErrorKind::InvalidToken => panic!("Token is invalid"), // Example on how to handle a specific error diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 7390286..fffeff5 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -2,9 +2,9 @@ use ring::constant_time::verify_slices_are_equal; use ring::{hmac, signature}; use crate::algorithms::Algorithm; +use crate::decoding::{DecodingKey, DecodingKeyKind}; use crate::encoding::EncodingKey; use crate::errors::Result; -use crate::pem::decoder::PemEncodedKey; use crate::serialization::{b64_decode, b64_encode}; pub(crate) mod ecdsa; @@ -62,57 +62,37 @@ fn verify_ring( /// `signature` is the signature part of a jwt (text after the second '.') /// /// `message` is base64(header) + "." + base64(claims) -/// For ECDSA/RSA, the `key` is the pem public key. If you want to verify using the public key -/// components (modulus/exponent), use `verify_rsa_components` instead. -pub fn verify(signature: &str, message: &str, key: &[u8], algorithm: Algorithm) -> Result { +pub fn verify( + signature: &str, + message: &str, + key: &DecodingKey, + algorithm: Algorithm, +) -> Result { match algorithm { Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => { // we just re-sign the message with the key and compare if they are equal - let signed = sign(message, &EncodingKey::from_secret(key), algorithm)?; + let signed = sign(message, &EncodingKey::from_secret(key.as_bytes()), algorithm)?; Ok(verify_slices_are_equal(signature.as_ref(), signed.as_ref()).is_ok()) } - Algorithm::ES256 | Algorithm::ES384 => { - let pem_key = PemEncodedKey::new(key)?; - verify_ring( - ecdsa::alg_to_ec_verification(algorithm), - signature, - message, - pem_key.as_ec_public_key()?, - ) - } + Algorithm::ES256 | Algorithm::ES384 => verify_ring( + ecdsa::alg_to_ec_verification(algorithm), + signature, + message, + key.as_bytes(), + ), Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 | Algorithm::PS256 | Algorithm::PS384 | Algorithm::PS512 => { - let pem_key = PemEncodedKey::new(key)?; - verify_ring( - rsa::alg_to_rsa_parameters(algorithm), - signature, - message, - pem_key.as_rsa_key()?, - ) + let alg = rsa::alg_to_rsa_parameters(algorithm); + match &key.kind { + DecodingKeyKind::SecretOrDer(bytes) => verify_ring(alg, signature, message, bytes), + DecodingKeyKind::RsaModulusExponent { n, e } => { + rsa::verify_from_components(alg, signature, message, (n, e)) + } + } } } } - -/// Verify the signature given using the (n, e) components of a RSA public key. -/// -/// `signature` is the signature part of a jwt (text after the second '.') -/// -/// `message` is base64(header) + "." + base64(claims) -pub fn verify_rsa_components( - signature: &str, - message: &str, - components: (&str, &str), - alg: Algorithm, -) -> Result { - let signature_bytes = b64_decode(signature)?; - rsa::verify_from_components( - rsa::alg_to_rsa_parameters(alg), - &signature_bytes, - message, - components, - ) -} diff --git a/src/crypto/rsa.rs b/src/crypto/rsa.rs index ec69da5..376c13a 100644 --- a/src/crypto/rsa.rs +++ b/src/crypto/rsa.rs @@ -50,12 +50,14 @@ pub(crate) fn sign( Ok(b64_encode(&signature)) } +/// Checks that a signature is valid based on the (n, e) RSA pubkey components pub(crate) fn verify_from_components( alg: &'static signature::RsaParameters, - signature_bytes: &[u8], + signature: &str, message: &str, components: (&str, &str), ) -> Result { + let signature_bytes = b64_decode(signature)?; let n = BigUint::from_bytes_be(&b64_decode(components.0)?).to_bytes_be(); let e = BigUint::from_bytes_be(&b64_decode(components.1)?).to_bytes_be(); let pubkey = signature::RsaPublicKeyComponents { n, e }; diff --git a/src/decoding.rs b/src/decoding.rs index d740052..bbd5705 100644 --- a/src/decoding.rs +++ b/src/decoding.rs @@ -1,8 +1,11 @@ +use std::borrow::Cow; + use serde::de::DeserializeOwned; -use crate::crypto::{verify, verify_rsa_components}; +use crate::crypto::verify; use crate::errors::{new_error, ErrorKind, Result}; use crate::header::Header; +use crate::pem::decoder::PemEncodedKey; use crate::serialization::from_jwt_part_claims; use crate::validation::{validate, Validation}; @@ -27,15 +30,78 @@ macro_rules! expect_two { }}; } -/// Internal way to differentiate between public key types -enum DecodingKey<'a> { - SecretOrPem(&'a [u8]), +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum DecodingKeyKind<'a> { + SecretOrDer(Cow<'a, [u8]>), RsaModulusExponent { n: &'a str, e: &'a str }, } -fn _decode( +/// All the different kind of keys we can use to decode a JWT +/// This key can be re-used so make sure you only initialize it once if you can for better performance +#[derive(Debug, Clone, PartialEq)] +pub struct DecodingKey<'a> { + pub(crate) kind: DecodingKeyKind<'a>, +} + +impl<'a> DecodingKey<'a> { + /// If you're using HMAC, use this. + pub fn from_secret(secret: &'a [u8]) -> Self { + DecodingKey { kind: DecodingKeyKind::SecretOrDer(Cow::Borrowed(secret)) } + } + + /// If you are loading a public RSA key in a PEM format, use this. + pub fn from_rsa_pem(key: &'a [u8]) -> Result { + let pem_key = PemEncodedKey::new(key)?; + let content = pem_key.as_rsa_key()?; + Ok(DecodingKey { kind: DecodingKeyKind::SecretOrDer(Cow::Owned(content.to_vec())) }) + } + + /// If you have (n, e) RSA public key components, use this. + pub fn from_rsa_components(modulus: &'a str, exponent: &'a str) -> Self { + DecodingKey { kind: DecodingKeyKind::RsaModulusExponent { n: modulus, e: exponent } } + } + + /// If you have a ECDSA public key in PEM format, use this. + pub fn from_ec_pem(key: &'a [u8]) -> Result { + let pem_key = PemEncodedKey::new(key)?; + let content = pem_key.as_ec_public_key()?; + Ok(DecodingKey { kind: DecodingKeyKind::SecretOrDer(Cow::Owned(content.to_vec())) }) + } + + /// If you know what you're doing and have the DER encoded public key, use this. + pub fn from_der(der: &'a [u8]) -> Self { + DecodingKey { kind: DecodingKeyKind::SecretOrDer(Cow::Borrowed(der)) } + } + + pub(crate) fn as_bytes(&self) -> &[u8] { + match &self.kind { + DecodingKeyKind::SecretOrDer(b) => &b, + DecodingKeyKind::RsaModulusExponent { .. } => unreachable!(), + } + } +} + +/// Decode and validate a JWT +/// +/// If the token or its signature is invalid or the claims fail validation, it will return an error. +/// +/// ```rust +/// use serde::{Deserialize, Serialize}; +/// use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm}; +/// +/// #[derive(Debug, Serialize, Deserialize)] +/// struct Claims { +/// sub: String, +/// company: String +/// } +/// +/// let token = "a.jwt.token".to_string(); +/// // Claims is a struct that implements Deserialize +/// let token_message = decode::(&token, &DecodingKey::from_secret("secret".as_ref()), &Validation::new(Algorithm::HS256)); +/// ``` +pub fn decode( token: &str, - key: DecodingKey, + key: &DecodingKey, validation: &Validation, ) -> Result> { let (signature, message) = expect_two!(token.rsplitn(2, '.')); @@ -46,14 +112,7 @@ fn _decode( return Err(new_error(ErrorKind::InvalidAlgorithm)); } - let is_valid = match key { - DecodingKey::SecretOrPem(k) => verify(signature, message, k, header.alg), - DecodingKey::RsaModulusExponent { n, e } => { - verify_rsa_components(signature, message, (n, e), header.alg) - } - }?; - - if !is_valid { + if !verify(signature, message, key, header.alg)? { return Err(new_error(ErrorKind::InvalidSignature)); } @@ -63,61 +122,6 @@ fn _decode( Ok(TokenData { header, claims: decoded_claims }) } -/// Decode and validate a JWT using a secret for HS and a public PEM format for RSA/EC -/// -/// If the token or its signature is invalid or the claims fail validation, it will return an error. -/// -/// ```rust -/// use serde::{Deserialize, Serialize}; -/// use jsonwebtoken::{decode, Validation, Algorithm}; -/// -/// #[derive(Debug, Serialize, Deserialize)] -/// struct Claims { -/// sub: String, -/// company: String -/// } -/// -/// let token = "a.jwt.token".to_string(); -/// // Claims is a struct that implements Deserialize -/// let token_message = decode::(&token, "secret".as_ref(), &Validation::new(Algorithm::HS256)); -/// ``` -pub fn decode( - token: &str, - key: &[u8], - validation: &Validation, -) -> Result> { - _decode(token, DecodingKey::SecretOrPem(key), validation) -} - -/// Decode and validate a JWT using (n, e) base64 encoded public key components for RSA -/// -/// If the token or its signature is invalid or the claims fail validation, it will return an error. -/// -/// ```rust -/// use serde::{Deserialize, Serialize}; -/// use jsonwebtoken::{decode_rsa_components, Validation, Algorithm}; -/// -/// #[derive(Debug, Serialize, Deserialize)] -/// struct Claims { -/// sub: String, -/// company: String -/// } -/// -/// let modulus = "some-base64-data"; -/// let exponent = "some-base64-data"; -/// let token = "a.jwt.token".to_string(); -/// // Claims is a struct that implements Deserialize -/// let token_message = decode_rsa_components::(&token, &modulus, &exponent, &Validation::new(Algorithm::HS256)); -/// ``` -pub fn decode_rsa_components( - token: &str, - modulus: &str, - exponent: &str, - validation: &Validation, -) -> Result> { - _decode(token, DecodingKey::RsaModulusExponent { n: modulus, e: exponent }, validation) -} - /// Decode a JWT without any signature verification/validations. /// /// NOTE: Do not use this unless you know what you are doing! If the token's signature is invalid, it will *not* return an error. diff --git a/src/encoding.rs b/src/encoding.rs index 8df4776..337eb4b 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -9,6 +9,7 @@ use crate::pem::decoder::PemEncodedKey; use crate::serialization::b64_encode_part; /// A key to encode a JWT with. Can be a secret, a PEM-encoded key or a DER-encoded key. +/// This key can be re-used so make sure you only initialize it once if you can for better performance #[derive(Debug, Clone, PartialEq)] pub struct EncodingKey<'a> { content: Cow<'a, [u8]>, @@ -20,8 +21,8 @@ impl<'a> EncodingKey<'a> { EncodingKey { content: Cow::Borrowed(secret) } } - /// If you are loading a RSA key from a .pem file - /// This errors if the key is not a valid RSA key + /// If you are loading a RSA key from a .pem file. + /// This errors if the key is not a valid RSA key. pub fn from_rsa_pem(key: &'a [u8]) -> Result { let pem_key = PemEncodedKey::new(key)?; let content = pem_key.as_rsa_key()?; @@ -42,7 +43,7 @@ impl<'a> EncodingKey<'a> { } /// Access the key, normal users do not need to use that. - pub fn inner(&'a self) -> &'a [u8] { + pub(crate) fn inner(&'a self) -> &'a [u8] { &self.content } } diff --git a/src/lib.rs b/src/lib.rs index 7b3d7b5..96ac22f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,9 +16,7 @@ mod serialization; mod validation; pub use algorithms::Algorithm; -pub use decoding::{ - dangerous_unsafe_decode, decode, decode_header, decode_rsa_components, TokenData, -}; +pub use decoding::{dangerous_unsafe_decode, decode, decode_header, DecodingKey, TokenData}; pub use encoding::{encode, EncodingKey}; pub use header::Header; pub use validation::Validation; diff --git a/tests/ecdsa/mod.rs b/tests/ecdsa/mod.rs index 2ccec50..8e39d78 100644 --- a/tests/ecdsa/mod.rs +++ b/tests/ecdsa/mod.rs @@ -1,7 +1,7 @@ use chrono::Utc; use jsonwebtoken::{ crypto::{sign, verify}, - decode, encode, Algorithm, EncodingKey, Header, Validation, + decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation, }; use serde::{Deserialize, Serialize}; @@ -12,30 +12,37 @@ pub struct Claims { exp: i64, } -// TODO: remove completely? -//#[test] -//fn round_trip_sign_verification_pk8() { -// let privkey = include_bytes!("private_ecdsa_key.pk8"); -// let encrypted = sign("hello world", privkey, Algorithm::ES256).unwrap(); -// let pubkey = include_bytes!("public_ecdsa_key.pk8"); -// let is_valid = verify(&encrypted, "hello world", pubkey, Algorithm::ES256).unwrap(); -// assert!(is_valid); -//} +#[test] +fn round_trip_sign_verification_pk8() { + let privkey = include_bytes!("private_ecdsa_key.pk8"); + let pubkey = include_bytes!("public_ecdsa_key.pk8"); + + let encrypted = sign("hello world", &EncodingKey::from_der(privkey), Algorithm::ES256).unwrap(); + let is_valid = verify(&encrypted, "hello world", &DecodingKey::from_der(pubkey), Algorithm::ES256).unwrap(); + assert!(is_valid); +} #[test] fn round_trip_sign_verification_pem() { - let privkey = include_bytes!("private_ecdsa_key.pem"); - let pubkey = include_bytes!("public_ecdsa_key.pem"); + let privkey_pem = include_bytes!("private_ecdsa_key.pem"); + let pubkey_pem = include_bytes!("public_ecdsa_key.pem"); let encrypted = - sign("hello world", &EncodingKey::from_ec_pem(privkey).unwrap(), Algorithm::ES256).unwrap(); - let is_valid = verify(&encrypted, "hello world", pubkey, Algorithm::ES256).unwrap(); + sign("hello world", &EncodingKey::from_ec_pem(privkey_pem).unwrap(), Algorithm::ES256) + .unwrap(); + let is_valid = verify( + &encrypted, + "hello world", + &DecodingKey::from_ec_pem(pubkey_pem).unwrap(), + Algorithm::ES256, + ) + .unwrap(); assert!(is_valid); } #[test] fn round_trip_claim() { - let privkey = include_bytes!("private_ecdsa_key.pem"); - let pubkey = include_bytes!("public_ecdsa_key.pem"); + let privkey_pem = include_bytes!("private_ecdsa_key.pem"); + let pubkey_pem = include_bytes!("public_ecdsa_key.pem"); let my_claims = Claims { sub: "b@b.com".to_string(), company: "ACME".to_string(), @@ -44,10 +51,15 @@ fn round_trip_claim() { let token = encode( &Header::new(Algorithm::ES256), &my_claims, - &EncodingKey::from_ec_pem(privkey).unwrap(), + &EncodingKey::from_ec_pem(privkey_pem).unwrap(), + ) + .unwrap(); + let token_data = decode::( + &token, + &DecodingKey::from_ec_pem(pubkey_pem).unwrap(), + &Validation::new(Algorithm::ES256), ) .unwrap(); - let token_data = decode::(&token, pubkey, &Validation::new(Algorithm::ES256)).unwrap(); assert_eq!(my_claims, token_data.claims); } @@ -55,8 +67,8 @@ fn round_trip_claim() { #[test] fn roundtrip_with_jwtio_example() { // We currently do not support SEC1 so we use the converted PKCS8 formatted - let privkey = include_bytes!("private_jwtio_pkcs8.pem"); - let pubkey = include_bytes!("public_jwtio.pem"); + let privkey_pem = include_bytes!("private_jwtio_pkcs8.pem"); + let pubkey_pem = include_bytes!("public_jwtio.pem"); let my_claims = Claims { sub: "b@b.com".to_string(), company: "ACME".to_string(), @@ -65,9 +77,14 @@ fn roundtrip_with_jwtio_example() { let token = encode( &Header::new(Algorithm::ES384), &my_claims, - &EncodingKey::from_ec_pem(privkey).unwrap(), + &EncodingKey::from_ec_pem(privkey_pem).unwrap(), + ) + .unwrap(); + let token_data = decode::( + &token, + &DecodingKey::from_ec_pem(pubkey_pem).unwrap(), + &Validation::new(Algorithm::ES384), ) .unwrap(); - let token_data = decode::(&token, pubkey, &Validation::new(Algorithm::ES384)).unwrap(); assert_eq!(my_claims, token_data.claims); } diff --git a/tests/hmac.rs b/tests/hmac.rs index d77f8bc..94ee664 100644 --- a/tests/hmac.rs +++ b/tests/hmac.rs @@ -1,8 +1,8 @@ use chrono::Utc; use jsonwebtoken::{ crypto::{sign, verify}, - dangerous_unsafe_decode, decode, decode_header, encode, Algorithm, EncodingKey, Header, - Validation, + dangerous_unsafe_decode, decode, decode_header, encode, Algorithm, DecodingKey, EncodingKey, + Header, Validation, }; use serde::{Deserialize, Serialize}; @@ -24,7 +24,8 @@ fn sign_hs256() { #[test] fn verify_hs256() { let sig = "c0zGLzKEFWj0VxWuufTXiRMk5tlI5MbGDAYhzaxIYjo"; - let valid = verify(sig, "hello world", b"secret", Algorithm::HS256).unwrap(); + let valid = + verify(sig, "hello world", &DecodingKey::from_secret(b"secret"), Algorithm::HS256).unwrap(); assert!(valid); } @@ -38,7 +39,9 @@ fn encode_with_custom_header() { let mut header = Header::default(); header.kid = Some("kid".to_string()); let token = encode(&header, &my_claims, &EncodingKey::from_secret(b"secret")).unwrap(); - let token_data = decode::(&token, b"secret", &Validation::default()).unwrap(); + let token_data = + decode::(&token, &DecodingKey::from_secret(b"secret"), &Validation::default()) + .unwrap(); assert_eq!(my_claims, token_data.claims); assert_eq!("kid", token_data.header.kid.unwrap()); } @@ -52,7 +55,9 @@ fn round_trip_claim() { }; let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(b"secret")).unwrap(); - let token_data = decode::(&token, b"secret", &Validation::default()).unwrap(); + let token_data = + decode::(&token, &DecodingKey::from_secret(b"secret"), &Validation::default()) + .unwrap(); assert_eq!(my_claims, token_data.claims); assert!(token_data.header.kid.is_none()); } @@ -60,7 +65,8 @@ fn round_trip_claim() { #[test] fn decode_token() { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.9r56oF7ZliOBlOAyiOFperTGxBtPykRQiWNFxhDCW98"; - let claims = decode::(token, b"secret", &Validation::default()); + let claims = + decode::(token, &DecodingKey::from_secret(b"secret"), &Validation::default()); println!("{:?}", claims); claims.unwrap(); } @@ -69,7 +75,8 @@ fn decode_token() { #[should_panic(expected = "InvalidToken")] fn decode_token_missing_parts() { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; - let claims = decode::(token, b"secret", &Validation::default()); + let claims = + decode::(token, &DecodingKey::from_secret(b"secret"), &Validation::default()); claims.unwrap(); } @@ -78,7 +85,8 @@ fn decode_token_missing_parts() { fn decode_token_invalid_signature() { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.wrong"; - let claims = decode::(token, b"secret", &Validation::default()); + let claims = + decode::(token, &DecodingKey::from_secret(b"secret"), &Validation::default()); claims.unwrap(); } @@ -86,14 +94,19 @@ fn decode_token_invalid_signature() { #[should_panic(expected = "InvalidAlgorithm")] fn decode_token_wrong_algorithm() { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.I1BvFoHe94AFf09O6tDbcSB8-jp8w6xZqmyHIwPeSdY"; - let claims = decode::(token, b"secret", &Validation::new(Algorithm::RS512)); + let claims = decode::( + token, + &DecodingKey::from_secret(b"secret"), + &Validation::new(Algorithm::RS512), + ); claims.unwrap(); } #[test] fn decode_token_with_bytes_secret() { let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.Hm0yvKH25TavFPz7J_coST9lZFYH1hQo0tvhvImmaks"; - let claims = decode::(token, b"\x01\x02\x03", &Validation::default()); + let claims = + decode::(token, &DecodingKey::from_secret(b"\x01\x02\x03"), &Validation::default()); assert!(claims.is_ok()); } diff --git a/tests/rsa/mod.rs b/tests/rsa/mod.rs index 814c645..08c3f8a 100644 --- a/tests/rsa/mod.rs +++ b/tests/rsa/mod.rs @@ -1,7 +1,7 @@ use chrono::Utc; use jsonwebtoken::{ crypto::{sign, verify}, - decode, decode_rsa_components, encode, Algorithm, EncodingKey, Header, Validation, + decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation, }; use serde::{Deserialize, Serialize}; @@ -29,7 +29,9 @@ fn round_trip_sign_verification_pem_pkcs1() { for &alg in RSA_ALGORITHMS { let encrypted = sign("hello world", &EncodingKey::from_rsa_pem(privkey_pem).unwrap(), alg).unwrap(); - let is_valid = verify(&encrypted, "hello world", pubkey_pem, alg).unwrap(); + let is_valid = + verify(&encrypted, "hello world", &DecodingKey::from_rsa_pem(pubkey_pem).unwrap(), alg) + .unwrap(); assert!(is_valid); } } @@ -42,7 +44,24 @@ fn round_trip_sign_verification_pem_pkcs8() { for &alg in RSA_ALGORITHMS { let encrypted = sign("hello world", &EncodingKey::from_rsa_pem(privkey_pem).unwrap(), alg).unwrap(); - let is_valid = verify(&encrypted, "hello world", pubkey_pem, alg).unwrap(); + let is_valid = + verify(&encrypted, "hello world", &DecodingKey::from_rsa_pem(pubkey_pem).unwrap(), alg) + .unwrap(); + assert!(is_valid); + } +} + +#[test] +fn round_trip_sign_verification_der() { + let privkey_der = include_bytes!("private_rsa_key.der"); + let pubkey_der = include_bytes!("public_rsa_key.der"); + + for &alg in RSA_ALGORITHMS { + let encrypted = + sign("hello world", &EncodingKey::from_der(privkey_der), alg).unwrap(); + let is_valid = + verify(&encrypted, "hello world", &DecodingKey::from_der(pubkey_der), alg) + .unwrap(); assert!(is_valid); } } @@ -54,15 +73,16 @@ fn round_trip_claim() { company: "ACME".to_string(), exp: Utc::now().timestamp() + 10000, }; - let privkey = include_bytes!("private_rsa_key_pkcs1.pem"); + let privkey_pem = include_bytes!("private_rsa_key_pkcs1.pem"); + let pubkey_pem = include_bytes!("public_rsa_key_pkcs1.pem"); for &alg in RSA_ALGORITHMS { let token = - encode(&Header::new(alg), &my_claims, &EncodingKey::from_rsa_pem(privkey).unwrap()) + encode(&Header::new(alg), &my_claims, &EncodingKey::from_rsa_pem(privkey_pem).unwrap()) .unwrap(); let token_data = decode::( &token, - include_bytes!("public_rsa_key_pkcs1.pem"), + &DecodingKey::from_rsa_pem(pubkey_pem).unwrap(), &Validation::new(alg), ) .unwrap(); @@ -88,7 +108,11 @@ fn rsa_modulus_exponent() { &EncodingKey::from_rsa_pem(privkey.as_ref()).unwrap(), ) .unwrap(); - let res = decode_rsa_components::(&encrypted, n, e, &Validation::new(Algorithm::RS256)); + let res = decode::( + &encrypted, + &DecodingKey::from_rsa_components(n, e), + &Validation::new(Algorithm::RS256), + ); assert!(res.is_ok()); } @@ -108,7 +132,12 @@ fn roundtrip_with_jwtio_example_jey() { let token = encode(&Header::new(alg), &my_claims, &EncodingKey::from_rsa_pem(privkey_pem).unwrap()) .unwrap(); - let token_data = decode::(&token, pubkey_pem, &Validation::new(alg)).unwrap(); + let token_data = decode::( + &token, + &DecodingKey::from_rsa_pem(pubkey_pem).unwrap(), + &Validation::new(alg), + ) + .unwrap(); assert_eq!(my_claims, token_data.claims); } } From 4dd2f12c6de76e455e82c1a910722bc8566814e6 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Thu, 2 Jan 2020 19:40:53 +0100 Subject: [PATCH 51/55] Remove EncodingKey lifetime --- src/encoding.rs | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/encoding.rs b/src/encoding.rs index 337eb4b..175e081 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -1,5 +1,3 @@ -use std::borrow::Cow; - use serde::ser::Serialize; use crate::crypto; @@ -11,39 +9,38 @@ use crate::serialization::b64_encode_part; /// A key to encode a JWT with. Can be a secret, a PEM-encoded key or a DER-encoded key. /// This key can be re-used so make sure you only initialize it once if you can for better performance #[derive(Debug, Clone, PartialEq)] -pub struct EncodingKey<'a> { - content: Cow<'a, [u8]>, +pub struct EncodingKey { + content: Vec, } -impl<'a> EncodingKey<'a> { +impl EncodingKey { /// If you're using HMAC, use that. - pub fn from_secret(secret: &'a [u8]) -> Self { - EncodingKey { content: Cow::Borrowed(secret) } + pub fn from_secret(secret: &[u8]) -> Self { + EncodingKey { content: secret.to_vec() } } /// If you are loading a RSA key from a .pem file. /// This errors if the key is not a valid RSA key. - pub fn from_rsa_pem(key: &'a [u8]) -> Result { + pub fn from_rsa_pem(key: &[u8]) -> Result { let pem_key = PemEncodedKey::new(key)?; let content = pem_key.as_rsa_key()?; - Ok(EncodingKey { content: Cow::Owned(content.to_vec()) }) + Ok(EncodingKey { content: content.to_vec() }) } /// If you are loading a ECDSA key from a .pem file /// This errors if the key is not a valid private EC key - pub fn from_ec_pem(key: &'a [u8]) -> Result { + pub fn from_ec_pem(key: &[u8]) -> Result { let pem_key = PemEncodedKey::new(key)?; let content = pem_key.as_ec_private_key()?; - Ok(EncodingKey { content: Cow::Owned(content.to_vec()) }) + Ok(EncodingKey { content: content.to_vec() }) } /// If you know what you're doing and have the DER-encoded key, for RSA or ECDSA - pub fn from_der(der: &'a [u8]) -> Self { - EncodingKey { content: Cow::Borrowed(der) } + pub fn from_der(der: &[u8]) -> Self { + EncodingKey { content: der.to_vec() } } - /// Access the key, normal users do not need to use that. - pub(crate) fn inner(&'a self) -> &'a [u8] { + pub(crate) fn inner(&self) -> &[u8] { &self.content } } From 689cc6d32e05b0c04be41b706dc904aab9000206 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Mon, 13 Jan 2020 19:38:33 +0100 Subject: [PATCH 52/55] Validate key type with algo in encode/decode --- src/algorithms.rs | 22 +++++++++++++++++++ src/decoding.rs | 54 ++++++++++++++++++++++++++++++++++++++++------ src/encoding.rs | 32 ++++++++++++++++++++------- src/errors.rs | 3 ++- tests/ecdsa/mod.rs | 7 ++++-- tests/hmac.rs | 12 +++++++++++ tests/rsa/mod.rs | 6 ++---- 7 files changed, 114 insertions(+), 22 deletions(-) diff --git a/src/algorithms.rs b/src/algorithms.rs index c9ff848..20edbf1 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -2,6 +2,13 @@ use crate::errors::{Error, ErrorKind, Result}; use serde::{Deserialize, Serialize}; use std::str::FromStr; +#[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)] +pub(crate) enum AlgorithmFamily { + Hmac, + Rsa, + Ec, +} + /// The algorithms supported for signing/verifying JWTs #[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)] pub enum Algorithm { @@ -58,6 +65,21 @@ impl FromStr for Algorithm { } } +impl Algorithm { + pub(crate) fn family(self) -> AlgorithmFamily { + match self { + Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => AlgorithmFamily::Hmac, + Algorithm::RS256 + | Algorithm::RS384 + | Algorithm::RS512 + | Algorithm::PS256 + | Algorithm::PS384 + | Algorithm::PS512 => AlgorithmFamily::Rsa, + Algorithm::ES256 | Algorithm::ES384 => AlgorithmFamily::Ec, + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/decoding.rs b/src/decoding.rs index bbd5705..36837f0 100644 --- a/src/decoding.rs +++ b/src/decoding.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use serde::de::DeserializeOwned; +use crate::algorithms::AlgorithmFamily; use crate::crypto::verify; use crate::errors::{new_error, ErrorKind, Result}; use crate::header::Header; @@ -40,37 +41,70 @@ pub(crate) enum DecodingKeyKind<'a> { /// This key can be re-used so make sure you only initialize it once if you can for better performance #[derive(Debug, Clone, PartialEq)] pub struct DecodingKey<'a> { + pub(crate) family: AlgorithmFamily, pub(crate) kind: DecodingKeyKind<'a>, } impl<'a> DecodingKey<'a> { /// If you're using HMAC, use this. pub fn from_secret(secret: &'a [u8]) -> Self { - DecodingKey { kind: DecodingKeyKind::SecretOrDer(Cow::Borrowed(secret)) } + DecodingKey { + family: AlgorithmFamily::Hmac, + kind: DecodingKeyKind::SecretOrDer(Cow::Borrowed(secret)), + } + } + + /// If you're using HMAC with a base64 encoded, use this. + pub fn from_base64_secret(secret: &str) -> Result { + let out = base64::decode(&secret)?; + Ok(DecodingKey { + family: AlgorithmFamily::Hmac, + kind: DecodingKeyKind::SecretOrDer(Cow::Owned(out)), + }) } /// If you are loading a public RSA key in a PEM format, use this. pub fn from_rsa_pem(key: &'a [u8]) -> Result { let pem_key = PemEncodedKey::new(key)?; let content = pem_key.as_rsa_key()?; - Ok(DecodingKey { kind: DecodingKeyKind::SecretOrDer(Cow::Owned(content.to_vec())) }) + Ok(DecodingKey { + family: AlgorithmFamily::Rsa, + kind: DecodingKeyKind::SecretOrDer(Cow::Owned(content.to_vec())), + }) } /// If you have (n, e) RSA public key components, use this. pub fn from_rsa_components(modulus: &'a str, exponent: &'a str) -> Self { - DecodingKey { kind: DecodingKeyKind::RsaModulusExponent { n: modulus, e: exponent } } + DecodingKey { + family: AlgorithmFamily::Rsa, + kind: DecodingKeyKind::RsaModulusExponent { n: modulus, e: exponent }, + } } /// If you have a ECDSA public key in PEM format, use this. pub fn from_ec_pem(key: &'a [u8]) -> Result { let pem_key = PemEncodedKey::new(key)?; let content = pem_key.as_ec_public_key()?; - Ok(DecodingKey { kind: DecodingKeyKind::SecretOrDer(Cow::Owned(content.to_vec())) }) + Ok(DecodingKey { + family: AlgorithmFamily::Ec, + kind: DecodingKeyKind::SecretOrDer(Cow::Owned(content.to_vec())), + }) } - /// If you know what you're doing and have the DER encoded public key, use this. - pub fn from_der(der: &'a [u8]) -> Self { - DecodingKey { kind: DecodingKeyKind::SecretOrDer(Cow::Borrowed(der)) } + /// If you know what you're doing and have a RSA DER encoded public key, use this. + pub fn from_rsa_der(der: &'a [u8]) -> Self { + DecodingKey { + family: AlgorithmFamily::Rsa, + kind: DecodingKeyKind::SecretOrDer(Cow::Borrowed(der)), + } + } + + /// If you know what you're doing and have a RSA EC encoded public key, use this. + pub fn from_ec_der(der: &'a [u8]) -> Self { + DecodingKey { + family: AlgorithmFamily::Ec, + kind: DecodingKeyKind::SecretOrDer(Cow::Borrowed(der)), + } } pub(crate) fn as_bytes(&self) -> &[u8] { @@ -104,6 +138,12 @@ pub fn decode( key: &DecodingKey, validation: &Validation, ) -> Result> { + for alg in &validation.algorithms { + if key.family != alg.family() { + return Err(new_error(ErrorKind::InvalidAlgorithm)); + } + } + let (signature, message) = expect_two!(token.rsplitn(2, '.')); let (claims, header) = expect_two!(message.rsplitn(2, '.')); let header = Header::from_encoded(header)?; diff --git a/src/encoding.rs b/src/encoding.rs index 175e081..3f7a690 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -1,7 +1,8 @@ use serde::ser::Serialize; +use crate::algorithms::AlgorithmFamily; use crate::crypto; -use crate::errors::Result; +use crate::errors::{new_error, ErrorKind, Result}; use crate::header::Header; use crate::pem::decoder::PemEncodedKey; use crate::serialization::b64_encode_part; @@ -10,13 +11,20 @@ use crate::serialization::b64_encode_part; /// This key can be re-used so make sure you only initialize it once if you can for better performance #[derive(Debug, Clone, PartialEq)] pub struct EncodingKey { + pub(crate) family: AlgorithmFamily, content: Vec, } impl EncodingKey { - /// If you're using HMAC, use that. + /// If you're using a HMAC secret that is not base64, use that. pub fn from_secret(secret: &[u8]) -> Self { - EncodingKey { content: secret.to_vec() } + EncodingKey { family: AlgorithmFamily::Hmac, content: secret.to_vec() } + } + + /// If you have a base64 HMAC secret, use that. + pub fn from_base64_secret(secret: &str) -> Result { + let out = base64::decode(&secret)?; + Ok(EncodingKey { family: AlgorithmFamily::Hmac, content: out }) } /// If you are loading a RSA key from a .pem file. @@ -24,7 +32,7 @@ impl EncodingKey { pub fn from_rsa_pem(key: &[u8]) -> Result { let pem_key = PemEncodedKey::new(key)?; let content = pem_key.as_rsa_key()?; - Ok(EncodingKey { content: content.to_vec() }) + Ok(EncodingKey { family: AlgorithmFamily::Rsa, content: content.to_vec() }) } /// If you are loading a ECDSA key from a .pem file @@ -32,12 +40,17 @@ impl EncodingKey { pub fn from_ec_pem(key: &[u8]) -> Result { let pem_key = PemEncodedKey::new(key)?; let content = pem_key.as_ec_private_key()?; - Ok(EncodingKey { content: content.to_vec() }) + Ok(EncodingKey { family: AlgorithmFamily::Ec, content: content.to_vec() }) } - /// If you know what you're doing and have the DER-encoded key, for RSA or ECDSA - pub fn from_der(der: &[u8]) -> Self { - EncodingKey { content: der.to_vec() } + /// If you know what you're doing and have the DER-encoded key, for RSA only + pub fn from_rsa_der(der: &[u8]) -> Self { + EncodingKey { family: AlgorithmFamily::Rsa, content: der.to_vec() } + } + + /// If you know what you're doing and have the DER-encoded key, for ECDSA + pub fn from_ec_der(der: &[u8]) -> Self { + EncodingKey { family: AlgorithmFamily::Ec, content: der.to_vec() } } pub(crate) fn inner(&self) -> &[u8] { @@ -68,6 +81,9 @@ impl EncodingKey { /// let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret("secret".as_ref())).unwrap(); /// ``` pub fn encode(header: &Header, claims: &T, key: &EncodingKey) -> Result { + if key.family != header.alg.family() { + return Err(new_error(ErrorKind::InvalidAlgorithm)); + } let encoded_header = b64_encode_part(&header)?; let encoded_claims = b64_encode_part(&claims)?; let message = [encoded_header.as_ref(), encoded_claims.as_ref()].join("."); diff --git a/src/errors.rs b/src/errors.rs index 87231ed..580e8b3 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -56,7 +56,8 @@ pub enum ErrorKind { InvalidSubject, /// When a token’s nbf claim represents a time in the future ImmatureSignature, - /// When the algorithm in the header doesn't match the one passed to `decode` + /// When the algorithm in the header doesn't match the one passed to `decode` or the encoding/decoding key + /// used doesn't match the alg requested InvalidAlgorithm, // 3rd party errors diff --git a/tests/ecdsa/mod.rs b/tests/ecdsa/mod.rs index 8e39d78..2362409 100644 --- a/tests/ecdsa/mod.rs +++ b/tests/ecdsa/mod.rs @@ -17,8 +17,11 @@ fn round_trip_sign_verification_pk8() { let privkey = include_bytes!("private_ecdsa_key.pk8"); let pubkey = include_bytes!("public_ecdsa_key.pk8"); - let encrypted = sign("hello world", &EncodingKey::from_der(privkey), Algorithm::ES256).unwrap(); - let is_valid = verify(&encrypted, "hello world", &DecodingKey::from_der(pubkey), Algorithm::ES256).unwrap(); + let encrypted = + sign("hello world", &EncodingKey::from_ec_der(privkey), Algorithm::ES256).unwrap(); + let is_valid = + verify(&encrypted, "hello world", &DecodingKey::from_ec_der(pubkey), Algorithm::ES256) + .unwrap(); assert!(is_valid); } diff --git a/tests/hmac.rs b/tests/hmac.rs index 94ee664..dcd9c59 100644 --- a/tests/hmac.rs +++ b/tests/hmac.rs @@ -102,6 +102,18 @@ fn decode_token_wrong_algorithm() { claims.unwrap(); } +#[test] +#[should_panic(expected = "InvalidAlgorithm")] +fn encode_wrong_alg_family() { + let my_claims = Claims { + sub: "b@b.com".to_string(), + company: "ACME".to_string(), + exp: Utc::now().timestamp() + 10000, + }; + let claims = encode(&Header::default(), &my_claims, &EncodingKey::from_rsa_der(b"secret")); + claims.unwrap(); +} + #[test] fn decode_token_with_bytes_secret() { let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.Hm0yvKH25TavFPz7J_coST9lZFYH1hQo0tvhvImmaks"; diff --git a/tests/rsa/mod.rs b/tests/rsa/mod.rs index 08c3f8a..60a2b71 100644 --- a/tests/rsa/mod.rs +++ b/tests/rsa/mod.rs @@ -57,11 +57,9 @@ fn round_trip_sign_verification_der() { let pubkey_der = include_bytes!("public_rsa_key.der"); for &alg in RSA_ALGORITHMS { - let encrypted = - sign("hello world", &EncodingKey::from_der(privkey_der), alg).unwrap(); + let encrypted = sign("hello world", &EncodingKey::from_rsa_der(privkey_der), alg).unwrap(); let is_valid = - verify(&encrypted, "hello world", &DecodingKey::from_der(pubkey_der), alg) - .unwrap(); + verify(&encrypted, "hello world", &DecodingKey::from_rsa_der(pubkey_der), alg).unwrap(); assert!(is_valid); } } From 1cc14b04eb1cf9902cfa55214c1ba664e938af37 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Tue, 21 Jan 2020 07:41:55 -0800 Subject: [PATCH 53/55] Beta 1 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 02a49ab..4bf6c71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jsonwebtoken" -version = "7.0.0-alpha.3" +version = "7.0.0-beta.1" authors = ["Vincent Prouillet "] license = "MIT" readme = "README.md" From c2f6093309d199088e04e9e0ce9cd99888001174 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Mon, 27 Jan 2020 20:52:46 -0800 Subject: [PATCH 54/55] Get rid of deprecrated std error description --- src/errors.rs | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index 580e8b3..d634581 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -80,28 +80,6 @@ pub enum ErrorKind { } impl StdError for Error { - fn description(&self) -> &str { - match *self.0 { - ErrorKind::InvalidToken => "invalid token", - ErrorKind::InvalidSignature => "invalid signature", - ErrorKind::InvalidEcdsaKey => "invalid ECDSA key", - ErrorKind::InvalidRsaKey => "invalid RSA key", - ErrorKind::ExpiredSignature => "expired signature", - ErrorKind::InvalidIssuer => "invalid issuer", - ErrorKind::InvalidAudience => "invalid audience", - ErrorKind::InvalidSubject => "invalid subject", - ErrorKind::ImmatureSignature => "immature signature", - ErrorKind::InvalidAlgorithm => "algorithms don't match", - ErrorKind::InvalidAlgorithmName => "not a known algorithm", - ErrorKind::InvalidKeyFormat => "invalid key format", - ErrorKind::Base64(ref err) => err.description(), - ErrorKind::Json(ref err) => err.description(), - ErrorKind::Utf8(ref err) => err.description(), - ErrorKind::Crypto(ref err) => err.description(), - ErrorKind::__Nonexhaustive => "unknown error", - } - } - fn cause(&self) -> Option<&dyn StdError> { match *self.0 { ErrorKind::InvalidToken => None, @@ -139,7 +117,7 @@ impl fmt::Display for Error { | ErrorKind::ImmatureSignature | ErrorKind::InvalidAlgorithm | ErrorKind::InvalidKeyFormat - | ErrorKind::InvalidAlgorithmName => write!(f, "{}", self.description()), + | ErrorKind::InvalidAlgorithmName => write!(f, "{}", self), ErrorKind::Json(ref err) => write!(f, "JSON error: {}", err), ErrorKind::Utf8(ref err) => write!(f, "UTF-8 error: {}", err), ErrorKind::Crypto(ref err) => write!(f, "Crypto error: {}", err), From b9989d14cf7521051e20a9245dc143161bbc993e Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Tue, 28 Jan 2020 18:18:21 -0800 Subject: [PATCH 55/55] Prepare for release --- CHANGELOG.md | 3 ++- Cargo.toml | 2 +- README.md | 8 +++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acd53a1..856fc38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -# 7.0.0 (unreleased) +# 7.0.0 (2020-01-28) - Add support for PS256, PS384 and PS512 - Add support for verifying with modulus/exponent components for RSA @@ -8,6 +8,7 @@ - Changed aud field type in Validation to `Option>`. Audience validation now tests for "any-of-these" audience membership. - Add support for keys in PEM format +- Add EncodingKey/DecodingKey API to improve performance and UX ## 6.0.1 (2019-05-10) diff --git a/Cargo.toml b/Cargo.toml index 4bf6c71..7456549 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jsonwebtoken" -version = "7.0.0-beta.1" +version = "7.0.0" authors = ["Vincent Prouillet "] license = "MIT" readme = "README.md" diff --git a/README.md b/README.md index 15dda9d..48db6cc 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,9 @@ Encoding a JWT takes 3 parameters: When using HS256, HS2384 or HS512, the key is always a shared secret like in the example above. When using RSA/EC, the key should always be the content of the private key in the PEM or DER format. +If your key is in PEM format, it is better performance wise to generate the `EncodingKey` once in a `lazy_static` or +something similar and reuse it. + ### Decoding ```rust @@ -121,9 +124,12 @@ The main use-case is for JWK where your public key is in a JSON format like so: ```rust // `token` is a struct with 2 fields: `header` and `claims` where `claims` is your own struct. -let token = decode::(&token, &EncodingKey::from_rsa_components(jwk["n"], jwk["e"]), &Validation::new(Algorithm::RS256))?; +let token = decode::(&token, &DecodingKey::from_rsa_components(jwk["n"], jwk["e"]), &Validation::new(Algorithm::RS256))?; ``` +If your key is in PEM format, it is better performance wise to generate the `DecodingKey` once in a `lazy_static` or +something similar and reuse it. + ### Convert SEC1 private key to PKCS8 `jsonwebtoken` currently only supports PKCS8 format for private EC keys. If your key has `BEGIN EC PRIVATE KEY` at the top, this is a SEC1 type and can be converted to PKCS8 like so: