├── .gitignore ├── test └── fixtures │ ├── rsa_public_key.der │ ├── ecdsa_private_key.p8 │ ├── ecdsa_public_key.der │ ├── rsa_private_key.der │ ├── rsa_public_key.pem │ ├── jwk_public_key.json │ ├── rsa_private_key.pem │ └── jwk_private_key.json ├── src ├── helpers │ ├── mod.rs │ ├── presence.rs │ ├── temporal_options.rs │ └── validation.rs ├── serde_custom │ ├── mod.rs │ ├── byte_sequence.rs │ ├── base64_url_uint.rs │ ├── option_byte_sequence.rs │ └── option_base64_url_uint.rs ├── macros.rs ├── test.rs ├── digest.rs ├── errors.rs ├── jws.rs └── jws │ ├── flattened.rs │ └── compact.rs ├── fuzz ├── .gitignore ├── README.md ├── corpus │ ├── fuzz_decryption │ │ └── valid_token │ └── fuzz_signature_validation │ │ └── valid_token ├── Cargo.toml └── fuzz_targets │ ├── fuzz_signature_validation.rs │ └── fuzz_decryption.rs ├── doc ├── README.md └── supported.md ├── LICENSE ├── Cargo.toml ├── README.md ├── .github └── workflows │ └── rust.yml ├── CHANGELOG.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .idea/ 3 | -------------------------------------------------------------------------------- /test/fixtures/rsa_public_key.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lawliet89/biscuit/HEAD/test/fixtures/rsa_public_key.der -------------------------------------------------------------------------------- /test/fixtures/ecdsa_private_key.p8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lawliet89/biscuit/HEAD/test/fixtures/ecdsa_private_key.p8 -------------------------------------------------------------------------------- /test/fixtures/ecdsa_public_key.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lawliet89/biscuit/HEAD/test/fixtures/ecdsa_public_key.der -------------------------------------------------------------------------------- /test/fixtures/rsa_private_key.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lawliet89/biscuit/HEAD/test/fixtures/rsa_private_key.der -------------------------------------------------------------------------------- /src/helpers/mod.rs: -------------------------------------------------------------------------------- 1 | mod presence; 2 | mod temporal_options; 3 | mod validation; 4 | 5 | pub use self::presence::*; 6 | pub use self::temporal_options::*; 7 | pub use self::validation::*; 8 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | corpus/**/* 3 | !corpus/fuzz_decryption/ 4 | !corpus/fuzz_decryption/valid_token 5 | !corpus/fuzz_signature_validation/ 6 | !corpus/fuzz_signature_validation/valid_token 7 | artifacts 8 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | For API documentation, refer to the links below. This directory serves auxiliary documentation that are linked from 4 | the API documentation. 5 | 6 | [stable](https://docs.rs/biscuit/) 7 | -------------------------------------------------------------------------------- /fuzz/README.md: -------------------------------------------------------------------------------- 1 | # fuzz 2 | 3 | ## Running the fuzz tests 4 | 5 | The fuzz tests can be run using `cargo fuzz` in nightly. 6 | 7 | ``` 8 | cargo install cargo-fuzz -f 9 | cargo fuzz list 10 | cargo +nightly fuzz run fuzz_parser 11 | ``` 12 | -------------------------------------------------------------------------------- /src/helpers/presence.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 2 | /// Defines whether a claim is required or not 3 | #[derive(Default)] 4 | pub enum Presence { 5 | /// Claim is optional 6 | #[default] 7 | Optional, 8 | /// Claim is required 9 | Required, 10 | } 11 | -------------------------------------------------------------------------------- /fuzz/corpus/fuzz_decryption/valid_token: -------------------------------------------------------------------------------- 1 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3d3dy5hY21lLmNvbS8iLCJzdWIiOiJKb2huIERvZSIsImF1ZCI6Imh0dHM6Ly9hY21lLWN1c3RvbWVyLmNvbS8iLCJuYmYiOjEyMzQsImNvbXBhbnkiOiJBQ01FIiwiZGVwYXJ0bWVudCI6IlRvaWxldCBDbGVhbmluZyJ9.dnx1OmRZSFxjCD1ivy4lveTT-sxay5Fq6vY6jnJvqeI -------------------------------------------------------------------------------- /fuzz/corpus/fuzz_signature_validation/valid_token: -------------------------------------------------------------------------------- 1 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3d3dy5hY21lLmNvbS8iLCJzdWIiOiJKb2huIERvZSIsImF1ZCI6Imh0dHM6Ly9hY21lLWN1c3RvbWVyLmNvbS8iLCJuYmYiOjEyMzQsImNvbXBhbnkiOiJBQ01FIiwiZGVwYXJ0bWVudCI6IlRvaWxldCBDbGVhbmluZyJ9.dnx1OmRZSFxjCD1ivy4lveTT-sxay5Fq6vY6jnJvqeI -------------------------------------------------------------------------------- /src/serde_custom/mod.rs: -------------------------------------------------------------------------------- 1 | //! Custom serialization and deserialization modules 2 | //! 3 | //! In general, users should not need to invoke these manually. These are exposed for potential use 4 | //! in your applications, should you wish to make extensions to the implementations provided. 5 | pub mod base64_url_uint; 6 | pub mod byte_sequence; 7 | pub mod option_base64_url_uint; 8 | pub mod option_byte_sequence; 9 | -------------------------------------------------------------------------------- /test/fixtures/rsa_public_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PUBLIC KEY----- 2 | MIIBCgKCAQEA1XM2Qy7bkaCpjjvClZwI15AXy9966m7c3sYR2nRuHb0UT7Q5EWPn 3 | l/s5LEOMxwrqiXltj8/2lkZlWtAuABabXxxMkVDTOZ2A3ObY9vBXsQX1/F7ndLCo 4 | /yCmfYDmcH04BGLSzcuRPm7p6nWFzVBK5FtpMLxxPQKZnja/RJz/ojhTdPOFCBkF 5 | YgeICi6LpH7oqGy+TDYdbVS5Xy4WaCYvecJ3TUI0uNXG0VoOlUk+MIqpjwAqeLuS 6 | jLePHn4GJDq21+r6tZ9kRndLBHn2WT+I92OXjxTvt/QitMZujrU/9ebcTTySlS2E 7 | E+BuLZ6x31DYIk/zvTGf9eQljQbFeLlSewIDAQAB 8 | -----END RSA PUBLIC KEY----- 9 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! unexpected_key_type_error { 2 | ($expected:path, $actual:expr) => { 3 | Error::WrongKeyType { 4 | actual: $actual.to_string(), 5 | expected: $expected.to_string(), 6 | } 7 | }; 8 | } 9 | 10 | macro_rules! unexpected_encryption_options_error { 11 | ($expected:expr, $actual:expr) => { 12 | Error::WrongEncryptionOptions { 13 | actual: $actual.to_string(), 14 | expected: $expected.to_string(), 15 | } 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "biscuit-fuzz" 3 | version = "0.0.1" 4 | authors = ["Automatically generated"] 5 | publish = false 6 | 7 | [package.metadata] 8 | cargo-fuzz = true 9 | 10 | [dependencies] 11 | serde_json = "1.0" 12 | [dependencies.biscuit] 13 | path = ".." 14 | [dependencies.libfuzzer-sys] 15 | git = "https://github.com/rust-fuzz/libfuzzer-sys.git" 16 | 17 | # Prevent this from interfering with workspaces 18 | [workspace] 19 | members = ["."] 20 | 21 | [[bin]] 22 | name = "fuzz_signature_validation" 23 | path = "fuzz_targets/fuzz_signature_validation.rs" 24 | 25 | [[bin]] 26 | name = "fuzz_decryption" 27 | path = "fuzz_targets/fuzz_decryption.rs" 28 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/fuzz_signature_validation.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #[macro_use] 3 | extern crate libfuzzer_sys; 4 | extern crate biscuit; 5 | extern crate serde_json; 6 | 7 | use biscuit::*; 8 | use biscuit::jws::*; 9 | use biscuit::jwa::*; 10 | 11 | fuzz_target!(|data: &[u8]| { 12 | let signing_secret = Secret::Bytes("secret".to_string().into_bytes()); 13 | 14 | let expected_token = std::str::from_utf8(data); 15 | if expected_token.is_err() { 16 | return; 17 | } 18 | let expected_token = expected_token.unwrap(); 19 | 20 | let token = JWT::::new_encoded(&expected_token); 21 | let _ = token.into_decoded(&signing_secret, SignatureAlgorithm::HS256); 22 | }); 23 | -------------------------------------------------------------------------------- /test/fixtures/jwk_public_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty": "EC", 5 | "crv": "P-256", 6 | "x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", 7 | "y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", 8 | "use": "enc", 9 | "kid": "1" 10 | }, 11 | { 12 | "kty": "RSA", 13 | "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", 14 | "e": "AQAB", 15 | "alg": "RS256", 16 | "kid": "2011-04-29" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/fuzz_decryption.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #[macro_use] 3 | extern crate libfuzzer_sys; 4 | extern crate biscuit; 5 | extern crate serde_json; 6 | 7 | use biscuit::{Empty, JWE}; 8 | use biscuit::jwk::JWK; 9 | use biscuit::jwa::{KeyManagementAlgorithm, ContentEncryptionAlgorithm}; 10 | 11 | fuzz_target!(|data: &[u8]| { 12 | let key: JWK = JWK::new_octet_key(&vec![0; 256 / 8], Default::default()); 13 | 14 | let token = std::str::from_utf8(data); 15 | if token.is_err() { 16 | return; 17 | } 18 | let token = token.unwrap(); 19 | 20 | let token: JWE = JWE::new_encrypted(&token); 21 | 22 | let _ = token.into_decrypted( 23 | &key, 24 | KeyManagementAlgorithm::A256GCMKW, 25 | ContentEncryptionAlgorithm::A256GCM, 26 | ); 27 | }); 28 | -------------------------------------------------------------------------------- /src/helpers/temporal_options.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Duration, Utc}; 2 | 3 | #[derive(Debug, Eq, PartialEq, Clone, Copy)] 4 | /// Options for validating temporal claims 5 | /// 6 | /// 7 | /// To deal with clock drifts, you might want to provide an `epsilon` error margin in the form of a 8 | /// `chrono::Duration` to allow time comparisons to fall within the margin. 9 | pub struct TemporalOptions { 10 | /// Allow for some leeway for clock drifts, limited to this duration during temporal validation 11 | pub epsilon: Duration, 12 | 13 | /// Specify a time to use in temporal validation instead of `Now` 14 | pub now: Option>, 15 | } 16 | 17 | impl Default for TemporalOptions { 18 | fn default() -> Self { 19 | TemporalOptions { 20 | epsilon: Duration::seconds(0), 21 | now: None, 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/helpers/validation.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Eq, PartialEq, Clone, Copy)] 2 | /// Defines whether a claim is validated or not 3 | /// 4 | /// The generic type T is used as the "options" for validating claims and is 5 | /// specific to each claim being validated. Refer to [`crate::ValidationOptions`] 6 | /// for the specifics of each claim. 7 | #[derive(Default)] 8 | pub enum Validation { 9 | /// This claim is not validated 10 | #[default] 11 | Ignored, 12 | 13 | /// Validate this claim with type T. 14 | /// Refer to [`crate::ValidationOptions`] for the specifics of each claim. 15 | Validate(T), 16 | } 17 | 18 | impl Validation { 19 | /// Map the value to another validation requirement, similar to how .map works on iter() 20 | pub fn map(self, f: F) -> Validation 21 | where 22 | F: FnOnce(T) -> U, 23 | { 24 | match self { 25 | Validation::Ignored => Validation::Ignored, 26 | Validation::Validate(t) => Validation::Validate(f(t)), 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test.rs: -------------------------------------------------------------------------------- 1 | use serde::de::DeserializeOwned; 2 | use serde::Serialize; 3 | 4 | use std::fmt::Debug; 5 | 6 | macro_rules! not_err { 7 | ($e:expr) => { 8 | match $e { 9 | Ok(e) => e, 10 | Err(e) => panic!("{} failed with {}", stringify!($e), e), 11 | } 12 | }; 13 | } 14 | 15 | /// Tests that `value` can be serialized to JSON, and then back to type `T` and that the deserialized type `T` 16 | /// is equal to the provided `value`. 17 | /// If `expected_json` is provided, it will be deserialized to `T` and checked for equality with `value`. 18 | pub fn assert_serde_json(value: &T, expected_json: Option<&str>) 19 | where 20 | T: Serialize + DeserializeOwned + Debug + PartialEq, 21 | { 22 | let serialized = not_err!(serde_json::to_string_pretty(value)); 23 | let deserialized: T = not_err!(serde_json::from_str(&serialized)); 24 | assert_eq!(value, &deserialized); 25 | 26 | if let Some(expected_json) = expected_json { 27 | let deserialized: T = not_err!(serde_json::from_str(expected_json)); 28 | assert_eq!(value, &deserialized); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/digest.rs: -------------------------------------------------------------------------------- 1 | //! Secure cryptographic digests 2 | //! 3 | //! Currently used by JWK thumbprints. 4 | //! This simply wraps the ring::digest module, while providing forward compatibility 5 | //! should the implementation change. 6 | 7 | /// A digest algorithm 8 | pub struct Algorithm(pub(crate) &'static ring::digest::Algorithm); 9 | 10 | // SHA-1 as specified in FIPS 180-4. Deprecated. 11 | // SHA-1 is not exposed at the moment, as the only user is JWK thumbprints, 12 | // which postdate SHA-1 deprecation and don't have a backwards-compatibility reason to use it. 13 | //pub static SHA1_FOR_LEGACY_USE_ONLY: Algorithm = Algorithm(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY); 14 | 15 | /// SHA-256 as specified in FIPS 180-4. 16 | pub static SHA256: Algorithm = Algorithm(&ring::digest::SHA256); 17 | 18 | /// SHA-384 as specified in FIPS 180-4. 19 | pub static SHA384: Algorithm = Algorithm(&ring::digest::SHA384); 20 | 21 | /// SHA-512 as specified in FIPS 180-4. 22 | pub static SHA512: Algorithm = Algorithm(&ring::digest::SHA512); 23 | 24 | /// SHA-512/256 as specified in FIPS 180-4. 25 | pub static SHA512_256: Algorithm = Algorithm(&ring::digest::SHA512_256); 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright for portions of this project are held by Vincent Prouillet, 2015 as part of 4 | `Keats/rust-jwt`. All other copyright for project Foo are held by Yong Wen Chua, 2017. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "biscuit" 3 | version = "0.7.0" 4 | edition = "2021" 5 | authors = ["Yong Wen Chua ", "Vincent Prouillet "] 6 | license = "MIT" 7 | readme = "README.md" 8 | description = """A library to work with Javascript Object Signing and Encryption(JOSE), 9 | including JSON Web Tokens (JWT), JSON Web Signature (JWS) and JSON Web Encryption (JWE).""" 10 | homepage = "https://github.com/lawliet89/biscuit" 11 | repository = "https://github.com/lawliet89/biscuit" 12 | documentation = "https://docs.rs/biscuit/" 13 | keywords = ["jwt", "json", "jose", "jwe", "jws"] 14 | categories = ["authentication", "web-programming"] 15 | 16 | [badges] 17 | travis-ci = { repository = "lawliet89/biscuit" } 18 | 19 | [lib] 20 | name = "biscuit" 21 | path = "src/lib.rs" 22 | test = true 23 | doctest = true 24 | doc = true 25 | 26 | [dependencies] 27 | chrono = { version = "0.4.20", default-features = false, features = ["clock"] } 28 | data-encoding = "2.3.2" 29 | once_cell = "1.16.0" 30 | num-bigint = "0.4" 31 | num-traits = "0.2" 32 | ring = "~0.17.5" 33 | serde = { version = "1.0.147", features=["derive"] } 34 | serde_json = { version = "1.0", features = ["preserve_order"] } 35 | 36 | [dev-dependencies] 37 | serde_test = "1.0" 38 | 39 | [features] 40 | # Treat warnings as a build error. 41 | strict = [] 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # biscuit 2 | 3 | [![Build Status](https://travis-ci.org/lawliet89/biscuit.svg)](https://travis-ci.org/lawliet89/biscuit) 4 | [![Crates.io](https://img.shields.io/crates/v/biscuit.svg)](https://crates.io/crates/biscuit) 5 | [![Repository](https://img.shields.io/github/tag/lawliet89/biscuit.svg)](https://github.com/lawliet89/biscuit) 6 | [![Documentation](https://docs.rs/biscuit/badge.svg)](https://docs.rs/biscuit) 7 | [![dependency status](https://deps.rs/repo/github/lawliet89/biscuit/status.svg)](https://deps.rs/repo/github/lawliet89/biscuit) 8 | 9 | - Documentation: [stable](https://docs.rs/biscuit/) 10 | - Changelog: [Link](https://github.com/lawliet89/biscuit/blob/master/CHANGELOG.md) 11 | 12 | A library to work with Javascript Object Signing and Encryption(JOSE), 13 | including JSON Web Tokens (JWT), JSON Web Signature (JWS) and JSON Web Encryption (JWE) 14 | 15 | This was based off [`Keats/rust-jwt`](https://github.com/Keats/rust-jwt). 16 | 17 | ## Installation 18 | 19 | Add the following to Cargo.toml: 20 | 21 | ```toml 22 | biscuit = "0.7.0" 23 | ``` 24 | 25 | To use the latest `master` branch, for example: 26 | 27 | ```toml 28 | biscuit = { git = "https://github.com/lawliet89/biscuit", branch = "master" } 29 | ``` 30 | 31 | ## Supported Features 32 | 33 | The crate, does not support all, and probably will never support all of 34 | the features described in the various RFCs, including some algorithms and verification. 35 | 36 | See the [documentation](https://github.com/lawliet89/biscuit/blob/master/doc/supported.md) for more information. 37 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: {} 6 | 7 | name: Continuous integration 8 | 9 | jobs: 10 | ci: 11 | strategy: 12 | matrix: 13 | rust: 14 | - stable 15 | - beta 16 | - nightly 17 | - 1.62.0 # MSRV 18 | os: 19 | - ubuntu-latest 20 | - windows-latest 21 | - macOS-latest 22 | 23 | runs-on: ${{ matrix.os }} 24 | steps: 25 | - uses: actions/checkout@v3 26 | name: Checkout 27 | 28 | - uses: dtolnay/rust-toolchain@master 29 | name: Verify Rust Toolchain 30 | with: 31 | toolchain: ${{ matrix.rust }} 32 | components: rustfmt, clippy 33 | 34 | - uses: Swatinem/rust-cache@v2 35 | name: Rust Cache 36 | 37 | - run: cargo fmt --all -- --check 38 | name: Format Check 39 | if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' 40 | 41 | - run: cargo clippy --all-targets --all-features -- -D warnings 42 | name: Clippy Lint (Stable) 43 | if: matrix.os == 'ubuntu-latest' && matrix.rust == 'stable' 44 | 45 | - run: cargo clippy --all-targets --all-features -- -D warnings -A "clippy::upper_case_acronyms" 46 | name: Clippy Lint (Non-stable) 47 | if: matrix.os == 'ubuntu-latest' && (matrix.rust == 'beta' || matrix.rust == 'nightly') 48 | 49 | - run: cargo build --verbose --all-features 50 | name: Build 51 | 52 | - run: cargo test --all-features 53 | name: Unit Tests 54 | 55 | - run: cargo doc --no-deps --all-features 56 | name: Build Documentation 57 | -------------------------------------------------------------------------------- /test/fixtures/rsa_private_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA1XM2Qy7bkaCpjjvClZwI15AXy9966m7c3sYR2nRuHb0UT7Q5 3 | EWPnl/s5LEOMxwrqiXltj8/2lkZlWtAuABabXxxMkVDTOZ2A3ObY9vBXsQX1/F7n 4 | dLCo/yCmfYDmcH04BGLSzcuRPm7p6nWFzVBK5FtpMLxxPQKZnja/RJz/ojhTdPOF 5 | CBkFYgeICi6LpH7oqGy+TDYdbVS5Xy4WaCYvecJ3TUI0uNXG0VoOlUk+MIqpjwAq 6 | eLuSjLePHn4GJDq21+r6tZ9kRndLBHn2WT+I92OXjxTvt/QitMZujrU/9ebcTTyS 7 | lS2EE+BuLZ6x31DYIk/zvTGf9eQljQbFeLlSewIDAQABAoIBACr9BC2trzz6HYvu 8 | zzawcTtw4soFnUy/vS4EuC3GCzNkFEYlJuUwuMDsMMyQYjboJOpBEWbIXIJRdTJA 9 | ATO1Wd9i5KzTmWbeKMjUmVfKee7GI4+LUZQ3zCFt4vodzstS/MgtWwVlfUAUuHmm 10 | 56a9CAhLvLi7CxddgbDSl9zqvbVewk5r/QHlHvErLeX1IkoK+qpFpCAq0hjGv1mB 11 | SfuGa6nNubgX9xCskqDXwPp2mCvuMVFhtt4F9fcGgMAuS3NrHc6aPV3Aej2URdax 12 | SM30xUCaCEtYWe+VQqwNGZiA0r4IXQP6byn3IYwnm3JAGkxLg7pBqEdBxZsZCvlr 13 | gVQLPNkCgYEA+5fOSRIT7OQ2PeDXb0frvfkJ7goLDEmrXa0jH2xXD0ZA5Yuf/I7I 14 | 53lphSnYDaMXEhUFpxc/JnxFVCDkEpCWDC/nRoo6zcGBPRF4YiSfYPujeJisf8im 15 | TaGDIruHNYws1M9pir0dYkbMr++MkYCWGb7U1KYVjtL4hYbQj9AHUH0CgYEA2TBd 16 | jNeWBXV+mxOT+ALdq5AOdIWba1/jJ3B5Vaf+Y4SwEcUDvIaI3IXrL+faQvzGLCuB 17 | DehsV/OTpy8xziYvFlhWVC6JuvZWNXVXRxC7oqvp161VOveZ+UfKQZx6BvGqw57O 18 | pP9LUR7OboK9QsoHTASwKFL7d0ZwshKJtjN6WFcCgYEA5gx/9jaOe5yMsHXn53v2 19 | 5gVSfBM42Op/xC8tH218CZ5udrX9+vxAXc+ZmcaSJJ4M2V7ZhVhvSOx2pB9TDFqi 20 | qNAghFKexEb8um9ACVV9Wjud1QadLFa3IeLeOqMIVgEveQOSeObFeHhOb0z11pGi 21 | LjZc+3hF3AuybL3B9M398i0CgYA9WTwTnJHz2Mx6YX1agPS8kWSD5XmRSvSPH2Ym 22 | m91vnvtdCZmUlyvxnqJgVc2BPoV71I4Pr6dq8JK0ltAquv5yAWHhRYQCG7MeRpbw 23 | q/lUadsT4RJCJc6Ia47mGZ0eeQUTXLhiQvqzX1BQRv3O7+I/xtM7kLUXa/5JTpM3 24 | tDLK4QKBgQC0q8at/rM1erbV97ujxqRzxLBFklM/Pv51lKy2ebewkHkjRUeimRXI 25 | q6yfNkHBFW/oPnR1h0rI7AsvjykVMDt4NmVQZZroPhzSN5DKgZmPGaDxgZkRiPEy 26 | +6tbSTlfl8hk3aljveIwwzTwveR26+Gy/DayW4rNN2jEUDVvc1bv5A== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/fixtures/jwk_private_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty": "EC", 5 | "crv": "P-256", 6 | "x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", 7 | "y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", 8 | "d": "870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE", 9 | "use": "enc", 10 | "kid": "1" 11 | }, 12 | { 13 | "kty": "RSA", 14 | "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", 15 | "e": "AQAB", 16 | "d": "X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqijwp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBznbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFzme1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q", 17 | "p": "83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPVnwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqVWlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs", 18 | "q": "3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyumqjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgxkIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk", 19 | "dp": "G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oimYwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_NmtuYZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0", 20 | "dq": "s9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUUvMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk", 21 | "qi": "GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzgUIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rxyR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU", 22 | "alg": "RS256", 23 | "kid": "2011-04-29" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/serde_custom/byte_sequence.rs: -------------------------------------------------------------------------------- 1 | //! Serialize a sequence of bytes as base64 URL encoding vice-versa for deserialization 2 | use std::fmt; 3 | 4 | use data_encoding::BASE64URL_NOPAD; 5 | use serde::de; 6 | use serde::{Deserializer, Serializer}; 7 | 8 | /// Serialize a byte sequence into Base64 URL encoded string 9 | pub fn serialize(value: &[u8], serializer: S) -> Result 10 | where 11 | S: Serializer, 12 | { 13 | let base64 = BASE64URL_NOPAD.encode(value); 14 | serializer.serialize_str(&base64) 15 | } 16 | 17 | /// Deserialize a byte sequence from Base64 URL encoded string 18 | pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 19 | where 20 | D: Deserializer<'de>, 21 | { 22 | struct BytesVisitor; 23 | 24 | impl<'de> de::Visitor<'de> for BytesVisitor { 25 | type Value = Vec; 26 | 27 | fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 28 | formatter.write_str("a URL safe base64 encoding of a byte sequence") 29 | } 30 | 31 | fn visit_str(self, value: &str) -> Result 32 | where 33 | E: de::Error, 34 | { 35 | let bytes = BASE64URL_NOPAD 36 | .decode(value.as_bytes()) 37 | .map_err(E::custom)?; 38 | Ok(bytes) 39 | } 40 | } 41 | 42 | deserializer.deserialize_str(BytesVisitor) 43 | } 44 | 45 | pub struct Wrapper<'a>(&'a [u8]); 46 | 47 | pub fn wrap(data: &[u8]) -> Wrapper { 48 | Wrapper(data) 49 | } 50 | 51 | impl<'a> serde::Serialize for Wrapper<'a> { 52 | fn serialize(&self, serializer: S) -> Result 53 | where 54 | S: Serializer, 55 | { 56 | serialize(self.0, serializer) 57 | } 58 | } 59 | 60 | #[cfg(test)] 61 | mod tests { 62 | use serde::{Deserialize, Serialize}; 63 | use serde_test::{assert_tokens, Token}; 64 | 65 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] 66 | struct TestStruct { 67 | #[serde(with = "super")] 68 | bytes: Vec, 69 | } 70 | 71 | #[test] 72 | fn serialization_round_trip() { 73 | let test_value = TestStruct { 74 | bytes: "hello world".to_string().into_bytes(), 75 | }; 76 | 77 | assert_tokens( 78 | &test_value, 79 | &[ 80 | Token::Struct { 81 | name: "TestStruct", 82 | len: 1, 83 | }, 84 | Token::Str("bytes"), 85 | Token::Str("aGVsbG8gd29ybGQ"), 86 | Token::StructEnd, 87 | ], 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/serde_custom/base64_url_uint.rs: -------------------------------------------------------------------------------- 1 | //! Serialize and Deserialize `num_bigint::BigUint` into `Base64urlUInt` form as described in 2 | //! [RFC 7518](https://tools.ietf.org/html/rfc7518). 3 | //! The integers are first converted into bytes in big-endian form and then base64 encoded. 4 | use std::fmt; 5 | 6 | use data_encoding::BASE64URL_NOPAD; 7 | use num_bigint::BigUint; 8 | use serde::de; 9 | use serde::{Deserializer, Serializer}; 10 | 11 | /// Serialize a `BigUint` into Base64 URL encoded big endian bytes 12 | pub fn serialize(value: &BigUint, serializer: S) -> Result 13 | where 14 | S: Serializer, 15 | { 16 | let bytes = value.to_bytes_be(); 17 | let base64 = BASE64URL_NOPAD.encode(bytes.as_slice()); 18 | serializer.serialize_str(&base64) 19 | } 20 | 21 | /// Deserialize a `BigUint` from Base64 URL encoded big endian bytes 22 | pub fn deserialize<'de, D>(deserializer: D) -> Result 23 | where 24 | D: Deserializer<'de>, 25 | { 26 | struct BigUintVisitor; 27 | 28 | impl<'de> de::Visitor<'de> for BigUintVisitor { 29 | type Value = BigUint; 30 | 31 | fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 32 | formatter.write_str("a Base64urlUInt string") 33 | } 34 | 35 | fn visit_str(self, value: &str) -> Result 36 | where 37 | E: de::Error, 38 | { 39 | let bytes = BASE64URL_NOPAD 40 | .decode(value.as_bytes()) 41 | .map_err(E::custom)?; 42 | Ok(BigUint::from_bytes_be(&bytes)) 43 | } 44 | } 45 | 46 | deserializer.deserialize_str(BigUintVisitor) 47 | } 48 | 49 | pub struct Wrapper<'a>(&'a BigUint); 50 | 51 | pub fn wrap(data: &BigUint) -> Wrapper { 52 | Wrapper(data) 53 | } 54 | 55 | impl<'a> serde::Serialize for Wrapper<'a> { 56 | fn serialize(&self, serializer: S) -> Result 57 | where 58 | S: Serializer, 59 | { 60 | serialize(self.0, serializer) 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use num_bigint::BigUint; 67 | use num_traits::cast::FromPrimitive; 68 | use serde::{Deserialize, Serialize}; 69 | use serde_test::{assert_tokens, Token}; 70 | 71 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] 72 | struct TestStruct { 73 | #[serde(with = "super")] 74 | bytes: BigUint, 75 | } 76 | 77 | #[test] 78 | fn serialization_round_trip() { 79 | let test_value = TestStruct { 80 | bytes: BigUint::from_u64(12345).unwrap(), 81 | }; 82 | 83 | assert_tokens( 84 | &test_value, 85 | &[ 86 | Token::Struct { 87 | name: "TestStruct", 88 | len: 1, 89 | }, 90 | Token::Str("bytes"), 91 | Token::Str("MDk"), 92 | Token::StructEnd, 93 | ], 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/serde_custom/option_byte_sequence.rs: -------------------------------------------------------------------------------- 1 | //! Serialize or deserialize an `Option>` 2 | use std::fmt; 3 | 4 | use data_encoding::BASE64URL_NOPAD; 5 | use serde::de; 6 | use serde::{Deserializer, Serializer}; 7 | 8 | /// Serialize a byte sequence into Base64 URL encoded string 9 | pub fn serialize(value: &Option>, serializer: S) -> Result 10 | where 11 | S: Serializer, 12 | { 13 | match *value { 14 | Some(ref value) => { 15 | let base64 = BASE64URL_NOPAD.encode(value.as_slice()); 16 | serializer.serialize_some(&base64) 17 | } 18 | None => serializer.serialize_none(), 19 | } 20 | } 21 | 22 | /// Deserialize a byte sequence from Base64 URL encoded string 23 | pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> 24 | where 25 | D: Deserializer<'de>, 26 | { 27 | struct BytesVisitor; 28 | 29 | impl<'de> de::Visitor<'de> for BytesVisitor { 30 | type Value = Option>; 31 | 32 | fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 33 | formatter.write_str("a URL safe base64 encoding of a byte sequence") 34 | } 35 | 36 | fn visit_none(self) -> Result 37 | where 38 | E: de::Error, 39 | { 40 | Ok(None) 41 | } 42 | 43 | fn visit_some(self, deserializer: D) -> Result 44 | where 45 | D: Deserializer<'de>, 46 | { 47 | deserializer.deserialize_str(self) 48 | } 49 | 50 | fn visit_str(self, value: &str) -> Result 51 | where 52 | E: de::Error, 53 | { 54 | let bytes = BASE64URL_NOPAD 55 | .decode(value.as_bytes()) 56 | .map_err(E::custom)?; 57 | Ok(Some(bytes)) 58 | } 59 | } 60 | 61 | deserializer.deserialize_option(BytesVisitor) 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use serde::{Deserialize, Serialize}; 67 | 68 | use serde_test::{assert_tokens, Token}; 69 | 70 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] 71 | struct TestStruct { 72 | #[serde(with = "super")] 73 | bytes: Option>, 74 | } 75 | 76 | #[test] 77 | fn some_serialization_round_trip() { 78 | let test_value = TestStruct { 79 | bytes: Some("hello world".to_string().into_bytes()), 80 | }; 81 | 82 | assert_tokens( 83 | &test_value, 84 | &[ 85 | Token::Struct { 86 | name: "TestStruct", 87 | len: 1, 88 | }, 89 | Token::Str("bytes"), 90 | Token::Some, 91 | Token::Str("aGVsbG8gd29ybGQ"), 92 | Token::StructEnd, 93 | ], 94 | ); 95 | } 96 | 97 | #[test] 98 | fn none_serialization_round_trip() { 99 | let test_value = TestStruct { bytes: None }; 100 | 101 | assert_tokens( 102 | &test_value, 103 | &[ 104 | Token::Struct { 105 | name: "TestStruct", 106 | len: 1, 107 | }, 108 | Token::Str("bytes"), 109 | Token::None, 110 | Token::StructEnd, 111 | ], 112 | ); 113 | } 114 | 115 | #[test] 116 | fn some_json_serialization_round_trip() { 117 | let test_value = TestStruct { 118 | bytes: Some("hello world".to_string().into_bytes()), 119 | }; 120 | let expected_json = r#"{"bytes":"aGVsbG8gd29ybGQ"}"#; 121 | 122 | let actual_json = not_err!(serde_json::to_string(&test_value)); 123 | assert_eq!(expected_json, actual_json); 124 | 125 | let deserialized_value: TestStruct = not_err!(serde_json::from_str(&actual_json)); 126 | assert_eq!(test_value, deserialized_value); 127 | } 128 | 129 | #[test] 130 | fn none_json_serialization_round_trip() { 131 | let test_value = TestStruct { bytes: None }; 132 | let expected_json = r#"{"bytes":null}"#; 133 | 134 | let actual_json = not_err!(serde_json::to_string(&test_value)); 135 | assert_eq!(expected_json, actual_json); 136 | 137 | let deserialized_value: TestStruct = not_err!(serde_json::from_str(&actual_json)); 138 | assert_eq!(test_value, deserialized_value); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/serde_custom/option_base64_url_uint.rs: -------------------------------------------------------------------------------- 1 | //! Serialize and Deserialize `num_bigint::BigUint` into `Base64urlUInt` form as described in 2 | //! [RFC 7518](https://tools.ietf.org/html/rfc7518). 3 | //! The integers are first converted into bytes in big-endian form and then base64 encoded. 4 | use std::fmt; 5 | 6 | use data_encoding::BASE64URL_NOPAD; 7 | use num_bigint::BigUint; 8 | use serde::de; 9 | use serde::{Deserializer, Serializer}; 10 | 11 | /// Serialize a `BigUint` into Base64 URL encoded big endian bytes 12 | pub fn serialize(value: &Option, serializer: S) -> Result 13 | where 14 | S: Serializer, 15 | { 16 | match *value { 17 | Some(ref value) => { 18 | let bytes = value.to_bytes_be(); 19 | let base64 = BASE64URL_NOPAD.encode(bytes.as_slice()); 20 | serializer.serialize_some(&base64) 21 | } 22 | None => serializer.serialize_none(), 23 | } 24 | } 25 | 26 | /// Deserialize a `BigUint` from Base64 URL encoded big endian bytes 27 | pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 28 | where 29 | D: Deserializer<'de>, 30 | { 31 | struct BigUintVisitor; 32 | 33 | impl<'de> de::Visitor<'de> for BigUintVisitor { 34 | type Value = Option; 35 | 36 | fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 37 | formatter.write_str("a Base64urlUInt string") 38 | } 39 | 40 | fn visit_none(self) -> Result 41 | where 42 | E: de::Error, 43 | { 44 | Ok(None) 45 | } 46 | 47 | fn visit_some(self, deserializer: D) -> Result 48 | where 49 | D: Deserializer<'de>, 50 | { 51 | deserializer.deserialize_str(self) 52 | } 53 | 54 | fn visit_str(self, value: &str) -> Result 55 | where 56 | E: de::Error, 57 | { 58 | let bytes = BASE64URL_NOPAD 59 | .decode(value.as_bytes()) 60 | .map_err(E::custom)?; 61 | Ok(Some(BigUint::from_bytes_be(&bytes))) 62 | } 63 | } 64 | 65 | deserializer.deserialize_option(BigUintVisitor) 66 | } 67 | 68 | #[cfg(test)] 69 | mod tests { 70 | use num_bigint::BigUint; 71 | use num_traits::cast::FromPrimitive; 72 | use serde::{Deserialize, Serialize}; 73 | 74 | use serde_test::{assert_tokens, Token}; 75 | 76 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] 77 | struct TestStruct { 78 | #[serde(with = "super")] 79 | bytes: Option, 80 | } 81 | 82 | #[test] 83 | fn some_serialization_round_trip() { 84 | let test_value = TestStruct { 85 | bytes: Some(BigUint::from_u64(12345).unwrap()), 86 | }; 87 | 88 | assert_tokens( 89 | &test_value, 90 | &[ 91 | Token::Struct { 92 | name: "TestStruct", 93 | len: 1, 94 | }, 95 | Token::Str("bytes"), 96 | Token::Some, 97 | Token::Str("MDk"), 98 | Token::StructEnd, 99 | ], 100 | ); 101 | } 102 | 103 | #[test] 104 | fn none_serialization_round_trip() { 105 | let test_value = TestStruct { bytes: None }; 106 | 107 | assert_tokens( 108 | &test_value, 109 | &[ 110 | Token::Struct { 111 | name: "TestStruct", 112 | len: 1, 113 | }, 114 | Token::Str("bytes"), 115 | Token::None, 116 | Token::StructEnd, 117 | ], 118 | ); 119 | } 120 | 121 | #[test] 122 | fn some_json_serialization_round_trip() { 123 | let test_value = TestStruct { 124 | bytes: Some(BigUint::from_u64(12345).unwrap()), 125 | }; 126 | let expected_json = r#"{"bytes":"MDk"}"#; 127 | 128 | let actual_json = not_err!(serde_json::to_string(&test_value)); 129 | assert_eq!(expected_json, actual_json); 130 | 131 | let deserialized_value: TestStruct = not_err!(serde_json::from_str(&actual_json)); 132 | assert_eq!(test_value, deserialized_value); 133 | } 134 | 135 | #[test] 136 | fn none_json_serialization_round_trip() { 137 | let test_value = TestStruct { bytes: None }; 138 | let expected_json = r#"{"bytes":null}"#; 139 | 140 | let actual_json = not_err!(serde_json::to_string(&test_value)); 141 | assert_eq!(expected_json, actual_json); 142 | 143 | let deserialized_value: TestStruct = not_err!(serde_json::from_str(&actual_json)); 144 | assert_eq!(test_value, deserialized_value); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | //! Errors returned will be converted to one of the structs in this module. 2 | use crate::SingleOrMultiple; 3 | use chrono::Duration; 4 | use std::{error, fmt, io, str, string}; 5 | 6 | #[derive(Debug)] 7 | /// All the errors we can encounter while signing/verifying tokens 8 | /// and a couple of custom one for when the token we are trying 9 | /// to verify is invalid 10 | pub enum Error { 11 | /// A generic error which is described by the contained string 12 | GenericError(String), 13 | /// Error returned from failed token decoding 14 | DecodeError(DecodeError), 15 | /// Error returned from failed token validation 16 | ValidationError(ValidationError), 17 | /// Error during the serialization or deserialization of tokens 18 | JsonError(serde_json::error::Error), 19 | /// Error during base64 encoding or decoding 20 | DecodeBase64(data_encoding::DecodeError), 21 | /// Error when decoding bytes to UTF8 string 22 | Utf8(str::Utf8Error), 23 | /// Errors related to IO 24 | IOError(io::Error), 25 | /// Key was rejected by Ring 26 | KeyRejected(ring::error::KeyRejected), 27 | 28 | /// Wrong key type was provided for the cryptographic operation 29 | WrongKeyType { 30 | /// Expected type of key 31 | expected: String, 32 | /// Actual type of key 33 | actual: String, 34 | }, 35 | 36 | /// Wrong variant of `EncryptionOptions` was provided for the encryption operation 37 | WrongEncryptionOptions { 38 | /// Expected variant of options 39 | expected: String, 40 | /// Actual variant of options 41 | actual: String, 42 | }, 43 | 44 | /// An unknown cryptographic error 45 | UnspecifiedCryptographicError, 46 | /// An unsupported or invalid operation 47 | UnsupportedOperation, 48 | } 49 | 50 | #[derive(Debug)] 51 | /// Errors from decoding tokens 52 | pub enum DecodeError { 53 | /// Token is invalid in structure or form 54 | InvalidToken, 55 | /// The number of compact parts is incorrect 56 | PartsLengthError { 57 | /// Expected number of parts 58 | expected: usize, 59 | /// Actual number of parts 60 | actual: usize, 61 | }, 62 | } 63 | 64 | #[derive(Debug, Eq, PartialEq, Clone)] 65 | /// Errors from validating tokens 66 | pub enum ValidationError { 67 | /// Token has an invalid signature (RFC7523 3.9) 68 | InvalidSignature, 69 | /// Token provided was signed or encrypted with an unexpected algorithm 70 | WrongAlgorithmHeader, 71 | /// A field required is missing from the token 72 | /// The parameter shows the name of the missing claim 73 | MissingRequiredClaims(Vec), 74 | /// The token's expiry has passed (exp check failed, RFC7523 3.4) 75 | /// The parameter show how long the token has expired 76 | Expired(Duration), 77 | /// The token is not yet valid (nbf check failed, RFC7523 3.5) 78 | /// The parameter show how much longer the token will start to be valid 79 | NotYetValid(Duration), 80 | /// The token has been created too far in the past (iat check failed, RFC7523 3.6) 81 | /// This is different from Expired because the token may not be expired yet, but the 82 | /// acceptor of the token may impose more strict requirement for the age of the token for 83 | /// some more sensitive operations. 84 | /// The parameter show how much older the token is than required 85 | TooOld(Duration), 86 | /// The token does not have or has the wrong issuer (iss check failed, RFC7523 3.1) 87 | InvalidIssuer(String), 88 | /// The token does not have or has the wrong audience (aud check failed, RFC7523 3.3 89 | InvalidAudience(SingleOrMultiple), 90 | /// The token doesn't contains the Kid claim in the header 91 | KidMissing, 92 | /// The by the Kid specified key, wasn't found in the KeySet 93 | KeyNotFound, 94 | /// The algorithm of the JWK is not supported for validating JWTs 95 | UnsupportedKeyAlgorithm, 96 | /// An algorithm is needed for verification but was not provided 97 | MissingAlgorithm, 98 | } 99 | 100 | macro_rules! impl_from_error { 101 | ($f:ty, $e:expr) => { 102 | impl From<$f> for Error { 103 | fn from(f: $f) -> Error { 104 | $e(f) 105 | } 106 | } 107 | }; 108 | } 109 | 110 | impl_from_error!(String, Error::GenericError); 111 | impl_from_error!(serde_json::error::Error, Error::JsonError); 112 | impl_from_error!(data_encoding::DecodeError, Error::DecodeBase64); 113 | impl_from_error!(str::Utf8Error, Error::Utf8); 114 | impl_from_error!(ValidationError, Error::ValidationError); 115 | impl_from_error!(DecodeError, Error::DecodeError); 116 | impl_from_error!(io::Error, Error::IOError); 117 | impl_from_error!(ring::error::KeyRejected, Error::KeyRejected); 118 | 119 | impl From for Error { 120 | fn from(_: ring::error::Unspecified) -> Self { 121 | Error::UnspecifiedCryptographicError 122 | } 123 | } 124 | 125 | impl From for Error { 126 | fn from(e: string::FromUtf8Error) -> Self { 127 | Error::Utf8(e.utf8_error()) 128 | } 129 | } 130 | 131 | impl fmt::Display for Error { 132 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 133 | use crate::Error::*; 134 | 135 | match *self { 136 | GenericError(ref err) => fmt::Display::fmt(err, f), 137 | JsonError(ref err) => fmt::Display::fmt(err, f), 138 | DecodeBase64(ref err) => fmt::Display::fmt(err, f), 139 | Utf8(ref err) => fmt::Display::fmt(err, f), 140 | DecodeError(ref err) => fmt::Display::fmt(err, f), 141 | ValidationError(ref err) => fmt::Display::fmt(err, f), 142 | IOError(ref err) => fmt::Display::fmt(err, f), 143 | KeyRejected(ref err) => fmt::Display::fmt(err, f), 144 | WrongKeyType { 145 | ref actual, 146 | ref expected, 147 | } => write!( 148 | f, 149 | "{} was expected for this cryptographic operation but {} was provided", 150 | expected, actual 151 | ), 152 | WrongEncryptionOptions { 153 | ref actual, 154 | ref expected, 155 | } => write!( 156 | f, 157 | "{} was expected for this cryptographic operation but {} was provided", 158 | expected, actual 159 | ), 160 | UnspecifiedCryptographicError => write!(f, "An unspecified cryptographic error"), 161 | UnsupportedOperation => write!(f, "This operation is not supported"), 162 | } 163 | } 164 | } 165 | 166 | impl error::Error for Error { 167 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { 168 | use crate::Error::*; 169 | 170 | match *self { 171 | JsonError(ref err) => Some(err), 172 | DecodeBase64(ref err) => Some(err), 173 | Utf8(ref err) => Some(err), 174 | DecodeError(ref err) => Some(err), 175 | ValidationError(ref err) => Some(err), 176 | IOError(ref err) => Some(err), 177 | _ => None, 178 | } 179 | } 180 | } 181 | 182 | impl fmt::Display for DecodeError { 183 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 184 | use self::DecodeError::*; 185 | 186 | match *self { 187 | InvalidToken => write!(f, "Invalid token"), 188 | PartsLengthError { expected, actual } => write!( 189 | f, 190 | "Expected {} parts in Compact JSON representation but got {}", 191 | expected, actual 192 | ), 193 | } 194 | } 195 | } 196 | 197 | impl error::Error for DecodeError { 198 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { 199 | None 200 | } 201 | } 202 | 203 | impl fmt::Display for ValidationError { 204 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 205 | use crate::ValidationError::*; 206 | 207 | match *self { 208 | MissingRequiredClaims(ref fields) => write!( 209 | f, 210 | "The following claims are required, but missing: {:?}", 211 | fields 212 | ), 213 | Expired(ago) => write!(f, "Token expired {} seconds ago", ago.num_seconds()), 214 | NotYetValid(nyv_for) => write!( 215 | f, 216 | "Token will be valid in {} seconds", 217 | nyv_for.num_seconds() 218 | ), 219 | TooOld(duration) => write!( 220 | f, 221 | "Token has been considered too old for {} seconds", 222 | duration.num_seconds() 223 | ), 224 | InvalidIssuer(ref iss) => write!(f, "Issuer of token is invalid: {:?}", iss), 225 | InvalidAudience(ref aud) => write!(f, "Audience of token is invalid: {:?}", aud), 226 | InvalidSignature => write!(f, "Invalid signature"), 227 | WrongAlgorithmHeader => write!( 228 | f, 229 | "Token provided was signed or encrypted with an unexpected algorithm" 230 | ), 231 | KidMissing => write!(f, "Header is missing kid"), 232 | KeyNotFound => write!(f, "Key not found in JWKS"), 233 | UnsupportedKeyAlgorithm => write!(f, "Algorithm of JWK not supported"), 234 | MissingAlgorithm => write!( 235 | f, 236 | "An algorithm is needed for verification but was not provided" 237 | ), 238 | } 239 | } 240 | } 241 | 242 | impl error::Error for ValidationError { 243 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { 244 | None 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.6.0 4 | 5 | ### Breaking Changes 6 | 7 | - Fix build errors and bump MSRV by @lawliet89 in https://github.com/lawliet89/biscuit/pull/308 8 | - [`jws::RegisteredHeader` field `web_key`](https://lawliet89.github.io/biscuit/biscuit/jws/struct.RegisteredHeader.html#structfield.web_key) 9 | is now of type `Option>` instead of `Option`. If you were not using JWKs, 10 | continue setting the value to `None` will not breaking. If you were previously serializing your 11 | JWK as JSON strings, you will now have to deserialize them into `jwk::JWK`. Please raise 12 | issues if you encounter any bugs. [[#189]](https://github.com/lawliet89/biscuit/pull/189) 13 | 14 | ### Enhancements 15 | 16 | - Add support for Flattened JWS [[#190]](https://github.com/lawliet89/biscuit/pull/190) 17 | - Added more documentation for using OpenSSL to manipulate keys [[#179]](https://github.com/lawliet89/biscuit/pull/179) 18 | - Derive Clone for `JWKSet` by @lawliet89 in https://github.com/lawliet89/biscuit/pull/204 19 | - Lints fixes 20 | 21 | ## 0.5.0 (2020-11-17) 22 | 23 | ### Breaking Changes 24 | 25 | - MSRV is now Rust 1.41 due to changes in `Cargo.lock` format. 26 | See [announcement](https://blog.rust-lang.org/2020/01/30/Rust-1.41.0.html#less-conflict-prone-cargolock-format). 27 | 28 | - The `jwk::AlgorithmParameters::OctetKey` enum variant is now a newtype variant which takes a 29 | `jwk::OctetKeyParameters` struct for its parameters. To migrate your existing code, you can do 30 | the following 31 | 32 | ```diff 33 | -jwk::AlgorithmParameters::OctetKey { 34 | +jwk::AlgorithmParameters::OctetKey(jwk::OctetKeyParameters { 35 | value: key, 36 | key_type: Default::default(), 37 | -} 38 | +}) 39 | ``` 40 | 41 | ([#125](https://github.com/lawliet89/biscuit/pull/125)) 42 | 43 | - `jws::Compact::decode_with_jwks` now supports JWK without an `alg` specified. However, a new 44 | parameter to specify an expected parameter had to be added to support this use case. This is to 45 | mitigate against issues like 46 | [this](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/). 47 | Existing usage of JWK with the `alg` specified can simply add a `None` as the second parameter. 48 | ([#130](https://github.com/lawliet89/biscuit/pull/130)) 49 | 50 | 51 | - Remove `StringOrUri` because it was hard to get the `Uri` type working properly. Replace all usage 52 | of `StringOrUri` with `String`s instead. ([#131](https://github.com/lawliet89/biscuit/pull/131)) 53 | 54 | ### Enhancements 55 | 56 | - Add new `jwk::AlgorithmParameters::OctetKeyPair` variant to support (de)serializing `OKP` 57 | key types. ([#125](https://github.com/lawliet89/biscuit/pull/125)) 58 | - Add support for JWK thumbprints (RFC 7638) ([#156](https://github.com/lawliet89/biscuit/pull/156)) 59 | - Allow verifying tokens with a `keypair` as `Secret` 60 | ([#132](https://github.com/lawliet89/biscuit/pull/132)) 61 | 62 | ### Bug Fixes 63 | 64 | - Fix computing Aad per the RFC by doing base64 encoding 65 | ([#147](https://github.com/lawliet89/biscuit/pull/147)) 66 | 67 | ## 0.4.2 (2020-01-07) 68 | 69 | ### Enhancements 70 | 71 | - Add `jws::Compact::decode_with_jwks` method to decode a JWT with JWKs 72 | ([#124](https://github.com/lawliet89/biscuit/pull/124)) 73 | 74 | ### Internal Changes 75 | 76 | - Replace `lazy_static` with `once_cell` ([#123](https://github.com/lawliet89/biscuit/pull/123)) 77 | 78 | ## 0.4.1 (2019-11-06) 79 | 80 | - Fix documentation build on 1.40 Nightly 81 | 82 | ## 0.4.0 (2019-11-06) 83 | 84 | There are no new feature except for some breaking changes to correct some errors. 85 | 86 | ### Breaking Changes 87 | 88 | ### `Octet` Misspelling 89 | 90 | All misspelling of `octect` have been corrected to `octet`. The following 91 | types have been renamed and the old misspelt version is no longer available. 92 | 93 | To migrate, you can simply do a case sensitive replace of `Octect` with `Octet` and 94 | `octect` with `octet` in your code. 95 | 96 | The following types have been renamed: 97 | 98 | - `jwk::KeyType::Octect` 🡒 `jwk::KeyType::Octet` 99 | - `jwk::KeyType::OctectKeyPair` 🡒 `jwk::KeyType::OctetKeyPair` 100 | - `jwk::OctectKeyType` 🡒 `jwk::OctetKeyType` 101 | - `jwk::OctectKeyType::Octect` 🡒 `jwk::OctetKeyType::Octet` 102 | - `jwk::AlgorithmParameters::OctectKey` 🡒 `jwk::AlgorithmParameters::OctetKey` 103 | 104 | The following functions have been renamed: 105 | 106 | - `jwk::JWK::new_octect_key` 🡒 `jwk::JWK::new_octet_key` 107 | - `jwk::JWK::octect_key` 🡒 `jwk::JWK::octet_key` 108 | - `jwk::AlgorithmParameters::octect_key` 🡒 `jwk::AlgorithmParameters::octet_key` 109 | 110 | ### Clippy `trivially_copy_pass_by_ref` lint 111 | 112 | This release also fixes the 113 | [Clippy `trivially_copy_pass_by_ref` lint](https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref) 114 | by modifying function arguments that would have taken a reference of a 1 byte value that 115 | implements `Copy` to take the value of itself. This mainly affects all struct methods 116 | of the following types 117 | 118 | There should be no need to modify your code for this because the types are `Copy`. 119 | 120 | - `jwa::SignatureAlgorithm` 121 | - `jwa::KeyManagementAlgorithm` 122 | - `jwa::ContentEncryptionAlgorithm` 123 | - `jwk::KeyType` 124 | 125 | ## 0.3.1 (2019-07-30) 126 | 127 | There are no new features except for ring dependency changes. 128 | 129 | - biscuit now depends on ring 0.16.5 130 | - Changed internal usage of ring's AEAD APIs 131 | - Removed `Compact::to_string`. `Compact` now implements `Display` which has a blanket 132 | implementation of `std::string::ToString`. Use that instead. This should not break any 133 | users because `std::string::ToString` is used by the `std` prelude. 134 | 135 | ## 0.3.0 (2019-07-19) 136 | 137 | There are no new features or API changes except for ring dependency changes. 138 | 139 | ### Breaking Changes 140 | 141 | - Minimum supported Rust version is now 1.36 due to Ring's usage of newer Rust features 142 | - biscuit now depends on ring 0.16 143 | 144 | ## 0.2.0 (2019-03-11) 145 | 146 | - Minimum Rust 1.31 is needed for editions support on dependencies 147 | - biscuit now depends on ring 0.14 148 | 149 | ## Version 0.1.1 (2019-03-18) 150 | 151 | - Minimum Rust 1.31 is needed for editions support on dependencies 152 | 153 | ## Version 0.1.0 (2018-10-29) 154 | 155 | ### Breaking Change 156 | 157 | - Minimum Rust 1.27.2 supported. Older versions might build, but this might not be supported. 158 | - Ring 0.13.2 minimum required. This breaks with other libraries using any other versions 159 | 160 | ### Features 161 | 162 | - Add ECDSA support (https://github.com/lawliet89/biscuit/pull/95) 163 | - Additional claims validation (https://github.com/lawliet89/biscuit/pull/99) 164 | - RSA signature validation with only the exponent and modulus (https://github.com/lawliet89/biscuit/pull/100) 165 | 166 | ## Version 0.0.8 (2018-02-14) 167 | 168 | There are breaking changes in this release: 169 | 170 | - `ring` was upgraded to 0.12. Until [#619](https://github.com/briansmith/ring/pull/619) lands, 171 | this crate will now be incompatible with all other crates that uses a different version of `ring`. 172 | - `jwa::rng` is no longer public 173 | - [#84](https://github.com/lawliet89/biscuit/pull/84) All AES GCM encryption now requires a user 174 | provided nonce. See [this example](https://lawliet89.github.io/biscuit/biscuit/type.JWE.html). 175 | - `SignatureAlgorithm::verify` now returns `Result<(), Error>` instead of `Result`. 176 | - Bumped various dependencies, although they should not break any user facing code: `lazy_static`, 177 | `data-encoding`. 178 | 179 | Other non-breaking changes include: 180 | 181 | - New helper 182 | [function](https://lawliet89.github.io/biscuit/biscuit/jwk/struct.JWKSet.html#method.find) in `JWKSet` to find key by Key ID 183 | - [New helper functions](https://github.com/lawliet89/biscuit/pull/88) in `jws::Compact` to retrieve 184 | parts without signature verification. 185 | 186 | ## Version 0.0.7 (2017-07-19) 187 | 188 | There are no breaking changes in this release. 189 | 190 | Added a convenience `validate_times` function to `jwe::Compact` and `jws::Compact` that allows 191 | quick temporal validation if their payloads are `ClaimSet`s. 192 | 193 | ## Version 0.0.6 (2017-07-05) 194 | 195 | This release adds no new features and breaks no API. It simply bumps `ring` to 0.11. 196 | 197 | ## Version 0.0.5 (2017-07-05) 198 | 199 | This release adds no new features and breaks no API. It simply bumps Chrono and Ring to their newest version. 200 | 201 | ## Version 0.0.4 (2017-05-15) 202 | 203 | Update dependency to `ring` 0.9.4 so that different versions of `ring` can no longer be used in a Rust build. 204 | 205 | There are no new features or API change. 206 | 207 | ## Version 0.0.3 (2017-04-23) 208 | 209 | Minor bug fix release. Fixed incorrect ECDSA signature verification. 210 | 211 | Thanks to @hobofan. 212 | 213 | ## Version 0.0.2 (2017-04-23) 214 | 215 | This is a major breaking release. Not all algorithms, verification, and features are 216 | supported yet. 217 | 218 | ### New Features 219 | 220 | - JSON Web Encryption support (JWE) 221 | - JSON Web Key (JWK) 222 | - Replaced `rustc_serialize` with `serde` 223 | - Support custom headers for JWS 224 | - Added a `biscuit::Empty` convenice empty struct that users can plug into type parameters when they do 225 | not need them, such as the type parameter of custom headers. 226 | - Added `SingleOrMultiple` and `StringOrUri` enums to better represent the types of values that the JOSE 227 | RFCs allow. 228 | 229 | ### Breaking Changes 230 | 231 | - `biscuit::JWT` is no longer a struct. It is now a type alias for `jws::Compact`, which according 232 | to the RFC, is the compact serialization of a JSON Web Signature (JWS). 233 | - Moved `biscuit::Algorithm` to `biscuit::jwa::SignatureAlgorithm` to better reflect its use. 234 | - Various internal traits that should be implementation detail and opaque to users of `biscuit` have been 235 | changed, added, or removed. 236 | 237 | ## Version 0.0.1 (2017-03-17) 238 | 239 | This is an initial release after forking from Version 1.1.6 of [`Keats/rust-jwt`](https://github.com/Keats/rust-jwt). 240 | 241 | - Added RSA signing and verification 242 | - Added ECDSA verification (signing support is pending addition of support in `ring`) 243 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "android-tzdata" 7 | version = "0.1.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 10 | 11 | [[package]] 12 | name = "android_system_properties" 13 | version = "0.1.5" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 16 | dependencies = [ 17 | "libc", 18 | ] 19 | 20 | [[package]] 21 | name = "autocfg" 22 | version = "1.1.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 25 | 26 | [[package]] 27 | name = "biscuit" 28 | version = "0.7.0" 29 | dependencies = [ 30 | "chrono", 31 | "data-encoding", 32 | "num-bigint", 33 | "num-traits", 34 | "once_cell", 35 | "ring", 36 | "serde", 37 | "serde_json", 38 | "serde_test", 39 | ] 40 | 41 | [[package]] 42 | name = "bumpalo" 43 | version = "3.14.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 46 | 47 | [[package]] 48 | name = "cc" 49 | version = "1.0.83" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 52 | dependencies = [ 53 | "libc", 54 | ] 55 | 56 | [[package]] 57 | name = "cfg-if" 58 | version = "1.0.0" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 61 | 62 | [[package]] 63 | name = "chrono" 64 | version = "0.4.31" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" 67 | dependencies = [ 68 | "android-tzdata", 69 | "iana-time-zone", 70 | "num-traits", 71 | "windows-targets", 72 | ] 73 | 74 | [[package]] 75 | name = "core-foundation-sys" 76 | version = "0.8.4" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" 79 | 80 | [[package]] 81 | name = "data-encoding" 82 | version = "2.4.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" 85 | 86 | [[package]] 87 | name = "equivalent" 88 | version = "1.0.1" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 91 | 92 | [[package]] 93 | name = "getrandom" 94 | version = "0.2.11" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" 97 | dependencies = [ 98 | "cfg-if", 99 | "libc", 100 | "wasi", 101 | ] 102 | 103 | [[package]] 104 | name = "hashbrown" 105 | version = "0.14.2" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" 108 | 109 | [[package]] 110 | name = "iana-time-zone" 111 | version = "0.1.58" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" 114 | dependencies = [ 115 | "android_system_properties", 116 | "core-foundation-sys", 117 | "iana-time-zone-haiku", 118 | "js-sys", 119 | "wasm-bindgen", 120 | "windows-core", 121 | ] 122 | 123 | [[package]] 124 | name = "iana-time-zone-haiku" 125 | version = "0.1.2" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 128 | dependencies = [ 129 | "cc", 130 | ] 131 | 132 | [[package]] 133 | name = "indexmap" 134 | version = "2.1.0" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" 137 | dependencies = [ 138 | "equivalent", 139 | "hashbrown", 140 | ] 141 | 142 | [[package]] 143 | name = "itoa" 144 | version = "1.0.9" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 147 | 148 | [[package]] 149 | name = "js-sys" 150 | version = "0.3.65" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" 153 | dependencies = [ 154 | "wasm-bindgen", 155 | ] 156 | 157 | [[package]] 158 | name = "libc" 159 | version = "0.2.150" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" 162 | 163 | [[package]] 164 | name = "log" 165 | version = "0.4.20" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 168 | 169 | [[package]] 170 | name = "num-bigint" 171 | version = "0.4.4" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" 174 | dependencies = [ 175 | "autocfg", 176 | "num-integer", 177 | "num-traits", 178 | ] 179 | 180 | [[package]] 181 | name = "num-integer" 182 | version = "0.1.45" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 185 | dependencies = [ 186 | "autocfg", 187 | "num-traits", 188 | ] 189 | 190 | [[package]] 191 | name = "num-traits" 192 | version = "0.2.17" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" 195 | dependencies = [ 196 | "autocfg", 197 | ] 198 | 199 | [[package]] 200 | name = "once_cell" 201 | version = "1.18.0" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 204 | 205 | [[package]] 206 | name = "proc-macro2" 207 | version = "1.0.69" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" 210 | dependencies = [ 211 | "unicode-ident", 212 | ] 213 | 214 | [[package]] 215 | name = "quote" 216 | version = "1.0.33" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 219 | dependencies = [ 220 | "proc-macro2", 221 | ] 222 | 223 | [[package]] 224 | name = "ring" 225 | version = "0.17.5" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" 228 | dependencies = [ 229 | "cc", 230 | "getrandom", 231 | "libc", 232 | "spin", 233 | "untrusted", 234 | "windows-sys", 235 | ] 236 | 237 | [[package]] 238 | name = "ryu" 239 | version = "1.0.15" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 242 | 243 | [[package]] 244 | name = "serde" 245 | version = "1.0.192" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" 248 | dependencies = [ 249 | "serde_derive", 250 | ] 251 | 252 | [[package]] 253 | name = "serde_derive" 254 | version = "1.0.192" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" 257 | dependencies = [ 258 | "proc-macro2", 259 | "quote", 260 | "syn", 261 | ] 262 | 263 | [[package]] 264 | name = "serde_json" 265 | version = "1.0.108" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" 268 | dependencies = [ 269 | "indexmap", 270 | "itoa", 271 | "ryu", 272 | "serde", 273 | ] 274 | 275 | [[package]] 276 | name = "serde_test" 277 | version = "1.0.176" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "5a2f49ace1498612d14f7e0b8245519584db8299541dfe31a06374a828d620ab" 280 | dependencies = [ 281 | "serde", 282 | ] 283 | 284 | [[package]] 285 | name = "spin" 286 | version = "0.9.8" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 289 | 290 | [[package]] 291 | name = "syn" 292 | version = "2.0.39" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" 295 | dependencies = [ 296 | "proc-macro2", 297 | "quote", 298 | "unicode-ident", 299 | ] 300 | 301 | [[package]] 302 | name = "unicode-ident" 303 | version = "1.0.12" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 306 | 307 | [[package]] 308 | name = "untrusted" 309 | version = "0.9.0" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 312 | 313 | [[package]] 314 | name = "wasi" 315 | version = "0.11.0+wasi-snapshot-preview1" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 318 | 319 | [[package]] 320 | name = "wasm-bindgen" 321 | version = "0.2.88" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" 324 | dependencies = [ 325 | "cfg-if", 326 | "wasm-bindgen-macro", 327 | ] 328 | 329 | [[package]] 330 | name = "wasm-bindgen-backend" 331 | version = "0.2.88" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" 334 | dependencies = [ 335 | "bumpalo", 336 | "log", 337 | "once_cell", 338 | "proc-macro2", 339 | "quote", 340 | "syn", 341 | "wasm-bindgen-shared", 342 | ] 343 | 344 | [[package]] 345 | name = "wasm-bindgen-macro" 346 | version = "0.2.88" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" 349 | dependencies = [ 350 | "quote", 351 | "wasm-bindgen-macro-support", 352 | ] 353 | 354 | [[package]] 355 | name = "wasm-bindgen-macro-support" 356 | version = "0.2.88" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" 359 | dependencies = [ 360 | "proc-macro2", 361 | "quote", 362 | "syn", 363 | "wasm-bindgen-backend", 364 | "wasm-bindgen-shared", 365 | ] 366 | 367 | [[package]] 368 | name = "wasm-bindgen-shared" 369 | version = "0.2.88" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" 372 | 373 | [[package]] 374 | name = "windows-core" 375 | version = "0.51.1" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" 378 | dependencies = [ 379 | "windows-targets", 380 | ] 381 | 382 | [[package]] 383 | name = "windows-sys" 384 | version = "0.48.0" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 387 | dependencies = [ 388 | "windows-targets", 389 | ] 390 | 391 | [[package]] 392 | name = "windows-targets" 393 | version = "0.48.5" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 396 | dependencies = [ 397 | "windows_aarch64_gnullvm", 398 | "windows_aarch64_msvc", 399 | "windows_i686_gnu", 400 | "windows_i686_msvc", 401 | "windows_x86_64_gnu", 402 | "windows_x86_64_gnullvm", 403 | "windows_x86_64_msvc", 404 | ] 405 | 406 | [[package]] 407 | name = "windows_aarch64_gnullvm" 408 | version = "0.48.5" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 411 | 412 | [[package]] 413 | name = "windows_aarch64_msvc" 414 | version = "0.48.5" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 417 | 418 | [[package]] 419 | name = "windows_i686_gnu" 420 | version = "0.48.5" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 423 | 424 | [[package]] 425 | name = "windows_i686_msvc" 426 | version = "0.48.5" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 429 | 430 | [[package]] 431 | name = "windows_x86_64_gnu" 432 | version = "0.48.5" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 435 | 436 | [[package]] 437 | name = "windows_x86_64_gnullvm" 438 | version = "0.48.5" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 441 | 442 | [[package]] 443 | name = "windows_x86_64_msvc" 444 | version = "0.48.5" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 447 | -------------------------------------------------------------------------------- /src/jws.rs: -------------------------------------------------------------------------------- 1 | //! JSON Web Signatures, including JWT signing and headers 2 | //! 3 | //! Defined in [RFC 7515](https://tools.ietf.org/html/rfc7515). For most common use, 4 | //! you will want to look at the [`Compact`](enum.Compact.html) enum. 5 | mod compact; 6 | mod flattened; 7 | 8 | pub use compact::Compact; 9 | pub use flattened::{Signable, SignedData}; 10 | 11 | use crate::errors::Error; 12 | use crate::jwa::SignatureAlgorithm; 13 | use crate::jwk; 14 | use crate::{CompactJson, Empty}; 15 | 16 | use num_bigint::BigUint; 17 | use ring::signature; 18 | use serde::{self, de::DeserializeOwned, Deserialize, Serialize}; 19 | use std::sync::Arc; 20 | 21 | /// The secrets used to sign and/or encrypt tokens 22 | #[derive(Clone)] 23 | pub enum Secret { 24 | /// Used with the `None` algorithm variant. 25 | None, 26 | /// Bytes used for HMAC secret. Can be constructed from a string literal 27 | /// 28 | /// # Examples 29 | /// ``` 30 | /// use biscuit::jws::Secret; 31 | /// 32 | /// let secret = Secret::bytes_from_str("secret"); 33 | /// ``` 34 | Bytes(Vec), 35 | /// An RSA Key pair constructed from a DER-encoded private key 36 | /// 37 | /// To generate a private key, use 38 | /// 39 | /// ```sh 40 | /// openssl genpkey -algorithm RSA \ 41 | /// -pkeyopt rsa_keygen_bits:2048 \ 42 | /// -outform der \ 43 | /// -out private_key.der 44 | /// ``` 45 | /// 46 | /// Often, keys generated for use in OpenSSL-based software are 47 | /// encoded in PEM format, which is not supported by *ring*. PEM-encoded 48 | /// keys that are in `RSAPrivateKey` format can be decoded into the using 49 | /// an OpenSSL command like this: 50 | /// 51 | /// ```sh 52 | /// openssl rsa -in private_key.pem -outform DER -out private_key.der 53 | /// ``` 54 | /// 55 | /// # Examples 56 | /// ``` 57 | /// use biscuit::jws::Secret; 58 | /// 59 | /// let secret = Secret::rsa_keypair_from_file("test/fixtures/rsa_private_key.der"); 60 | /// ``` 61 | RsaKeyPair(Arc), 62 | /// An ECDSA Key pair constructed from a PKCS8 DER encoded private key 63 | /// 64 | /// To generate a private key, use 65 | /// 66 | /// ```sh 67 | /// openssl ecparam -genkey -name prime256v1 | \ 68 | /// openssl pkcs8 -topk8 -nocrypt -outform DER > ecdsa_private_key.p8 69 | /// ``` 70 | /// 71 | /// # Examples 72 | /// ``` 73 | /// use biscuit::jws::Secret; 74 | /// 75 | /// let secret = Secret::ecdsa_keypair_from_file(biscuit::jwa::SignatureAlgorithm::ES256, "test/fixtures/ecdsa_private_key.p8"); 76 | /// ``` 77 | EcdsaKeyPair(Arc), 78 | /// Bytes of a DER encoded RSA Public Key 79 | /// 80 | /// To generate the public key from your DER-encoded private key 81 | /// 82 | /// ```sh 83 | /// openssl rsa -in private_key.der \ 84 | /// -inform DER 85 | /// -RSAPublicKey_out \ 86 | /// -outform DER \ 87 | /// -out public_key.der 88 | /// ``` 89 | /// 90 | /// To convert a PEM formatted public key 91 | /// 92 | /// ```sh 93 | /// openssl rsa -RSAPublicKey_in \ 94 | /// -in public_key.pem \ 95 | /// -inform PEM \ 96 | /// -outform DER \ 97 | /// -RSAPublicKey_out \ 98 | /// -out public_key.der 99 | /// ``` 100 | /// 101 | /// Note that the underlying crate (ring) does not support the format used 102 | /// by OpenSSL. You can check the format using 103 | /// 104 | /// ```sh 105 | /// openssl asn1parse -inform DER -in public_key.der 106 | /// ``` 107 | /// 108 | /// It should output something like 109 | /// 110 | /// ```sh 111 | /// 0:d=0 hl=4 l= 290 cons: SEQUENCE 112 | /// 4:d=1 hl=2 l= 13 cons: SEQUENCE 113 | /// 6:d=2 hl=2 l= 9 prim: OBJECT :rsaEncryption 114 | /// 17:d=2 hl=2 l= 0 prim: NULL 115 | /// 19:d=1 hl=4 l= 271 prim: BIT STRING 116 | /// ``` 117 | /// 118 | /// There is a header here that indicates the content of the file 119 | /// (a public key for `rsaEncryption`). The actual key is contained 120 | /// within the BIT STRING at the end. The bare public key can be 121 | /// extracted with 122 | /// 123 | /// ```sh 124 | /// openssl asn1parse -inform DER \ 125 | /// -in public_key.der \ 126 | /// -offset 24 \ 127 | /// -out public_key_extracted.der 128 | /// ``` 129 | /// 130 | /// Run the following to verify that the key is in the right format 131 | /// 132 | /// ```sh 133 | /// openssl asn1parse -inform DER -in public_key_extracted.der 134 | /// ``` 135 | /// 136 | /// The right format looks like this (the `<>` elements show the actual 137 | /// numbers) 138 | /// 139 | /// ```sh 140 | /// 0:d=0 hl=4 l= 266 cons: SEQUENCE 141 | /// 4:d=1 hl=4 l= 257 prim: INTEGER : 142 | /// 265:d=1 hl=2 l= 3 prim: INTEGER : 143 | /// ``` 144 | /// 145 | /// Every other format will be rejected by ring with an unspecified error. 146 | /// Note that OpenSSL is no longer able to interpret this file as a public key, 147 | /// since it no longer contains the expected header. 148 | /// 149 | /// # Examples 150 | /// ``` 151 | /// use biscuit::jws::Secret; 152 | /// 153 | /// let secret = Secret::public_key_from_file("test/fixtures/rsa_public_key.der"); 154 | PublicKey(Vec), 155 | /// Use the modulus (`n`) and exponent (`e`) of an RSA key directly 156 | /// 157 | /// These parameters can be obtained from a JWK directly using 158 | /// [`jwk::RSAKeyParameters::jws_public_key_secret`] 159 | RSAModulusExponent { 160 | /// RSA modulus 161 | n: BigUint, 162 | /// RSA exponent 163 | e: BigUint, 164 | }, 165 | } 166 | 167 | impl Secret { 168 | fn read_bytes(path: &str) -> Result, Error> { 169 | use std::fs::File; 170 | use std::io::prelude::*; 171 | 172 | let mut file = File::open(path)?; 173 | let metadata = file.metadata()?; 174 | let mut bytes: Vec = Vec::with_capacity(metadata.len() as usize); 175 | let _ = file.read_to_end(&mut bytes)?; 176 | Ok(bytes) 177 | } 178 | 179 | /// Convenience function to create a secret bytes array from a string 180 | /// See example in the [`Secret::Bytes`] variant documentation for usage. 181 | pub fn bytes_from_str(secret: &str) -> Self { 182 | Secret::Bytes(secret.to_string().into_bytes()) 183 | } 184 | 185 | /// Convenience function to get the RSA Keypair from a DER encoded RSA private key. 186 | /// See example in the [`Secret::RsaKeyPair`] variant documentation for usage. 187 | pub fn rsa_keypair_from_file(path: &str) -> Result { 188 | let der = Self::read_bytes(path)?; 189 | let key_pair = signature::RsaKeyPair::from_der(der.as_slice())?; 190 | Ok(Secret::RsaKeyPair(Arc::new(key_pair))) 191 | } 192 | 193 | /// Convenience function to get the ECDSA Keypair from a PKCS8-DER encoded EC private key. 194 | pub fn ecdsa_keypair_from_file( 195 | algorithm: SignatureAlgorithm, 196 | path: &str, 197 | ) -> Result { 198 | let der = Self::read_bytes(path)?; 199 | let ring_algorithm = match algorithm { 200 | SignatureAlgorithm::ES256 => &signature::ECDSA_P256_SHA256_FIXED_SIGNING, 201 | SignatureAlgorithm::ES384 => &signature::ECDSA_P384_SHA384_FIXED_SIGNING, 202 | _ => return Err(Error::UnsupportedOperation), 203 | }; 204 | let key_pair = signature::EcdsaKeyPair::from_pkcs8( 205 | ring_algorithm, 206 | der.as_slice(), 207 | &ring::rand::SystemRandom::new(), 208 | )?; 209 | Ok(Secret::EcdsaKeyPair(Arc::new(key_pair))) 210 | } 211 | 212 | /// Convenience function to create a Public key from a DER encoded RSA or ECDSA public key 213 | /// See examples in the [`Secret::PublicKey`] variant documentation for usage. 214 | pub fn public_key_from_file(path: &str) -> Result { 215 | let der = Self::read_bytes(path)?; 216 | Ok(Secret::PublicKey(der.to_vec())) 217 | } 218 | } 219 | 220 | impl From for Secret { 221 | fn from(rsa: jwk::RSAKeyParameters) -> Self { 222 | rsa.jws_public_key_secret() 223 | } 224 | } 225 | 226 | /// JWS Header, consisting of the registered fields and other custom fields 227 | #[derive(Debug, Eq, PartialEq, Clone, Default, Serialize, Deserialize)] 228 | pub struct Header { 229 | /// Registered header fields 230 | #[serde(flatten)] 231 | pub registered: RegisteredHeader, 232 | /// Private header fields 233 | #[serde(flatten)] 234 | pub private: T, 235 | } 236 | 237 | impl CompactJson for Header {} 238 | 239 | impl Header { 240 | /// Convenience function to create a header with only registered headers 241 | pub fn from_registered_header(registered: RegisteredHeader) -> Self { 242 | Self { 243 | registered, 244 | ..Default::default() 245 | } 246 | } 247 | } 248 | 249 | impl From for Header { 250 | fn from(registered: RegisteredHeader) -> Self { 251 | Self::from_registered_header(registered) 252 | } 253 | } 254 | 255 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 256 | /// Registered JWS header fields. 257 | /// The alg defaults to HS256 and typ is automatically 258 | /// set to `JWT`. All the other fields are optional. 259 | /// The fields are defined by [RFC7519#5](https://tools.ietf.org/html/rfc7519#section-5) and additionally in 260 | /// [RFC7515#4.1](https://tools.ietf.org/html/rfc7515#section-4.1). 261 | // TODO: Implement verification for registered headers and support custom headers 262 | pub struct RegisteredHeader { 263 | /// Algorithms, as defined in [RFC 7518](https://tools.ietf.org/html/rfc7518), used to sign or encrypt the JWT 264 | /// Serialized to `alg`. 265 | /// Defined in [RFC7515#4.1.1](https://tools.ietf.org/html/rfc7515#section-4.1.1). 266 | #[serde(rename = "alg")] 267 | pub algorithm: SignatureAlgorithm, 268 | 269 | /// Media type of the complete JWS. Serialized to `typ`. 270 | /// Defined in [RFC7519#5.1](https://tools.ietf.org/html/rfc7519#section-5.1) and additionally 271 | /// [RFC7515#4.1.9](https://tools.ietf.org/html/rfc7515#section-4.1.9). 272 | /// The "typ" value "JOSE" can be used by applications to indicate that 273 | /// this object is a JWS or JWE using the JWS Compact Serialization or 274 | /// the JWE Compact Serialization. The "typ" value "JOSE+JSON" can be 275 | /// used by applications to indicate that this object is a JWS or JWE 276 | /// using the JWS JSON Serialization or the JWE JSON Serialization. 277 | /// Other type values can also be used by applications. 278 | #[serde(rename = "typ", skip_serializing_if = "Option::is_none")] 279 | pub media_type: Option, 280 | 281 | /// Content Type of the secured payload. 282 | /// Typically used to indicate the presence of a nested JOSE object which is signed or encrypted. 283 | /// Serialized to `cty`. 284 | /// Defined in [RFC7519#5.2](https://tools.ietf.org/html/rfc7519#section-5.2) and additionally 285 | /// [RFC7515#4.1.10](https://tools.ietf.org/html/rfc7515#section-4.1.10). 286 | #[serde(rename = "cty", skip_serializing_if = "Option::is_none")] 287 | pub content_type: Option, 288 | 289 | /// The JSON Web Key Set URL. This is currently not implemented (correctly). 290 | /// Serialized to `jku`. 291 | /// Defined in [RFC7515#4.1.2](https://tools.ietf.org/html/rfc7515#section-4.1.2). 292 | #[serde(rename = "jku", skip_serializing_if = "Option::is_none")] 293 | pub web_key_url: Option, 294 | 295 | /// The JSON Web Key. 296 | /// Serialized to `jwk`. 297 | /// Defined in [RFC7515#4.1.3](https://tools.ietf.org/html/rfc7515#section-4.1.3). 298 | #[serde(rename = "jwk", skip_serializing_if = "Option::is_none")] 299 | pub web_key: Option>, 300 | 301 | /// The Key ID. This is currently not implemented (correctly). 302 | /// Serialized to `kid`. 303 | /// Defined in [RFC7515#4.1.3](https://tools.ietf.org/html/rfc7515#section-4.1.3). 304 | #[serde(rename = "kid", skip_serializing_if = "Option::is_none")] 305 | pub key_id: Option, 306 | 307 | /// X.509 Public key cerfificate URL. This is currently not implemented (correctly). 308 | /// Serialized to `x5u`. 309 | /// Defined in [RFC7515#4.1.5](https://tools.ietf.org/html/rfc7515#section-4.1.5). 310 | #[serde(rename = "x5u", skip_serializing_if = "Option::is_none")] 311 | pub x509_url: Option, 312 | 313 | /// X.509 public key certificate chain. This is currently not implemented (correctly). 314 | /// Serialized to `x5c`. 315 | /// Defined in [RFC7515#4.1.6](https://tools.ietf.org/html/rfc7515#section-4.1.6). 316 | #[serde(rename = "x5c", skip_serializing_if = "Option::is_none")] 317 | pub x509_chain: Option>, 318 | 319 | /// X.509 Certificate thumbprint. This is currently not implemented (correctly). 320 | /// Also not implemented, is the SHA-256 thumbprint variant of this header. 321 | /// Serialized to `x5t`. 322 | /// Defined in [RFC7515#4.1.7](https://tools.ietf.org/html/rfc7515#section-4.1.7). 323 | // TODO: How to make sure the headers are mutually exclusive? 324 | #[serde(rename = "x5t", skip_serializing_if = "Option::is_none")] 325 | pub x509_fingerprint: Option, 326 | 327 | /// List of critical extended headers. 328 | /// This is currently not implemented (correctly). 329 | /// Serialized to `crit`. 330 | /// Defined in [RFC7515#4.1.11](https://tools.ietf.org/html/rfc7515#section-4.1.11). 331 | #[serde(rename = "crit", skip_serializing_if = "Option::is_none")] 332 | pub critical: Option>, 333 | } 334 | 335 | impl Default for RegisteredHeader { 336 | fn default() -> RegisteredHeader { 337 | RegisteredHeader { 338 | algorithm: SignatureAlgorithm::default(), 339 | media_type: Some("JWT".to_string()), 340 | content_type: None, 341 | web_key_url: None, 342 | web_key: None, 343 | key_id: None, 344 | x509_url: None, 345 | x509_chain: None, 346 | x509_fingerprint: None, 347 | critical: None, 348 | } 349 | } 350 | } 351 | 352 | #[cfg(test)] 353 | mod tests { 354 | use super::RegisteredHeader; 355 | 356 | #[test] 357 | fn header_serialization_round_trip_no_optional() { 358 | let expected = RegisteredHeader::default(); 359 | let expected_json = r#"{"alg":"HS256","typ":"JWT"}"#; 360 | 361 | let encoded = not_err!(serde_json::to_string(&expected)); 362 | assert_eq!(expected_json, encoded); 363 | 364 | let decoded: RegisteredHeader = not_err!(serde_json::from_str(&encoded)); 365 | assert_eq!(decoded, expected); 366 | } 367 | 368 | #[test] 369 | fn header_serialization_round_trip_with_optional() { 370 | let expected = RegisteredHeader { 371 | key_id: Some("kid".to_string()), 372 | ..Default::default() 373 | }; 374 | 375 | let expected_json = r#"{"alg":"HS256","typ":"JWT","kid":"kid"}"#; 376 | 377 | let encoded = not_err!(serde_json::to_string(&expected)); 378 | assert_eq!(expected_json, encoded); 379 | 380 | let decoded: RegisteredHeader = not_err!(serde_json::from_str(&encoded)); 381 | assert_eq!(decoded, expected); 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /doc/supported.md: -------------------------------------------------------------------------------- 1 | # Supported Features 2 | 3 | The crate, does not support all, and probably will never support all of 4 | the features described in the various RFCs, including some algorithms and verification. 5 | 6 | A checkmark (✔) usually indicates that the particular feature is supported in the library, although 7 | there might be caveats. Refer to the remark for the feature. 8 | 9 | A cross (✘) usually indicates that the feature is _not_ supported by the library. If there is no 10 | intention to ever support it, it will be noted in the remark. 11 | 12 | A field that can be serialized or deserialized by the library, but with no particular handling will 13 | be listed as unsupported. 14 | 15 | ## JWT Registered Claims 16 | 17 | JWT Registered Claims is defined in 18 | [Section 4 of RFC 7519](https://tools.ietf.org/html/rfc7519#section-4). 19 | 20 | | Registered Claim | Support | Remarks | 21 | |:----------------:|:-------:|:---------------------------:| 22 | | `iss` | ✔ | Validation is left to user. | 23 | | `sub` | ✔ | Validation is left to user. | 24 | | `aud` | ✔ | Validation is left to user. | 25 | | `exp` | ✔ | Validation provided. | 26 | | `nbf` | ✔ | Validation provided. | 27 | | `iat` | ✔ | Validation provided. | 28 | | `jti` | ✔ | Validation is left to user. | 29 | 30 | ## JWT Private Claims 31 | 32 | Optional private claims are supported as part of the [`biscuit::ClaimsSet`](https://lawliet89.github.io/biscuit/biscuit/struct.ClaimsSet.html) 33 | struct. (_as of v0.0.2_) 34 | 35 | ## JSON Web Key (JWK) 36 | 37 | JWK is defined in [RFC 7517](https://tools.ietf.org/html/rfc7517). 38 | 39 | Both `JWK` and `JWKSet`are supported (_as of v0.0.2_). 40 | 41 | [JWK Thumbprint](https://tools.ietf.org/html/rfc7638) is supported (_as of v0.5.0_). 42 | 43 | JWK Common Parameters are defined in 44 | [RFC 7517 Section 4](https://tools.ietf.org/html/rfc7517#section-4). 45 | 46 | Additional key type specific parameters are defined in 47 | [RFC 7518 Section 6](https://tools.ietf.org/html/rfc7518#section-6), and additionally in 48 | [RFC 8037](https://tools.ietf.org/html/rfc8037). 49 | 50 | JWK is currently not used in signing JWS, pending features in `ring`. See this 51 | [issue](https://github.com/briansmith/ring/issues/445) in `ring`. 52 | 53 | ### JWK Common Parameters 54 | 55 | | Parameter | Support | Remarks | 56 | |:----------:|:-------:|:----------------------------------------------------------------------------:| 57 | | `kty` | ✔ | Used during cryptographic operations to ensure the key is of the right type. | 58 | | `use` | ✘ | Can be (de)serialized; but usage is not validated. | 59 | | `key_ops` | ✘ | Can be (de)serialized; but key operation is not validated. | 60 | | `alg | ✘ | Can be (de)serialized; but usage with algorithm is not validated. | 61 | | `kid` | ✘ | Can be (de)serialized; but not processed. | 62 | | `x5u` | ✘ | Can be (de)serialized; but no processing is handled at the moment. | 63 | | `x5c` | ✘ | Can be (de)serialized; but no processing is handled at the moment. | 64 | | `x5t` | ✘ | Can be (de)serialized; but no processing is handled at the moment. | 65 | | `x5t#S256` | ✘ | Cannot be (de)serialized. | 66 | 67 | #### JWK Key Types 68 | 69 | The list of key types can be found 70 | [here](https://www.iana.org/assignments/jose/jose.xhtml#web-key-types). 71 | 72 | _Support in the table below simply means that they can be (de)serialized from JSON input._ Support 73 | for their use with the various algorithms is listed in the relevant section on this page. 74 | 75 | | Key Type | Support | Remarks | 76 | |:--------:|:-------:|:-------:| 77 | | `EC` | ✔ | | 78 | | `RSA` | ✔ | | 79 | | `oct` | ✔ | | 80 | | `OKP` | ✔ | | 81 | 82 | ### JWK Parameters for Elliptic Curve Keys 83 | 84 | | Parameter | Support | Remarks | 85 | |:---------:|:-------:|:------------------------------------------------------------------------------------------------------:| 86 | | `crv` | ✘ | Can be (de)serialized; but cannot be used in signing and verification yet pending support from `ring`. | 87 | | `x` | ✘ | Can be (de)serialized; but cannot be used in signing and verification yet pending support from `ring`. | 88 | | `y` | ✘ | Can be (de)serialized; but cannot be used in signing and verification yet pending support from `ring`. | 89 | 90 | #### JWK Elliptic Curve 91 | 92 | The list of key types can be found 93 | [here](https://www.iana.org/assignments/jose/jose.xhtml#web-key-elliptic-curve). 94 | 95 | _Support in the table below simply means that they can be (de)serialized from JSON input._ Support 96 | for their use with the various algorithms is listed in the relevant section on this page. 97 | 98 | | Key Type | Support | Remarks | 99 | |:--------:|:-------:|:--------------------------------------------------------------------:| 100 | | `P-256` | ✔ | | 101 | | `P-384` | ✔ | | 102 | | `P-521` | ✘ | [No plan to support.](https://github.com/briansmith/ring/issues/268) | 103 | 104 | ### JWK Parameters for RSA Keys 105 | 106 | | Parameter | Support | Remarks | 107 | |:-----------:|:-------:|--------------------------------------------------------------------------------------------------------| 108 | | `n` | ✘ | Can be (de)serialized; but cannot be used in signing and verification yet pending support from `ring`. | 109 | | `e` | ✘ | Can be (de)serialized; but cannot be used in signing and verification yet pending support from `ring`. | 110 | | `d` | ✘ | Can be (de)serialized; but cannot be used in signing and verification yet pending support from `ring`. | 111 | | `p` | ✘ | Can be (de)serialized; but cannot be used in signing and verification yet pending support from `ring`. | 112 | | `q` | ✘ | Can be (de)serialized; but cannot be used in signing and verification yet pending support from `ring`. | 113 | | `dp` | ✘ | Can be (de)serialized; but cannot be used in signing and verification yet pending support from `ring`. | 114 | | `dq` | ✘ | Can be (de)serialized; but cannot be used in signing and verification yet pending support from `ring`. | 115 | | `qi` | ✘ | Can be (de)serialized; but cannot be used in signing and verification yet pending support from `ring`. | 116 | | `oth` | ✘ | Can be (de)serialized; but cannot be used in signing and verification yet pending support from `ring`. | 117 | | `oth` → `r` | ✘ | Can be (de)serialized; but cannot be used in signing and verification yet pending support from `ring`. | 118 | | `oth` → `d` | ✘ | Can be (de)serialized; but cannot be used in signing and verification yet pending support from `ring`. | 119 | | `oth` → `t` | ✘ | Can be (de)serialized; but cannot be used in signing and verification yet pending support from `ring`. | 120 | 121 | ### JWK Parameters for Symmetric Keys 122 | 123 | | Parameter | Support | Remarks | 124 | |:---------:|:-------:|:-------:| 125 | | `k` | ✔ | | 126 | 127 | ### JWK Parameters for Octet Key Pair 128 | 129 | | Parameter | Support | Remarks | 130 | |:---------:|:-------:|:-------:| 131 | | `crv` | ✘ | | 132 | | `x` | ✘ | | 133 | | `d` | ✘ | | 134 | 135 | #### JWK Elliptic Curve 136 | 137 | The list of key types can be found 138 | [here](https://www.iana.org/assignments/jose/jose.xhtml#web-key-elliptic-curve). 139 | 140 | _Support in the table below simply means that they can be (de)serialized from JSON input._ Support 141 | for their use with the various algorithms is listed in the relevant section on this page. 142 | 143 | | Key Type | Support | Remarks | 144 | |:---------:|:-------:|:-------:| 145 | | `Ed25519` | ✘ | | 146 | | `Ed448` | ✘ | | 147 | | `X25519` | ✘ | | 148 | | `X448` | ✘ | | 149 | 150 | ## JSON Web Signature (JWS) 151 | 152 | JWS is defined in [RFC 7515](https://tools.ietf.org/html/rfc7515). 153 | 154 | [JWS Unencoded Payload Option](https://tools.ietf.org/html/rfc7797) is not supported. 155 | 156 | ## JWS Registered Headers 157 | 158 | The headers are defined in [RFC 7515 Section 4](https://tools.ietf.org/html/rfc7515#section-4), and 159 | the `b64` header is defined in [RFC 7797 Section 3](https://tools.ietf.org/html/rfc7797#section-3). 160 | 161 | A list can be found 162 | [here](https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-header-parameters). 163 | 164 | | Registered Header | Support | Remarks | 165 | |:-----------------:|:-------:|:------------------------------------------------------------------:| 166 | | `alg` | ✔ | Not all algorithms supported — see below. | 167 | | `jku` | ✘ | Can be (de)serialized, but no processing is handled at the moment. | 168 | | `jwk` | ✘ | Can be (de)serialized, but no processing is handled at the moment. | 169 | | `kid` | ✘ | Can be (de)serialized, but no processing is handled at the moment. | 170 | | `x5u` | ✘ | Can be (de)serialized, but no processing is handled at the moment. | 171 | | `x5c` | ✘ | Can be (de)serialized, but no processing is handled at the moment. | 172 | | `x5t` | ✘ | Can be (de)serialized, but no processing is handled at the moment. | 173 | | `x5t#S256` | ✘ | Cannot be (de)serialized. | 174 | | `typ` | ✘ | Can be (de)serialized, but no processing is handled at the moment. | 175 | | `cty` | ✘ | Can be (de)serialized, but no processing is handled at the moment. | 176 | | `crit` | ✘ | Can be (de)serialized, but no processing is handled at the moment. | 177 | | `b64` | ✘ | Cannot be (de)serialized. | 178 | 179 | ## JWS Private Headers 180 | 181 | Supported as part of 182 | [`biscuit::jws::Header`](https://lawliet89.github.io/biscuit/biscuit/jws/struct.Header.html) 183 | (_as of v0.0.2_) 184 | 185 | ### JWS Algorithms 186 | 187 | The algorithms are described [here](https://tools.ietf.org/html/rfc7518#section-3) and additionally 188 | [here](https://tools.ietf.org/html/rfc8037). 189 | 190 | A list can be found 191 | [here](https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms). 192 | 193 | | Algorithm | Support | Remarks | 194 | |:---------:|:-------:|:--------------------------------------------------------------------:| 195 | | `none` | ✔ | | 196 | | `HS256` | ✔ | | 197 | | `HS384` | ✔ | | 198 | | `HS512` | ✔ | | 199 | | `RS256` | ✔ | | 200 | | `RS384` | ✔ | | 201 | | `RS512` | ✔ | | 202 | | `ES256` | ✘ | Only verification of signature | 203 | | `ES384` | ✘ | [No plan to support.](https://github.com/briansmith/ring/issues/268) | 204 | | `ES512` | ✘ | Only verification of signature | 205 | | `PS256` | ✔ | | 206 | | `PS384` | ✔ | | 207 | | `PS512` | ✔ | | 208 | | `EdDSA` | ✘ | | 209 | 210 | ### JWS Serialization 211 | 212 | | Format | Support | Remarks | 213 | |----------------|---------|--------------------| 214 | | Compact | ✔ | | 215 | | General JSON | ✘ | | 216 | | Flattened JSON | ✔ | As of v0.6.0 | 217 | 218 | ## JSON Web Encryption (JWE) 219 | 220 | JWE is defined in [RFC 7516](https://tools.ietf.org/html/rfc7516), and supported since v0.0.2. 221 | 222 | ### JWE Registered Headers 223 | 224 | The headers are defined in [RFC 7516 Section 4](https://tools.ietf.org/html/rfc7516#section-4). 225 | 226 | A list can be found 227 | [here](https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-header-parameters). 228 | 229 | | Registered Header | Support | Remarks | 230 | |:-----------------:|:-------:|--------------------------------------------------------------------| 231 | | `alg` | ✔ | Not all algorithms supported — see below. | 232 | | `enc` | ✔ | Not all algorithms supported — see below. | 233 | | `zip` | ✘ | Can be (de)serialized; but no compression us supported. | 234 | | `jku` | ✘ | Can be (de)serialized; but no processing is handled at the moment. | 235 | | `jwk` | ✘ | Can be (de)serialized; but no processing is handled at the moment. | 236 | | `kid` | ✘ | Can be (de)serialized; but no processing is handled at the moment. | 237 | | `x5u` | ✘ | Can be (de)serialized; but no processing is handled at the moment. | 238 | | `x5c` | ✘ | Can be (de)serialized; but no processing is handled at the moment. | 239 | | `x5t` | ✘ | Can be (de)serialized; but no processing is handled at the moment. | 240 | | `x5t#S256` | ✘ | Cannot be (de)serialized. | 241 | | `typ` | ✘ | Can be (de)serialized; but no processing is handled at the moment. | 242 | | `cty` | ✘ | Can be (de)serialized; but no processing is handled at the moment. | 243 | | `crit` | ✘ | Can be (de)serialized; but no processing is handled at the moment. | 244 | | `iss` | ✘ | Cannot be (de)serialized. | 245 | | `sub` | ✘ | Cannot be (de)serialized. | 246 | | `aud` | ✘ | Cannot be (de)serialized. | 247 | 248 | ### JWE Private Headers 249 | 250 | Supported as part of 251 | [`biscuit::jwe::Header`](https://lawliet89.github.io/biscuit/biscuit/jwe/struct.Header.html) 252 | (_as of v0.0.2_) 253 | 254 | ### JWE Header Parameters Used for ECDH Key Agreement 255 | 256 | This is defined in [RFC 7518 Section 4.6.1](https://tools.ietf.org/html/rfc7518#section-4.6.1). 257 | 258 | A list can be found 259 | [here](https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-header-parameters). 260 | 261 | | Parameter | Support | Remarks | 262 | |:---------:|:-------:|:-------:| 263 | | `epk` | ✘ | | 264 | | `apu` | ✘ | | 265 | | `apv` | ✘ | | 266 | 267 | ### JWE Header Parameters Used for AES GCM Key Encryption 268 | 269 | This is defined in [RFC 7518 Section 4.7.1](https://tools.ietf.org/html/rfc7518#section-4.7.1). 270 | 271 | A list can be found 272 | [here](https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-header-parameters). 273 | 274 | | Parameter | Support | Remarks | 275 | |:---------:|:-------:|:-------:| 276 | | `iv` | ✔ | | 277 | | `tag` | ✔ | | 278 | 279 | 280 | ### JWE Header Parameters Used for PBES2 Key Encryption 281 | 282 | This is defined in [RFC 7518 Section 4.8.1](https://tools.ietf.org/html/rfc7518#section-4.8.1). 283 | 284 | A list can be found 285 | [here](https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-header-parameters). 286 | 287 | | Parameter | Support | Remarks | 288 | |:---------:|:-------:|:-------:| 289 | | `p2s` | ✘ | | 290 | | `p2c` | ✘ | | 291 | 292 | ### JWE Algorithms for Key Management 293 | 294 | The algorithms are described [here](https://tools.ietf.org/html/rfc7518#section-4) and additionally 295 | [here](https://tools.ietf.org/html/rfc8037). 296 | 297 | A list can be found 298 | [here](https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms). 299 | 300 | | Algorithm | Support | Remarks | 301 | |:--------------------:|:-------:|:----------------------------------------------------------------------------------------------------------------------:| 302 | | `RSA1_5` | ✘ | | 303 | | `RSA-OAEP` | ✘ | | 304 | | `RSA-OAEP-256` | ✘ | | 305 | | `A128KW` | ✘ | | 306 | | `A192KW` | ✘ | | 307 | | `A256KW` | ✘ | | 308 | | `dir` | ✔ | | 309 | | `ECDH-ES` | ✘ | | 310 | | `ECDH-ES+A128KW` | ✘ | | 311 | | `ECDH-ES+A192KW` | ✘ | | 312 | | `ECDH-ES+A256KW` | ✘ | | 313 | | `A128GCMKW` | ✔ | | 314 | | `A192GCMKW` | ✘ | Probably will never be supported — see [comment](https://github.com/briansmith/ring/issues/112#issuecomment-291755372) | 315 | | `A256GCMKW` | ✔ | | 316 | | `PBES2-HS256+A128KW` | ✘ | | 317 | | `PBES2-HS384+A192KW` | ✘ | | 318 | | `PBES2-HS512+A256KW` | ✘ | | 319 | 320 | ### JWE Algorithms for Content Encryption 321 | 322 | The algorithms are described [here](https://tools.ietf.org/html/rfc7518#section-5) and additionally 323 | [here](https://tools.ietf.org/html/rfc8037). 324 | 325 | A list can be found 326 | [here](https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms). 327 | 328 | | Algorithm | Support | Remarks | 329 | |:---------------:|:-------:|:----------------------------------------------------------------------------------------------------------------------:| 330 | | `A128CBC-HS256` | ✘ | | 331 | | `A192CBC-HS384` | ✘ | | 332 | | `A256CBC-HS512` | ✘ | | 333 | | `A128GCM` | ✔ | | 334 | | `A192GCM` | ✘ | Probably will never be supported — see [comment](https://github.com/briansmith/ring/issues/112#issuecomment-291755372) | 335 | | `A256GCM` | ✔ | | 336 | 337 | ### JWE Serialization 338 | 339 | | Format | Support | Remarks | 340 | |----------------|---------|---------| 341 | | Compact | ✔ | | 342 | | General JSON | ✘ | | 343 | | Flattened JSON | ✘ | | 344 | -------------------------------------------------------------------------------- /src/jws/flattened.rs: -------------------------------------------------------------------------------- 1 | //! Flattened JWS signatures: see RFC 7515 section 7.2.2 2 | //! Flattened signatures are JSON (unlike compact signatures), 3 | //! and support a single signature protecting a set of headers and a 4 | //! payload. 5 | //! 6 | //! The RFC specifies unprotected headers as well, but this implementation 7 | //! doesn't support them. 8 | 9 | use super::{Header, RegisteredHeader, Secret}; 10 | use crate::errors::{Error, ValidationError}; 11 | use crate::jwa::SignatureAlgorithm; 12 | use crate::serde_custom; 13 | use data_encoding::BASE64URL_NOPAD; 14 | use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize}; 15 | 16 | // Not using CompactPart::to_bytes here, bounds are overly restrictive 17 | fn serialize_header(header: &Header) -> Result, serde_json::Error> { 18 | // I don't think RegisteredHeader can fail to serialize, 19 | // but the private header fields are user controlled and might 20 | serde_json::to_vec(header) 21 | } 22 | 23 | // Warning: pay attention to parameter order 24 | // Note: this is valid UTF-8, but gets used as bytes later 25 | fn signing_input(protected_header: &[u8], payload: &[u8]) -> Vec { 26 | let hlen = BASE64URL_NOPAD.encode_len(protected_header.len()); 27 | let plen = BASE64URL_NOPAD.encode_len(payload.len()); 28 | let mut r = Vec::with_capacity(hlen + plen + 1); 29 | r.append(&mut BASE64URL_NOPAD.encode(protected_header).into_bytes()); 30 | r.push(b'.'); 31 | r.append(&mut BASE64URL_NOPAD.encode(payload).into_bytes()); 32 | r 33 | } 34 | 35 | /// Data that can be turned into a JWS 36 | /// 37 | /// This struct ensures that the serialized data is stable; 38 | /// [`Signable::protected_header_serialized`] and [`Signable::payload`] 39 | /// will always return the same bytes; [`Signable::protected_header_registered`] 40 | /// will always return a reference to the same [`RegisteredHeader`] 41 | /// struct. 42 | /// 43 | /// This allows [`SignedData`] to retain the data as it was signed, 44 | /// carrying a signature that remains verifiable. 45 | /// 46 | /// # Examples 47 | /// ``` 48 | /// use biscuit::jws::{Header, RegisteredHeader, Signable}; 49 | /// use biscuit::jwa::SignatureAlgorithm; 50 | /// use biscuit::Empty; 51 | /// let header = Header::::from(RegisteredHeader { 52 | /// algorithm: SignatureAlgorithm::ES256, 53 | /// ..Default::default() 54 | /// }); 55 | /// let payload = b"These bytes cannot be altered"; 56 | /// let data = Signable::new(header, payload.to_vec())?; 57 | /// # Ok::<(), serde_json::Error>(()) 58 | /// ``` 59 | #[derive(Debug, Clone)] 60 | pub struct Signable { 61 | // We need both fields for the protected header 62 | // so we can trust that signed data is stable 63 | protected_header_registered: RegisteredHeader, 64 | protected_header_serialized: Vec, 65 | payload: Vec, 66 | } 67 | 68 | impl Signable { 69 | /// Build a Signable from a header and a payload 70 | /// 71 | /// Header and payload will both be protected by the signature, 72 | /// we do not make use of unprotected headers 73 | /// 74 | /// # Errors 75 | /// Errors are returned if headers can't be serialized; 76 | /// this would only happen if the `H` type carrying extension headers 77 | /// can not be serialized. 78 | pub fn new( 79 | header: Header, 80 | payload: Vec, 81 | ) -> Result { 82 | let protected_header_serialized = serialize_header(&header)?; 83 | let protected_header_registered = header.registered; 84 | Ok(Self { 85 | protected_header_registered, 86 | protected_header_serialized, 87 | payload, 88 | }) 89 | } 90 | 91 | /// Convenience function to build a SignedData from this Signable 92 | /// See [`SignedData::sign`] 93 | pub fn sign(self, secret: Secret) -> Result { 94 | SignedData::sign(self, secret) 95 | } 96 | 97 | /// JWS Signing Input 98 | fn signing_input(&self) -> Vec { 99 | signing_input(&self.protected_header_serialized, &self.payload) 100 | } 101 | 102 | /// Return a reference to the registered (known to biscuit) 103 | /// protected headers 104 | pub fn protected_header_registered(&self) -> &RegisteredHeader { 105 | &self.protected_header_registered 106 | } 107 | 108 | /// Return a reference to protected headers as they were serialized 109 | pub fn protected_header_serialized(&self) -> &[u8] { 110 | &self.protected_header_serialized 111 | } 112 | 113 | /// Deserialize protected headers 114 | /// 115 | /// This allows access to protected headers beyond those 116 | /// that are recognized with RegisteredHeader 117 | pub fn deserialize_protected_header( 118 | &self, 119 | ) -> serde_json::Result> { 120 | serde_json::from_slice(&self.protected_header_serialized) 121 | } 122 | 123 | /// Return a reference to the payload bytes 124 | pub fn payload(&self) -> &[u8] { 125 | &self.payload 126 | } 127 | 128 | /// Deserialize a JSON payload 129 | /// 130 | /// # Note 131 | /// JWS does not put any requirement on payload bytes, which 132 | /// need not be JSON 133 | pub fn deserialize_json_payload(&self) -> serde_json::Result { 134 | serde_json::from_slice(&self.payload) 135 | } 136 | } 137 | 138 | /// Signed data (with a single signature) 139 | /// 140 | /// This representation preserves the exact serialisation of 141 | /// the payload and protected headers, but it is independent of how 142 | /// the signature may be serialized (eg, flattened or compact JWS) 143 | /// 144 | /// Signed data can be obtained by either deserializing a valid JWS, 145 | /// or by signing a Signable 146 | #[derive(Clone)] 147 | pub struct SignedData { 148 | data: Signable, 149 | #[allow(dead_code)] 150 | secret: Secret, 151 | signature: Vec, 152 | } 153 | 154 | impl SignedData { 155 | /// Sign using a secret 156 | /// 157 | /// # Example 158 | /// ``` 159 | /// use biscuit::jwa::SignatureAlgorithm; 160 | /// use biscuit::jws::{Header, RegisteredHeader, Secret, Signable, SignedData}; 161 | /// use biscuit::Empty; 162 | /// use ring::signature::{ECDSA_P256_SHA256_FIXED_SIGNING, EcdsaKeyPair}; 163 | /// use std::sync::Arc; 164 | /// 165 | /// let header = Header::::from(RegisteredHeader { 166 | /// algorithm: SignatureAlgorithm::ES256, 167 | /// ..Default::default() 168 | /// }); 169 | /// let payload = b"These bytes cannot be altered"; 170 | /// let data = Signable::new(header, payload.to_vec())?; 171 | /// let pkcs8 = EcdsaKeyPair::generate_pkcs8( 172 | /// &ECDSA_P256_SHA256_FIXED_SIGNING, 173 | /// &ring::rand::SystemRandom::new())?; 174 | /// let keypair = EcdsaKeyPair::from_pkcs8( 175 | /// &ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), 176 | /// &ring::rand::SystemRandom::new())?; 177 | /// let secret = Secret::EcdsaKeyPair(Arc::new(keypair)); 178 | /// let signed = SignedData::sign(data, secret)?; 179 | /// # Ok::<(), biscuit::errors::Error>(()) 180 | /// ``` 181 | pub fn sign(data: Signable, secret: Secret) -> Result { 182 | let signature = data 183 | .protected_header_registered 184 | .algorithm 185 | .sign(&data.signing_input(), &secret)?; 186 | Ok(Self { 187 | data, 188 | secret, 189 | signature, 190 | }) 191 | } 192 | 193 | /// Serialize using Flattened JWS JSON Serialization 194 | /// 195 | /// See [RFC 7515 section 7.2.2](https://tools.ietf.org/html/rfc7515#section-7.2.2) 196 | pub fn serialize_flattened(&self) -> String { 197 | let payload = self.data.payload.clone(); 198 | let protected_header = self.data.protected_header_serialized.clone(); 199 | let signature = self.signature.clone(); 200 | let s = FlattenedRaw { 201 | payload, 202 | protected_header, 203 | signature, 204 | signatures: (), 205 | unprotected_header: (), 206 | }; 207 | // This shouldn't fail, because FlattenedRaw strucs are 208 | // always representable in JSON 209 | serde_json::to_string(&s).expect("Failed to serialize FlattenedRaw to JSON") 210 | } 211 | 212 | /// Verify a Flattened JWS JSON Serialization carries a valid signature 213 | /// 214 | /// # Example 215 | /// ``` 216 | /// use biscuit::jwa::SignatureAlgorithm; 217 | /// use biscuit::jws::{Secret, SignedData}; 218 | /// use data_encoding::HEXUPPER; 219 | /// let public_key = 220 | /// "043727F96AAD416887DD75CC2E333C3D8E06DCDF968B6024579449A2B802EFC891F638C75\ 221 | /// 1CF687E6FF9A280E11B7036585E60CA32BB469C3E57998A289E0860A6"; 222 | /// let jwt = "{\ 223 | /// \"payload\":\"eyJ0b2tlbl90eXBlIjoic2VydmljZSIsImlhdCI6MTQ5MjkzODU4OH0\",\ 224 | /// \"protected\":\"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9\",\ 225 | /// \"signature\":\"do_XppIOFthPWlTXL95CIBfgRdyAxbcIsUfM0YxMjCjqvp4ehHFA3I-JasABKzC8CAy4ndhCHsZdpAtKkqZMEA\"}"; 226 | /// let secret = Secret::PublicKey(HEXUPPER.decode(public_key.as_bytes()).unwrap()); 227 | /// let signed = SignedData::verify_flattened( 228 | /// jwt.as_bytes(), 229 | /// secret, 230 | /// SignatureAlgorithm::ES256 231 | /// )?; 232 | /// # Ok::<(), biscuit::errors::Error>(()) 233 | /// ``` 234 | pub fn verify_flattened( 235 | data: &[u8], 236 | secret: Secret, 237 | algorithm: SignatureAlgorithm, 238 | ) -> Result { 239 | let raw: FlattenedRaw = serde_json::from_slice(data)?; 240 | algorithm 241 | .verify(&raw.signature, &raw.signing_input(), &secret) 242 | .map_err(|_| ValidationError::InvalidSignature)?; 243 | let protected_header_registered: RegisteredHeader = 244 | serde_json::from_slice(&raw.protected_header)?; 245 | if protected_header_registered.algorithm != algorithm { 246 | Err(ValidationError::WrongAlgorithmHeader)?; 247 | } 248 | let data = Signable { 249 | protected_header_registered, 250 | protected_header_serialized: raw.protected_header, 251 | payload: raw.payload, 252 | }; 253 | Ok(Self { 254 | data, 255 | secret, 256 | signature: raw.signature, 257 | }) 258 | } 259 | 260 | /// Access the data protected by the signature 261 | pub fn data(&self) -> &Signable { 262 | &self.data 263 | } 264 | } 265 | 266 | fn deserialize_reject<'de, D>(_de: D) -> Result<(), D::Error> 267 | where 268 | D: Deserializer<'de>, 269 | { 270 | Err(serde::de::Error::custom("invalid field")) 271 | } 272 | 273 | /// This is for serialization, and deserialisation when the signature 274 | /// hasn't been verified, not exposed externally 275 | #[derive(Serialize, Deserialize)] 276 | struct FlattenedRaw { 277 | #[serde(rename = "protected", with = "serde_custom::byte_sequence")] 278 | protected_header: Vec, 279 | 280 | #[serde(with = "serde_custom::byte_sequence")] 281 | payload: Vec, 282 | 283 | #[serde(with = "serde_custom::byte_sequence")] 284 | signature: Vec, 285 | 286 | // These fields must be understood and rejected 287 | // (unlike unknown fields, which must be ignored) 288 | // This member indicates non-flattened, generalized signatures 289 | #[serde(default, deserialize_with = "deserialize_reject", skip_serializing)] 290 | #[allow(dead_code)] 291 | signatures: (), 292 | 293 | // Headers unprotected by the signature are rejected 294 | #[serde( 295 | rename = "header", 296 | default, 297 | deserialize_with = "deserialize_reject", 298 | skip_serializing 299 | )] 300 | #[allow(dead_code)] 301 | unprotected_header: (), 302 | } 303 | 304 | impl FlattenedRaw { 305 | /// JWS Signing Input 306 | fn signing_input(&self) -> Vec { 307 | signing_input(&self.protected_header, &self.payload) 308 | } 309 | } 310 | 311 | #[cfg(test)] 312 | mod tests { 313 | use std::str::{self, FromStr}; 314 | 315 | use serde::{Deserialize, Serialize}; 316 | 317 | use super::{Header, Secret, Signable, SignedData}; 318 | use crate::jwa::SignatureAlgorithm; 319 | use crate::jws::RegisteredHeader; 320 | use crate::{ClaimsSet, CompactJson, CompactPart, Empty, RegisteredClaims, SingleOrMultiple}; 321 | 322 | #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] 323 | struct PrivateClaims { 324 | company: String, 325 | department: String, 326 | } 327 | 328 | impl CompactJson for PrivateClaims {} 329 | 330 | // HS256 key - "secret" 331 | static HS256_PAYLOAD: &str = "{\"protected\":\"eyJhbGciOiJIUz\ 332 | I1NiIsInR5cCI6IkpXVCJ9\",\"payload\":\"eyJpc3MiOiJodHRwczovL3\ 333 | d3dy5hY21lLmNvbS8iLCJzdWIiOiJKb2huIERvZSIsImF1ZCI6Imh0dHBzOi8vYWNtZ\ 334 | S1jdXN0b21lci5jb20vIiwibmJmIjoxMjM0LCJjb21wYW55IjoiQUNNRSIsImRlcGFy\ 335 | dG1lbnQiOiJUb2lsZXQgQ2xlYW5pbmcifQ\",\"signature\":\"VFCl2un1Kc17odzOe2Ehf4DVrW\ 336 | ddu3U4Ux3GFpOZHtc\"}"; 337 | 338 | #[test] 339 | fn flattened_jws_round_trip_none() { 340 | let expected_value = not_err!(serde_json::to_value( 341 | "{\"protected\":\"eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0\",\ 342 | \"payload\":\"eyJpc3MiOiJodHRwczovL3d3dy5hY21lLmNvbS8iLCJz\ 343 | dWIiOiJKb2huIERvZSIsImF1ZCI6Imh0dHBzOi8vYWNtZS1jdXN0b21lci5j\ 344 | b20vIiwibmJmIjoxMjM0LCJjb21wYW55IjoiQUNNRSIsImRlcGFydG1lbnQi\ 345 | OiJUb2lsZXQgQ2xlYW5pbmcifQ\",\ 346 | \"signature\":\"\"}" 347 | )); 348 | 349 | let expected_claims = ClaimsSet:: { 350 | registered: RegisteredClaims { 351 | issuer: Some(not_err!(FromStr::from_str("https://www.acme.com/"))), 352 | subject: Some(not_err!(FromStr::from_str("John Doe"))), 353 | audience: Some(SingleOrMultiple::Single(not_err!(FromStr::from_str( 354 | "https://acme-customer.com/" 355 | )))), 356 | not_before: Some(1234.into()), 357 | ..Default::default() 358 | }, 359 | private: PrivateClaims { 360 | department: "Toilet Cleaning".to_string(), 361 | company: "ACME".to_string(), 362 | }, 363 | }; 364 | 365 | let expected_jwt = not_err!(SignedData::sign( 366 | not_err!(Signable::new::( 367 | From::from(RegisteredHeader { 368 | algorithm: SignatureAlgorithm::None, 369 | ..Default::default() 370 | }), 371 | expected_claims.to_bytes().unwrap(), 372 | )), 373 | Secret::None, 374 | )); 375 | let token = expected_jwt.serialize_flattened(); 376 | assert_eq!(expected_value, not_err!(serde_json::to_value(&token))); 377 | 378 | let biscuit: SignedData = not_err!(SignedData::verify_flattened( 379 | token.as_bytes(), 380 | Secret::None, 381 | SignatureAlgorithm::None, 382 | )); 383 | let actual_claims: ClaimsSet = 384 | not_err!(biscuit.data().deserialize_json_payload()); 385 | assert_eq!(&expected_claims, &actual_claims); 386 | } 387 | 388 | #[test] 389 | fn flattened_jws_round_trip_hs256() { 390 | let expected_claims = ClaimsSet:: { 391 | registered: RegisteredClaims { 392 | issuer: Some(not_err!(FromStr::from_str("https://www.acme.com/"))), 393 | subject: Some(not_err!(FromStr::from_str("John Doe"))), 394 | audience: Some(SingleOrMultiple::Single(not_err!(FromStr::from_str( 395 | "https://acme-customer.com/" 396 | )))), 397 | not_before: Some(1234.into()), 398 | ..Default::default() 399 | }, 400 | private: PrivateClaims { 401 | department: "Toilet Cleaning".to_string(), 402 | company: "ACME".to_string(), 403 | }, 404 | }; 405 | 406 | let expected_jwt = not_err!(SignedData::sign( 407 | not_err!(Signable::new( 408 | From::from(RegisteredHeader { 409 | algorithm: SignatureAlgorithm::HS256, 410 | ..Default::default() 411 | }), 412 | expected_claims.to_bytes().unwrap(), 413 | )), 414 | Secret::Bytes("secret".to_string().into_bytes()) 415 | )); 416 | let token = expected_jwt.serialize_flattened(); 417 | assert_eq!( 418 | not_err!(serde_json::to_value(HS256_PAYLOAD)), 419 | not_err!(serde_json::to_value(&token)) 420 | ); 421 | 422 | let biscuit = not_err!(SignedData::verify_flattened( 423 | token.as_bytes(), 424 | Secret::Bytes("secret".to_string().into_bytes()), 425 | SignatureAlgorithm::HS256 426 | )); 427 | assert_eq!( 428 | &expected_claims, 429 | ¬_err!(biscuit.data().deserialize_json_payload()) 430 | ); 431 | } 432 | 433 | #[test] 434 | fn flattened_jws_round_trip_rs256() { 435 | let expected_value = not_err!(serde_json::to_value( 436 | "{\"protected\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9\",\ 437 | \"payload\":\"eyJpc3MiOiJodHRwczovL3d3dy5hY21lLmNvbS8iLCJ\ 438 | zdWIiOiJKb2huIERvZSIsImF1ZCI6Imh0dHBzOi8vYWNtZS1jdXN0b21lci\ 439 | 5jb20vIiwibmJmIjoxMjM0LCJjb21wYW55IjoiQUNNRSIsImRlcGFydG1lb\ 440 | nQiOiJUb2lsZXQgQ2xlYW5pbmcifQ\",\ 441 | \"signature\":\"Gat3NBUTaCyvroil66U0nId4-l6VqbtJYIsM9wRbWo4\ 442 | 5oYoN-NxYIyl8M-9AlEPseg-4SIuo-A-jccJOWGeWWwy-Een_92wg18II58\ 443 | luHz7vAyclw1maJBKHmuj8f2wE_Ky8ir3iTpTGkJQ3IUU9SuU9Fkvajm4jg\ 444 | WUtRPpjHm_IqyxV8NkHNyN0p5CqeuRC8sZkOSFkm9b0WnWYRVls1QOjBnN9\ 445 | w9zW9wg9DGwj10pqg8hQ5sy-C3J-9q1zJgGDXInkhPLjitO9wzWg4yfVt-C\ 446 | JNiHsJT7RY_EN2VmbG8UOjHp8xUPpfqUKyoQttKaQkJHdjP_b47LO4ZKI4U\ 447 | ivlA\"}" 448 | )); 449 | 450 | let expected_claims = ClaimsSet:: { 451 | registered: RegisteredClaims { 452 | issuer: Some(not_err!(FromStr::from_str("https://www.acme.com/"))), 453 | subject: Some(not_err!(FromStr::from_str("John Doe"))), 454 | audience: Some(SingleOrMultiple::Single(not_err!(FromStr::from_str( 455 | "https://acme-customer.com/" 456 | )))), 457 | not_before: Some(1234.into()), 458 | ..Default::default() 459 | }, 460 | private: PrivateClaims { 461 | department: "Toilet Cleaning".to_string(), 462 | company: "ACME".to_string(), 463 | }, 464 | }; 465 | let private_key = 466 | Secret::rsa_keypair_from_file("test/fixtures/rsa_private_key.der").unwrap(); 467 | 468 | let expected_jwt = not_err!(SignedData::sign( 469 | not_err!(Signable::new( 470 | From::from(RegisteredHeader { 471 | algorithm: SignatureAlgorithm::RS256, 472 | ..Default::default() 473 | }), 474 | expected_claims.to_bytes().unwrap(), 475 | )), 476 | private_key, 477 | )); 478 | let token = expected_jwt.serialize_flattened(); 479 | assert_eq!(expected_value, not_err!(serde_json::to_value(&token))); 480 | 481 | let public_key = Secret::public_key_from_file("test/fixtures/rsa_public_key.der").unwrap(); 482 | let biscuit = not_err!(SignedData::verify_flattened( 483 | token.as_bytes(), 484 | public_key, 485 | SignatureAlgorithm::RS256, 486 | )); 487 | assert_eq!( 488 | expected_claims, 489 | not_err!(biscuit.data().deserialize_json_payload()) 490 | ); 491 | } 492 | 493 | #[test] 494 | fn flattened_jws_verify_es256() { 495 | use data_encoding::HEXUPPER; 496 | 497 | // This is a ECDSA Public key in `SubjectPublicKey` form. 498 | // Conversion is not available in `ring` yet. 499 | // See https://github.com/lawliet89/biscuit/issues/71#issuecomment-296445140 for a 500 | // way to retrieve it from `SubjectPublicKeyInfo`. 501 | let public_key = 502 | "043727F96AAD416887DD75CC2E333C3D8E06DCDF968B6024579449A2B802EFC891F638C75\ 503 | 1CF687E6FF9A280E11B7036585E60CA32BB469C3E57998A289E0860A6"; 504 | let jwt = "{\ 505 | \"payload\":\"eyJ0b2tlbl90eXBlIjoic2VydmljZSIsImlhdCI6MTQ5MjkzODU4OH0\",\ 506 | \"protected\":\"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9\",\ 507 | \"signature\":\"do_XppIOFthPWlTXL95CIBfgRdyAxbcIsUfM0YxMjCjqvp4ehHFA3I-JasABKzC8CAy4ndhCHsZdpAtKkqZMEA\"}"; 508 | let signing_secret = Secret::PublicKey(not_err!(HEXUPPER.decode(public_key.as_bytes()))); 509 | 510 | let token = not_err!(SignedData::verify_flattened( 511 | jwt.as_bytes(), 512 | signing_secret, 513 | SignatureAlgorithm::ES256 514 | )); 515 | let jwt_val: super::FlattenedRaw = not_err!(serde_json::from_str(jwt)); 516 | assert_eq!(jwt_val.payload.as_slice(), token.data().payload()); 517 | assert_eq!( 518 | jwt_val.protected_header.as_slice(), 519 | token.data().protected_header_serialized() 520 | ); 521 | assert_eq!(&jwt_val.signature, &token.signature); 522 | } 523 | 524 | #[test] 525 | fn flattened_jws_encode_with_additional_header_fields() { 526 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 527 | struct CustomHeader { 528 | something: String, 529 | } 530 | 531 | let expected_claims = ClaimsSet:: { 532 | registered: RegisteredClaims { 533 | issuer: Some(not_err!(FromStr::from_str("https://www.acme.com/"))), 534 | subject: Some(not_err!(FromStr::from_str("John Doe"))), 535 | audience: Some(SingleOrMultiple::Single(not_err!(FromStr::from_str( 536 | "https://acme-customer.com/" 537 | )))), 538 | not_before: Some(1234.into()), 539 | ..Default::default() 540 | }, 541 | private: PrivateClaims { 542 | department: "Toilet Cleaning".to_string(), 543 | company: "ACME".to_string(), 544 | }, 545 | }; 546 | 547 | let header = Header { 548 | registered: Default::default(), 549 | private: CustomHeader { 550 | something: "foobar".to_string(), 551 | }, 552 | }; 553 | 554 | let expected_jwt = not_err!(SignedData::sign( 555 | not_err!(Signable::new( 556 | header.clone(), 557 | expected_claims.to_bytes().unwrap() 558 | )), 559 | Secret::Bytes("secret".to_string().into_bytes()) 560 | )); 561 | let token = expected_jwt.serialize_flattened(); 562 | let biscuit = not_err!(SignedData::verify_flattened( 563 | token.as_bytes(), 564 | Secret::Bytes("secret".to_string().into_bytes()), 565 | SignatureAlgorithm::HS256, 566 | )); 567 | assert_eq!( 568 | &header, 569 | ¬_err!(biscuit 570 | .data() 571 | .deserialize_protected_header::()) 572 | ); 573 | } 574 | 575 | #[test] 576 | #[should_panic(expected = "InvalidSignature")] 577 | fn flattened_jws_decode_token_invalid_signature_hs256() { 578 | let claims = SignedData::verify_flattened( 579 | "{\"protected\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\",\ 580 | \"payload\":\"eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ\",\ 581 | \"signature\":\"pKscJVk7-aHxfmQKlaZxh5uhuKhGMAa-1F5IX5mfUwI\"}" 582 | .as_bytes(), 583 | Secret::Bytes("secret".to_string().into_bytes()), 584 | SignatureAlgorithm::HS256, 585 | ); 586 | let _ = claims.unwrap(); 587 | } 588 | 589 | #[test] 590 | #[should_panic(expected = "InvalidSignature")] 591 | fn flattened_jws_decode_token_invalid_signature_rs256() { 592 | let public_key = Secret::public_key_from_file("test/fixtures/rsa_public_key.der").unwrap(); 593 | let claims = SignedData::verify_flattened( 594 | "{\"protected\":\"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9\",\ 595 | \"payload\":\"eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ\",\ 596 | \"signature\":\"pKscJVk7-aHxfmQKlaZxh5uhuKhGMAa-1F5IX5mfUwI\"}" 597 | .as_bytes(), 598 | public_key, 599 | SignatureAlgorithm::RS256, 600 | ); 601 | let _ = claims.unwrap(); 602 | } 603 | 604 | #[test] 605 | #[should_panic(expected = "WrongAlgorithmHeader")] 606 | fn flattened_jws_decode_token_wrong_algorithm() { 607 | let claims = SignedData::verify_flattened( 608 | "{\"protected\":\"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9\",\ 609 | \"payload\":\"eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ\",\ 610 | \"signature\":\"pKscJVk7-aHxfmQKlaZxh5uhuKhGMAa-1F5IX5mfUwI\"}" 611 | .as_bytes(), 612 | Secret::Bytes("secret".to_string().into_bytes()), 613 | SignatureAlgorithm::HS256, 614 | ); 615 | let _ = claims.unwrap(); 616 | } 617 | 618 | #[test] 619 | #[should_panic(expected = "invalid field")] 620 | fn flattened_jws_verify_reject_multiple_signatures() { 621 | let _ = SignedData::verify_flattened( 622 | br#"{"signatures": [], "protected": "", "payload": "", "signature: ""}"#, 623 | Secret::None, 624 | SignatureAlgorithm::None, 625 | ) 626 | .unwrap(); 627 | } 628 | 629 | #[test] 630 | #[should_panic(expected = "invalid field")] 631 | fn flattened_jws_verify_reject_unprotected_headers() { 632 | let _ = SignedData::verify_flattened( 633 | br#"{"header": "", "protected": "", "payload": "", "signature: ""}"#, 634 | Secret::None, 635 | SignatureAlgorithm::None, 636 | ) 637 | .unwrap(); 638 | } 639 | } 640 | -------------------------------------------------------------------------------- /src/jws/compact.rs: -------------------------------------------------------------------------------- 1 | use serde::de::DeserializeOwned; 2 | use serde::{self, Deserialize, Serialize}; 3 | use std::str; 4 | 5 | use crate::errors::{DecodeError, Error, ValidationError}; 6 | use crate::jwa::{Algorithm, SignatureAlgorithm}; 7 | use crate::jwk::{AlgorithmParameters, JWKSet}; 8 | use crate::CompactPart; 9 | 10 | use super::{Header, Secret}; 11 | 12 | /// Compact representation of a JWS 13 | /// 14 | /// This representation contains a payload (type `T`) (e.g. a claims set) and is (optionally) signed. This is the 15 | /// most common form of tokens used. The JWS can contain additional header fields provided by type `H`. 16 | /// 17 | /// Serialization/deserialization is handled by serde. Before you transport the token, make sure you 18 | /// turn it into the encoded form first. 19 | /// 20 | /// # Examples 21 | /// ## Signing and verifying a JWT with HS256 22 | /// See an example in the [`biscuit::JWT`](../type.JWT.html) type alias. 23 | #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] 24 | #[serde(untagged)] 25 | pub enum Compact { 26 | /// Decoded form of the JWS. 27 | /// This variant cannot be serialized or deserialized and will return an error. 28 | #[serde(skip_serializing)] 29 | #[serde(skip_deserializing)] 30 | Decoded { 31 | /// Embedded header 32 | header: Header, 33 | /// Payload, usually a claims set 34 | payload: T, 35 | }, 36 | /// Encoded and (optionally) signed JWT. Use this form to send to your clients 37 | Encoded(crate::Compact), 38 | } 39 | 40 | impl Compact 41 | where 42 | T: CompactPart, 43 | H: Serialize + DeserializeOwned, 44 | { 45 | /// New decoded JWT 46 | pub fn new_decoded(header: Header, payload: T) -> Self { 47 | Compact::Decoded { header, payload } 48 | } 49 | 50 | /// New encoded JWT 51 | pub fn new_encoded(token: &str) -> Self { 52 | Compact::Encoded(crate::Compact::decode(token)) 53 | } 54 | 55 | /// Consumes self and convert into encoded form. If the token is already encoded, 56 | /// this is a no-op. 57 | // TODO: Is the no-op dangerous? What if the secret between the previous encode and this time is different? 58 | pub fn into_encoded(self, secret: &Secret) -> Result { 59 | match self { 60 | Compact::Encoded(_) => Ok(self), 61 | Compact::Decoded { .. } => self.encode(secret), 62 | } 63 | } 64 | 65 | /// Encode the JWT passed and sign the payload using the algorithm from the header and the secret 66 | /// The secret is dependent on the signing algorithm 67 | pub fn encode(&self, secret: &Secret) -> Result { 68 | match *self { 69 | Compact::Decoded { 70 | ref header, 71 | ref payload, 72 | } => { 73 | let mut compact = crate::Compact::with_capacity(3); 74 | compact.push(header)?; 75 | compact.push(payload)?; 76 | let encoded_payload = compact.encode(); 77 | let signature = header 78 | .registered 79 | .algorithm 80 | .sign(encoded_payload.as_bytes(), secret)?; 81 | compact.push(&signature)?; 82 | Ok(Compact::Encoded(compact)) 83 | } 84 | Compact::Encoded(_) => Err(Error::UnsupportedOperation), 85 | } 86 | } 87 | 88 | /// Consumes self and convert into decoded form, verifying the signature, if any. 89 | /// If the token is already decoded, this is a no-op 90 | // TODO: Is the no-op dangerous? What if the secret between the previous decode and this time is different? 91 | pub fn into_decoded( 92 | self, 93 | secret: &Secret, 94 | algorithm: SignatureAlgorithm, 95 | ) -> Result { 96 | match self { 97 | Compact::Encoded(_) => self.decode(secret, algorithm), 98 | Compact::Decoded { .. } => Ok(self), 99 | } 100 | } 101 | 102 | /// Decode a token into the JWT struct and verify its signature using the concrete Secret 103 | /// If the token or its signature is invalid, it will return an error 104 | pub fn decode(&self, secret: &Secret, algorithm: SignatureAlgorithm) -> Result { 105 | match *self { 106 | Compact::Decoded { .. } => Err(Error::UnsupportedOperation), 107 | Compact::Encoded(ref encoded) => { 108 | if encoded.len() != 3 { 109 | Err(DecodeError::PartsLengthError { 110 | actual: encoded.len(), 111 | expected: 3, 112 | })? 113 | } 114 | 115 | let signature: Vec = encoded.part(2)?; 116 | let payload = &encoded.parts[0..2].join("."); 117 | 118 | algorithm 119 | .verify(signature.as_ref(), payload.as_ref(), secret) 120 | .map_err(|_| ValidationError::InvalidSignature)?; 121 | 122 | let header: Header = encoded.part(0)?; 123 | if header.registered.algorithm != algorithm { 124 | Err(ValidationError::WrongAlgorithmHeader)?; 125 | } 126 | let decoded_claims: T = encoded.part(1)?; 127 | 128 | Ok(Self::new_decoded(header, decoded_claims)) 129 | } 130 | } 131 | } 132 | 133 | /// Decode a token into the JWT struct and verify its signature using a JWKS 134 | /// 135 | /// If the JWK does not contain an optional algorithm parameter, you will have to specify 136 | /// the expected algorithm or an error will be returned. 137 | /// 138 | /// If the JWK specifies an algorithm and you provide an expected algorithm, 139 | /// both will be checked for equality. If they do not match, an error will be returned. 140 | /// 141 | /// If the token or its signature is invalid, it will return an error 142 | pub fn decode_with_jwks( 143 | &self, 144 | jwks: &JWKSet, 145 | expected_algorithm: Option, 146 | ) -> Result { 147 | match *self { 148 | Compact::Decoded { .. } => Err(Error::UnsupportedOperation), 149 | Compact::Encoded(ref encoded) => { 150 | if encoded.len() != 3 { 151 | Err(DecodeError::PartsLengthError { 152 | actual: encoded.len(), 153 | expected: 3, 154 | })? 155 | } 156 | 157 | let signature: Vec = encoded.part(2)?; 158 | let payload = &encoded.parts[0..2].join("."); 159 | 160 | let header: Header = encoded.part(0)?; 161 | let key_id = header 162 | .registered 163 | .key_id 164 | .as_ref() 165 | .ok_or(ValidationError::KidMissing)?; 166 | let jwk = jwks.find(key_id).ok_or(ValidationError::KeyNotFound)?; 167 | 168 | let algorithm = match jwk.common.algorithm { 169 | Some(jwk_alg) => { 170 | let algorithm = match jwk_alg { 171 | Algorithm::Signature(algorithm) => algorithm, 172 | _ => Err(ValidationError::UnsupportedKeyAlgorithm)?, 173 | }; 174 | 175 | if header.registered.algorithm != algorithm { 176 | Err(ValidationError::WrongAlgorithmHeader)?; 177 | } 178 | 179 | if let Some(expected_algorithm) = expected_algorithm { 180 | if expected_algorithm != algorithm { 181 | Err(ValidationError::WrongAlgorithmHeader)?; 182 | } 183 | } 184 | 185 | algorithm 186 | } 187 | None => match expected_algorithm { 188 | Some(expected_algorithm) => { 189 | if expected_algorithm != header.registered.algorithm { 190 | Err(ValidationError::WrongAlgorithmHeader)?; 191 | } 192 | expected_algorithm 193 | } 194 | None => Err(ValidationError::MissingAlgorithm)?, 195 | }, 196 | }; 197 | 198 | let secret = match &jwk.algorithm { 199 | AlgorithmParameters::EllipticCurve(ec) => ec.jws_public_key_secret(), 200 | AlgorithmParameters::RSA(rsa) => rsa.jws_public_key_secret(), 201 | AlgorithmParameters::OctetKey(oct) => Secret::Bytes(oct.value.clone()), 202 | _ => Err(ValidationError::UnsupportedKeyAlgorithm)?, 203 | }; 204 | 205 | algorithm 206 | .verify(signature.as_ref(), payload.as_ref(), &secret) 207 | .map_err(|_| ValidationError::InvalidSignature)?; 208 | 209 | let decoded_claims: T = encoded.part(1)?; 210 | 211 | Ok(Self::new_decoded(header, decoded_claims)) 212 | } 213 | } 214 | } 215 | 216 | /// Decode a token into the JWT struct and verify its signature using a JWKS, ignoring kid. 217 | /// 218 | /// If the JWK does not contain an optional algorithm parameter, you will have to specify 219 | /// the expected algorithm or an error will be returned. 220 | /// 221 | /// If the JWK specifies an algorithm and you provide an expected algorithm, 222 | /// both will be checked for equality. If they do not match, an error will be returned. 223 | /// 224 | /// If the token or its signature is invalid, it will return an error 225 | pub fn decode_with_jwks_ignore_kid(&self, jwks: &JWKSet) -> Result { 226 | match *self { 227 | Compact::Decoded { .. } => Err(Error::UnsupportedOperation), 228 | Compact::Encoded(ref encoded) => { 229 | if encoded.len() != 3 { 230 | Err(DecodeError::PartsLengthError { 231 | actual: encoded.len(), 232 | expected: 3, 233 | })? 234 | } 235 | 236 | let signature: Vec = encoded.part(2)?; 237 | let payload = &encoded.parts[0..2].join("."); 238 | 239 | let header: Header = encoded.part(0)?; 240 | 241 | let secrets = jwks 242 | .keys 243 | .iter() 244 | .filter_map(|jwk| { 245 | let jwk_alg = jwk.common.algorithm.and_then(|it| { 246 | if let Algorithm::Signature(alg) = it { 247 | Some(alg) 248 | } else { 249 | None 250 | } 251 | }); 252 | // if no algorithm is set it will try the key anyway. if it is it will only accept the right one 253 | if jwk.common.algorithm.is_some() 254 | && Some(header.registered.algorithm) != jwk_alg 255 | { 256 | return None; 257 | } 258 | match &jwk.algorithm { 259 | AlgorithmParameters::EllipticCurve(ec) => { 260 | Some(ec.jws_public_key_secret()) 261 | } 262 | AlgorithmParameters::RSA(rsa) => Some(rsa.jws_public_key_secret()), 263 | AlgorithmParameters::OctetKey(oct) => { 264 | Some(Secret::Bytes(oct.value.clone())) 265 | } 266 | _ => None, 267 | } 268 | }) 269 | .collect::>(); 270 | 271 | if secrets.is_empty() { 272 | Err(ValidationError::UnsupportedKeyAlgorithm)? 273 | } 274 | 275 | if !secrets.iter().any(|secret| { 276 | header 277 | .registered 278 | .algorithm 279 | .verify(signature.as_ref(), payload.as_ref(), secret) 280 | .is_ok() 281 | }) { 282 | Err(ValidationError::InvalidSignature)? 283 | } 284 | 285 | let decoded_claims: T = encoded.part(1)?; 286 | 287 | Ok(Self::new_decoded(header, decoded_claims)) 288 | } 289 | } 290 | } 291 | 292 | /// Convenience method to get a reference to the encoded string from an encoded compact JWS 293 | pub fn encoded(&self) -> Result<&crate::Compact, Error> { 294 | match *self { 295 | Compact::Decoded { .. } => Err(Error::UnsupportedOperation), 296 | Compact::Encoded(ref encoded) => Ok(encoded), 297 | } 298 | } 299 | 300 | /// Convenience method to get a mutable reference to the encoded string from an encoded compact JWS 301 | pub fn encoded_mut(&mut self) -> Result<&mut crate::Compact, Error> { 302 | match *self { 303 | Compact::Decoded { .. } => Err(Error::UnsupportedOperation), 304 | Compact::Encoded(ref mut encoded) => Ok(encoded), 305 | } 306 | } 307 | 308 | /// Convenience method to get a reference to the claims set from a decoded compact JWS 309 | pub fn payload(&self) -> Result<&T, Error> { 310 | match *self { 311 | Compact::Decoded { ref payload, .. } => Ok(payload), 312 | Compact::Encoded(_) => Err(Error::UnsupportedOperation), 313 | } 314 | } 315 | 316 | /// Convenience method to get a reference to the claims set from a decoded compact JWS 317 | pub fn payload_mut(&mut self) -> Result<&mut T, Error> { 318 | match *self { 319 | Compact::Decoded { 320 | ref mut payload, .. 321 | } => Ok(payload), 322 | Compact::Encoded(_) => Err(Error::UnsupportedOperation), 323 | } 324 | } 325 | 326 | /// Convenience method to get a reference to the header from a decoded compact JWS 327 | pub fn header(&self) -> Result<&Header, Error> { 328 | match *self { 329 | Compact::Decoded { ref header, .. } => Ok(header), 330 | Compact::Encoded(_) => Err(Error::UnsupportedOperation), 331 | } 332 | } 333 | 334 | /// Convenience method to get a reference to the header from a decoded compact JWS 335 | pub fn header_mut(&mut self) -> Result<&mut Header, Error> { 336 | match *self { 337 | Compact::Decoded { ref mut header, .. } => Ok(header), 338 | Compact::Encoded(_) => Err(Error::UnsupportedOperation), 339 | } 340 | } 341 | 342 | /// Consumes self, and move the payload and header out and return them as a tuple 343 | /// 344 | /// # Panics 345 | /// Panics if the JWS is not decoded 346 | pub fn unwrap_decoded(self) -> (Header, T) { 347 | match self { 348 | Compact::Decoded { header, payload } => (header, payload), 349 | Compact::Encoded(_) => panic!("JWS is encoded"), 350 | } 351 | } 352 | 353 | /// Consumes self, and move the encoded Compact out and return it 354 | /// 355 | /// # Panics 356 | /// Panics if the JWS is not encoded 357 | pub fn unwrap_encoded(self) -> crate::Compact { 358 | match self { 359 | Compact::Decoded { .. } => panic!("JWS is decoded"), 360 | Compact::Encoded(encoded) => encoded, 361 | } 362 | } 363 | 364 | /// Without decoding and verifying the JWS, retrieve a copy of the header. 365 | /// 366 | /// ## Warning 367 | /// Use this at your own risk. It is not advisable to trust unverified content. 368 | pub fn unverified_header(&self) -> Result, Error> { 369 | match *self { 370 | Compact::Decoded { .. } => Err(Error::UnsupportedOperation), 371 | Compact::Encoded(ref compact) => compact.part(0), 372 | } 373 | } 374 | 375 | /// Without decoding and verifying the JWS, retrieve a copy of the payload. 376 | /// 377 | /// ## Warning 378 | /// Use this at your own risk. It is not advisable to trust unverified content. 379 | pub fn unverified_payload(&self) -> Result { 380 | match *self { 381 | Compact::Decoded { .. } => Err(Error::UnsupportedOperation), 382 | Compact::Encoded(ref compact) => compact.part(1), 383 | } 384 | } 385 | 386 | /// Get a copy of the signature 387 | pub fn signature(&self) -> Result, Error> { 388 | match *self { 389 | Compact::Decoded { .. } => Err(Error::UnsupportedOperation), 390 | Compact::Encoded(ref compact) => compact.part(2), 391 | } 392 | } 393 | } 394 | 395 | /// Convenience implementation for a Compact that contains a `ClaimsSet` 396 | impl Compact, H> 397 | where 398 | crate::ClaimsSet

: CompactPart, 399 | H: Serialize + DeserializeOwned, 400 | { 401 | /// Validate the temporal claims in the decoded token 402 | /// 403 | /// If `None` is provided for options, the defaults will apply. 404 | /// 405 | /// By default, no temporal claims (namely `iat`, `exp`, `nbf`) 406 | /// are required, and they will pass validation if they are missing. 407 | pub fn validate(&self, options: crate::ValidationOptions) -> Result<(), Error> { 408 | self.payload()?.registered.validate(options)?; 409 | Ok(()) 410 | } 411 | } 412 | 413 | /// Implementation for embedded inside a JWE. 414 | // FIXME: Maybe use a separate trait instead? 415 | impl CompactPart for Compact { 416 | fn to_bytes(&self) -> Result, Error> { 417 | let encoded = self.encoded()?; 418 | Ok(encoded.to_string().into_bytes()) 419 | } 420 | 421 | fn from_bytes(bytes: &[u8]) -> Result { 422 | let string = str::from_utf8(bytes)?; 423 | Ok(Self::new_encoded(string)) 424 | } 425 | } 426 | 427 | #[cfg(test)] 428 | mod tests { 429 | use std::str::{self, FromStr}; 430 | 431 | use serde::{Deserialize, Serialize}; 432 | 433 | use super::{Compact, Header, Secret, SignatureAlgorithm}; 434 | use crate::jwk::JWKSet; 435 | use crate::jws::RegisteredHeader; 436 | use crate::{ClaimsSet, CompactJson, Empty, RegisteredClaims, SingleOrMultiple}; 437 | 438 | #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] 439 | struct PrivateClaims { 440 | company: String, 441 | department: String, 442 | } 443 | 444 | impl CompactJson for PrivateClaims {} 445 | 446 | // HS256 key - "secret" 447 | static HS256_PAYLOAD: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ 448 | eyJpc3MiOiJodHRwczovL3d3dy5hY21lLmNvbS8iLCJzdWIiOiJKb2huIERvZSIsImF1ZCI6Imh0dHBzOi8vYWNtZ\ 449 | S1jdXN0b21lci5jb20vIiwibmJmIjoxMjM0LCJjb21wYW55IjoiQUNNRSIsImRlcGFydG1lbnQiOiJUb2lsZXQgQ2x\ 450 | lYW5pbmcifQ.VFCl2un1Kc17odzOe2Ehf4DVrWddu3U4Ux3GFpOZHtc"; 451 | 452 | #[test] 453 | #[should_panic(expected = "the enum variant Compact::Decoded cannot be serialized")] 454 | fn decoded_compact_jws_cannot_be_serialized() { 455 | let expected_claims = ClaimsSet:: { 456 | registered: RegisteredClaims { 457 | issuer: Some(not_err!(FromStr::from_str("https://www.acme.com/"))), 458 | subject: Some(not_err!(FromStr::from_str("John Doe"))), 459 | audience: Some(SingleOrMultiple::Single(not_err!(FromStr::from_str( 460 | "https://acme-customer.com/" 461 | )))), 462 | not_before: Some(1234.into()), 463 | ..Default::default() 464 | }, 465 | private: PrivateClaims { 466 | department: "Toilet Cleaning".to_string(), 467 | company: "ACME".to_string(), 468 | }, 469 | }; 470 | 471 | let biscuit = Compact::new_decoded( 472 | From::from(RegisteredHeader { 473 | algorithm: SignatureAlgorithm::None, 474 | ..Default::default() 475 | }), 476 | expected_claims, 477 | ); 478 | let _ = serde_json::to_string(&biscuit).unwrap(); 479 | } 480 | 481 | #[test] 482 | #[should_panic(expected = "data did not match any variant of untagged enum Compact")] 483 | fn decoded_compact_jws_cannot_be_deserialized() { 484 | let json = r#"{"header":{"alg":"none","typ":"JWT"}, 485 | "payload":{"iss":"https://www.acme.com/","sub":"John Doe", 486 | "aud":"https://acme-customer.com/","nbf":1234, 487 | "company":"ACME","department":"Toilet Cleaning"}}"#; 488 | let _ = serde_json::from_str::>(json).unwrap(); 489 | } 490 | 491 | #[test] 492 | fn compact_jws_round_trip_none() { 493 | let expected_token = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.\ 494 | eyJpc3MiOiJodHRwczovL3d3dy5hY21lLmNvbS8iLCJzdWIiOiJKb2huIERvZSIsImF1ZCI6Imh0dHBzOi8vY\ 495 | WNtZS1jdXN0b21lci5jb20vIiwibmJmIjoxMjM0LCJjb21wYW55IjoiQUNNRSIsImRlcGFydG1lbnQiOiJUb2l\ 496 | sZXQgQ2xlYW5pbmcifQ."; 497 | 498 | let expected_claims = ClaimsSet:: { 499 | registered: RegisteredClaims { 500 | issuer: Some(not_err!(FromStr::from_str("https://www.acme.com/"))), 501 | subject: Some(not_err!(FromStr::from_str("John Doe"))), 502 | audience: Some(SingleOrMultiple::Single(not_err!(FromStr::from_str( 503 | "https://acme-customer.com/" 504 | )))), 505 | not_before: Some(1234.into()), 506 | ..Default::default() 507 | }, 508 | private: PrivateClaims { 509 | department: "Toilet Cleaning".to_string(), 510 | company: "ACME".to_string(), 511 | }, 512 | }; 513 | 514 | let expected_jwt = Compact::new_decoded( 515 | From::from(RegisteredHeader { 516 | algorithm: SignatureAlgorithm::None, 517 | ..Default::default() 518 | }), 519 | expected_claims.clone(), 520 | ); 521 | let token = not_err!(expected_jwt.into_encoded(&Secret::None)); 522 | assert_eq!(expected_token, not_err!(token.encoded()).to_string()); 523 | 524 | let biscuit = not_err!(token.into_decoded(&Secret::None, SignatureAlgorithm::None)); 525 | let actual_claims = not_err!(biscuit.payload()); 526 | assert_eq!(expected_claims, *actual_claims); 527 | } 528 | 529 | #[test] 530 | fn compact_jws_round_trip_hs256() { 531 | let expected_claims = ClaimsSet:: { 532 | registered: RegisteredClaims { 533 | issuer: Some(not_err!(FromStr::from_str("https://www.acme.com/"))), 534 | subject: Some(not_err!(FromStr::from_str("John Doe"))), 535 | audience: Some(SingleOrMultiple::Single(not_err!(FromStr::from_str( 536 | "https://acme-customer.com/" 537 | )))), 538 | not_before: Some(1234.into()), 539 | ..Default::default() 540 | }, 541 | private: PrivateClaims { 542 | department: "Toilet Cleaning".to_string(), 543 | company: "ACME".to_string(), 544 | }, 545 | }; 546 | 547 | let expected_jwt = Compact::new_decoded( 548 | From::from(RegisteredHeader { 549 | algorithm: SignatureAlgorithm::HS256, 550 | ..Default::default() 551 | }), 552 | expected_claims.clone(), 553 | ); 554 | let token = 555 | not_err!(expected_jwt.into_encoded(&Secret::Bytes("secret".to_string().into_bytes()))); 556 | assert_eq!(HS256_PAYLOAD, not_err!(token.encoded()).to_string()); 557 | 558 | let biscuit = not_err!(token.into_decoded( 559 | &Secret::Bytes("secret".to_string().into_bytes()), 560 | SignatureAlgorithm::HS256 561 | )); 562 | assert_eq!(expected_claims, *not_err!(biscuit.payload())); 563 | } 564 | 565 | #[test] 566 | fn compact_jws_round_trip_rs256() { 567 | let expected_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.\ 568 | eyJpc3MiOiJodHRwczovL3d3dy5hY21lLmNvbS8iLCJzdWIiOiJKb2huIERvZSIsImF1Z\ 569 | CI6Imh0dHBzOi8vYWNtZS1jdXN0b21lci5jb20vIiwibmJmIjoxMjM0LCJjb21wYW55Ij\ 570 | oiQUNNRSIsImRlcGFydG1lbnQiOiJUb2lsZXQgQ2xlYW5pbmcifQ.\ 571 | Gat3NBUTaCyvroil66U0nId4-l6VqbtJYIsM9wRbWo45oYoN-NxYIyl8M-9AlEPseg-4SIuo-A-jccJOWGeWWwy-E\ 572 | en_92wg18II58luHz7vAyclw1maJBKHmuj8f2wE_Ky8ir3iTpTGkJQ3IUU9SuU9Fkvajm4jgWUtRPpjHm_IqyxV8N\ 573 | kHNyN0p5CqeuRC8sZkOSFkm9b0WnWYRVls1QOjBnN9w9zW9wg9DGwj10pqg8hQ5sy-C3J-9q1zJgGDXInkhPLjitO\ 574 | 9wzWg4yfVt-CJNiHsJT7RY_EN2VmbG8UOjHp8xUPpfqUKyoQttKaQkJHdjP_b47LO4ZKI4UivlA"; 575 | 576 | let expected_claims = ClaimsSet:: { 577 | registered: RegisteredClaims { 578 | issuer: Some(not_err!(FromStr::from_str("https://www.acme.com/"))), 579 | subject: Some(not_err!(FromStr::from_str("John Doe"))), 580 | audience: Some(SingleOrMultiple::Single(not_err!(FromStr::from_str( 581 | "https://acme-customer.com/" 582 | )))), 583 | not_before: Some(1234.into()), 584 | ..Default::default() 585 | }, 586 | private: PrivateClaims { 587 | department: "Toilet Cleaning".to_string(), 588 | company: "ACME".to_string(), 589 | }, 590 | }; 591 | let private_key = 592 | Secret::rsa_keypair_from_file("test/fixtures/rsa_private_key.der").unwrap(); 593 | 594 | let expected_jwt = Compact::new_decoded( 595 | From::from(RegisteredHeader { 596 | algorithm: SignatureAlgorithm::RS256, 597 | ..Default::default() 598 | }), 599 | expected_claims.clone(), 600 | ); 601 | let token = not_err!(expected_jwt.into_encoded(&private_key)); 602 | assert_eq!(expected_token, not_err!(token.encoded()).to_string()); 603 | 604 | let public_key = Secret::public_key_from_file("test/fixtures/rsa_public_key.der").unwrap(); 605 | let biscuit = not_err!(token.into_decoded(&public_key, SignatureAlgorithm::RS256)); 606 | assert_eq!(expected_claims, *not_err!(biscuit.payload())); 607 | } 608 | 609 | #[test] 610 | fn compact_jws_verify_es256() { 611 | use data_encoding::HEXUPPER; 612 | 613 | // This is a ECDSA Public key in `SubjectPublicKey` form. 614 | // Conversion is not available in `ring` yet. 615 | // See https://github.com/lawliet89/biscuit/issues/71#issuecomment-296445140 for a 616 | // way to retrieve it from `SubjectPublicKeyInfo`. 617 | let public_key = 618 | "043727F96AAD416887DD75CC2E333C3D8E06DCDF968B6024579449A2B802EFC891F638C75\ 619 | 1CF687E6FF9A280E11B7036585E60CA32BB469C3E57998A289E0860A6"; 620 | let jwt = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.\ 621 | eyJ0b2tlbl90eXBlIjoic2VydmljZSIsImlhdCI6MTQ5MjkzODU4OH0.\ 622 | do_XppIOFthPWlTXL95CIBfgRdyAxbcIsUfM0YxMjCjqvp4ehHFA3I-JasABKzC8CAy4ndhCHsZdpAtK\ 623 | kqZMEA"; 624 | let signing_secret = Secret::PublicKey(not_err!(HEXUPPER.decode(public_key.as_bytes()))); 625 | 626 | let token = Compact::, Empty>::new_encoded(jwt); 627 | let _ = not_err!(token.into_decoded(&signing_secret, SignatureAlgorithm::ES256)); 628 | } 629 | 630 | #[test] 631 | fn compact_jws_verify_es256_jwks() { 632 | let jwks: JWKSet = serde_json::from_str( 633 | r#"{ 634 | "keys": [ 635 | { 636 | "kty": "EC", 637 | "crv": "P-256", 638 | "x": "Nyf5aq1BaIfddcwuMzw9jgbc35aLYCRXlEmiuALvyJE", 639 | "y": "9jjHUc9ofm_5ooDhG3A2WF5gyjK7Rpw-V5mKKJ4IYKY" 640 | } 641 | ] 642 | }"#, 643 | ) 644 | .unwrap(); 645 | let jwt = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.\ 646 | eyJ0b2tlbl90eXBlIjoic2VydmljZSIsImlhdCI6MTQ5MjkzODU4OH0.\ 647 | do_XppIOFthPWlTXL95CIBfgRdyAxbcIsUfM0YxMjCjqvp4ehHFA3I-JasABKzC8CAy4ndhCHsZdpAtK\ 648 | kqZMEA"; 649 | 650 | let token = Compact::, Empty>::new_encoded(jwt); 651 | let _ = not_err!(token.decode_with_jwks_ignore_kid(&jwks)); 652 | } 653 | 654 | #[test] 655 | fn compact_jws_encode_with_additional_header_fields() { 656 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 657 | struct CustomHeader { 658 | something: String, 659 | } 660 | 661 | let expected_claims = ClaimsSet:: { 662 | registered: RegisteredClaims { 663 | issuer: Some(not_err!(FromStr::from_str("https://www.acme.com/"))), 664 | subject: Some(not_err!(FromStr::from_str("John Doe"))), 665 | audience: Some(SingleOrMultiple::Single(not_err!(FromStr::from_str( 666 | "https://acme-customer.com/" 667 | )))), 668 | not_before: Some(1234.into()), 669 | ..Default::default() 670 | }, 671 | private: PrivateClaims { 672 | department: "Toilet Cleaning".to_string(), 673 | company: "ACME".to_string(), 674 | }, 675 | }; 676 | 677 | let header = Header { 678 | registered: Default::default(), 679 | private: CustomHeader { 680 | something: "foobar".to_string(), 681 | }, 682 | }; 683 | 684 | let expected_jwt = Compact::new_decoded(header.clone(), expected_claims); 685 | let token = 686 | not_err!(expected_jwt.into_encoded(&Secret::Bytes("secret".to_string().into_bytes()))); 687 | let biscuit = not_err!(token.into_decoded( 688 | &Secret::Bytes("secret".to_string().into_bytes()), 689 | SignatureAlgorithm::HS256 690 | )); 691 | assert_eq!(header, *not_err!(biscuit.header())); 692 | } 693 | 694 | #[test] 695 | #[should_panic(expected = "PartsLengthError { expected: 3, actual: 1 }")] 696 | fn compact_jws_decode_token_missing_parts() { 697 | let token = 698 | Compact::::new_encoded("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"); 699 | let claims = token.decode( 700 | &Secret::Bytes("secret".to_string().into_bytes()), 701 | SignatureAlgorithm::HS256, 702 | ); 703 | let _ = claims.unwrap(); 704 | } 705 | 706 | #[test] 707 | #[should_panic(expected = "InvalidSignature")] 708 | fn compact_jws_decode_token_invalid_signature_hs256() { 709 | let token = Compact::::new_encoded( 710 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ 711 | eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.\ 712 | pKscJVk7-aHxfmQKlaZxh5uhuKhGMAa-1F5IX5mfUwI", 713 | ); 714 | let claims = token.decode( 715 | &Secret::Bytes("secret".to_string().into_bytes()), 716 | SignatureAlgorithm::HS256, 717 | ); 718 | let _ = claims.unwrap(); 719 | } 720 | 721 | #[test] 722 | #[should_panic(expected = "InvalidSignature")] 723 | fn compact_jws_decode_token_invalid_signature_rs256() { 724 | let token = Compact::::new_encoded( 725 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ 726 | eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.\ 727 | pKscJVk7-aHxfmQKlaZxh5uhuKhGMAa-1F5IX5mfUwI", 728 | ); 729 | let public_key = Secret::public_key_from_file("test/fixtures/rsa_public_key.der").unwrap(); 730 | let claims = token.decode(&public_key, SignatureAlgorithm::RS256); 731 | let _ = claims.unwrap(); 732 | } 733 | 734 | #[test] 735 | #[should_panic(expected = "WrongAlgorithmHeader")] 736 | fn compact_jws_decode_token_wrong_algorithm() { 737 | let token = Compact::::new_encoded( 738 | "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.\ 739 | eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.\ 740 | pKscJVk7-aHxfmQKlaZxh5uhuKhGMAa-1F5IX5mfUwI", 741 | ); 742 | let claims = token.decode( 743 | &Secret::Bytes("secret".to_string().into_bytes()), 744 | SignatureAlgorithm::HS256, 745 | ); 746 | let _ = claims.unwrap(); 747 | } 748 | 749 | #[test] 750 | fn compact_jws_round_trip_hs256_for_bytes_payload() { 751 | let expected_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImN0eSI6IlJhbmRvbSBieXRlcyJ9.\ 752 | eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcG\ 753 | xlLmNvbS9pc19yb290Ijp0cnVlfQ.E5ahoj_gMO8WZzSUhquWuBkPLGZm18zaLbyHUQA7TIs"; 754 | let payload: Vec = vec![ 755 | 123, 34, 105, 115, 115, 34, 58, 34, 106, 111, 101, 34, 44, 13, 10, 32, 34, 101, 120, 756 | 112, 34, 58, 49, 51, 48, 48, 56, 49, 57, 51, 56, 48, 44, 13, 10, 32, 34, 104, 116, 116, 757 | 112, 58, 47, 47, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 47, 105, 115, 95, 758 | 114, 111, 111, 116, 34, 58, 116, 114, 117, 101, 125, 759 | ]; 760 | 761 | let expected_jwt = Compact::new_decoded( 762 | From::from(RegisteredHeader { 763 | algorithm: SignatureAlgorithm::HS256, 764 | content_type: Some("Random bytes".to_string()), 765 | ..Default::default() 766 | }), 767 | payload.clone(), 768 | ); 769 | let token = 770 | not_err!(expected_jwt.into_encoded(&Secret::Bytes("secret".to_string().into_bytes()))); 771 | assert_eq!(expected_token, not_err!(token.encoded()).to_string()); 772 | 773 | let biscuit = not_err!(token.into_decoded( 774 | &Secret::Bytes("secret".to_string().into_bytes()), 775 | SignatureAlgorithm::HS256 776 | )); 777 | assert_eq!(payload, *not_err!(biscuit.payload())); 778 | } 779 | 780 | #[test] 781 | fn compact_jws_decode_with_jwks_shared_secret() { 782 | let token = Compact::::new_encoded( 783 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleTAifQ.\ 784 | eyJjb21wYW55IjoiQUNNRSIsImRlcGFydG1lbnQiOiJUb2lsZXQgQ2xlYW5pbmcifQ.\ 785 | nz0a8aSweo6W0K2P7keByUPWl0HLVG45pTDznij5uKw", 786 | ); 787 | 788 | let jwks: JWKSet = serde_json::from_str( 789 | r#"{ 790 | "keys": [ 791 | { 792 | "kty": "oct", 793 | "use": "sig", 794 | "kid": "key0", 795 | "k": "-clnNQnBupZt23N8McUcZytLhan9OmjlJXmqS7daoeY", 796 | "alg": "HS256" 797 | } 798 | ] 799 | }"#, 800 | ) 801 | .unwrap(); 802 | 803 | let _ = token.decode_with_jwks(&jwks, None).expect("to succeed"); 804 | } 805 | 806 | /// JWK has algorithm and user provided a matching expected algorithm 807 | #[test] 808 | fn compact_jws_decode_with_jwks_shared_secret_matching_alg() { 809 | let token = Compact::::new_encoded( 810 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleTAifQ.\ 811 | eyJjb21wYW55IjoiQUNNRSIsImRlcGFydG1lbnQiOiJUb2lsZXQgQ2xlYW5pbmcifQ.\ 812 | nz0a8aSweo6W0K2P7keByUPWl0HLVG45pTDznij5uKw", 813 | ); 814 | 815 | let jwks: JWKSet = serde_json::from_str( 816 | r#"{ 817 | "keys": [ 818 | { 819 | "kty": "oct", 820 | "use": "sig", 821 | "kid": "key0", 822 | "k": "-clnNQnBupZt23N8McUcZytLhan9OmjlJXmqS7daoeY", 823 | "alg": "HS256" 824 | } 825 | ] 826 | }"#, 827 | ) 828 | .unwrap(); 829 | 830 | let _ = token 831 | .decode_with_jwks(&jwks, Some(SignatureAlgorithm::HS256)) 832 | .expect("to succeed"); 833 | } 834 | 835 | /// JWK has algorithm and user provided a non-matching expected algorithm 836 | #[test] 837 | #[should_panic(expected = "WrongAlgorithmHeader")] 838 | fn compact_jws_decode_with_jwks_shared_secret_mismatched_alg() { 839 | let token = Compact::::new_encoded( 840 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleTAifQ.\ 841 | eyJjb21wYW55IjoiQUNNRSIsImRlcGFydG1lbnQiOiJUb2lsZXQgQ2xlYW5pbmcifQ.\ 842 | nz0a8aSweo6W0K2P7keByUPWl0HLVG45pTDznij5uKw", 843 | ); 844 | 845 | let jwks: JWKSet = serde_json::from_str( 846 | r#"{ 847 | "keys": [ 848 | { 849 | "kty": "oct", 850 | "use": "sig", 851 | "kid": "key0", 852 | "k": "-clnNQnBupZt23N8McUcZytLhan9OmjlJXmqS7daoeY", 853 | "alg": "HS256" 854 | } 855 | ] 856 | }"#, 857 | ) 858 | .unwrap(); 859 | 860 | let _ = token 861 | .decode_with_jwks(&jwks, Some(SignatureAlgorithm::RS256)) 862 | .unwrap(); 863 | } 864 | 865 | /// JWK has no algorithm and user provided a header matching expected algorithm 866 | #[test] 867 | fn compact_jws_decode_with_jwks_without_alg() { 868 | let token = Compact::::new_encoded( 869 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleTAifQ.\ 870 | eyJjb21wYW55IjoiQUNNRSIsImRlcGFydG1lbnQiOiJUb2lsZXQgQ2xlYW5pbmcifQ.\ 871 | nz0a8aSweo6W0K2P7keByUPWl0HLVG45pTDznij5uKw", 872 | ); 873 | 874 | let jwks: JWKSet = serde_json::from_str( 875 | r#"{ 876 | "keys": [ 877 | { 878 | "kty": "oct", 879 | "use": "sig", 880 | "kid": "key0", 881 | "k": "-clnNQnBupZt23N8McUcZytLhan9OmjlJXmqS7daoeY" 882 | } 883 | ] 884 | }"#, 885 | ) 886 | .unwrap(); 887 | 888 | let _ = token 889 | .decode_with_jwks(&jwks, Some(SignatureAlgorithm::HS256)) 890 | .expect("to succeed"); 891 | } 892 | 893 | /// JWK has no algorithm and user provided a header not-matching expected algorithm 894 | #[test] 895 | #[should_panic(expected = "WrongAlgorithmHeader")] 896 | fn compact_jws_decode_with_jwks_without_alg_non_matching() { 897 | let token = Compact::::new_encoded( 898 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleTAifQ.\ 899 | eyJjb21wYW55IjoiQUNNRSIsImRlcGFydG1lbnQiOiJUb2lsZXQgQ2xlYW5pbmcifQ.\ 900 | nz0a8aSweo6W0K2P7keByUPWl0HLVG45pTDznij5uKw", 901 | ); 902 | 903 | let jwks: JWKSet = serde_json::from_str( 904 | r#"{ 905 | "keys": [ 906 | { 907 | "kty": "oct", 908 | "use": "sig", 909 | "kid": "key0", 910 | "k": "-clnNQnBupZt23N8McUcZytLhan9OmjlJXmqS7daoeY" 911 | } 912 | ] 913 | }"#, 914 | ) 915 | .unwrap(); 916 | 917 | let _ = token 918 | .decode_with_jwks(&jwks, Some(SignatureAlgorithm::RS256)) 919 | .unwrap(); 920 | } 921 | 922 | /// JWK has no algorithm and user did not provide any expected algorithm 923 | #[test] 924 | #[should_panic(expected = "MissingAlgorithm")] 925 | fn compact_jws_decode_with_jwks_missing_alg() { 926 | let token = Compact::::new_encoded( 927 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleTAifQ.\ 928 | eyJjb21wYW55IjoiQUNNRSIsImRlcGFydG1lbnQiOiJUb2lsZXQgQ2xlYW5pbmcifQ.\ 929 | nz0a8aSweo6W0K2P7keByUPWl0HLVG45pTDznij5uKw", 930 | ); 931 | 932 | let jwks: JWKSet = serde_json::from_str( 933 | r#"{ 934 | "keys": [ 935 | { 936 | "kty": "oct", 937 | "use": "sig", 938 | "kid": "key0", 939 | "k": "-clnNQnBupZt23N8McUcZytLhan9OmjlJXmqS7daoeY" 940 | } 941 | ] 942 | }"#, 943 | ) 944 | .unwrap(); 945 | 946 | let _ = token.decode_with_jwks(&jwks, None).unwrap(); 947 | } 948 | 949 | #[test] 950 | fn compact_jws_decode_with_jwks_rsa() { 951 | let token = Compact::::new_encoded( 952 | "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleTAifQ.\ 953 | eyJjb21wYW55IjoiQUNNRSIsImRlcGFydG1lbnQiOiJUb2lsZXQgQ2xlYW5pbmcifQ.\ 954 | MImpi6zezEy0PE5uHU7hM1I0VaNPQx4EAYjEnq2v4gyypmfgKqzrSntSACHZvPsLHDN\ 955 | Ui8PGBM13NcF5IxhybHRM_LVMlMK2rlmQQR7NYueV1psfdSh6fGcYoDxuiZnzybpSxP\ 956 | 5Fy8wGe-BgoL5EIPzzhfQBZagzliztLt8RarXHbXnK_KxN1GE5_q5V_ZvjpNr3FExuC\ 957 | cKSvjhlkWR__CmTpv4FWZDkWXJgABLSd0Fe1soUNXMNaqzeTH-xSIYMv06Jckfky6Ds\ 958 | OKcqWyA5QGNScRkSh4fu4jkIiPlituJhFi3hYgIfGTGQMDt2TsiaUCZdfyLhipGwHzmMijeHiQ", 959 | ); 960 | 961 | let jwks: JWKSet = serde_json::from_str( 962 | r#"{ 963 | "keys": [ 964 | { 965 | "kty": "RSA", 966 | "e": "AQAB", 967 | "use": "sig", 968 | "kid": "key0", 969 | "alg": "RS256", 970 | "n": "rx7xQsC4XuzCW1YZwm3JUftsScV3v82VmuuIcmUOBGyLpeChfHwwr61UZOVL6yiFSIoGlS1KbVkyZ5xf8FCQGdRuAYvx2sH4E0D9gOdjAauXIx7ADbG5wfTHqiyYcWezovzdXZb4F7HCaBkaKhtg8FTkTozQz5m6stzcFatcSUZpNM6lCSGoi0kFfucEAV2cNoWUaW1WnYyGB2sxupSIako9updQIHfAqiDSbawO8uBymNjiQJS3evImjLcJajAYzrmK1biSu5uJuw3RReYef3QUvLY9o2T6LV3QiIWi3MeBktjhwAvCKzcOeU34py946AJm6USXkwit_hlFx5DzgQ" 971 | } 972 | ] 973 | }"#, 974 | ) 975 | .unwrap(); 976 | 977 | let _ = token.decode_with_jwks(&jwks, None).expect("to succeed"); 978 | } 979 | 980 | #[test] 981 | #[should_panic(expected = "PartsLengthError { expected: 3, actual: 2 }")] 982 | fn compact_jws_decode_with_jwks_missing_parts() { 983 | let token = Compact::::new_encoded( 984 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleTAifQ.\ 985 | eyJjb21wYW55IjoiQUNNRSIsImRlcGFydG1lbnQiOiJUb2lsZXQgQ2xlYW5pbmcifQ", 986 | ); 987 | 988 | let jwks: JWKSet = serde_json::from_str( 989 | r#"{ 990 | "keys": [ 991 | { 992 | "kty": "oct", 993 | "use": "sig", 994 | "kid": "key0", 995 | "k": "-clnNQnBupZt23N8McUcZytLhan9OmjlJXmqS7daoeY", 996 | "alg": "HS256" 997 | } 998 | ] 999 | }"#, 1000 | ) 1001 | .unwrap(); 1002 | 1003 | let _ = token.decode_with_jwks(&jwks, None).unwrap(); 1004 | } 1005 | 1006 | #[test] 1007 | #[should_panic(expected = "WrongAlgorithmHeader")] 1008 | fn compact_jws_decode_with_jwks_wrong_algorithm() { 1009 | let token = Compact::::new_encoded( 1010 | "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCIsImtpZCI6ImtleTAifQ.\ 1011 | eyJjb21wYW55IjoiQUNNRSIsImRlcGFydG1lbnQiOiJUb2lsZXQgQ2xlYW5pbmcifQ.\ 1012 | nz0a8aSweo6W0K2P7keByUPWl0HLVG45pTDznij5uKw", 1013 | ); 1014 | 1015 | let jwks: JWKSet = serde_json::from_str( 1016 | r#"{ 1017 | "keys": [ 1018 | { 1019 | "kty": "oct", 1020 | "use": "sig", 1021 | "kid": "key0", 1022 | "k": "-clnNQnBupZt23N8McUcZytLhan9OmjlJXmqS7daoeY", 1023 | "alg": "HS256" 1024 | } 1025 | ] 1026 | }"#, 1027 | ) 1028 | .unwrap(); 1029 | 1030 | let _ = token.decode_with_jwks(&jwks, None).unwrap(); 1031 | } 1032 | 1033 | #[test] 1034 | #[should_panic(expected = "KeyNotFound")] 1035 | fn compact_jws_decode_with_jwks_key_not_found() { 1036 | let token = Compact::::new_encoded( 1037 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleTAifQ.\ 1038 | eyJjb21wYW55IjoiQUNNRSIsImRlcGFydG1lbnQiOiJUb2lsZXQgQ2xlYW5pbmcifQ.\ 1039 | nz0a8aSweo6W0K2P7keByUPWl0HLVG45pTDznij5uKw", 1040 | ); 1041 | 1042 | let jwks: JWKSet = serde_json::from_str( 1043 | r#"{ 1044 | "keys": [ 1045 | { 1046 | "kty": "oct", 1047 | "use": "sig", 1048 | "kid": "keyX", 1049 | "k": "-clnNQnBupZt23N8McUcZytLhan9OmjlJXmqS7daoeY", 1050 | "alg": "HS256" 1051 | } 1052 | ] 1053 | }"#, 1054 | ) 1055 | .unwrap(); 1056 | 1057 | let _ = token.decode_with_jwks(&jwks, None).unwrap(); 1058 | } 1059 | 1060 | #[test] 1061 | #[should_panic(expected = "KidMissing")] 1062 | fn compact_jws_decode_with_jwks_kid_missing() { 1063 | let token = Compact::::new_encoded( 1064 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ 1065 | eyJjb21wYW55IjoiQUNNRSIsImRlcGFydG1lbnQiOiJUb2lsZXQgQ2xlYW5pbmcifQ.\ 1066 | QhdrScTpNXF2d0RbG_UTWu2gPKZfzANj6XC4uh-wOoU", 1067 | ); 1068 | 1069 | let jwks: JWKSet = serde_json::from_str( 1070 | r#"{ 1071 | "keys": [ 1072 | { 1073 | "kty": "oct", 1074 | "use": "sig", 1075 | "kid": "key0", 1076 | "k": "-clnNQnBupZt23N8McUcZytLhan9OmjlJXmqS7daoeY", 1077 | "alg": "HS256" 1078 | } 1079 | ] 1080 | }"#, 1081 | ) 1082 | .unwrap(); 1083 | 1084 | let _ = token.decode_with_jwks(&jwks, None).unwrap(); 1085 | } 1086 | 1087 | #[test] 1088 | #[should_panic(expected = "UnsupportedKeyAlgorithm")] 1089 | fn compact_jws_decode_with_jwks_algorithm_not_supported() { 1090 | let token = Compact::::new_encoded( 1091 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleTAifQ.\ 1092 | eyJjb21wYW55IjoiQUNNRSIsImRlcGFydG1lbnQiOiJUb2lsZXQgQ2xlYW5pbmcifQ.\ 1093 | nz0a8aSweo6W0K2P7keByUPWl0HLVG45pTDznij5uKw", 1094 | ); 1095 | 1096 | let jwks: JWKSet = serde_json::from_str( 1097 | r#"{ 1098 | "keys": [ 1099 | { 1100 | "kty": "oct", 1101 | "use": "sig", 1102 | "kid": "key0", 1103 | "k": "-clnNQnBupZt23N8McUcZytLhan9OmjlJXmqS7daoeY", 1104 | "alg": "A128CBC-HS256" 1105 | } 1106 | ] 1107 | }"#, 1108 | ) 1109 | .unwrap(); 1110 | 1111 | let _ = token.decode_with_jwks(&jwks, None).unwrap(); 1112 | } 1113 | 1114 | #[test] 1115 | #[should_panic(expected = "InvalidSignature")] 1116 | fn compact_jws_decode_with_p256_invalid_signature() { 1117 | let token = Compact::::new_encoded( 1118 | "eyJhbGciOiAiRVMyNTYiLCJ0eXAiOiAiSldUIiwia2lkIjogImtleTAifQ.\ 1119 | eyJjb21wYW55IjoiQUNNRSIsImRlcGFydG1lbnQiOiJUb2lsZXQgQ2xlYW5pbmcifQ.\ 1120 | nz0a8aSweo6W0K2P7keByUPWl0HLVG45pTDznij5uKw", 1121 | ); 1122 | 1123 | let jwks: JWKSet = serde_json::from_str( 1124 | r#"{ 1125 | "keys": [ 1126 | { 1127 | "kty": "EC", 1128 | "d": "oEMWfLRjrJdYa8OdfNz2_X2UrTet1Lnu2fIdlq7-Qd8", 1129 | "use": "sig", 1130 | "crv": "P-256", 1131 | "kid": "key0", 1132 | "x": "ZnXv09eyorTiF0AdN6HW-kltr0tt0GbgmD2_VGGlapI", 1133 | "y": "vERyG9Enhy8pEZ6V_pomH8aGjO7cINteCmnV5B9y0f0", 1134 | "alg": "ES256" 1135 | } 1136 | ] 1137 | }"#, 1138 | ) 1139 | .unwrap(); 1140 | 1141 | let _ = token.decode_with_jwks(&jwks, None).unwrap(); 1142 | } 1143 | 1144 | #[test] 1145 | fn unverified_header_is_returned_correctly() { 1146 | let encoded_token: Compact, Empty> = 1147 | Compact::new_encoded(HS256_PAYLOAD); 1148 | let expected_header = From::from(RegisteredHeader { 1149 | algorithm: SignatureAlgorithm::HS256, 1150 | ..Default::default() 1151 | }); 1152 | 1153 | let unverified_header = not_err!(encoded_token.unverified_header()); 1154 | assert_eq!(unverified_header, expected_header); 1155 | } 1156 | 1157 | #[test] 1158 | fn unverified_payload_is_returned_correctly() { 1159 | let encoded_token: Compact, Empty> = 1160 | Compact::new_encoded(HS256_PAYLOAD); 1161 | let expected_payload = ClaimsSet:: { 1162 | registered: RegisteredClaims { 1163 | issuer: Some(not_err!(FromStr::from_str("https://www.acme.com/"))), 1164 | subject: Some(not_err!(FromStr::from_str("John Doe"))), 1165 | audience: Some(SingleOrMultiple::Single(not_err!(FromStr::from_str( 1166 | "https://acme-customer.com/" 1167 | )))), 1168 | not_before: Some(1234.into()), 1169 | ..Default::default() 1170 | }, 1171 | private: PrivateClaims { 1172 | department: "Toilet Cleaning".to_string(), 1173 | company: "ACME".to_string(), 1174 | }, 1175 | }; 1176 | 1177 | let unverified_payload = not_err!(encoded_token.unverified_payload()); 1178 | assert_eq!(unverified_payload, expected_payload); 1179 | } 1180 | 1181 | #[test] 1182 | fn signature_is_returned_correctly() { 1183 | let encoded_token: Compact, Empty> = 1184 | Compact::new_encoded(HS256_PAYLOAD); 1185 | let expected_signature = data_encoding::BASE64URL_NOPAD 1186 | .decode(b"VFCl2un1Kc17odzOe2Ehf4DVrWddu3U4Ux3GFpOZHtc") 1187 | .expect("to not error"); 1188 | 1189 | let signature = not_err!(encoded_token.signature()); 1190 | assert_eq!(signature, expected_signature); 1191 | } 1192 | } 1193 | --------------------------------------------------------------------------------