Add EncodingKey

This commit is contained in:
Vincent Prouillet 2019-12-29 18:42:35 +01:00
parent 771f955690
commit 0abeeac25f
13 changed files with 157 additions and 93 deletions

View File

@ -38,7 +38,7 @@ Complete examples are available in the examples directory: a basic one and one w
In terms of imports and structs:
```rust
use serde::{Serialize, Deserialize};
use jsonwebtoken::{encode, decode, Header, Algorithm, Validation};
use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey};
/// Our claims struct, it needs to derive `Serialize` and/or `Deserialize`
#[derive(Debug, Serialize, Deserialize)]
@ -53,7 +53,7 @@ struct Claims {
The default algorithm is HS256, which uses a shared secret.
```rust
let token = encode(&Header::default(), &my_claims, "secret".as_ref())?;
let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret("secret".as_ref()))?;
```
#### Custom headers & changing algorithm
@ -63,7 +63,7 @@ If you want to set the `kid` parameter or change the algorithm for example:
```rust
let mut header = Header::new(Algorithm::HS512);
header.kid = Some("blabla".to_owned());
let token = encode(&header, &my_claims, "secret".as_ref())?;
let token = encode(&header, &my_claims, &EncodingKey::from_secret("secret".as_ref()))?;
```
Look at `examples/custom_header.rs` for a full working example.
@ -71,7 +71,7 @@ Look at `examples/custom_header.rs` for a full working example.
```rust
// HS256
let token = encode(&Header::default(), &my_claims, "secret".as_ref())?;
let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret("secret".as_ref()))?;
// RSA
let token = encode(&Header::new(Algorithm::RS256), &my_claims, include_str!("privkey.pem"))?;
```

View File

@ -1,5 +1,5 @@
use chrono::prelude::*;
use jsonwebtoken::{Header, Validation};
use jsonwebtoken::{EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
const SECRET: &str = "some-secret";
@ -51,8 +51,9 @@ mod jwt_numeric_date {
let claims = Claims { sub: sub.clone(), iat, exp };
let token = encode(&Header::default(), &claims, SECRET.as_ref())
.expect("Failed to encode claims");
let token =
encode(&Header::default(), &claims, &EncodingKey::from_secret(SECRET.as_ref()))
.expect("Failed to encode claims");
assert_eq!(&token, EXPECTED_TOKEN);
@ -82,7 +83,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let claims = Claims { sub: sub.clone(), iat, exp };
let token = jsonwebtoken::encode(&Header::default(), &claims, SECRET.as_ref())?;
let token = jsonwebtoken::encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(SECRET.as_ref()),
)?;
println!("serialized token: {}", &token);

View File

@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
use jsonwebtoken::errors::ErrorKind;
use jsonwebtoken::{decode, encode, Algorithm, Header, Validation};
use jsonwebtoken::{decode, encode, Algorithm, EncodingKey, Header, Validation};
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
@ -19,7 +19,7 @@ fn main() {
header.kid = Some("signing_key".to_owned());
header.alg = Algorithm::HS512;
let token = match encode(&header, &my_claims, key) {
let token = match encode(&header, &my_claims, &EncodingKey::from_secret(key)) {
Ok(t) => t,
Err(_) => panic!(), // in practice you would return the error
};

View File

@ -1,5 +1,5 @@
use jsonwebtoken::errors::ErrorKind;
use jsonwebtoken::{decode, encode, Header, Validation};
use jsonwebtoken::{decode, encode, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
@ -10,10 +10,10 @@ struct Claims {
}
fn main() {
let key = b"secret";
let my_claims =
Claims { sub: "b@b.com".to_owned(), company: "ACME".to_owned(), exp: 10000000000 };
let key = b"secret";
let token = match encode(&Header::default(), &my_claims, key) {
let token = match encode(&Header::default(), &my_claims, &EncodingKey::from_secret(key)) {
Ok(t) => t,
Err(_) => panic!(), // in practice you would return the error
};

View File

@ -2,7 +2,6 @@ use ring::{rand, signature};
use crate::algorithms::Algorithm;
use crate::errors::Result;
use crate::pem::decoder::PemEncodedKey;
use crate::serialization::b64_encode;
/// Only used internally when validating EC, to map from our enum to the Ring EcdsaVerificationAlgorithm structs.
@ -26,13 +25,13 @@ pub(crate) fn alg_to_ec_signing(alg: Algorithm) -> &'static signature::EcdsaSign
}
/// The actual ECDSA signing + encoding
/// The key needs to be in PKCS8 format
pub fn sign(
alg: &'static signature::EcdsaSigningAlgorithm,
key: &[u8],
message: &str,
) -> Result<String> {
let pem_key = PemEncodedKey::new(key)?;
let signing_key = signature::EcdsaKeyPair::from_pkcs8(alg, pem_key.as_ec_private_key()?)?;
let signing_key = signature::EcdsaKeyPair::from_pkcs8(alg, key)?;
let rng = rand::SystemRandom::new();
let out = signing_key.sign(&rng, message.as_bytes())?;
Ok(b64_encode(out.as_ref()))

View File

@ -2,6 +2,7 @@ use ring::constant_time::verify_slices_are_equal;
use ring::{hmac, signature};
use crate::algorithms::Algorithm;
use crate::encoding::EncodingKey;
use crate::errors::Result;
use crate::pem::decoder::PemEncodedKey;
use crate::serialization::{b64_decode, b64_encode};
@ -20,16 +21,14 @@ pub(crate) fn sign_hmac(alg: hmac::Algorithm, key: &[u8], message: &str) -> Resu
/// the base64 url safe encoded of the result.
///
/// If you just want to encode a JWT, use `encode` instead.
///
/// `key` is the secret for HMAC and a pem encoded string otherwise
pub fn sign(message: &str, key: &[u8], algorithm: Algorithm) -> Result<String> {
pub fn sign(message: &str, key: &EncodingKey, algorithm: Algorithm) -> Result<String> {
match algorithm {
Algorithm::HS256 => sign_hmac(hmac::HMAC_SHA256, key, message),
Algorithm::HS384 => sign_hmac(hmac::HMAC_SHA384, key, message),
Algorithm::HS512 => sign_hmac(hmac::HMAC_SHA512, key, message),
Algorithm::HS256 => sign_hmac(hmac::HMAC_SHA256, key.inner(), message),
Algorithm::HS384 => sign_hmac(hmac::HMAC_SHA384, key.inner(), message),
Algorithm::HS512 => sign_hmac(hmac::HMAC_SHA512, key.inner(), message),
Algorithm::ES256 | Algorithm::ES384 => {
ecdsa::sign(ecdsa::alg_to_ec_signing(algorithm), key, message)
ecdsa::sign(ecdsa::alg_to_ec_signing(algorithm), key.inner(), message)
}
Algorithm::RS256
@ -37,7 +36,7 @@ pub fn sign(message: &str, key: &[u8], algorithm: Algorithm) -> Result<String> {
| Algorithm::RS512
| Algorithm::PS256
| Algorithm::PS384
| Algorithm::PS512 => rsa::sign(rsa::alg_to_rsa_signing(algorithm), key, message),
| Algorithm::PS512 => rsa::sign(rsa::alg_to_rsa_signing(algorithm), key.inner(), message),
}
}
@ -69,7 +68,7 @@ pub fn verify(signature: &str, message: &str, key: &[u8], algorithm: Algorithm)
match algorithm {
Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => {
// we just re-sign the message with the key and compare if they are equal
let signed = sign(message, key, algorithm)?;
let signed = sign(message, &EncodingKey::from_secret(key), algorithm)?;
Ok(verify_slices_are_equal(signature.as_ref(), signed.as_ref()).is_ok())
}
Algorithm::ES256 | Algorithm::ES384 => {

View File

@ -3,7 +3,6 @@ use simple_asn1::BigUint;
use crate::algorithms::Algorithm;
use crate::errors::{ErrorKind, Result};
use crate::pem::decoder::PemEncodedKey;
use crate::serialization::{b64_decode, b64_encode};
/// Only used internally when validating RSA, to map from our enum to the Ring param structs.
@ -33,15 +32,14 @@ pub(crate) fn alg_to_rsa_signing(alg: Algorithm) -> &'static dyn signature::RsaE
}
/// The actual RSA signing + encoding
/// The key needs to be in PKCS8 format
/// Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html
pub(crate) fn sign(
alg: &'static dyn signature::RsaEncoding,
key: &[u8],
message: &str,
) -> Result<String> {
let pem_key = PemEncodedKey::new(key)?;
let key_pair = signature::RsaKeyPair::from_der(pem_key.as_rsa_key()?)
.map_err(|_| ErrorKind::InvalidRsaKey)?;
let key_pair = signature::RsaKeyPair::from_der(key).map_err(|_| ErrorKind::InvalidRsaKey)?;
let mut signature = vec![0; key_pair.public_modulus_len()];
let rng = rand::SystemRandom::new();

79
src/encoding.rs Normal file
View File

@ -0,0 +1,79 @@
use std::borrow::Cow;
use serde::ser::Serialize;
use crate::crypto;
use crate::errors::Result;
use crate::header::Header;
use crate::pem::decoder::PemEncodedKey;
use crate::serialization::b64_encode_part;
/// A key to encode a JWT with. Can be a secret, a PEM-encoded key or a DER-encoded key.
#[derive(Debug, Clone, PartialEq)]
pub struct EncodingKey<'a> {
content: Cow<'a, [u8]>,
}
impl<'a> EncodingKey<'a> {
/// If you're using HMAC, use that.
pub fn from_secret(secret: &'a [u8]) -> Self {
EncodingKey { content: Cow::Borrowed(secret) }
}
/// If you are loading a RSA key from a .pem file
/// This errors if the key is not a valid RSA key
pub fn from_rsa_pem(key: &'a [u8]) -> Result<Self> {
let pem_key = PemEncodedKey::new(key)?;
let content = pem_key.as_rsa_key()?;
Ok(EncodingKey { content: Cow::Owned(content.to_vec()) })
}
/// If you are loading a ECDSA key from a .pem file
/// This errors if the key is not a valid private EC key
pub fn from_ec_pem(key: &'a [u8]) -> Result<Self> {
let pem_key = PemEncodedKey::new(key)?;
let content = pem_key.as_ec_private_key()?;
Ok(EncodingKey { content: Cow::Owned(content.to_vec()) })
}
/// If you know what you're doing and have the DER-encoded key, for RSA or ECDSA
pub fn from_der(der: &'a [u8]) -> Self {
EncodingKey { content: Cow::Borrowed(der) }
}
/// Access the key, normal users do not need to use that.
pub fn inner(&'a self) -> &'a [u8] {
&self.content
}
}
/// Encode the header and claims given and sign the payload using the algorithm from the header and the key.
/// If the algorithm given is RSA or EC, the key needs to be in the PEM format.
///
/// ```rust
/// use serde::{Deserialize, Serialize};
/// use jsonwebtoken::{encode, Algorithm, Header, EncodingKey};
///
/// #[derive(Debug, Serialize, Deserialize)]
/// struct Claims {
/// sub: String,
/// company: String
/// }
///
/// let my_claims = Claims {
/// sub: "b@b.com".to_owned(),
/// company: "ACME".to_owned()
/// };
///
/// // my_claims is a struct that implements Serialize
/// // This will create a JWT using HS256 as algorithm
/// let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret("secret".as_ref())).unwrap();
/// ```
pub fn encode<T: Serialize>(header: &Header, claims: &T, key: &EncodingKey) -> Result<String> {
let encoded_header = b64_encode_part(&header)?;
let encoded_claims = b64_encode_part(&claims)?;
let message = [encoded_header.as_ref(), encoded_claims.as_ref()].join(".");
let signature = crypto::sign(&*message, key, header.alg)?;
Ok([message, signature].join("."))
}

