From e340d755f7d24e913d0474f1bb6c20cb205aa876 Mon Sep 17 00:00:00 2001 From: Kenny <3454741+kvc0@users.noreply.github.com> Date: Sun, 21 Apr 2024 14:58:36 -0700 Subject: [PATCH 1/2] support decoding byte slices All of the backend infrastructure was in place to support &[u8], and some functions already take `impl AsRef<[u8]>`. However the main decode() and decode_header() functions require a &str. This change updates a few internal signatures and adds a _bytes() version of decode and decode_header. When you're doing many requests per second, the cost of doing an extra utf-8 check over header payloads is significant. By supporting a &[u8] decode, users can let base64 and the crypto implementation in question handle its own bytewise validity. They already do this today in addition to the extra utf-8 scan. --- benches/jwt.rs | 40 +++++++++++++++++++++++++++--- src/crypto/mod.rs | 7 +++--- src/crypto/rsa.rs | 2 +- src/decoding.rs | 61 ++++++++++++++++++++++++++++++++++++++++------ src/lib.rs | 4 ++- tests/ecdsa/mod.rs | 4 +-- tests/eddsa/mod.rs | 4 +-- 7 files changed, 102 insertions(+), 20 deletions(-) diff --git a/benches/jwt.rs b/benches/jwt.rs index d2fee79e..52dab57f 100644 --- a/benches/jwt.rs +++ b/benches/jwt.rs @@ -1,5 +1,8 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; +use jsonwebtoken::{ + decode, decode_bytes, decode_header, decode_header_bytes, encode, Algorithm, DecodingKey, + EncodingKey, Header, Validation, +}; use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] @@ -18,18 +21,47 @@ fn bench_encode(c: &mut Criterion) { } fn bench_decode(c: &mut Criterion) { - let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; + let token = b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; let key = DecodingKey::from_secret("secret".as_ref()); - c.bench_function("bench_decode", |b| { + let mut group = c.benchmark_group("decode"); + group.throughput(criterion::Throughput::Bytes(token.len() as u64)); + + group.bench_function("bytes", |b| { b.iter(|| { - decode::( + decode_bytes::( black_box(token), black_box(&key), black_box(&Validation::new(Algorithm::HS256)), ) }) }); + + group.bench_function("str", |b| { + b.iter(|| { + decode::( + // Simulate the cost of validating &str before decoding + black_box(std::str::from_utf8(black_box(token)).expect("valid utf8")), + black_box(&key), + black_box(&Validation::new(Algorithm::HS256)), + ) + }) + }); + + drop(group); + let mut group = c.benchmark_group("header"); + group.throughput(criterion::Throughput::Bytes(token.len() as u64)); + + group.bench_function("str", |b| { + b.iter(|| { + decode_header( + // Simulate the cost of validating &str before decoding + black_box(std::str::from_utf8(black_box(token)).expect("valid utf8")), + ) + }) + }); + + group.bench_function("bytes", |b| b.iter(|| decode_header_bytes(black_box(token)))); } criterion_group!(benches, bench_encode, bench_decode); diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index c2957dc8..9c89170a 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -46,7 +46,7 @@ pub fn sign(message: &[u8], key: &EncodingKey, algorithm: Algorithm) -> Result, message: &[u8], key: &[u8], ) -> Result { @@ -66,16 +66,17 @@ fn verify_ring( /// /// `message` is base64(header) + "." + base64(claims) pub fn verify( - signature: &str, + signature: impl AsRef<[u8]>, message: &[u8], key: &DecodingKey, algorithm: Algorithm, ) -> Result { + let signature = signature.as_ref(); match algorithm { Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => { // we just re-sign the message with the key and compare if they are equal let signed = sign(message, &EncodingKey::from_secret(key.as_bytes()), algorithm)?; - Ok(verify_slices_are_equal(signature.as_ref(), signed.as_ref()).is_ok()) + Ok(verify_slices_are_equal(signature, signed.as_ref()).is_ok()) } Algorithm::ES256 | Algorithm::ES384 => verify_ring( ecdsa::alg_to_ec_verification(algorithm), diff --git a/src/crypto/rsa.rs b/src/crypto/rsa.rs index 4c97db3c..e5af3b29 100644 --- a/src/crypto/rsa.rs +++ b/src/crypto/rsa.rs @@ -51,7 +51,7 @@ pub(crate) fn sign( /// Checks that a signature is valid based on the (n, e) RSA pubkey components pub(crate) fn verify_from_components( alg: &'static signature::RsaParameters, - signature: &str, + signature: impl AsRef<[u8]>, message: &[u8], components: (&[u8], &[u8]), ) -> Result { diff --git a/src/decoding.rs b/src/decoding.rs index 8d87f03d..017a5bda 100644 --- a/src/decoding.rs +++ b/src/decoding.rs @@ -204,11 +204,11 @@ impl DecodingKey { /// Verify signature of a JWT, and return header object and raw payload /// /// If the token or its signature is invalid, it will return an error. -fn verify_signature<'a>( - token: &'a str, +fn verify_signature_bytes<'a>( + token: &'a [u8], key: &DecodingKey, validation: &Validation, -) -> Result<(Header, &'a str)> { +) -> Result<(Header, &'a [u8])> { if validation.validate_signature && validation.algorithms.is_empty() { return Err(new_error(ErrorKind::MissingAlgorithm)); } @@ -221,15 +221,15 @@ fn verify_signature<'a>( } } - let (signature, message) = expect_two!(token.rsplitn(2, '.')); - let (payload, header) = expect_two!(message.rsplitn(2, '.')); + let (signature, message) = expect_two!(token.rsplitn(2, |b| *b == b'.')); + let (header, payload) = expect_two!(message.splitn(2, |b| *b == b'.')); let header = Header::from_encoded(header)?; if validation.validate_signature && !validation.algorithms.contains(&header.alg) { return Err(new_error(ErrorKind::InvalidAlgorithm)); } - if validation.validate_signature && !verify(signature, message.as_bytes(), key, header.alg)? { + if validation.validate_signature && !verify(signature, message, key, header.alg)? { return Err(new_error(ErrorKind::InvalidSignature)); } @@ -259,7 +259,38 @@ pub fn decode( key: &DecodingKey, validation: &Validation, ) -> Result> { - match verify_signature(token, key, validation) { + decode_bytes(token.as_bytes(), key, validation) +} + +/// Decode and validate a JWT +/// +/// If the token or its signature is invalid or the claims fail validation, it will return an error. +/// +/// This differs from decode() in the case that you only have bytes. By decoding as bytes you can +/// avoid taking a pass over your bytes to validate them as a utf-8 string. Since the decoding and +/// validation is all done in terms of bytes, the &str step is unnecessary. +/// If you already have a &str, decode is more convenient. If you have bytes, consider using this. +/// +/// ```rust +/// use serde::{Deserialize, Serialize}; +/// use jsonwebtoken::{decode_bytes, DecodingKey, Validation, Algorithm}; +/// +/// #[derive(Debug, Serialize, Deserialize)] +/// struct Claims { +/// sub: String, +/// company: String +/// } +/// +/// let token = b"a.jwt.token"; +/// // Claims is a struct that implements Deserialize +/// let token_message = decode_bytes::(token, &DecodingKey::from_secret("secret".as_ref()), &Validation::new(Algorithm::HS256)); +/// ``` +pub fn decode_bytes( + token: &[u8], + key: &DecodingKey, + validation: &Validation, +) -> Result> { + match verify_signature_bytes(token, key, validation) { Err(e) => Err(e), Ok((header, claims)) => { let decoded_claims = DecodedJwtPartClaims::from_jwt_part_claims(claims)?; @@ -286,3 +317,19 @@ pub fn decode_header(token: &str) -> Result
{ let (_, header) = expect_two!(message.rsplitn(2, '.')); Header::from_encoded(header) } + +/// Decode a JWT without any signature verification/validations and return its [Header](struct.Header.html). +/// +/// If the token has an invalid format (ie 3 parts separated by a `.`), it will return an error. +/// +/// ```rust +/// use jsonwebtoken::decode_header_bytes; +/// +/// let token = b"a.jwt.token"; +/// let header = decode_header_bytes(token); +/// ``` +pub fn decode_header_bytes(token: &[u8]) -> Result
{ + let (_, message) = expect_two!(token.rsplitn(2, |b| *b == b'.')); + let (_, header) = expect_two!(message.rsplitn(2, |b| *b == b'.')); + Header::from_encoded(header) +} diff --git a/src/lib.rs b/src/lib.rs index 0c8664bf..a592ae79 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,9 @@ mod serialization; mod validation; pub use algorithms::Algorithm; -pub use decoding::{decode, decode_header, DecodingKey, TokenData}; +pub use decoding::{ + decode, decode_bytes, decode_header, decode_header_bytes, DecodingKey, TokenData, +}; pub use encoding::{encode, EncodingKey}; pub use header::Header; pub use validation::{get_current_timestamp, Validation}; diff --git a/tests/ecdsa/mod.rs b/tests/ecdsa/mod.rs index 8c06910f..73934764 100644 --- a/tests/ecdsa/mod.rs +++ b/tests/ecdsa/mod.rs @@ -26,7 +26,7 @@ fn round_trip_sign_verification_pk8() { let encrypted = sign(b"hello world", &EncodingKey::from_ec_der(privkey), Algorithm::ES256).unwrap(); let is_valid = - verify(&encrypted, b"hello world", &DecodingKey::from_ec_der(pubkey), Algorithm::ES256) + verify(encrypted, b"hello world", &DecodingKey::from_ec_der(pubkey), Algorithm::ES256) .unwrap(); assert!(is_valid); } @@ -41,7 +41,7 @@ fn round_trip_sign_verification_pem() { sign(b"hello world", &EncodingKey::from_ec_pem(privkey_pem).unwrap(), Algorithm::ES256) .unwrap(); let is_valid = verify( - &encrypted, + encrypted, b"hello world", &DecodingKey::from_ec_pem(pubkey_pem).unwrap(), Algorithm::ES256, diff --git a/tests/eddsa/mod.rs b/tests/eddsa/mod.rs index 85dd0245..d7d89d23 100644 --- a/tests/eddsa/mod.rs +++ b/tests/eddsa/mod.rs @@ -26,7 +26,7 @@ fn round_trip_sign_verification_pk8() { let encrypted = sign(b"hello world", &EncodingKey::from_ed_der(privkey), Algorithm::EdDSA).unwrap(); let is_valid = - verify(&encrypted, b"hello world", &DecodingKey::from_ed_der(pubkey), Algorithm::EdDSA) + verify(encrypted, b"hello world", &DecodingKey::from_ed_der(pubkey), Algorithm::EdDSA) .unwrap(); assert!(is_valid); } @@ -41,7 +41,7 @@ fn round_trip_sign_verification_pem() { sign(b"hello world", &EncodingKey::from_ed_pem(privkey_pem).unwrap(), Algorithm::EdDSA) .unwrap(); let is_valid = verify( - &encrypted, + encrypted, b"hello world", &DecodingKey::from_ed_pem(pubkey_pem).unwrap(), Algorithm::EdDSA, From abf4e7e217fb750f5a6a5091f3e8c98ba53cd2ae Mon Sep 17 00:00:00 2001 From: Kenny <3454741+kvc0@users.noreply.github.com> Date: Tue, 23 Apr 2024 20:58:00 -0700 Subject: [PATCH 2/2] back to 1 fn, with `impl AsRef<[u8]>` --- benches/jwt.rs | 22 ++++------------- src/decoding.rs | 63 +++++++------------------------------------------ src/lib.rs | 4 +--- 3 files changed, 14 insertions(+), 75 deletions(-) diff --git a/benches/jwt.rs b/benches/jwt.rs index 52dab57f..8d6d9e9c 100644 --- a/benches/jwt.rs +++ b/benches/jwt.rs @@ -1,7 +1,6 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; use jsonwebtoken::{ - decode, decode_bytes, decode_header, decode_header_bytes, encode, Algorithm, DecodingKey, - EncodingKey, Header, Validation, + decode, decode_header, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation, }; use serde::{Deserialize, Serialize}; @@ -21,27 +20,16 @@ fn bench_encode(c: &mut Criterion) { } fn bench_decode(c: &mut Criterion) { - let token = b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; let key = DecodingKey::from_secret("secret".as_ref()); let mut group = c.benchmark_group("decode"); group.throughput(criterion::Throughput::Bytes(token.len() as u64)); - group.bench_function("bytes", |b| { - b.iter(|| { - decode_bytes::( - black_box(token), - black_box(&key), - black_box(&Validation::new(Algorithm::HS256)), - ) - }) - }); - group.bench_function("str", |b| { b.iter(|| { decode::( - // Simulate the cost of validating &str before decoding - black_box(std::str::from_utf8(black_box(token)).expect("valid utf8")), + black_box(token), black_box(&key), black_box(&Validation::new(Algorithm::HS256)), ) @@ -56,12 +44,10 @@ fn bench_decode(c: &mut Criterion) { b.iter(|| { decode_header( // Simulate the cost of validating &str before decoding - black_box(std::str::from_utf8(black_box(token)).expect("valid utf8")), + black_box(token), ) }) }); - - group.bench_function("bytes", |b| b.iter(|| decode_header_bytes(black_box(token)))); } criterion_group!(benches, bench_encode, bench_decode); diff --git a/src/decoding.rs b/src/decoding.rs index 017a5bda..bcc78c63 100644 --- a/src/decoding.rs +++ b/src/decoding.rs @@ -222,7 +222,7 @@ fn verify_signature_bytes<'a>( } let (signature, message) = expect_two!(token.rsplitn(2, |b| *b == b'.')); - let (header, payload) = expect_two!(message.splitn(2, |b| *b == b'.')); + let (payload, header) = expect_two!(message.rsplitn(2, |b| *b == b'.')); let header = Header::from_encoded(header)?; if validation.validate_signature && !validation.algorithms.contains(&header.alg) { @@ -250,46 +250,16 @@ fn verify_signature_bytes<'a>( /// company: String /// } /// -/// let token = "a.jwt.token".to_string(); +/// let token = "a.jwt.token"; /// // Claims is a struct that implements Deserialize -/// let token_message = decode::(&token, &DecodingKey::from_secret("secret".as_ref()), &Validation::new(Algorithm::HS256)); +/// let token_message = decode::(token, &DecodingKey::from_secret("secret".as_ref()), &Validation::new(Algorithm::HS256)); /// ``` pub fn decode( - token: &str, - key: &DecodingKey, - validation: &Validation, -) -> Result> { - decode_bytes(token.as_bytes(), key, validation) -} - -/// Decode and validate a JWT -/// -/// If the token or its signature is invalid or the claims fail validation, it will return an error. -/// -/// This differs from decode() in the case that you only have bytes. By decoding as bytes you can -/// avoid taking a pass over your bytes to validate them as a utf-8 string. Since the decoding and -/// validation is all done in terms of bytes, the &str step is unnecessary. -/// If you already have a &str, decode is more convenient. If you have bytes, consider using this. -/// -/// ```rust -/// use serde::{Deserialize, Serialize}; -/// use jsonwebtoken::{decode_bytes, DecodingKey, Validation, Algorithm}; -/// -/// #[derive(Debug, Serialize, Deserialize)] -/// struct Claims { -/// sub: String, -/// company: String -/// } -/// -/// let token = b"a.jwt.token"; -/// // Claims is a struct that implements Deserialize -/// let token_message = decode_bytes::(token, &DecodingKey::from_secret("secret".as_ref()), &Validation::new(Algorithm::HS256)); -/// ``` -pub fn decode_bytes( - token: &[u8], + token: impl AsRef<[u8]>, key: &DecodingKey, validation: &Validation, ) -> Result> { + let token = token.as_ref(); match verify_signature_bytes(token, key, validation) { Err(e) => Err(e), Ok((header, claims)) => { @@ -309,26 +279,11 @@ pub fn decode_bytes( /// ```rust /// use jsonwebtoken::decode_header; /// -/// let token = "a.jwt.token".to_string(); -/// let header = decode_header(&token); -/// ``` -pub fn decode_header(token: &str) -> Result
{ - let (_, message) = expect_two!(token.rsplitn(2, '.')); - let (_, header) = expect_two!(message.rsplitn(2, '.')); - Header::from_encoded(header) -} - -/// Decode a JWT without any signature verification/validations and return its [Header](struct.Header.html). -/// -/// If the token has an invalid format (ie 3 parts separated by a `.`), it will return an error. -/// -/// ```rust -/// use jsonwebtoken::decode_header_bytes; -/// -/// let token = b"a.jwt.token"; -/// let header = decode_header_bytes(token); +/// let token = "a.jwt.token"; +/// let header = decode_header(token); /// ``` -pub fn decode_header_bytes(token: &[u8]) -> Result
{ +pub fn decode_header(token: impl AsRef<[u8]>) -> Result
{ + let token = token.as_ref(); let (_, message) = expect_two!(token.rsplitn(2, |b| *b == b'.')); let (_, header) = expect_two!(message.rsplitn(2, |b| *b == b'.')); Header::from_encoded(header) diff --git a/src/lib.rs b/src/lib.rs index a592ae79..0c8664bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,9 +18,7 @@ mod serialization; mod validation; pub use algorithms::Algorithm; -pub use decoding::{ - decode, decode_bytes, decode_header, decode_header_bytes, DecodingKey, TokenData, -}; +pub use decoding::{decode, decode_header, DecodingKey, TokenData}; pub use encoding::{encode, EncodingKey}; pub use header::Header; pub use validation::{get_current_timestamp, Validation};