diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..83bdfe8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: ci +on: [push, pull_request] + +jobs: + tests: + name: Tests + runs-on: ${{ matrix.os }} + strategy: + matrix: + build: [pinned, stable, nightly] + include: + - build: pinned + os: ubuntu-18.04 + rust: 1.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 bd1df1f..acd53a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,10 @@ - Add support for PS256, PS384 and PS512 - Add support for verifying with modulus/exponent components for RSA -- Change API for both sign/verify to take a `Key` enum rather than bytes - Update to 2018 edition - Changed aud field type in Validation to `Option>`. Audience validation now tests for "any-of-these" audience membership. +- Add support for keys in PEM format ## 6.0.1 (2019-05-10) diff --git a/Cargo.toml b/Cargo.toml index c7a8393..aa66e7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,27 @@ [package] name = "jsonwebtoken" version = "7.0.0" -authors = ["Vincent Prouillet "] +authors = ["Vincent Prouillet "] license = "MIT" readme = "README.md" -description = "Create and parse JWT in a strongly typed way." -homepage = "https://github.com/Keats/rust-jwt" -repository = "https://github.com/Keats/rust-jwt" -keywords = ["jwt", "web", "api", "token", "json"] +description = "Create and decode JWTs in a strongly typed way." +homepage = "https://github.com/Keats/jsonwebtoken" +repository = "https://github.com/Keats/jsonwebtoken" +keywords = ["jwt", "web", "api", "token", "json", "jwk"] edition = "2018" [dependencies] serde_json = "1.0" -serde_derive = "1.0" -serde = "1.0" +serde = {version = "1.0", features = ["derive"] } ring = { version = "0.16.5", features = ["std"] } -base64 = "0.10" +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-developed" } diff --git a/README.md b/README.md index ecdb723..a1cbce6 100644 --- a/README.md +++ b/README.md @@ -4,25 +4,41 @@ [API documentation on docs.rs](https://docs.rs/jsonwebtoken/) +See [JSON Web Tokens](https://en.wikipedia.org/wiki/JSON_Web_Token) for more information on what are JSON Web Tokens. + ## Installation Add the following to Cargo.toml: ```toml jsonwebtoken = "7" -serde_derive = "1" -serde = "1" +serde = {version = "1.0", features = ["derive"] } ``` +The minimum required Rust version is 1.36. + +## Algorithms +This library currently supports the following: + +- HS256 +- HS384 +- HS512 +- RS256 +- RS384 +- RS512 +- PS256 +- PS384 +- PS512 +- ES256 +- ES384 + + ## How to use Complete examples are available in the examples directory: a basic one and one with a custom header. In terms of imports and structs: ```rust -extern crate jsonwebtoken as jwt; -#[macro_use] -extern crate serde_derive; - -use jwt::{encode, decode, Header, Algorithm, Validation}; +use serde::{Serialize, Deserialize}; +use jsonwebtoken::{encode, decode, Header, Algorithm, Validation}; /// Our claims struct, it needs to derive `Serialize` and/or `Deserialize` #[derive(Debug, Serialize, Deserialize)] @@ -33,8 +49,8 @@ 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())?; @@ -45,38 +61,92 @@ All the parameters from the RFC are supported but the default header only has `t If you want to set the `kid` parameter or change the algorithm for example: ```rust -let mut header = Header::default(); +let mut header = Header::new(Algorithm::HS512); header.kid = Some("blabla".to_owned()); -header.alg = Algorithm::HS512; let token = encode(&header, &my_claims, "secret".as_ref())?; ``` Look at `examples/custom_header.rs` for a full working example. -### Decoding +### Encoding + ```rust +// HS256 +let token = encode(&Header::default(), &my_claims, "secret".as_ref())?; +// RSA +let token = encode(&Header::new(Algorithm::RS256), &my_claims, include_str!("privkey.pem"))?; +``` +Encoding a JWT takes 3 parameters: + +- a header: the `Header` struct +- some claims: your own struct +- a key/secret + +When using HS256, HS2384 or HS512, the key is always a shared secret like in the example above. When using +RSA/EC, the key should always be the content of the private key in the PEM format. + +### Decoding + +```rust +// `token` is a struct with 2 fields: `header` and `claims` where `claims` is your own struct. let token = decode::(&token, "secret".as_ref(), &Validation::default())?; -// token is a struct with 2 params: header and claims ``` `decode` can error for a variety of reasons: - the token or its signature is invalid -- error while decoding base64 or the result of decoding base64 is not valid UTF-8 +- the token had invalid base64 - validation of at least one reserved claim failed -In some cases, for example if you don't know the algorithm used, you will want to only decode the header: +As with encoding, when using HS256, HS2384 or HS512, the key is always a shared secret like in the example above. When using +RSA/EC, the key should always be the content of the public key in the PEM format. + +In some cases, for example if you don't know the algorithm used or need to grab the `kid`, you can 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` claim. `nbf` is also validated 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_rsa_components::(&token, jwk["n"], jwk["e"], &Validation::new(Algorithm::RS256))?; +``` + +### Converting .der to .pem + +You can use openssl for that: + +```bash +openssl rsa -inform DER -outform PEM -in mykey.der -out mykey.pem +``` + +### 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. +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`. @@ -97,33 +167,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 -- PS256 -- PS384 -- PS512 -- 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 47ba571..9efe6e1 100644 --- a/benches/jwt.rs +++ b/benches/jwt.rs @@ -1,10 +1,8 @@ #![feature(test)] -extern crate jsonwebtoken as jwt; extern crate test; -#[macro_use] -extern crate serde_derive; -use jwt::{decode, encode, Header, Hmac, Validation}; +use jsonwebtoken::{decode, encode, Header, Validation}; +use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] struct Claims { @@ -16,7 +14,7 @@ struct Claims { fn bench_encode(b: &mut test::Bencher) { let claim = Claims { sub: "b@b.com".to_owned(), company: "ACME".to_owned() }; - b.iter(|| encode(&Header::default(), &claim, Hmac::from(b"secret"))); + b.iter(|| encode(&Header::default(), &claim, "secret".as_ref())); } #[bench] diff --git a/examples/custom_chrono.rs b/examples/custom_chrono.rs index 2d3bcca..e0b1185 100644 --- a/examples/custom_chrono.rs +++ b/examples/custom_chrono.rs @@ -1,11 +1,6 @@ -extern crate jsonwebtoken as jwt; -extern crate serde; -#[macro_use] -extern crate serde_derive; -extern crate chrono; - use chrono::prelude::*; -use jwt::{Header, Key, Validation}; +use jsonwebtoken::{Header, Validation}; +use serde::{Deserialize, Serialize}; const SECRET: &str = "some-secret"; @@ -44,9 +39,6 @@ mod jwt_numeric_date { #[cfg(test)] mod tests { - use super::*; - use jwt::{Header, Validation}; - const EXPECTED_TOKEN: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJDdXN0b20gRGF0ZVRpbWUgc2VyL2RlIiwiaWF0IjowLCJleHAiOjMyNTAzNjgwMDAwfQ.RTgha0S53MjPC2pMA4e2oMzaBxSY3DMjiYR2qFfV55A"; use super::super::{Claims, SECRET}; @@ -59,14 +51,13 @@ mod jwt_numeric_date { let claims = Claims { sub: sub.clone(), iat, exp }; - let token = jwt::encode(&Header::default(), &claims, Key::Hmac(SECRET.as_ref())) + let token = encode(&Header::default(), &claims, SECRET.as_ref()) .expect("Failed to encode claims"); assert_eq!(&token, EXPECTED_TOKEN); - let decoded = - jwt::decode::(&token, Key::Hmac(SECRET.as_ref()), &Validation::default()) - .expect("Failed to decode token"); + let decoded = decode::(&token, SECRET.as_ref(), &Validation::default()) + .expect("Failed to decode token"); assert_eq!(decoded.claims, claims); } @@ -77,7 +68,7 @@ mod jwt_numeric_date { let overflow_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJDdXN0b20gRGF0ZVRpbWUgc2VyL2RlIiwiaWF0IjowLCJleHAiOjkyMjMzNzIwMzY4NTQ3NzYwMDB9.G2PKreA27U8_xOwuIeCYXacFYeR46f9FyENIZfCrvEc"; let decode_result = - jwt::decode::(&overflow_token, SECRET.as_ref(), &Validation::default()); + decode::(&overflow_token, SECRET.as_ref(), &Validation::default()); assert!(decode_result.is_err()); } @@ -91,12 +82,12 @@ fn main() -> Result<(), Box> { let claims = Claims { sub: sub.clone(), iat, exp }; - let token = jwt::encode(&Header::default(), &claims, Key::Hmac(SECRET.as_ref()))?; + let token = jsonwebtoken::encode(&Header::default(), &claims, SECRET.as_ref())?; println!("serialized token: {}", &token); let token_data = - jwt::decode::(&token, Key::Hmac(SECRET.as_ref()), &Validation::default())?; + jsonwebtoken::decode::(&token, SECRET.as_ref(), &Validation::default())?; println!("token data:\n{:#?}", &token_data); Ok(()) diff --git a/examples/custom_header.rs b/examples/custom_header.rs index d4c94da..f298c8d 100644 --- a/examples/custom_header.rs +++ b/examples/custom_header.rs @@ -1,9 +1,7 @@ -extern crate jsonwebtoken as jwt; -#[macro_use] -extern crate serde_derive; +use serde::{Deserialize, Serialize}; -use jwt::errors::ErrorKind; -use jwt::{decode, encode, Algorithm, Header, Key, Validation}; +use jsonwebtoken::errors::ErrorKind; +use jsonwebtoken::{decode, encode, Algorithm, Header, Validation}; #[derive(Debug, Serialize, Deserialize)] struct Claims { @@ -21,20 +19,19 @@ fn main() { header.kid = Some("signing_key".to_owned()); header.alg = Algorithm::HS512; - let token = match encode(&header, &my_claims, Key::Hmac(key)) { + let token = match encode(&header, &my_claims, key) { Ok(t) => t, Err(_) => panic!(), // in practice you would return the error }; println!("{:?}", token); - let token_data = - match decode::(&token, Key::Hmac(key), &Validation::new(Algorithm::HS512)) { - Ok(c) => c, - Err(err) => match *err.kind() { - ErrorKind::InvalidToken => panic!(), // Example on how to handle a specific error - _ => panic!(), - }, - }; + let token_data = match decode::(&token, key, &Validation::new(Algorithm::HS512)) { + Ok(c) => c, + Err(err) => match *err.kind() { + ErrorKind::InvalidToken => panic!(), // Example on how to handle a specific error + _ => panic!(), + }, + }; println!("{:?}", token_data.claims); println!("{:?}", token_data.header); } diff --git a/examples/validation.rs b/examples/validation.rs index 552eef0..6c22d3f 100644 --- a/examples/validation.rs +++ b/examples/validation.rs @@ -1,9 +1,6 @@ -extern crate jsonwebtoken as jwt; -#[macro_use] -extern crate serde_derive; - -use jwt::errors::ErrorKind; -use jwt::{decode, encode, Header, Key, Validation}; +use jsonwebtoken::errors::ErrorKind; +use jsonwebtoken::{decode, encode, Header, Validation}; +use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] struct Claims { @@ -16,13 +13,13 @@ fn main() { let my_claims = Claims { sub: "b@b.com".to_owned(), company: "ACME".to_owned(), exp: 10000000000 }; let key = b"secret"; - let token = match encode(&Header::default(), &my_claims, Key::Hmac(key)) { + let token = match encode(&Header::default(), &my_claims, key) { Ok(t) => t, Err(_) => panic!(), // in practice you would return the error }; let validation = Validation { sub: Some("b@b.com".to_string()), ..Validation::default() }; - let token_data = match decode::(&token, Key::Hmac(key), &validation) { + let token_data = match decode::(&token, key, &validation) { Ok(c) => c, Err(err) => match *err.kind() { ErrorKind::InvalidToken => panic!("Token is invalid"), // Example on how to handle a specific error diff --git a/src/algorithms.rs b/src/algorithms.rs index 34f8556..c9ff848 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -1,7 +1,8 @@ -use crate::errors::{new_error, Error, ErrorKind, Result}; +use crate::errors::{Error, ErrorKind, Result}; +use serde::{Deserialize, Serialize}; use std::str::FromStr; -/// The algorithms supported for signing/verifying +/// The algorithms supported for signing/verifying JWTs #[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)] pub enum Algorithm { /// HMAC using SHA-256 @@ -52,7 +53,26 @@ impl FromStr for Algorithm { "PS384" => Ok(Algorithm::PS384), "PS512" => Ok(Algorithm::PS512), "RS512" => Ok(Algorithm::RS512), - _ => Err(new_error(ErrorKind::InvalidAlgorithmName)), + _ => Err(ErrorKind::InvalidAlgorithmName.into()), } } } + +#[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 3556fb8..0000000 --- a/src/crypto.rs +++ /dev/null @@ -1,187 +0,0 @@ -use base64; -use ring::constant_time::verify_slices_are_equal; -use ring::{hmac, rand, signature}; - -use crate::algorithms::Algorithm; -use crate::errors::{new_error, ErrorKind, Result}; -use crate::keys::Key; - -/// The actual HS signing + encoding -fn sign_hmac(alg: hmac::Algorithm, key: Key, signing_input: &str) -> Result { - let signing_key = match key { - Key::Hmac(bytes) => hmac::Key::new(alg, bytes), - _ => return Err(ErrorKind::InvalidKeyFormat)?, - }; - 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: Key, - signing_input: &str, -) -> Result { - let signing_key = match key { - Key::Pkcs8(bytes) => signature::EcdsaKeyPair::from_pkcs8(alg, bytes)?, - _ => { - return Err(new_error(ErrorKind::InvalidKeyFormat)); - } - }; - let rng = rand::SystemRandom::new(); - let sig = signing_key.sign(&rng, signing_input.as_bytes())?; - Ok(base64::encode_config(&sig, base64::URL_SAFE_NO_PAD)) -} - -/// The actual RSA signing + encoding -/// Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html -fn sign_rsa( - alg: &'static dyn signature::RsaEncoding, - key: Key, - signing_input: &str, -) -> Result { - let key_pair = match key { - Key::Der(bytes) => { - signature::RsaKeyPair::from_der(bytes).map_err(|_| ErrorKind::InvalidRsaKey)? - } - Key::Pkcs8(bytes) => { - signature::RsaKeyPair::from_pkcs8(bytes).map_err(|_| ErrorKind::InvalidRsaKey)? - } - _ => { - return Err(ErrorKind::InvalidKeyFormat)?; - } - }; - - 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: Key, algorithm: Algorithm) -> Result { - match algorithm { - Algorithm::HS256 => sign_hmac(hmac::HMAC_SHA256, key, signing_input), - Algorithm::HS384 => sign_hmac(hmac::HMAC_SHA384, key, signing_input), - Algorithm::HS512 => sign_hmac(hmac::HMAC_SHA512, key, signing_input), - - Algorithm::ES256 => { - sign_ecdsa(&signature::ECDSA_P256_SHA256_FIXED_SIGNING, key, signing_input) - } - Algorithm::ES384 => { - sign_ecdsa(&signature::ECDSA_P384_SHA384_FIXED_SIGNING, key, signing_input) - } - - Algorithm::RS256 => sign_rsa(&signature::RSA_PKCS1_SHA256, key, signing_input), - Algorithm::RS384 => sign_rsa(&signature::RSA_PKCS1_SHA384, key, signing_input), - Algorithm::RS512 => sign_rsa(&signature::RSA_PKCS1_SHA512, key, signing_input), - - Algorithm::PS256 => sign_rsa(&signature::RSA_PSS_SHA256, key, signing_input), - Algorithm::PS384 => sign_rsa(&signature::RSA_PSS_SHA384, key, signing_input), - Algorithm::PS512 => sign_rsa(&signature::RSA_PSS_SHA512, key, signing_input), - } -} - -/// See Ring docs for more details -fn verify_ring( - alg: &'static dyn signature::VerificationAlgorithm, - signature: &str, - signing_input: &str, - key: &[u8], -) -> Result { - let signature_bytes = base64::decode_config(signature, base64::URL_SAFE_NO_PAD)?; - let public_key = signature::UnparsedPublicKey::new(alg, key); - let res = public_key.verify(signing_input.as_bytes(), &signature_bytes); - - Ok(res.is_ok()) -} - -fn verify_ring_es( - alg: &'static dyn signature::VerificationAlgorithm, - signature: &str, - signing_input: &str, - key: Key, -) -> Result { - let bytes = match key { - Key::Pkcs8(bytes) => bytes, - _ => { - return Err(ErrorKind::InvalidKeyFormat)?; - } - }; - verify_ring(alg, signature, signing_input, bytes) -} - -fn verify_ring_rsa( - alg: &'static signature::RsaParameters, - signature: &str, - signing_input: &str, - key: Key, -) -> Result { - match key { - Key::Der(bytes) | Key::Pkcs8(bytes) => verify_ring(alg, signature, signing_input, bytes), - Key::ModulusExponent(n, e) => { - let public_key = signature::RsaPublicKeyComponents { n, e }; - - let signature_bytes = base64::decode_config(signature, base64::URL_SAFE_NO_PAD)?; - - let res = public_key.verify(alg, signing_input.as_bytes(), &signature_bytes); - - Ok(res.is_ok()) - } - _ => Err(ErrorKind::InvalidKeyFormat)?, - } -} - -/// 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: Key, - algorithm: Algorithm, -) -> Result { - match algorithm { - Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => { - // we just re-sign the data with the key and compare if they are equal - let signed = sign(signing_input, key, algorithm)?; - Ok(verify_slices_are_equal(signature.as_ref(), signed.as_ref()).is_ok()) - } - Algorithm::ES256 => { - verify_ring_es(&signature::ECDSA_P256_SHA256_FIXED, signature, signing_input, key) - } - Algorithm::ES384 => { - verify_ring_es(&signature::ECDSA_P384_SHA384_FIXED, signature, signing_input, key) - } - Algorithm::RS256 => { - verify_ring_rsa(&signature::RSA_PKCS1_2048_8192_SHA256, signature, signing_input, key) - } - Algorithm::RS384 => { - verify_ring_rsa(&signature::RSA_PKCS1_2048_8192_SHA384, signature, signing_input, key) - } - Algorithm::RS512 => { - verify_ring_rsa(&signature::RSA_PKCS1_2048_8192_SHA512, signature, signing_input, key) - } - Algorithm::PS256 => { - verify_ring_rsa(&signature::RSA_PSS_2048_8192_SHA256, signature, signing_input, key) - } - Algorithm::PS384 => { - verify_ring_rsa(&signature::RSA_PSS_2048_8192_SHA384, signature, signing_input, key) - } - Algorithm::PS512 => { - verify_ring_rsa(&signature::RSA_PSS_2048_8192_SHA512, signature, signing_input, key) - } - } -} diff --git a/src/crypto/ecdsa.rs b/src/crypto/ecdsa.rs new file mode 100644 index 0000000..25099a7 --- /dev/null +++ b/src/crypto/ecdsa.rs @@ -0,0 +1,39 @@ +use ring::{rand, signature}; + +use crate::algorithms::Algorithm; +use crate::errors::Result; +use crate::pem::decoder::PemEncodedKey; +use crate::serialization::b64_encode; + +/// Only used internally when validating EC, to map from our enum to the Ring EcdsaVerificationAlgorithm structs. +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 +pub fn sign( + alg: &'static signature::EcdsaSigningAlgorithm, + key: &[u8], + message: &str, +) -> Result { + let pem_key = PemEncodedKey::new(key)?; + let signing_key = signature::EcdsaKeyPair::from_pkcs8(alg, pem_key.as_ec_private_key()?)?; + let 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..98dc440 --- /dev/null +++ b/src/crypto/mod.rs @@ -0,0 +1,119 @@ +use ring::constant_time::verify_slices_are_equal; +use ring::{hmac, signature}; + +use crate::algorithms::Algorithm; +use crate::errors::Result; +use crate::pem::decoder::PemEncodedKey; +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. +/// +/// `key` is the secret for HMAC and a pem encoded string otherwise +pub fn sign(message: &str, key: &[u8], algorithm: Algorithm) -> Result { + match algorithm { + Algorithm::HS256 => sign_hmac(hmac::HMAC_SHA256, key, message), + Algorithm::HS384 => sign_hmac(hmac::HMAC_SHA384, key, message), + Algorithm::HS512 => sign_hmac(hmac::HMAC_SHA512, key, message), + + Algorithm::ES256 | Algorithm::ES384 => { + ecdsa::sign(ecdsa::alg_to_ec_signing(algorithm), key, message) + } + + Algorithm::RS256 + | Algorithm::RS384 + | Algorithm::RS512 + | Algorithm::PS256 + | Algorithm::PS384 + | Algorithm::PS512 => rsa::sign(rsa::alg_to_rsa_signing(algorithm), key, message), + } +} + +/// See Ring docs for more details +fn verify_ring( + alg: &'static dyn signature::VerificationAlgorithm, + signature: &str, + message: &str, + key: &[u8], +) -> Result { + let signature_bytes = b64_decode(signature)?; + let public_key = signature::UnparsedPublicKey::new(alg, key); + let res = public_key.verify(message.as_bytes(), &signature_bytes); + + Ok(res.is_ok()) +} + +/// Compares the signature given with a re-computed signature for HMAC or using the public key +/// for RSA/EC. +/// +/// 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) +/// For ECDSA/RSA, the `key` is the pem public key. If you want to verify using the public key +/// components (modulus/exponent), use `verify_rsa_components` instead. +pub fn verify(signature: &str, message: &str, key: &[u8], algorithm: Algorithm) -> Result { + match algorithm { + Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => { + // we just re-sign the message with the key and compare if they are equal + let signed = sign(message, key, algorithm)?; + Ok(verify_slices_are_equal(signature.as_ref(), signed.as_ref()).is_ok()) + } + Algorithm::ES256 | Algorithm::ES384 => { + let pem_key = PemEncodedKey::new(key)?; + verify_ring( + ecdsa::alg_to_ec_verification(algorithm), + signature, + message, + pem_key.as_ec_public_key()?, + ) + } + Algorithm::RS256 + | Algorithm::RS384 + | Algorithm::RS512 + | Algorithm::PS256 + | Algorithm::PS384 + | Algorithm::PS512 => { + let pem_key = PemEncodedKey::new(key)?; + verify_ring( + rsa::alg_to_rsa_parameters(algorithm), + signature, + message, + pem_key.as_rsa_key()?, + ) + } + } +} + +/// Verify the signature given using the (n, e) components of a RSA public key. +/// +/// `signature` is the signature part of a jwt (text after the second '.') +/// +/// `message` is base64(header) + "." + base64(claims) +pub fn verify_rsa_components( + signature: &str, + message: &str, + components: (&str, &str), + alg: Algorithm, +) -> Result { + let signature_bytes = b64_decode(signature)?; + rsa::verify_from_components( + rsa::alg_to_rsa_parameters(alg), + &signature_bytes, + message, + components, + ) +} diff --git a/src/crypto/rsa.rs b/src/crypto/rsa.rs new file mode 100644 index 0000000..89da359 --- /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::pem::decoder::PemEncodedKey; +use crate::serialization::{b64_decode, b64_encode}; + +/// Only used internally when validating RSA, to map from our enum to the Ring param structs. +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 +/// Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html +pub(crate) fn sign( + alg: &'static dyn signature::RsaEncoding, + key: &[u8], + message: &str, +) -> Result { + let pem_key = PemEncodedKey::new(key)?; + let key_pair = signature::RsaKeyPair::from_der(pem_key.as_rsa_key()?) + .map_err(|_| ErrorKind::InvalidRsaKey)?; + + let 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)) +} + +pub(crate) fn verify_from_components( + alg: &'static signature::RsaParameters, + signature_bytes: &[u8], + message: &str, + components: (&str, &str), +) -> Result { + let n = BigUint::from_bytes_be(&b64_decode(components.0)?).to_bytes_be(); + let e = BigUint::from_bytes_be(&b64_decode(components.1)?).to_bytes_be(); + let pubkey = signature::RsaPublicKeyComponents { n, e }; + let res = pubkey.verify(alg, 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..d740052 --- /dev/null +++ b/src/decoding.rs @@ -0,0 +1,163 @@ +use serde::de::DeserializeOwned; + +use crate::crypto::{verify, verify_rsa_components}; +use crate::errors::{new_error, ErrorKind, Result}; +use crate::header::Header; +use crate::serialization::from_jwt_part_claims; +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)), + } + }}; +} + +/// Internal way to differentiate between public key types +enum DecodingKey<'a> { + SecretOrPem(&'a [u8]), + RsaModulusExponent { n: &'a str, e: &'a str }, +} + +fn _decode( + token: &str, + key: DecodingKey, + validation: &Validation, +) -> Result> { + let (signature, message) = expect_two!(token.rsplitn(2, '.')); + let (claims, header) = expect_two!(message.rsplitn(2, '.')); + let header = Header::from_encoded(header)?; + + if !validation.algorithms.contains(&header.alg) { + return Err(new_error(ErrorKind::InvalidAlgorithm)); + } + + let is_valid = match key { + DecodingKey::SecretOrPem(k) => verify(signature, message, k, header.alg), + DecodingKey::RsaModulusExponent { n, e } => { + verify_rsa_components(signature, message, (n, e), header.alg) + } + }?; + + if !is_valid { + return Err(new_error(ErrorKind::InvalidSignature)); + } + + let (decoded_claims, claims_map): (T, _) = from_jwt_part_claims(claims)?; + validate(&claims_map, validation)?; + + Ok(TokenData { header, claims: decoded_claims }) +} + +/// Decode and validate a JWT using a secret for HS and a public PEM format for RSA/EC +/// +/// If the token or its signature is invalid or the claims fail validation, it will return an error. +/// +/// ```rust +/// use serde::{Deserialize, Serialize}; +/// use jsonwebtoken::{decode, Validation, Algorithm}; +/// +/// #[derive(Debug, Serialize, Deserialize)] +/// struct Claims { +/// sub: String, +/// company: String +/// } +/// +/// let token = "a.jwt.token".to_string(); +/// // Claims is a struct that implements Deserialize +/// let token_message = decode::(&token, "secret".as_ref(), &Validation::new(Algorithm::HS256)); +/// ``` +pub fn decode( + token: &str, + key: &[u8], + validation: &Validation, +) -> Result> { + _decode(token, DecodingKey::SecretOrPem(key), validation) +} + +/// Decode and validate a JWT using (n, e) base64 encoded public key components for RSA +/// +/// If the token or its signature is invalid or the claims fail validation, it will return an error. +/// +/// ```rust +/// use serde::{Deserialize, Serialize}; +/// use jsonwebtoken::{decode_rsa_components, Validation, Algorithm}; +/// +/// #[derive(Debug, Serialize, Deserialize)] +/// struct Claims { +/// sub: String, +/// company: String +/// } +/// +/// let modulus = "some-base64-data"; +/// let exponent = "some-base64-data"; +/// let token = "a.jwt.token".to_string(); +/// // Claims is a struct that implements Deserialize +/// let token_message = decode_rsa_components::(&token, &modulus, &exponent, &Validation::new(Algorithm::HS256)); +/// ``` +pub fn decode_rsa_components( + token: &str, + modulus: &str, + exponent: &str, + validation: &Validation, +) -> Result> { + _decode(token, DecodingKey::RsaModulusExponent { n: modulus, e: exponent }, validation) +} + +/// Decode a JWT without any signature verification/validations. +/// +/// NOTE: Do not use this unless you know what you are doing! If the token's signature is invalid, it will *not* return an error. +/// +/// ```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/errors.rs b/src/errors.rs index 317a5cb..87231ed 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -45,7 +45,7 @@ pub enum ErrorKind { /// When a key is provided with an invalid format InvalidKeyFormat, - // validation error + // Validation errors /// When a token’s `exp` claim indicates that it has expired ExpiredSignature, /// When a token’s `iss` claim does not match the expected issuer @@ -91,11 +91,13 @@ impl StdError for Error { ErrorKind::InvalidSubject => "invalid subject", ErrorKind::ImmatureSignature => "immature signature", ErrorKind::InvalidAlgorithm => "algorithms don't match", + ErrorKind::InvalidAlgorithmName => "not a known algorithm", + ErrorKind::InvalidKeyFormat => "invalid key format", ErrorKind::Base64(ref err) => err.description(), ErrorKind::Json(ref err) => err.description(), ErrorKind::Utf8(ref err) => err.description(), ErrorKind::Crypto(ref err) => err.description(), - _ => unreachable!(), + ErrorKind::__Nonexhaustive => "unknown error", } } @@ -111,11 +113,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, } } } @@ -123,21 +127,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.description()), ErrorKind::Json(ref err) => write!(f, "JSON error: {}", err), ErrorKind::Utf8(ref err) => write!(f, "UTF-8 error: {}", err), ErrorKind::Crypto(ref err) => write!(f, "Crypto error: {}", err), - _ => unreachable!(), + ErrorKind::Base64(ref err) => write!(f, "Base64 error: {}", err), + ErrorKind::__Nonexhaustive => write!(f, "Unknown error"), } } } diff --git a/src/header.rs b/src/header.rs index 2a17da9..c6ac473 100644 --- a/src/header.rs +++ b/src/header.rs @@ -1,4 +1,8 @@ +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/keys.rs b/src/keys.rs deleted file mode 100644 index cbabfa4..0000000 --- a/src/keys.rs +++ /dev/null @@ -1,15 +0,0 @@ -/// The supported RSA key formats, see the documentation for ring::signature::RsaKeyPair -/// for more information -pub enum Key<'a> { - /// An unencrypted PKCS#8-encoded key. Can be used with both ECDSA and RSA - /// algorithms when signing. See ring for information. - Pkcs8(&'a [u8]), - /// A binary DER-encoded ASN.1 key. Can only be used with RSA algorithms - /// when signing. See ring for more information - Der(&'a [u8]), - /// This is not a key format, but provided for convenience since HMAC is - /// a supported signing algorithm. - Hmac(&'a [u8]), - /// A Modulus/exponent for a RSA public key - ModulusExponent(&'a [u8], &'a [u8]), -} diff --git a/src/lib.rs b/src/lib.rs index 5061bd7..77d34ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,45 +3,37 @@ //! Documentation: [stable](https://docs.rs/jsonwebtoken/) #![deny(missing_docs)] -#[macro_use] -extern crate serde_derive; -extern crate base64; -extern crate chrono; -extern crate ring; -extern crate serde; -extern crate serde_json; - mod algorithms; -mod crypto; -/// All the errors +/// Lower level functions, if you want to do something other than JWTs +pub mod crypto; +mod decoding; +/// All the errors that can be encountered while encoding/decoding JWTs pub mod errors; mod header; -mod keys; +mod pem; mod serialization; mod validation; pub use algorithms::Algorithm; -pub use crypto::{sign, verify}; +pub use decoding::{ + dangerous_unsafe_decode, decode, decode_header, decode_rsa_components, TokenData, +}; pub use header::Header; -pub use keys::Key; -pub use serialization::TokenData; pub use validation::Validation; -use serde::de::DeserializeOwned; use serde::ser::Serialize; -use crate::errors::{new_error, ErrorKind, Result}; -use crate::serialization::{from_jwt_part, from_jwt_part_claims, to_jwt_part}; -use crate::validation::validate; +use crate::errors::Result; +use crate::serialization::b64_encode_part; -/// Encode the header and claims given and sign the payload using the algorithm from the header and the key +/// Encode the header and claims given and sign the payload using the algorithm from the header and the key. +/// If the algorithm given is RSA or EC, the key needs to be in the PEM format. /// -/// ```rust,ignore -/// #[macro_use] -/// extern crate serde_derive; +/// ```rust +/// use serde::{Deserialize, Serialize}; /// use jsonwebtoken::{encode, Algorithm, Header}; /// -/// /// #[derive(Debug, Serialize, Deserialize)] +/// #[derive(Debug, Serialize, Deserialize)] /// struct Claims { /// sub: String, /// company: String @@ -54,113 +46,13 @@ use crate::validation::validate; /// /// // my_claims is a struct that implements Serialize /// // This will create a JWT using HS256 as algorithm -/// let token = encode(&Header::default(), &my_claims, Key::Hmac("secret".as_ref())).unwrap(); +/// let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap(); /// ``` -pub fn encode(header: &Header, claims: &T, key: Key) -> Result { - 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)?; +pub fn encode(header: &Header, claims: &T, key: &[u8]) -> Result { + let encoded_header = b64_encode_part(&header)?; + let encoded_claims = b64_encode_part(&claims)?; + let message = [encoded_header.as_ref(), encoded_claims.as_ref()].join("."); + let signature = crypto::sign(&*message, key, header.alg)?; - Ok([signing_input, signature].join(".")) -} - -/// Used in decode: takes the result of a rsplit and ensure we only get 2 parts -/// Errors if we don't -macro_rules! expect_two { - ($iter:expr) => {{ - let mut i = $iter; - match (i.next(), i.next(), i.next()) { - (Some(first), Some(second), None) => (first, second), - _ => return Err(new_error(ErrorKind::InvalidToken)), - } - }}; -} - -/// Decode a token into a struct containing 2 fields: `claims` and `header`. -/// -/// If the token or its signature is invalid or the claims fail validation, it will return an error. -/// -/// ```rust,ignore -/// #[macro_use] -/// extern crate serde_derive; -/// use jsonwebtoken::{decode, Validation, Algorithm}; -/// -/// #[derive(Debug, Serialize, Deserialize)] -/// struct Claims { -/// sub: String, -/// company: String -/// } -/// -/// let token = "a.jwt.token".to_string(); -/// // Claims is a struct that implements Deserialize -/// let token_data = decode::(&token, Key::Hmac("secret"), &Validation::new(Algorithm::HS256)); -/// ``` -pub fn decode( - token: &str, - key: Key, - 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) + Ok([message, signature].join(".")) } 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 2a47688..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 crate::errors::Result; -use crate::header::Header; -/// The return type of a successful call to decode -#[derive(Debug)] -pub struct TokenData { - /// The decoded JWT header - pub header: Header, - /// The decoded JWT claims - pub claims: T, +pub(crate) fn b64_encode(input: &[u8]) -> String { + base64::encode_config(input, base64::URL_SAFE_NO_PAD) } -/// 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 8c7b2ca..48d79c7 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -1,14 +1,15 @@ use std::collections::HashSet; -use chrono::Utc; +use std::time::{SystemTime, UNIX_EPOCH}; + use serde_json::map::Map; use serde_json::{from_value, Value}; use crate::algorithms::Algorithm; use crate::errors::{new_error, ErrorKind, Result}; -/// Contains the various validations that are applied after decoding a token. +/// Contains the various validations that are applied after decoding a JWT. /// -/// All time validation happen on UTC timestamps. +/// All time validation happen on UTC timestamps as seconds. /// /// ```rust /// use jsonwebtoken::Validation; @@ -30,7 +31,7 @@ pub struct Validation { /// account for clock skew. /// /// Defaults to `0`. - pub leeway: i64, + pub leeway: u64, /// Whether to validate the `exp` field. /// /// It will return an error if the time in the `exp` field is past. @@ -96,12 +97,17 @@ impl Default for Validation { } } +fn get_current_timestamp() -> u64 { + let start = SystemTime::now(); + start.duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs() +} + pub fn validate(claims: &Map, options: &Validation) -> Result<()> { - let now = Utc::now().timestamp(); + let now = get_current_timestamp(); if options.validate_exp { if let Some(exp) = claims.get("exp") { - if from_value::(exp.clone())? < now - options.leeway { + if from_value::(exp.clone())? < now - options.leeway { return Err(new_error(ErrorKind::ExpiredSignature)); } } else { @@ -111,7 +117,7 @@ pub fn validate(claims: &Map, options: &Validation) -> Result<()> if options.validate_nbf { if let Some(nbf) = claims.get("nbf") { - if from_value::(nbf.clone())? > now + options.leeway { + if from_value::(nbf.clone())? > now + options.leeway { return Err(new_error(ErrorKind::ImmatureSignature)); } } else { @@ -141,8 +147,8 @@ pub fn validate(claims: &Map, options: &Validation) -> Result<()> if let Some(ref correct_aud) = options.aud { if let Some(aud) = claims.get("aud") { - let provided_aud: HashSet = from_value(aud.clone())?; - if provided_aud.intersection(correct_aud).count() == 0 { + let provided_aud: HashSet = from_value(aud.clone())?; + if provided_aud.intersection(correct_aud).count() == 0 { return Err(new_error(ErrorKind::InvalidAudience)); } } else { @@ -155,18 +161,17 @@ pub fn validate(claims: &Map, options: &Validation) -> Result<()> #[cfg(test)] mod tests { - use chrono::Utc; use serde_json::map::Map; use serde_json::to_value; - use super::{validate, Validation}; + use super::{get_current_timestamp, validate, Validation}; use crate::errors::ErrorKind; #[test] fn exp_in_future_ok() { let mut claims = Map::new(); - claims.insert("exp".to_string(), to_value(Utc::now().timestamp() + 10000).unwrap()); + claims.insert("exp".to_string(), to_value(get_current_timestamp() + 10000).unwrap()); let res = validate(&claims, &Validation::default()); assert!(res.is_ok()); } @@ -174,7 +179,7 @@ mod tests { #[test] fn exp_in_past_fails() { let mut claims = Map::new(); - claims.insert("exp".to_string(), to_value(Utc::now().timestamp() - 100000).unwrap()); + claims.insert("exp".to_string(), to_value(get_current_timestamp() - 100000).unwrap()); let res = validate(&claims, &Validation::default()); assert!(res.is_err()); @@ -187,7 +192,7 @@ mod tests { #[test] fn exp_in_past_but_in_leeway_ok() { let mut claims = Map::new(); - claims.insert("exp".to_string(), to_value(Utc::now().timestamp() - 500).unwrap()); + claims.insert("exp".to_string(), to_value(get_current_timestamp() - 500).unwrap()); let validation = Validation { leeway: 1000 * 60, ..Default::default() }; let res = validate(&claims, &validation); assert!(res.is_ok()); @@ -208,7 +213,7 @@ mod tests { #[test] fn nbf_in_past_ok() { let mut claims = Map::new(); - claims.insert("nbf".to_string(), to_value(Utc::now().timestamp() - 10000).unwrap()); + claims.insert("nbf".to_string(), to_value(get_current_timestamp() - 10000).unwrap()); let validation = Validation { validate_exp: false, validate_nbf: true, ..Validation::default() }; let res = validate(&claims, &validation); @@ -218,7 +223,7 @@ mod tests { #[test] fn nbf_in_future_fails() { let mut claims = Map::new(); - claims.insert("nbf".to_string(), to_value(Utc::now().timestamp() + 100000).unwrap()); + claims.insert("nbf".to_string(), to_value(get_current_timestamp() + 100000).unwrap()); let validation = Validation { validate_exp: false, validate_nbf: true, ..Validation::default() }; let res = validate(&claims, &validation); @@ -233,7 +238,7 @@ mod tests { #[test] fn nbf_in_future_but_in_leeway_ok() { let mut claims = Map::new(); - claims.insert("nbf".to_string(), to_value(Utc::now().timestamp() + 500).unwrap()); + claims.insert("nbf".to_string(), to_value(get_current_timestamp() + 500).unwrap()); let validation = Validation { leeway: 1000 * 60, validate_nbf: true, @@ -403,4 +408,28 @@ mod tests { _ => assert!(false), }; } + + // https://github.com/Keats/jsonwebtoken/issues/51 + #[test] + fn does_validation_in_right_order() { + let mut claims = Map::new(); + claims.insert("exp".to_string(), to_value(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) + } + }; + } } diff --git a/tests/ecdsa.rs b/tests/ecdsa.rs deleted file mode 100644 index 12cf78d..0000000 --- a/tests/ecdsa.rs +++ /dev/null @@ -1,47 +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, Key, Validation}; - -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -pub 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", Key::Pkcs8(&privkey[..]), Algorithm::ES256).unwrap(); - let pubkey = include_bytes!("public_ecdsa_key.pk8"); - let is_valid = verify(&encrypted, "hello world", Key::Pkcs8(pubkey), Algorithm::ES256).unwrap(); - assert!(is_valid); -} - -#[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, Key::Pkcs8(&privkey[..])).unwrap(); - let pubkey = include_bytes!("public_ecdsa_key.pk8"); - let token_data = - decode::(&token, Key::Pkcs8(pubkey), &Validation::new(Algorithm::ES256)).unwrap(); - assert_eq!(my_claims, token_data.claims); - assert!(token_data.header.kid.is_none()); -} - -#[test] -#[should_panic(expected = "InvalidKeyFormat")] -fn fails_with_non_pkcs8_key_format() { - let privkey = include_bytes!("private_rsa_key.der"); - let _encrypted = sign("hello world", Key::Der(&privkey[..]), Algorithm::ES256).unwrap(); -} diff --git a/tests/ecdsa/mod.rs b/tests/ecdsa/mod.rs new file mode 100644 index 0000000..d90242e --- /dev/null +++ b/tests/ecdsa/mod.rs @@ -0,0 +1,62 @@ +use chrono::Utc; +use jsonwebtoken::{ + crypto::{sign, verify}, + decode, encode, Algorithm, Header, Validation, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct Claims { + sub: String, + company: String, + exp: i64, +} + +// TODO: remove completely? +//#[test] +//fn round_trip_sign_verification_pk8() { +// let privkey = include_bytes!("private_ecdsa_key.pk8"); +// let encrypted = sign("hello world", privkey, Algorithm::ES256).unwrap(); +// let pubkey = include_bytes!("public_ecdsa_key.pk8"); +// let is_valid = verify(&encrypted, "hello world", pubkey, Algorithm::ES256).unwrap(); +// assert!(is_valid); +//} + +#[test] +fn round_trip_sign_verification_pem() { + let privkey = include_bytes!("private_ecdsa_key.pem"); + let pubkey = include_bytes!("public_ecdsa_key.pem"); + let encrypted = sign("hello world", privkey, Algorithm::ES256).unwrap(); + let is_valid = verify(&encrypted, "hello world", pubkey, Algorithm::ES256).unwrap(); + assert!(is_valid); +} + +#[test] +fn round_trip_claim() { + let privkey = include_bytes!("private_ecdsa_key.pem"); + let pubkey = include_bytes!("public_ecdsa_key.pem"); + let my_claims = Claims { + sub: "b@b.com".to_string(), + company: "ACME".to_string(), + exp: Utc::now().timestamp() + 10000, + }; + let token = encode(&Header::new(Algorithm::ES256), &my_claims, privkey).unwrap(); + let token_data = decode::(&token, pubkey, &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 = include_bytes!("private_jwtio_pkcs8.pem"); + let pubkey = include_bytes!("public_jwtio.pem"); + let my_claims = Claims { + sub: "b@b.com".to_string(), + company: "ACME".to_string(), + exp: Utc::now().timestamp() + 10000, + }; + let token = encode(&Header::new(Algorithm::ES384), &my_claims, privkey).unwrap(); + let token_data = decode::(&token, pubkey, &Validation::new(Algorithm::ES384)).unwrap(); + assert_eq!(my_claims, token_data.claims); +} diff --git a/tests/ecdsa/private_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..98e2d56 --- /dev/null +++ b/tests/hmac.rs @@ -0,0 +1,132 @@ +use chrono::Utc; +use jsonwebtoken::{ + crypto::{sign, verify}, + dangerous_unsafe_decode, decode, decode_header, encode, Algorithm, 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", 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, b"secret").unwrap(); + let token_data = decode::(&token, b"secret", &Validation::default()).unwrap(); + assert_eq!(my_claims, token_data.claims); + assert_eq!("kid", token_data.header.kid.unwrap()); +} + +#[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, b"secret").unwrap(); + let token_data = decode::(&token, b"secret", &Validation::default()).unwrap(); + assert_eq!(my_claims, token_data.claims); + assert!(token_data.header.kid.is_none()); +} + +#[test] +fn decode_token() { + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.9r56oF7ZliOBlOAyiOFperTGxBtPykRQiWNFxhDCW98"; + let claims = decode::(token, 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, 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, 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, b"secret", &Validation::new(Algorithm::RS512)); + claims.unwrap(); +} + +#[test] +fn decode_token_with_bytes_secret() { + let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.Hm0yvKH25TavFPz7J_coST9lZFYH1hQo0tvhvImmaks"; + let claims = decode::(token, b"\x01\x02\x03", &Validation::default()); + assert!(claims.is_ok()); +} + +#[test] +fn decode_header_only() { + let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjb21wYW55IjoiMTIzNDU2Nzg5MCIsInN1YiI6IkpvaG4gRG9lIn0.S"; + let header = decode_header(token).unwrap(); + assert_eq!(header.alg, Algorithm::HS256); + assert_eq!(header.typ, Some("JWT".to_string())); +} + +#[test] +fn dangerous_unsafe_decode_token() { + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.9r56oF7ZliOBlOAyiOFperTGxBtPykRQiWNFxhDCW98"; + let claims = dangerous_unsafe_decode::(token); + claims.unwrap(); +} + +#[test] +#[should_panic(expected = "InvalidToken")] +fn dangerous_unsafe_decode_token_missing_parts() { + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; + let claims = dangerous_unsafe_decode::(token); + claims.unwrap(); +} + +#[test] +fn dangerous_unsafe_decode_token_invalid_signature() { + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.wrong"; + let claims = dangerous_unsafe_decode::(token); + claims.unwrap(); +} + +#[test] +fn dangerous_unsafe_decode_token_wrong_algorithm() { + let token = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.fLxey-hxAKX5rNHHIx1_Ch0KmrbiuoakDVbsJjLWrx8fbjKjrPuWMYEJzTU3SBnYgnZokC-wqSdqckXUOunC-g"; + let claims = dangerous_unsafe_decode::(token); + claims.unwrap(); +} diff --git a/tests/lib.rs b/tests/lib.rs index cce95a1..3760c89 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,175 +1,2 @@ -extern crate jsonwebtoken; -#[macro_use] -extern crate serde_derive; -extern crate chrono; - -use chrono::Utc; -use jsonwebtoken::{ - dangerous_unsafe_decode, decode, decode_header, encode, sign, verify, Algorithm, Header, Key, - Validation, -}; -use std::str::FromStr; - -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -pub struct Claims { - sub: String, - company: String, - exp: i64, -} - -#[test] -fn sign_hs256() { - let result = sign("hello world", Key::Hmac(b"secret"), Algorithm::HS256).unwrap(); - let expected = "c0zGLzKEFWj0VxWuufTXiRMk5tlI5MbGDAYhzaxIYjo"; - assert_eq!(result, expected); -} - -#[test] -fn verify_hs256() { - let sig = "c0zGLzKEFWj0VxWuufTXiRMk5tlI5MbGDAYhzaxIYjo"; - let valid = verify(sig, "hello world", Key::Hmac(b"secret"), Algorithm::HS256).unwrap(); - assert!(valid); -} - -#[test] -fn encode_with_custom_header() { - let my_claims = Claims { - sub: "b@b.com".to_string(), - company: "ACME".to_string(), - exp: Utc::now().timestamp() + 10000, - }; - let mut header = Header::default(); - header.kid = Some("kid".to_string()); - let token = encode(&header, &my_claims, Key::Hmac(b"secret")).unwrap(); - let token_data = - decode::(&token, Key::Hmac(b"secret"), &Validation::default()).unwrap(); - assert_eq!(my_claims, token_data.claims); - assert_eq!("kid", token_data.header.kid.unwrap()); -} - -#[test] -fn round_trip_claim() { - let my_claims = Claims { - sub: "b@b.com".to_string(), - company: "ACME".to_string(), - exp: Utc::now().timestamp() + 10000, - }; - let token = encode(&Header::default(), &my_claims, Key::Hmac(b"secret")).unwrap(); - let token_data = - decode::(&token, Key::Hmac(b"secret"), &Validation::default()).unwrap(); - assert_eq!(my_claims, token_data.claims); - assert!(token_data.header.kid.is_none()); -} - -#[test] -fn decode_token() { - let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.9r56oF7ZliOBlOAyiOFperTGxBtPykRQiWNFxhDCW98"; - let claims = decode::(token, Key::Hmac(b"secret"), &Validation::default()); - println!("{:?}", claims); - claims.unwrap(); -} - -#[test] -#[should_panic(expected = "InvalidToken")] -fn decode_token_missing_parts() { - let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; - let claims = decode::(token, Key::Hmac(b"secret"), &Validation::default()); - claims.unwrap(); -} - -#[test] -#[should_panic(expected = "InvalidSignature")] -fn decode_token_invalid_signature() { - let token = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.wrong"; - let claims = decode::(token, Key::Hmac(b"secret"), &Validation::default()); - claims.unwrap(); -} - -#[test] -#[should_panic(expected = "InvalidAlgorithm")] -fn decode_token_wrong_algorithm() { - let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.I1BvFoHe94AFf09O6tDbcSB8-jp8w6xZqmyHIwPeSdY"; - let claims = decode::(token, Key::Hmac(b"secret"), &Validation::new(Algorithm::RS512)); - claims.unwrap(); -} - -#[test] -fn decode_token_with_bytes_secret() { - let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.Hm0yvKH25TavFPz7J_coST9lZFYH1hQo0tvhvImmaks"; - let claims = decode::(token, Key::Hmac(b"\x01\x02\x03"), &Validation::default()); - assert!(claims.is_ok()); -} - -#[test] -fn decode_header_only() { - let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjb21wYW55IjoiMTIzNDU2Nzg5MCIsInN1YiI6IkpvaG4gRG9lIn0.S"; - let header = decode_header(token).unwrap(); - assert_eq!(header.alg, Algorithm::HS256); - assert_eq!(header.typ, Some("JWT".to_string())); -} - -#[test] -fn dangerous_unsafe_decode_token() { - let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.9r56oF7ZliOBlOAyiOFperTGxBtPykRQiWNFxhDCW98"; - let claims = dangerous_unsafe_decode::(token); - claims.unwrap(); -} - -#[test] -#[should_panic(expected = "InvalidToken")] -fn dangerous_unsafe_decode_token_missing_parts() { - let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; - let claims = dangerous_unsafe_decode::(token); - claims.unwrap(); -} - -#[test] -fn dangerous_unsafe_decode_token_invalid_signature() { - let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.wrong"; - let claims = dangerous_unsafe_decode::(token); - claims.unwrap(); -} - -#[test] -fn dangerous_unsafe_decode_token_wrong_algorithm() { - let token = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.fLxey-hxAKX5rNHHIx1_Ch0KmrbiuoakDVbsJjLWrx8fbjKjrPuWMYEJzTU3SBnYgnZokC-wqSdqckXUOunC-g"; - let claims = dangerous_unsafe_decode::(token); - claims.unwrap(); -} - -// https://github.com/Keats/jsonwebtoken/issues/51 -#[test] -fn does_validation_in_right_order() { - let my_claims = Claims { - sub: "b@b.com".to_string(), - company: "ACME".to_string(), - exp: Utc::now().timestamp() + 10000, - }; - let token = encode(&Header::default(), &my_claims, Key::Hmac(b"secret")).unwrap(); - let v = Validation { - leeway: 5, - validate_exp: true, - iss: Some("iss no check".to_string()), - sub: Some("sub no check".to_string()), - ..Validation::default() - }; - let res = decode::(&token, Key::Hmac(b"secret"), &v); - assert!(res.is_err()); - println!("{:?}", res); - //assert!(res.is_ok()); -} - -#[test] -fn generate_algorithm_enum_from_str() { - assert!(Algorithm::from_str("HS256").is_ok()); - assert!(Algorithm::from_str("HS384").is_ok()); - assert!(Algorithm::from_str("HS512").is_ok()); - assert!(Algorithm::from_str("RS256").is_ok()); - assert!(Algorithm::from_str("RS384").is_ok()); - assert!(Algorithm::from_str("RS512").is_ok()); - assert!(Algorithm::from_str("PS256").is_ok()); - assert!(Algorithm::from_str("PS384").is_ok()); - assert!(Algorithm::from_str("PS512").is_ok()); - assert!(Algorithm::from_str("").is_err()); -} +mod ecdsa; +mod rsa; diff --git a/tests/rsa.rs b/tests/rsa.rs deleted file mode 100644 index 3a32399..0000000 --- a/tests/rsa.rs +++ /dev/null @@ -1,100 +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, Key, Validation}; - -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() { - let privkey = include_bytes!("private_rsa_key.der"); - for &alg in RSA_ALGORITHMS { - let encrypted = sign("hello world", Key::Der(&privkey[..]), alg).unwrap(); - let is_valid = - verify(&encrypted, "hello world", Key::Der(include_bytes!("public_rsa_key.der")), alg) - .unwrap(); - assert!(is_valid); - } -} - -#[test] -fn round_trip_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_rsa_key.der"); - - for &alg in RSA_ALGORITHMS { - let token = encode(&Header::new(alg), &my_claims, Key::Der(&privkey[..])).unwrap(); - let token_data = decode::( - &token, - Key::Der(include_bytes!("public_rsa_key.der")), - &Validation::new(alg), - ) - .unwrap(); - assert_eq!(my_claims, token_data.claims); - assert!(token_data.header.kid.is_none()); - } -} - -#[test] -#[should_panic(expected = "InvalidRsaKey")] -fn fails_with_different_key_format() { - let privkey = include_bytes!("private_rsa_key.der"); - sign("hello world", Key::Pkcs8(&privkey[..]), Algorithm::RS256).unwrap(); -} - -#[test] -fn rsa_modulus_exponent() { - let modulus: Vec = vec![ - 0xc9, 0x11, 0x3a, 0xac, 0x7b, 0x8d, 0x47, 0x44, 0x1b, 0x1c, 0xed, 0xc7, 0xdc, 0xab, 0x76, - 0xa4, 0xe2, 0x86, 0x56, 0x14, 0x2a, 0x19, 0x95, 0xc8, 0x9c, 0xe7, 0x6e, 0x40, 0xdc, 0x57, - 0xce, 0xe2, 0xa5, 0xbd, 0x04, 0xcb, 0x51, 0x3b, 0xf8, 0x97, 0x8b, 0x20, 0x82, 0x1e, 0x7f, - 0x09, 0x86, 0x22, 0xfd, 0xcb, 0xc8, 0xf9, 0x25, 0xd5, 0x4f, 0xd9, 0x0f, 0x59, 0x22, 0x97, - 0xc4, 0x95, 0xc1, 0x5d, 0xdf, 0xf8, 0x2e, 0x4b, 0xdc, 0x3e, 0xe5, 0x1a, 0x90, 0x1a, 0x00, - 0x91, 0xf8, 0x7e, 0x7a, 0x21, 0x55, 0x32, 0x1d, 0x95, 0xad, 0x4c, 0x96, 0xca, 0x3d, 0xcc, - 0x16, 0x5d, 0x07, 0x4d, 0x51, 0x7d, 0x2b, 0x04, 0x57, 0x2c, 0x07, 0x30, 0x91, 0x11, 0x22, - 0x4b, 0x79, 0xe9, 0x4e, 0x11, 0xd1, 0xc8, 0x8c, 0x6e, 0xcb, 0x46, 0x4c, 0x79, 0x97, 0xf1, - 0x54, 0xbe, 0x5a, 0xac, 0xc8, 0x70, 0xd5, 0x24, 0x44, 0x2c, 0x1f, 0x07, 0xa0, 0x67, 0xc6, - 0xfc, 0x0b, 0x47, 0xf3, 0xd0, 0x48, 0x13, 0xd8, 0xc3, 0x04, 0x76, 0x7d, 0x74, 0xb7, 0xa5, - 0x2b, 0xd6, 0xb5, 0xf3, 0x8c, 0xc0, 0x7f, 0xc2, 0xf0, 0xa0, 0xf2, 0xf1, 0xbc, 0x96, 0xf7, - 0x22, 0x5e, 0x67, 0x9d, 0xca, 0x8f, 0x71, 0x27, 0xca, 0x0c, 0x3a, 0x1d, 0x30, 0x50, 0x48, - 0x31, 0xce, 0x25, 0x43, 0x30, 0xca, 0x2f, 0x98, 0x2f, 0x9a, 0x25, 0xcb, 0x5c, 0x1d, 0x40, - 0x18, 0xb9, 0xbc, 0x28, 0x18, 0xdf, 0x13, 0xcb, 0x37, 0x2f, 0x9c, 0x6a, 0x8b, 0xec, 0x94, - 0xa1, 0xdf, 0xa3, 0xf0, 0xcb, 0x6f, 0x22, 0x3f, 0x35, 0xd9, 0xd9, 0x12, 0xe1, 0x03, 0x22, - 0x45, 0x53, 0x7f, 0x6f, 0x2d, 0xa1, 0xdd, 0x96, 0x3c, 0x2d, 0x85, 0x46, 0xae, 0xa6, 0x57, - 0x65, 0x37, 0x20, 0x9f, 0x6b, 0xa3, 0x9f, 0xcb, 0x8a, 0x8d, 0x72, 0xd9, 0x54, 0x3e, 0x53, - 0x75, - ]; - let exponent: Vec = vec![0x01, 0x00, 0x01]; - let privkey = include_bytes!("private_rsa_key.der"); - - let encrypted = sign("hello world", Key::Der(&privkey[..]), Algorithm::RS256).unwrap(); - let is_valid = verify( - &encrypted, - "hello world", - Key::ModulusExponent(&modulus, &exponent), - Algorithm::RS256, - ) - .unwrap(); - assert!(is_valid); -} diff --git a/tests/rsa/mod.rs b/tests/rsa/mod.rs new file mode 100644 index 0000000..9b3301e --- /dev/null +++ b/tests/rsa/mod.rs @@ -0,0 +1,110 @@ +use chrono::Utc; +use jsonwebtoken::{ + crypto::{sign, verify}, + decode, decode_rsa_components, encode, Algorithm, 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", privkey_pem, alg).unwrap(); + let is_valid = verify(&encrypted, "hello world", pubkey_pem, alg).unwrap(); + assert!(is_valid); + } +} + +#[test] +fn round_trip_sign_verification_pem_pkcs8() { + let privkey_pem = 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", privkey_pem, alg).unwrap(); + let is_valid = verify(&encrypted, "hello world", pubkey_pem, 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 = include_bytes!("private_rsa_key_pkcs1.pem"); + + for &alg in RSA_ALGORITHMS { + let token = encode(&Header::new(alg), &my_claims, privkey).unwrap(); + let token_data = decode::( + &token, + include_bytes!("public_rsa_key_pkcs1.pem"), + &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, privkey.as_ref()).unwrap(); + let res = decode_rsa_components::(&encrypted, n, e, &Validation::new(Algorithm::RS256)); + assert!(res.is_ok()); +} + +#[test] +#[should_panic(expected = "InvalidKeyFormat")] +fn fails_with_non_pkcs8_key_format() { + let _encrypted = + sign("hello world", include_bytes!("private_rsa_key_pkcs1.pem"), Algorithm::ES256).unwrap(); +} + +// https://jwt.io/ is often used for examples so ensure their example works with jsonwebtoken +#[test] +fn roundtrip_with_jwtio_example_jey() { + let privkey_pem = include_bytes!("private_jwtio.pem"); + let pubkey_pem = include_bytes!("public_jwtio.pem"); + + let my_claims = Claims { + sub: "b@b.com".to_string(), + company: "ACME".to_string(), + exp: Utc::now().timestamp() + 10000, + }; + + for &alg in RSA_ALGORITHMS { + let token = encode(&Header::new(alg), &my_claims, privkey_pem).unwrap(); + let token_data = decode::(&token, pubkey_pem, &Validation::new(alg)).unwrap(); + assert_eq!(my_claims, token_data.claims); + } +} diff --git a/tests/rsa/private_jwtio.pem b/tests/rsa/private_jwtio.pem new file mode 100644 index 0000000..61056a5 --- /dev/null +++ b/tests/rsa/private_jwtio.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw +kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr +m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi +NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV +3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2 +QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs +kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go +amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM ++bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9 +D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC +0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y +lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+ +hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp +bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X ++jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B +BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC +2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx +QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz +5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9 +Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0 +NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j +8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma +3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K +y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB +jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE= +-----END RSA PRIVATE KEY----- diff --git a/tests/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/rsa/public_rsa_key_pkcs1.pem b/tests/rsa/public_rsa_key_pkcs1.pem new file mode 100644 index 0000000..99e2a40 --- /dev/null +++ b/tests/rsa/public_rsa_key_pkcs1.pem @@ -0,0 +1,8 @@ +-----BEGIN RSA PUBLIC KEY----- +MIIBCgKCAQEAyRE6rHuNR0QbHO3H3Kt2pOKGVhQqGZXInOduQNxXzuKlvQTLUTv4 +l4sggh5/CYYi/cvI+SXVT9kPWSKXxJXBXd/4LkvcPuUakBoAkfh+eiFVMh2VrUyW +yj3MFl0HTVF9KwRXLAcwkREiS3npThHRyIxuy0ZMeZfxVL5arMhw1SRELB8HoGfG +/AtH89BIE9jDBHZ9dLelK9a184zAf8LwoPLxvJb3Il5nncqPcSfKDDodMFBIMc4l +QzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xqi+yUod+j8MtvIj812dkS4QMiRVN/by2h +3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5TdQIDAQAB +-----END RSA PUBLIC KEY----- diff --git a/tests/public_rsa_key.pem b/tests/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