View File

@ -7,6 +7,7 @@ mod algorithms;
/// Lower level functions, if you want to do something other than JWTs
pub mod crypto;
mod decoding;
mod encoding;
/// All the errors that can be encountered while encoding/decoding JWTs
pub mod errors;
mod header;
@ -18,41 +19,6 @@ pub use algorithms::Algorithm;
pub use decoding::{
dangerous_unsafe_decode, decode, decode_header, decode_rsa_components, TokenData,
};
pub use encoding::{encode, EncodingKey};
pub use header::Header;
pub use validation::Validation;
use serde::ser::Serialize;
use crate::errors::Result;
use crate::serialization::b64_encode_part;
/// Encode the header and claims given and sign the payload using the algorithm from the header and the key.
/// If the algorithm given is RSA or EC, the key needs to be in the PEM format.
///
/// ```rust
/// use serde::{Deserialize, Serialize};
/// use jsonwebtoken::{encode, Algorithm, Header};
///
/// #[derive(Debug, Serialize, Deserialize)]
/// struct Claims {
/// sub: String,
/// company: String
/// }
///
/// let my_claims = Claims {
/// sub: "b@b.com".to_owned(),
/// company: "ACME".to_owned()
/// };
///
/// // my_claims is a struct that implements Serialize
/// // This will create a JWT using HS256 as algorithm
/// let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap();
/// ```
pub fn encode<T: Serialize>(header: &Header, claims: &T, key: &[u8]) -> Result<String> {
let encoded_header = b64_encode_part(&header)?;
let encoded_claims = b64_encode_part(&claims)?;
let message = [encoded_header.as_ref(), encoded_claims.as_ref()].join(".");
let signature = crypto::sign(&*message, key, header.alg)?;
Ok([message, signature].join("."))
}

