Skip to content

Commit

Permalink
support decoding byte slices
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
kvc0 committed Apr 21, 2024
1 parent afbb44e commit e340d75
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 20 deletions.
40 changes: 36 additions & 4 deletions benches/jwt.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -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::<Claims>(
decode_bytes::<Claims>(
black_box(token),
black_box(&key),
black_box(&Validation::new(Algorithm::HS256)),
)
})
});

group.bench_function("str", |b| {
b.iter(|| {
decode::<Claims>(
// 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);
Expand Down
7 changes: 4 additions & 3 deletions src/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub fn sign(message: &[u8], key: &EncodingKey, algorithm: Algorithm) -> Result<S
/// See Ring docs for more details
fn verify_ring(
alg: &'static dyn signature::VerificationAlgorithm,
signature: &str,
signature: impl AsRef<[u8]>,
message: &[u8],
key: &[u8],
) -> Result<bool> {
Expand All @@ -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<bool> {
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),
Expand Down
2 changes: 1 addition & 1 deletion src/crypto/rsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> {
Expand Down
61 changes: 54 additions & 7 deletions src/decoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand All @@ -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));
}

Expand Down Expand Up @@ -259,7 +259,38 @@ pub fn decode<T: DeserializeOwned>(
key: &DecodingKey,
validation: &Validation,
) -> Result<TokenData<T>> {
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::<Claims>(token, &DecodingKey::from_secret("secret".as_ref()), &Validation::new(Algorithm::HS256));
/// ```
pub fn decode_bytes<T: DeserializeOwned>(
token: &[u8],
key: &DecodingKey,
validation: &Validation,
) -> Result<TokenData<T>> {
match verify_signature_bytes(token, key, validation) {
Err(e) => Err(e),
Ok((header, claims)) => {
let decoded_claims = DecodedJwtPartClaims::from_jwt_part_claims(claims)?;
Expand All @@ -286,3 +317,19 @@ pub fn decode_header(token: &str) -> Result<Header> {
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<Header> {
let (_, message) = expect_two!(token.rsplitn(2, |b| *b == b'.'));
let (_, header) = expect_two!(message.rsplitn(2, |b| *b == b'.'));
Header::from_encoded(header)
}
4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
4 changes: 2 additions & 2 deletions tests/ecdsa/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions tests/eddsa/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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,
Expand Down

0 comments on commit e340d75

Please sign in to comment.