diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6bcf8e8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: ci +on: + push: + branches: + - master + 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.36.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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 929b36e..856fc38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +# 7.0.0 (2020-01-28) + +- Add support for PS256, PS384 and PS512 +- Add support for verifying with modulus/exponent components for RSA +- 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 +- Add EncodingKey/DecodingKey API to improve performance and UX + ## 6.0.1 (2019-05-10) - Fix Algorithm mapping in FromStr for RSA diff --git a/Cargo.toml b/Cargo.toml index 581ce58..7456549 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,27 @@ [package] name = "jsonwebtoken" -version = "6.0.1" -authors = ["Vincent Prouillet "] +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", "jwk"] +edition = "2018" [dependencies] serde_json = "1.0" -serde_derive = "1.0" -serde = "1.0" -ring = "0.14.4" -base64 = "0.10" -untrusted = "0.6" +serde = {version = "1.0", features = ["derive"] } +ring = { version = "0.16.5", features = ["std"] } +base64 = "0.11" +# For PEM decoding +pem = "0.7" +simple_asn1 = "0.4" + +[dev-dependencies] +# For the custom chrono example chrono = "0.4" + +[badges] +maintenance = { status = "passively-maintained" } diff --git a/README.md b/README.md index 269c1a4..48db6cc 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,32 @@ [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 JSON Web Tokens are. + ## Installation Add the following to Cargo.toml: ```toml -jsonwebtoken = "6" -serde_derive = "1" -serde = "1" +jsonwebtoken = "7" +serde = {version = "1.0", features = ["derive"] } ``` -## Help wanted for v7 +The minimum required Rust version is 1.36. -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! +## Algorithms +This library currently supports the following: + +- HS256 +- HS384 +- HS512 +- RS256 +- RS384 +- RS512 +- PS256 +- PS384 +- PS512 +- ES256 +- ES384 ## How to use @@ -27,11 +37,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, EncodingKey, DecodingKey}; /// Our claims struct, it needs to derive `Serialize` and/or `Deserialize` #[derive(Debug, Serialize, Deserialize)] @@ -42,11 +49,11 @@ struct Claims { } ``` -### Encoding -The default algorithm is HS256. +### Header +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 @@ -54,38 +61,90 @@ 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())?; +let token = encode(&header, &my_claims, &EncodingKey::from_secret("secret".as_ref()))?; ``` Look at `examples/custom_header.rs` for a full working example. -### Decoding +### Encoding + ```rust -let token = decode::(&token, "secret".as_ref(), &Validation::default())?; -// token is a struct with 2 params: header and claims +// HS256 +let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret("secret".as_ref()))?; +// RSA +let token = encode(&Header::new(Algorithm::RS256), &my_claims, &EncodingKey::from_rsa_pem(include_bytes!("privkey.pem"))?)?; +``` +Encoding a JWT takes 3 parameters: + +- a header: the `Header` struct +- some claims: your own struct +- 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 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 +// `token` is a struct with 2 fields: `header` and `claims` where `claims` is your own struct. +let token = decode::(&token, &DecodingKey::from_secret("secret".as_ref()), &Validation::default())?; ``` `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 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: ```rust let header = decode_header(&token)?; ``` -This does not perform any validation on the token. +This does not perform any signature verification or validate the token claims. -#### Validation -This library validates automatically the `exp` and `nbf` claims if present. You can also validate the `sub`, `iss` and `aud` but +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 in 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 +// `token` is a struct with 2 fields: `header` and `claims` where `claims` is your own struct. +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: + +```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 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. +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`. @@ -106,30 +165,4 @@ 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 -- ES256 -- 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. -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. +Look at `examples/validation.rs` for a full working example. diff --git a/benches/jwt.rs b/benches/jwt.rs index dab789c..41a4fc9 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, Validation}; +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; +use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] struct Claims { @@ -15,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 new file mode 100644 index 0000000..561ad10 --- /dev/null +++ b/examples/custom_chrono.rs @@ -0,0 +1,106 @@ +use chrono::prelude::*; +use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation}; +use serde::{Deserialize, Serialize}; + +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 { + 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 = + encode(&Header::default(), &claims, &EncodingKey::from_secret(SECRET.as_ref())) + .expect("Failed to encode claims"); + + assert_eq!(&token, EXPECTED_TOKEN); + + let decoded = decode::( + &token, + &DecodingKey::from_secret(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 = + 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 = jsonwebtoken::encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(SECRET.as_ref()), + )?; + + println!("serialized token: {}", &token); + + 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 99d346d..3ea556a 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, Validation}; +use jsonwebtoken::errors::ErrorKind; +use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; #[derive(Debug, Serialize, Deserialize)] struct Claims { @@ -15,26 +13,29 @@ 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, &EncodingKey::from_secret(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)) { - 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, + &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 + _ => panic!(), + }, + }; println!("{:?}", token_data.claims); println!("{:?}", token_data.header); } diff --git a/examples/validation.rs b/examples/validation.rs index 31fb7eb..928770c 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, Validation}; +use jsonwebtoken::errors::ErrorKind; +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; +use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] struct Claims { @@ -13,16 +10,16 @@ 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 = "secret"; - let token = match encode(&Header::default(), &my_claims, key.as_ref()) { + let token = match encode(&Header::default(), &my_claims, &EncodingKey::from_secret(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, &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/algorithms.rs b/src/algorithms.rs new file mode 100644 index 0000000..20edbf1 --- /dev/null +++ b/src/algorithms.rs @@ -0,0 +1,100 @@ +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 { + /// 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, + + /// RSASSA-PSS using SHA-256 + PS256, + /// RSASSA-PSS using SHA-384 + PS384, + /// RSASSA-PSS using SHA-512 + PS512, +} + +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), + "PS256" => Ok(Algorithm::PS256), + "PS384" => Ok(Algorithm::PS384), + "PS512" => Ok(Algorithm::PS512), + "RS512" => Ok(Algorithm::RS512), + _ => Err(ErrorKind::InvalidAlgorithmName.into()), + } + } +} + +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::*; + + #[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/crypto.rs b/src/crypto.rs deleted file mode 100644 index 76dccd2..0000000 --- a/src/crypto.rs +++ /dev/null @@ -1,162 +0,0 @@ -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)), - } - } -} - -/// 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); - 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))?; - 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)) -} - -/// 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)?, - ); - 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. -pub fn sign(signing_input: &str, key: &[u8], 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::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), - } -} - -/// See Ring docs for more details -fn verify_ring( - alg: &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_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); - - Ok(res.is_ok()) -} - -/// Compares the signature given with a re-computed signature for HMAC or using the public key -/// for RSA. -/// -/// 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) -pub fn verify( - signature: &str, - signing_input: &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)?; - 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) - } - 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) - } - } -} diff --git a/src/crypto/ecdsa.rs b/src/crypto/ecdsa.rs new file mode 100644 index 0000000..afa0f79 --- /dev/null +++ b/src/crypto/ecdsa.rs @@ -0,0 +1,38 @@ +use ring::{rand, signature}; + +use crate::algorithms::Algorithm; +use crate::errors::Result; +use crate::serialization::b64_encode; + +/// 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 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"), + } +} + +/// 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 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 new file mode 100644 index 0000000..fffeff5 --- /dev/null +++ b/src/crypto/mod.rs @@ -0,0 +1,98 @@ +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::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(b64_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. +/// +/// If you just want to encode a JWT, use `encode` instead. +pub fn sign(message: &str, key: &EncodingKey, algorithm: Algorithm) -> Result { + match algorithm { + 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.inner(), message) + } + + Algorithm::RS256 + | Algorithm::RS384 + | Algorithm::RS512 + | Algorithm::PS256 + | Algorithm::PS384 + | Algorithm::PS512 => rsa::sign(rsa::alg_to_rsa_signing(algorithm), key.inner(), 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. +/// +/// If you just want to decode a JWT, use `decode` instead. +/// +/// `signature` is the signature part of a jwt (text after the second '.') +/// +/// `message` is base64(header) + "." + base64(claims) +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.as_bytes()), algorithm)?; + Ok(verify_slices_are_equal(signature.as_ref(), signed.as_ref()).is_ok()) + } + 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 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)) + } + } + } + } +} diff --git a/src/crypto/rsa.rs b/src/crypto/rsa.rs new file mode 100644 index 0000000..376c13a --- /dev/null +++ b/src/crypto/rsa.rs @@ -0,0 +1,66 @@ +use ring::{rand, signature}; +use simple_asn1::BigUint; + +use crate::algorithms::Algorithm; +use crate::errors::{ErrorKind, Result}; +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 { + 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"), + } +} + +/// 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 +/// 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 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(); + key_pair + .sign(alg, &rng, message.as_bytes(), &mut signature) + .map_err(|_| ErrorKind::InvalidRsaKey)?; + + 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: &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 }; + let res = pubkey.verify(alg, message.as_ref(), &signature_bytes); + Ok(res.is_ok()) +} diff --git a/src/decoding.rs b/src/decoding.rs new file mode 100644 index 0000000..36837f0 --- /dev/null +++ b/src/decoding.rs @@ -0,0 +1,207 @@ +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; +use crate::pem::decoder::PemEncodedKey; +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 { + ($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)), + } + }}; +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum DecodingKeyKind<'a> { + SecretOrDer(Cow<'a, [u8]>), + RsaModulusExponent { n: &'a str, e: &'a str }, +} + +/// 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) 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 { + 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 { + 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 { + 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 { + family: AlgorithmFamily::Ec, + kind: DecodingKeyKind::SecretOrDer(Cow::Owned(content.to_vec())), + }) + } + + /// 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] { + 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, + 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)?; + + if !validation.algorithms.contains(&header.alg) { + return Err(new_error(ErrorKind::InvalidAlgorithm)); + } + + if !verify(signature, message, key, header.alg)? { + 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 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. +/// +/// ```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 JWT without any signature verification/validations and return its [Header](struct.Header.html). +/// +/// If the token has an invalid format (ie 3 parts separated by a `.`), 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/encoding.rs b/src/encoding.rs new file mode 100644 index 0000000..3f7a690 --- /dev/null +++ b/src/encoding.rs @@ -0,0 +1,93 @@ +use serde::ser::Serialize; + +use crate::algorithms::AlgorithmFamily; +use crate::crypto; +use crate::errors::{new_error, ErrorKind, 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. +/// 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 a HMAC secret that is not base64, use that. + pub fn from_secret(secret: &[u8]) -> Self { + 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. + /// This errors if the key is not a valid RSA key. + pub fn from_rsa_pem(key: &[u8]) -> Result { + let pem_key = PemEncodedKey::new(key)?; + let content = pem_key.as_rsa_key()?; + Ok(EncodingKey { family: AlgorithmFamily::Rsa, 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: &[u8]) -> Result { + let pem_key = PemEncodedKey::new(key)?; + let content = pem_key.as_ec_private_key()?; + Ok(EncodingKey { family: AlgorithmFamily::Ec, content: content.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] { + &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 { + 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("."); + let signature = crypto::sign(&*message, key, header.alg)?; + + Ok([message, signature].join(".")) +} diff --git a/src/errors.rs b/src/errors.rs index 8111596..d634581 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -42,8 +42,10 @@ 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 + // 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 @@ -54,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 @@ -77,27 +80,7 @@ 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::Base64(ref err) => err.description(), - ErrorKind::Json(ref err) => err.description(), - ErrorKind::Utf8(ref err) => err.description(), - ErrorKind::Crypto(ref err) => err.description(), - _ => unreachable!(), - } - } - - fn cause(&self) -> Option<&StdError> { + fn cause(&self) -> Option<&dyn StdError> { match *self.0 { ErrorKind::InvalidToken => None, ErrorKind::InvalidSignature => None, @@ -109,11 +92,13 @@ impl StdError for Error { ErrorKind::InvalidSubject => None, ErrorKind::ImmatureSignature => None, ErrorKind::InvalidAlgorithm => None, + ErrorKind::InvalidAlgorithmName => None, + ErrorKind::InvalidKeyFormat => 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!(), + ErrorKind::__Nonexhaustive => None, } } } @@ -121,21 +106,23 @@ 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::InvalidKeyFormat + | 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), - _ => unreachable!(), + ErrorKind::Base64(ref err) => write!(f, "Base64 error: {}", err), + ErrorKind::__Nonexhaustive => write!(f, "Unknown error"), } } } diff --git a/src/header.rs b/src/header.rs index 0db2614..c6ac473 100644 --- a/src/header.rs +++ b/src/header.rs @@ -1,4 +1,8 @@ -use crypto::Algorithm; +use serde::{Deserialize, Serialize}; + +use crate::algorithms::Algorithm; +use crate::errors::Result; +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. @@ -42,7 +46,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, @@ -53,6 +57,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 = b64_decode(encoded_part)?; + 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 592e565..96ac22f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,164 +1,22 @@ //! Create and parses JWT (JSON Web Tokens) //! //! Documentation: [stable](https://docs.rs/jsonwebtoken/) -#![recursion_limit = "300"] #![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 untrusted; - -mod crypto; -/// All the errors, generated using error-chain +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; +mod pem; mod serialization; mod validation; -pub use crypto::{sign, verify, Algorithm}; +pub use algorithms::Algorithm; +pub use decoding::{dangerous_unsafe_decode, decode, decode_header, DecodingKey, TokenData}; +pub use encoding::{encode, EncodingKey}; pub use header::Header; -pub use serialization::TokenData; 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; - -/// 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; -/// 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 = to_jwt_part(&header)?; - let encoded_claims = to_jwt_part(&claims)?; - let signing_input = [encoded_header.as_ref(), encoded_claims.as_ref()].join("."); - let signature = sign(&*signing_input, 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, "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_jwt_part(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_jwt_part(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, '.')); - from_jwt_part(header) -} diff --git a/src/pem/decoder.rs b/src/pem/decoder.rs new file mode 100644 index 0000000..ded5ac5 --- /dev/null +++ b/src/pem/decoder.rs @@ -0,0 +1,205 @@ +use crate::errors::{ErrorKind, Result}; + +use simple_asn1::{BigUint, OID}; + +/// Supported PEM files for EC and RSA Public and Private Keys +#[derive(Debug, PartialEq)] +enum PemType { + EcPublic, + EcPrivate, + RsaPublic, + RsaPrivate, +} + +#[derive(Debug, PartialEq)] +enum Standard { + // Only for RSA + Pkcs1, + // RSA/EC + 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` +/// 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 +#[derive(Debug)] +pub(crate) struct PemEncodedKey { + content: Vec, + asn1: Vec, + pem_type: PemType, + standard: Standard, +} + +impl PemEncodedKey { + /// Read the PEM file for later key use + pub fn new(input: &[u8]) -> 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.into()), + }; + + 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::RsaPrivate, + standard: Standard::Pkcs1, + }), + "RSA PUBLIC KEY" => Ok(PemEncodedKey { + content: pem_contents, + asn1: asn1_content, + pem_type: PemType::RsaPublic, + 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 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::EcPrivate + } else { + PemType::EcPublic + } + } + Classification::Rsa => { + if is_private { + PemType::RsaPrivate + } else { + PemType::RsaPublic + } + } + }; + Ok(PemEncodedKey { + content: pem_contents, + asn1: asn1_content, + pem_type, + standard: Standard::Pkcs8, + }) + } + None => Err(ErrorKind::InvalidKeyFormat.into()), + }, + + // Unknown/unsupported type + _ => Err(ErrorKind::InvalidKeyFormat.into()), + } + } + Err(_) => Err(ErrorKind::InvalidKeyFormat.into()), + } + } + + /// Can only be PKCS8 + pub fn as_ec_private_key(&self) -> Result<&[u8]> { + match self.standard { + Standard::Pkcs1 => Err(ErrorKind::InvalidKeyFormat.into()), + Standard::Pkcs8 => match self.pem_type { + 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()), + }, + } + } +} + +// 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: &[simple_asn1::ASN1Block]) -> 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()); + } + _ => (), + } + } + + Err(ErrorKind::InvalidEcdsaKey.into()) +} + +/// Find whether this is EC or RSA +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, 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 { + 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); + } + if oid == rsa_public_key_oid { + return Some(Classification::Rsa); + } + } + _ => {} + } + } + None +} diff --git a/src/pem/mod.rs b/src/pem/mod.rs new file mode 100644 index 0000000..99753de --- /dev/null +++ b/src/pem/mod.rs @@ -0,0 +1 @@ +pub(crate) mod decoder; diff --git a/src/serialization.rs b/src/serialization.rs index 995eee3..23d0883 100644 --- a/src/serialization.rs +++ b/src/serialization.rs @@ -1,43 +1,32 @@ -use base64; use serde::de::DeserializeOwned; 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; -/// 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) } -/// 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)) +pub(crate) fn b64_decode(input: &str) -> Result> { + base64::decode_config(input, base64::URL_SAFE_NO_PAD).map_err(|e| e.into()) } -/// 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(crate) fn b64_encode_part(input: &T) -> Result { + let json = to_string(input)?; + Ok(b64_encode(json.as_bytes())) } -/// Decodes from base64 and deserializes from JSON to a struct AND a hashmap -pub fn from_jwt_part_claims, T: DeserializeOwned>( +/// Decodes from base64 and deserializes from JSON to a struct AND a hashmap of Value so we can +/// run validation on it +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(b64_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/src/validation.rs b/src/validation.rs index 293147a..b6805aa 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -1,14 +1,15 @@ -use chrono::Utc; -use serde::ser::Serialize; +use std::collections::HashSet; +use std::time::{SystemTime, UNIX_EPOCH}; + use serde_json::map::Map; -use serde_json::{from_value, to_value, Value}; +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. +/// Contains the various validations that are applied after decoding a JWT. /// -/// All time validation happen on UTC timestamps. +/// All time validation happen on UTC timestamps as seconds. /// /// ```rust /// use jsonwebtoken::Validation; @@ -21,7 +22,7 @@ use 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)] @@ -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. @@ -43,13 +44,11 @@ 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, + 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. /// @@ -75,10 +74,9 @@ 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) { - self.aud = Some(to_value(audience).unwrap()); + /// `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()) } } @@ -99,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 { @@ -114,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 { @@ -144,9 +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") { - if aud != correct_aud { - 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)); } @@ -157,18 +171,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::{get_current_timestamp, validate, Validation}; - use errors::ErrorKind; + 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()); } @@ -176,7 +189,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()); @@ -189,7 +202,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()); @@ -210,7 +223,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); @@ -220,7 +233,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); @@ -235,7 +248,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, @@ -345,9 +358,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()); } @@ -365,7 +378,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); @@ -380,9 +393,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()); @@ -396,7 +409,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()); @@ -405,4 +418,48 @@ 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(get_current_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) + } + }; + } + + // 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()); + } } diff --git a/tests/ecdsa.rs b/tests/ecdsa.rs deleted file mode 100644 index 72f13df..0000000 --- a/tests/ecdsa.rs +++ /dev/null @@ -1,38 +0,0 @@ -extern crate jsonwebtoken; -#[macro_use] -extern crate serde_derive; -extern crate chrono; - -use chrono::Utc; -use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Header, Validation}; - -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -struct Claims { - sub: String, - company: String, - exp: i64, -} - -#[test] -fn round_trip_sign_verification() { - 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_claim() { - 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.pk8"); - let token = encode(&Header::new(Algorithm::ES256), &my_claims, 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()); -} diff --git a/tests/ecdsa/mod.rs b/tests/ecdsa/mod.rs new file mode 100644 index 0000000..2362409 --- /dev/null +++ b/tests/ecdsa/mod.rs @@ -0,0 +1,93 @@ +use chrono::Utc; +use jsonwebtoken::{ + crypto::{sign, verify}, + decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct Claims { + sub: String, + company: String, + exp: i64, +} + +#[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_ec_der(privkey), Algorithm::ES256).unwrap(); + let is_valid = + verify(&encrypted, "hello world", &DecodingKey::from_ec_der(pubkey), Algorithm::ES256) + .unwrap(); + assert!(is_valid); +} + +#[test] +fn round_trip_sign_verification_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_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_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(), + exp: Utc::now().timestamp() + 10000, + }; + let token = encode( + &Header::new(Algorithm::ES256), + &my_claims, + &EncodingKey::from_ec_pem(privkey_pem).unwrap(), + ) + .unwrap(); + let token_data = decode::( + &token, + &DecodingKey::from_ec_pem(pubkey_pem).unwrap(), + &Validation::new(Algorithm::ES256), + ) + .unwrap(); + assert_eq!(my_claims, token_data.claims); +} + +// 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_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(), + exp: Utc::now().timestamp() + 10000, + }; + let token = encode( + &Header::new(Algorithm::ES384), + &my_claims, + &EncodingKey::from_ec_pem(privkey_pem).unwrap(), + ) + .unwrap(); + let token_data = decode::( + &token, + &DecodingKey::from_ec_pem(pubkey_pem).unwrap(), + &Validation::new(Algorithm::ES384), + ) + .unwrap(); + assert_eq!(my_claims, token_data.claims); +} diff --git a/tests/ecdsa/private_ecdsa_key.pem b/tests/ecdsa/private_ecdsa_key.pem new file mode 100644 index 0000000..4613a0d --- /dev/null +++ b/tests/ecdsa/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_ecdsa_key.pk8 b/tests/ecdsa/private_ecdsa_key.pk8 similarity index 100% rename from tests/private_ecdsa_key.pk8 rename to tests/ecdsa/private_ecdsa_key.pk8 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_ecdsa_key.pem b/tests/ecdsa/public_ecdsa_key.pem new file mode 100644 index 0000000..f8b9c8e --- /dev/null +++ b/tests/ecdsa/public_ecdsa_key.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEw7JAoU/gJbZJvV+zCOvU9yFJq0FN +C/edCMRM78P8eQTBCDUTK1ywSYaszvQZvneiW6gNtWEJndSreEcyyUdVvg== +-----END PUBLIC KEY----- diff --git a/tests/public_ecdsa_key.pk8 b/tests/ecdsa/public_ecdsa_key.pk8 similarity index 100% rename from tests/public_ecdsa_key.pk8 rename to tests/ecdsa/public_ecdsa_key.pk8 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/hmac.rs b/tests/hmac.rs new file mode 100644 index 0000000..dcd9c59 --- /dev/null +++ b/tests/hmac.rs @@ -0,0 +1,160 @@ +use chrono::Utc; +use jsonwebtoken::{ + crypto::{sign, verify}, + dangerous_unsafe_decode, decode, decode_header, encode, Algorithm, DecodingKey, EncodingKey, + Header, Validation, +}; +use serde::{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", &EncodingKey::from_secret(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", &DecodingKey::from_secret(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, &EncodingKey::from_secret(b"secret")).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()); +} + +#[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, &EncodingKey::from_secret(b"secret")).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()); +} + +#[test] +fn decode_token() { + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.9r56oF7ZliOBlOAyiOFperTGxBtPykRQiWNFxhDCW98"; + let claims = + decode::(token, &DecodingKey::from_secret(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, &DecodingKey::from_secret(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, &DecodingKey::from_secret(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, + &DecodingKey::from_secret(b"secret"), + &Validation::new(Algorithm::RS512), + ); + 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"; + let claims = + decode::(token, &DecodingKey::from_secret(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 129ee3d..3760c89 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,170 +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, - Validation, -}; -use std::str::FromStr; - -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -struct Claims { - sub: String, - company: String, - exp: i64, -} - -#[test] -fn sign_hs256() { - let result = sign("hello world", 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", 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, "secret".as_ref()).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()); -} - -#[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, "secret".as_ref()).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()); -} - -#[test] -fn decode_token() { - let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.9r56oF7ZliOBlOAyiOFperTGxBtPykRQiWNFxhDCW98"; - let claims = decode::(token, "secret".as_ref(), &Validation::default()); - println!("{:?}", claims); - claims.unwrap(); -} - -#[test] -#[should_panic(expected = "InvalidToken")] -fn decode_token_missing_parts() { - let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; - let claims = decode::(token, "secret".as_ref(), &Validation::default()); - claims.unwrap(); -} - -#[test] -#[should_panic(expected = "InvalidSignature")] -fn decode_token_invalid_signature() { - let token = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.wrong"; - let claims = decode::(token, "secret".as_ref(), &Validation::default()); - claims.unwrap(); -} - -#[test] -#[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)); - 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()); - 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, "secret".as_ref()).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, "secret".as_ref(), &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("").is_err()); -} +mod ecdsa; +mod rsa; diff --git a/tests/rsa.rs b/tests/rsa.rs deleted file mode 100644 index def1f9f..0000000 --- a/tests/rsa.rs +++ /dev/null @@ -1,44 +0,0 @@ -extern crate jsonwebtoken; -#[macro_use] -extern crate serde_derive; -extern crate chrono; - -use chrono::Utc; -use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Header, Validation}; - -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -struct Claims { - sub: String, - company: String, - exp: i64, -} - -#[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); -} - -#[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::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()); -} diff --git a/tests/rsa/mod.rs b/tests/rsa/mod.rs new file mode 100644 index 0000000..60a2b71 --- /dev/null +++ b/tests/rsa/mod.rs @@ -0,0 +1,141 @@ +use chrono::Utc; +use jsonwebtoken::{ + crypto::{sign, verify}, + decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation, +}; +use serde::{Deserialize, Serialize}; + +const RSA_ALGORITHMS: &[Algorithm] = &[ + Algorithm::RS256, + Algorithm::RS384, + Algorithm::RS512, + Algorithm::PS256, + Algorithm::PS384, + Algorithm::PS512, +]; + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct Claims { + sub: String, + company: String, + exp: i64, +} + +#[test] +fn round_trip_sign_verification_pem_pkcs1() { + 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 encrypted = + sign("hello world", &EncodingKey::from_rsa_pem(privkey_pem).unwrap(), 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_pem_pkcs8() { + 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 encrypted = + sign("hello world", &EncodingKey::from_rsa_pem(privkey_pem).unwrap(), 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_rsa_der(privkey_der), alg).unwrap(); + let is_valid = + verify(&encrypted, "hello world", &DecodingKey::from_rsa_der(pubkey_der), alg).unwrap(); + assert!(is_valid); + } +} + +#[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 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_pem).unwrap()) + .unwrap(); + let token_data = decode::( + &token, + &DecodingKey::from_rsa_pem(pubkey_pem).unwrap(), + &Validation::new(alg), + ) + .unwrap(); + assert_eq!(my_claims, token_data.claims); + assert!(token_data.header.kid.is_none()); + } +} + +#[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, + &EncodingKey::from_rsa_pem(privkey.as_ref()).unwrap(), + ) + .unwrap(); + let res = decode::( + &encrypted, + &DecodingKey::from_rsa_components(n, e), + &Validation::new(Algorithm::RS256), + ); + assert!(res.is_ok()); +} + +// 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, &EncodingKey::from_rsa_pem(privkey_pem).unwrap()) + .unwrap(); + let token_data = decode::( + &token, + &DecodingKey::from_rsa_pem(pubkey_pem).unwrap(), + &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/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.pem b/tests/rsa/private_rsa_key_pkcs1.pem similarity index 100% rename from tests/private_rsa_key.pem rename to tests/rsa/private_rsa_key_pkcs1.pem diff --git a/tests/rsa/private_rsa_key_pkcs8.pem b/tests/rsa/private_rsa_key_pkcs8.pem new file mode 100644 index 0000000..2df7451 --- /dev/null +++ b/tests/rsa/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/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----- 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_8.pem b/tests/rsa/public_rsa_key_pkcs1.pem similarity index 100% rename from tests/public_rsa_key_8.pem rename to tests/rsa/public_rsa_key_pkcs1.pem diff --git a/tests/public_rsa_key.pem b/tests/rsa/public_rsa_key_pkcs8.pem similarity index 100% rename from tests/public_rsa_key.pem rename to tests/rsa/public_rsa_key_pkcs8.pem