View File

@ -159,7 +159,7 @@ pub fn validate(claims: &Map<String, Value>, options: &Validation) -> Result<()>
return Err(new_error(ErrorKind::InvalidAudience));
}
}
_ => return Err(new_error(ErrorKind::InvalidAudience))
_ => return Err(new_error(ErrorKind::InvalidAudience)),
};
} else {
return Err(new_error(ErrorKind::InvalidAudience));
@ -447,17 +447,17 @@ mod tests {
#[test]
fn aud_use_validation_struct() {
let mut claims = Map::new();
claims.insert("aud".to_string(), to_value("my-googleclientid1234.apps.googleusercontent.com").unwrap());
claims.insert(
"aud".to_string(),
to_value("my-googleclientid1234.apps.googleusercontent.com").unwrap(),
);
let aud = "my-googleclientid1234.apps.googleusercontent.com".to_string();
let mut aud_hashset = std::collections::HashSet::new();
aud_hashset.insert(aud);
let validation = Validation {
aud: Some(aud_hashset),
validate_exp: false,
..Validation::default()
};
let validation =
Validation { aud: Some(aud_hashset), validate_exp: false, ..Validation::default() };
let res = validate(&claims, &validation);
println!("{:?}", res);
assert!(res.is_ok());

View File

@ -1,7 +1,7 @@
use chrono::Utc;
use jsonwebtoken::{
crypto::{sign, verify},
decode, encode, Algorithm, Header, Validation,
decode, encode, Algorithm, EncodingKey, Header, Validation,
};
use serde::{Deserialize, Serialize};
@ -26,7 +26,8 @@ pub struct Claims {
fn round_trip_sign_verification_pem() {
let privkey = include_bytes!("private_ecdsa_key.pem");
let pubkey = include_bytes!("public_ecdsa_key.pem");
let encrypted = sign("hello world", privkey, Algorithm::ES256).unwrap();
let encrypted =
sign("hello world", &EncodingKey::from_ec_pem(privkey).unwrap(), Algorithm::ES256).unwrap();
let is_valid = verify(&encrypted, "hello world", pubkey, Algorithm::ES256).unwrap();
assert!(is_valid);
}
@ -40,7 +41,12 @@ fn round_trip_claim() {
company: "ACME".to_string(),
exp: Utc::now().timestamp() + 10000,
};
let token = encode(&Header::new(Algorithm::ES256), &my_claims, privkey).unwrap();
let token = encode(
&Header::new(Algorithm::ES256),
&my_claims,
&EncodingKey::from_ec_pem(privkey).unwrap(),
)
.unwrap();
let token_data = decode::<Claims>(&token, pubkey, &Validation::new(Algorithm::ES256)).unwrap();
assert_eq!(my_claims, token_data.claims);
}
@ -56,7 +62,12 @@ fn roundtrip_with_jwtio_example() {
company: "ACME".to_string(),
exp: Utc::now().timestamp() + 10000,
};
let token = encode(&Header::new(Algorithm::ES384), &my_claims, privkey).unwrap();
let token = encode(
&Header::new(Algorithm::ES384),
&my_claims,
&EncodingKey::from_ec_pem(privkey).unwrap(),
)
.unwrap();
let token_data = decode::<Claims>(&token, pubkey, &Validation::new(Algorithm::ES384)).unwrap();
assert_eq!(my_claims, token_data.claims);
}

View File

@ -1,7 +1,8 @@
use chrono::Utc;
use jsonwebtoken::{
crypto::{sign, verify},
dangerous_unsafe_decode, decode, decode_header, encode, Algorithm, Header, Validation,
dangerous_unsafe_decode, decode, decode_header, encode, Algorithm, EncodingKey, Header,
Validation,
};
use serde::{Deserialize, Serialize};
@ -14,7 +15,8 @@ pub struct Claims {
#[test]
fn sign_hs256() {
let result = sign("hello world", b"secret", Algorithm::HS256).unwrap();
let result =
sign("hello world", &EncodingKey::from_secret(b"secret"), Algorithm::HS256).unwrap();
let expected = "c0zGLzKEFWj0VxWuufTXiRMk5tlI5MbGDAYhzaxIYjo";
assert_eq!(result, expected);
}
@ -35,7 +37,7 @@ fn encode_with_custom_header() {
};
let mut header = Header::default();
header.kid = Some("kid".to_string());
let token = encode(&header, &my_claims, b"secret").unwrap();
let token = encode(&header, &my_claims, &EncodingKey::from_secret(b"secret")).unwrap();
let token_data = decode::<Claims>(&token, b"secret", &Validation::default()).unwrap();
assert_eq!(my_claims, token_data.claims);
assert_eq!("kid", token_data.header.kid.unwrap());
@ -48,7 +50,8 @@ fn round_trip_claim() {
company: "ACME".to_string(),
exp: Utc::now().timestamp() + 10000,
};
let token = encode(&Header::default(), &my_claims, b"secret").unwrap();
let token =
encode(&Header::default(), &my_claims, &EncodingKey::from_secret(b"secret")).unwrap();
let token_data = decode::<Claims>(&token, b"secret", &Validation::default()).unwrap();
assert_eq!(my_claims, token_data.claims);
assert!(token_data.header.kid.is_none());

View File

@ -1,7 +1,7 @@
use chrono::Utc;
use jsonwebtoken::{
crypto::{sign, verify},
decode, decode_rsa_components, encode, Algorithm, Header, Validation,
decode, decode_rsa_components, encode, Algorithm, EncodingKey, Header, Validation,
};
use serde::{Deserialize, Serialize};
@ -27,7 +27,8 @@ fn round_trip_sign_verification_pem_pkcs1() {
let pubkey_pem = include_bytes!("public_rsa_key_pkcs1.pem");
for &alg in RSA_ALGORITHMS {
let encrypted = sign("hello world", privkey_pem, alg).unwrap();
let encrypted =
sign("hello world", &EncodingKey::from_rsa_pem(privkey_pem).unwrap(), alg).unwrap();
let is_valid = verify(&encrypted, "hello world", pubkey_pem, alg).unwrap();
assert!(is_valid);
}
@ -39,7 +40,8 @@ fn round_trip_sign_verification_pem_pkcs8() {
let pubkey_pem = include_bytes!("public_rsa_key_pkcs8.pem");
for &alg in RSA_ALGORITHMS {
let encrypted = sign("hello world", privkey_pem, alg).unwrap();
let encrypted =
sign("hello world", &EncodingKey::from_rsa_pem(privkey_pem).unwrap(), alg).unwrap();
let is_valid = verify(&encrypted, "hello world", pubkey_pem, alg).unwrap();
assert!(is_valid);
}
@ -55,7 +57,9 @@ fn round_trip_claim() {
let privkey = include_bytes!("private_rsa_key_pkcs1.pem");
for &alg in RSA_ALGORITHMS {
let token = encode(&Header::new(alg), &my_claims, privkey).unwrap();
let token =
encode(&Header::new(alg), &my_claims, &EncodingKey::from_rsa_pem(privkey).unwrap())
.unwrap();
let token_data = decode::<Claims>(
&token,
include_bytes!("public_rsa_key_pkcs1.pem"),
@ -78,18 +82,16 @@ fn rsa_modulus_exponent() {
let n = "yRE6rHuNR0QbHO3H3Kt2pOKGVhQqGZXInOduQNxXzuKlvQTLUTv4l4sggh5_CYYi_cvI-SXVT9kPWSKXxJXBXd_4LkvcPuUakBoAkfh-eiFVMh2VrUyWyj3MFl0HTVF9KwRXLAcwkREiS3npThHRyIxuy0ZMeZfxVL5arMhw1SRELB8HoGfG_AtH89BIE9jDBHZ9dLelK9a184zAf8LwoPLxvJb3Il5nncqPcSfKDDodMFBIMc4lQzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xqi-yUod-j8MtvIj812dkS4QMiRVN_by2h3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5TdQ";
let e = "AQAB";
let encrypted = encode(&Header::new(Algorithm::RS256), &my_claims, privkey.as_ref()).unwrap();
let encrypted = encode(
&Header::new(Algorithm::RS256),
&my_claims,
&EncodingKey::from_rsa_pem(privkey.as_ref()).unwrap(),
)
.unwrap();
let res = decode_rsa_components::<Claims>(&encrypted, n, e, &Validation::new(Algorithm::RS256));
assert!(res.is_ok());
}
#[test]
#[should_panic(expected = "InvalidKeyFormat")]
fn fails_with_non_pkcs8_key_format() {
let _encrypted =
sign("hello world", include_bytes!("private_rsa_key_pkcs1.pem"), Algorithm::ES256).unwrap();
}
// https://jwt.io/ is often used for examples so ensure their example works with jsonwebtoken
#[test]
fn roundtrip_with_jwtio_example_jey() {
@ -103,7 +105,9 @@ fn roundtrip_with_jwtio_example_jey() {
};
for &alg in RSA_ALGORITHMS {
let token = encode(&Header::new(alg), &my_claims, privkey_pem).unwrap();
let token =
encode(&Header::new(alg), &my_claims, &EncodingKey::from_rsa_pem(privkey_pem).unwrap())
.unwrap();
let token_data = decode::<Claims>(&token, pubkey_pem, &Validation::new(alg)).unwrap();
assert_eq!(my_claims, token_data.claims);
}