Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clarify Key formats #89

Merged
merged 5 commits into from
May 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ keywords = ["jwt", "web", "api", "token", "json"]
serde_json = "1.0"
serde_derive = "1.0"
serde = "1.0"
ring = "0.14.4"
ring = "0.14.6"
base64 = "0.10"
untrusted = "0.6"
chrono = "0.4"
4 changes: 2 additions & 2 deletions benches/jwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ extern crate test;
#[macro_use]
extern crate serde_derive;

use jwt::{decode, encode, Header, Validation};
use jwt::{decode, encode, Header, Hmac, Validation};

#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
struct Claims {
Expand All @@ -16,7 +16,7 @@ struct Claims {
fn bench_encode(b: &mut test::Bencher) {
let claim = Claims { sub: "b@b.com".to_owned(), company: "ACME".to_owned() };

b.iter(|| encode(&Header::default(), &claim, "secret".as_ref()));
b.iter(|| encode(&Header::default(), &claim, Hmac::from(b"secret")));
}

#[bench]
Expand Down
4 changes: 2 additions & 2 deletions examples/custom_header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ struct Claims {
fn main() {
let my_claims =
Claims { sub: "b@b.com".to_owned(), company: "ACME".to_owned(), exp: 10000000000 };
let key = "secret";
let key = b"secret";

let mut header = Header::default();
header.kid = Some("signing_key".to_owned());
header.alg = Algorithm::HS512;

let token = match encode(&header, &my_claims, key.as_ref()) {
let token = match encode(&header, &my_claims, jwt::Der::from(key)) {
Ok(t) => t,
Err(_) => panic!(), // in practice you would return the error
};
Expand Down
4 changes: 2 additions & 2 deletions examples/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ struct Claims {
fn main() {
let my_claims =
Claims { sub: "b@b.com".to_owned(), company: "ACME".to_owned(), exp: 10000000000 };
let key = "secret";
let token = match encode(&Header::default(), &my_claims, key.as_ref()) {
let key = b"secret";
let token = match encode(&Header::default(), &my_claims, jwt::Hmac::from(key)) {
Ok(t) => t,
Err(_) => panic!(), // in practice you would return the error
};
Expand Down
204 changes: 179 additions & 25 deletions src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,28 +56,58 @@ impl FromStr for Algorithm {
}

/// The actual HS signing + encoding
fn sign_hmac(alg: &'static digest::Algorithm, key: &[u8], signing_input: &str) -> Result<String> {
let signing_key = hmac::SigningKey::new(alg, key);
fn sign_hmac<K: Key>(
alg: &'static digest::Algorithm,
key: K,
signing_input: &str,
) -> Result<String> {
let signing_key = hmac::SigningKey::new(alg, key.as_ref());
let digest = hmac::sign(&signing_key, signing_input.as_bytes());

Ok(base64::encode_config::<hmac::Signature>(&digest, base64::URL_SAFE_NO_PAD))
}

/// The actual ECDSA signing + encoding
fn sign_ecdsa(alg: &'static signature::EcdsaSigningAlgorithm, key: &[u8], signing_input: &str) -> Result<String> {
let signing_key = signature::EcdsaKeyPair::from_pkcs8(alg, untrusted::Input::from(key))?;
fn sign_ecdsa<K: Key>(
alg: &'static signature::EcdsaSigningAlgorithm,
key: K,
signing_input: &str,
) -> Result<String> {
let signing_key = match key.format() {
KeyFormat::PKCS8 => {
signature::EcdsaKeyPair::from_pkcs8(alg, untrusted::Input::from(key.as_ref()))?
}
_ => {
return Err(ErrorKind::InvalidKeyFormat)?;
}
};

let rng = rand::SystemRandom::new();
let sig = signing_key.sign(&rng, untrusted::Input::from(signing_input.as_bytes()))?;
Ok(base64::encode_config(&sig, base64::URL_SAFE_NO_PAD))
}

/// The actual RSA signing + encoding
/// Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html
fn sign_rsa(alg: &'static signature::RsaEncoding, key: &[u8], signing_input: &str) -> Result<String> {
let key_pair = Arc::new(
signature::RsaKeyPair::from_der(untrusted::Input::from(key))
.map_err(|_| ErrorKind::InvalidRsaKey)?,
);
fn sign_rsa<K: Key>(
alg: &'static signature::RsaEncoding,
key: K,
signing_input: &str,
) -> Result<String> {
let key_bytes = untrusted::Input::from(key.as_ref());
let key_pair = match key.format() {
KeyFormat::DER => {
signature::RsaKeyPair::from_der(key_bytes).map_err(|_| ErrorKind::InvalidRsaKey)?
}
KeyFormat::PKCS8 => {
signature::RsaKeyPair::from_pkcs8(key_bytes).map_err(|_| ErrorKind::InvalidRsaKey)?
}
_ => {
return Err(ErrorKind::InvalidKeyFormat)?;
}
};

let key_pair = Arc::new(key_pair);
let mut signature = vec![0; key_pair.public_modulus_len()];
let rng = rand::SystemRandom::new();
key_pair
Expand All @@ -91,14 +121,18 @@ fn sign_rsa(alg: &'static signature::RsaEncoding, key: &[u8], signing_input: &st
/// the base64 url safe encoded of the result.
///
/// Only use this function if you want to do something other than JWT.
pub fn sign(signing_input: &str, key: &[u8], algorithm: Algorithm) -> Result<String> {
pub fn sign<K: Key>(signing_input: &str, key: K, algorithm: Algorithm) -> Result<String> {
match algorithm {
Algorithm::HS256 => sign_hmac(&digest::SHA256, key, signing_input),
Algorithm::HS384 => sign_hmac(&digest::SHA384, key, signing_input),
Algorithm::HS512 => sign_hmac(&digest::SHA512, key, signing_input),

Algorithm::ES256 => sign_ecdsa(&signature::ECDSA_P256_SHA256_FIXED_SIGNING, key, signing_input),
Algorithm::ES384 => sign_ecdsa(&signature::ECDSA_P384_SHA384_FIXED_SIGNING, key, signing_input),
Algorithm::ES256 => {
sign_ecdsa(&signature::ECDSA_P256_SHA256_FIXED_SIGNING, key, signing_input)
}
Algorithm::ES384 => {
sign_ecdsa(&signature::ECDSA_P384_SHA384_FIXED_SIGNING, key, signing_input)
}

Algorithm::RS256 => sign_rsa(&signature::RSA_PKCS1_SHA256, key, signing_input),
Algorithm::RS384 => sign_rsa(&signature::RSA_PKCS1_SHA384, key, signing_input),
Expand Down Expand Up @@ -134,29 +168,149 @@ fn verify_ring(
pub fn verify(
signature: &str,
signing_input: &str,
key: &[u8],
public_key: &[u8],
algorithm: Algorithm,
) -> Result<bool> {
match algorithm {
Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => {
// we just re-sign the data with the key and compare if they are equal
let signed = sign(signing_input, key, algorithm)?;
let signed = sign(signing_input, Hmac::from(&public_key), algorithm)?;
Ok(verify_slices_are_equal(signature.as_ref(), signed.as_ref()).is_ok())
}
Algorithm::ES256 => {
verify_ring(&signature::ECDSA_P256_SHA256_FIXED, signature, signing_input, key)
verify_ring(&signature::ECDSA_P256_SHA256_FIXED, signature, signing_input, public_key)
}
Algorithm::ES384 => {
verify_ring(&signature::ECDSA_P384_SHA384_FIXED, signature, signing_input, key)
}
Algorithm::RS256 => {
verify_ring(&signature::RSA_PKCS1_2048_8192_SHA256, signature, signing_input, key)
}
Algorithm::RS384 => {
verify_ring(&signature::RSA_PKCS1_2048_8192_SHA384, signature, signing_input, key)
}
Algorithm::RS512 => {
verify_ring(&signature::RSA_PKCS1_2048_8192_SHA512, signature, signing_input, key)
verify_ring(&signature::ECDSA_P384_SHA384_FIXED, signature, signing_input, public_key)
}
Algorithm::RS256 => verify_ring(
&signature::RSA_PKCS1_2048_8192_SHA256,
signature,
signing_input,
public_key,
),
Algorithm::RS384 => verify_ring(
&signature::RSA_PKCS1_2048_8192_SHA384,
signature,
signing_input,
public_key,
),
Algorithm::RS512 => verify_ring(
&signature::RSA_PKCS1_2048_8192_SHA512,
signature,
signing_input,
public_key,
),
}
}

/// The supported RSA key formats, see the documentation for ring::signature::RsaKeyPair
/// for more information
pub enum KeyFormat {
/// An unencrypted PKCS#8-encoded key. Can be used with both ECDSA and RSA
/// algorithms when signing. See ring for information.
PKCS8,
/// A binary DER-encoded ASN.1 key. Can only be used with RSA algorithms
/// when signing. See ring for more information
DER,
/// This is not a key format, but provided for convenience since HMAC is
/// a supported signing algorithm.
HMAC,
}

/// A tiny abstraction on top of raw key buffers to add key format
/// information
pub trait Key: AsRef<[u8]> {
/// The format of the key
fn format(&self) -> KeyFormat;
}

/// This blanket implementation aligns with the key loading as of version 6.0.0
// impl<T> Key for T
// where
// T: AsRef<[u8]>,
// {
// fn format(&self) -> KeyFormat {
// KeyFormat::DER
// }
// }

/// A convenience wrapper for a key buffer as an unencrypted PKCS#8-encoded,
/// see ring for more details
pub struct Pkcs8<'a> {
key_bytes: &'a [u8],
}

impl<'a> Key for Pkcs8<'a> {
fn format(&self) -> KeyFormat {
KeyFormat::PKCS8
}
}

impl<'a> AsRef<[u8]> for Pkcs8<'a> {
fn as_ref(&self) -> &[u8] {
self.key_bytes
}
}

impl<'a, T> From<&'a T> for Pkcs8<'a>
where
T: AsRef<[u8]>,
{
fn from(key: &'a T) -> Self {
Self { key_bytes: key.as_ref() }
}
}

/// A convenience wrapper for a key buffer as a binary DER-encoded ASN.1 key,
/// see ring for more details
pub struct Der<'a> {
key_bytes: &'a [u8],
}

impl<'a> Key for Der<'a> {
fn format(&self) -> KeyFormat {
KeyFormat::DER
}
}

impl<'a> AsRef<[u8]> for Der<'a> {
fn as_ref(&self) -> &[u8] {
self.key_bytes
}
}

impl<'a, T> From<&'a T> for Der<'a>
where
T: AsRef<[u8]>,
{
fn from(key: &'a T) -> Self {
Self { key_bytes: key.as_ref() }
}
}

/// Convenience wrapper for an HMAC key
pub struct Hmac<'a> {
key_bytes: &'a [u8],
}

impl<'a> Key for Hmac<'a> {
fn format(&self) -> KeyFormat {
KeyFormat::HMAC
}
}

impl<'a> AsRef<[u8]> for Hmac<'a> {
fn as_ref(&self) -> &[u8] {
self.key_bytes
}
}

impl<'a, T> From<&'a T> for Hmac<'a>
where
T: AsRef<[u8]>,
{
fn from(key: &'a T) -> Self {
Self { key_bytes: key.as_ref() }
}
}
2 changes: 2 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ pub enum ErrorKind {
InvalidRsaKey,
/// When the algorithm from string doesn't match the one passed to `from_str`
InvalidAlgorithmName,
/// When a key is provided with an invalid format
InvalidKeyFormat,

// validation error
/// When a token’s `exp` claim indicates that it has expired
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ mod header;
mod serialization;
mod validation;

pub use crypto::{sign, verify, Algorithm};
pub use crypto::{sign, verify, Algorithm, Der, Hmac, Key, KeyFormat, Pkcs8};
pub use header::Header;
pub use serialization::TokenData;
pub use validation::Validation;
Expand Down Expand Up @@ -54,7 +54,7 @@ use validation::validate;
/// // 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> {
pub fn encode<T: Serialize, K: Key>(header: &Header, claims: &T, key: K) -> Result<String> {
let encoded_header = to_jwt_part(&header)?;
let encoded_claims = to_jwt_part(&claims)?;
let signing_input = [encoded_header.as_ref(), encoded_claims.as_ref()].join(".");
Expand Down
14 changes: 11 additions & 3 deletions tests/ecdsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ extern crate serde_derive;
extern crate chrono;

use chrono::Utc;
use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Header, Validation};
use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Der, Header, Pkcs8, Validation};

#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
struct Claims {
Expand All @@ -16,7 +16,7 @@ struct Claims {
#[test]
fn round_trip_sign_verification() {
let privkey = include_bytes!("private_ecdsa_key.pk8");
let encrypted = sign("hello world", privkey, Algorithm::ES256).unwrap();
let encrypted = sign("hello world", Pkcs8::from(&&privkey[..]), Algorithm::ES256).unwrap();
let pubkey = include_bytes!("public_ecdsa_key.pk8");
let is_valid = verify(&encrypted, "hello world", pubkey, Algorithm::ES256).unwrap();
assert!(is_valid);
Expand All @@ -30,9 +30,17 @@ fn round_trip_claim() {
exp: Utc::now().timestamp() + 10000,
};
let privkey = include_bytes!("private_ecdsa_key.pk8");
let token = encode(&Header::new(Algorithm::ES256), &my_claims, privkey).unwrap();
let token =
encode(&Header::new(Algorithm::ES256), &my_claims, Pkcs8::from(&&privkey[..])).unwrap();
let pubkey = include_bytes!("public_ecdsa_key.pk8");
let token_data = decode::<Claims>(&token, pubkey, &Validation::new(Algorithm::ES256)).unwrap();
assert_eq!(my_claims, token_data.claims);
assert!(token_data.header.kid.is_none());
}

#[test]
#[should_panic(expected = "InvalidKeyFormat")]
fn fails_with_non_pkcs8_key_format() {
let privkey = include_bytes!("private_rsa_key.der");
let _encrypted = sign("hello world", Der::from(&&privkey[..]), Algorithm::ES256).unwrap();
}
Loading