1
0
mirror of https://github.com/BitskiCo/jwk-rs synced 2024-11-22 03:49:22 +00:00

Add jwt and panicking conversions

This commit is contained in:
Nick Hynes 2020-07-16 02:32:11 +00:00
parent 7a8536dabe
commit 72368980c4
No known key found for this signature in database
GPG Key ID: 5B3463E9F1D73C83
5 changed files with 128 additions and 58 deletions

View File

@ -24,7 +24,8 @@ yasna = { version = "0.3", optional = true, features = ["num-bigint"] }
zeroize = { version = "1.1", features = ["zeroize_derive"] } zeroize = { version = "1.1", features = ["zeroize_derive"] }
[features] [features]
convert = ["num-bigint", "yasna"] pkcs-convert = ["num-bigint", "yasna"]
jwt-convert = ["pkcs-convert", "jsonwebtoken"]
generate = ["p256", "rand"] generate = ["p256", "rand"]
[dev-dependencies] [dev-dependencies]

View File

@ -34,13 +34,16 @@ let jwt_str = r#"{
"k": "Wpj30SfkzM_m0Sa_B2NqNw", "k": "Wpj30SfkzM_m0Sa_B2NqNw",
"alg": "HS256" "alg": "HS256"
}"#; }"#;
let jwk: jwk::JsonWebKey = jwt_str.parse().unwrap(); let the_jwk: jwk::JsonWebKey = jwt_str.parse().unwrap();
println!("{:#?}", jwk); // looks like `jwt_str` but with reordered fields. println!("{:#?}", the_jwk); // looks like `jwt_str` but with reordered fields.
``` ```
### Using with other crates ### Using with other crates
*Note:* The following example requires the `jwt-convert` feature.
```rust ```rust
#[cfg(all(feature = "generate", feature = "jwt-convert"))] {
extern crate jsonwebtoken as jwt; extern crate jsonwebtoken as jwt;
extern crate jsonwebkey as jwk; extern crate jsonwebkey as jwk;
@ -50,24 +53,24 @@ struct TokenClaims {}
let mut my_jwk = jwk::JsonWebKey::new(jwk::Key::generate_p256()); let mut my_jwk = jwk::JsonWebKey::new(jwk::Key::generate_p256());
my_jwk.set_algorithm(jwk::Algorithm::ES256); my_jwk.set_algorithm(jwk::Algorithm::ES256);
let encoding_key = jwt::EncodingKey::from_ec_der(&my_jwk.key.to_der().unwrap()); let alg: jwt::Algorithm = my_jwk.algorithm.unwrap().into();
let token = jwt::encode( let token = jwt::encode(
&jwt::Header::new(my_jwk.algorithm.unwrap().into()), &jwt::Header::new(alg),
&TokenClaims {}, &TokenClaims {},
&encoding_key, &my_jwk.key.to_encoding_key(),
).unwrap(); ).unwrap();
let public_pem = my_jwk.key.to_public().unwrap().to_pem().unwrap(); let mut validation = jwt::Validation::new(alg);
let decoding_key = jwt::DecodingKey::from_ec_pem(public_pem.as_bytes()).unwrap();
let mut validation = jwt::Validation::new(my_jwk.algorithm.unwrap().into());
validation.validate_exp = false; validation.validate_exp = false;
jwt::decode::<TokenClaims>(&token, &decoding_key, &validation).unwrap(); jwt::decode::<TokenClaims>(&token, &my_jwk.key.to_decoding_key(), &validation).unwrap();
}
``` ```
## Features ## Features
* `convert` - enables `Key::{to_der, to_pem}`. * `pkcs-convert` - enables `Key::{to_der, to_pem}`.
This pulls in the [yasna](https://crates.io/crates/yasna) crate. This pulls in the [yasna](https://crates.io/crates/yasna) crate.
* `generate` - enables `Key::{generate_p256, generate_symmetric}`. * `generate` - enables `Key::{generate_p256, generate_symmetric}`.
This pulls in the [p256](https://crates.io/crates/p256) and [rand](https://crates.io/crates/rand) crates. This pulls in the [p256](https://crates.io/crates/p256) and [rand](https://crates.io/crates/rand) crates.
* `jsonwebtoken` - enables conversions to types in the [jsonwebtoken](https://crates.io/crates/jsonwebtoken) crate. * `jwt-convert` - enables conversions to types in the
[jsonwebtoken](https://crates.io/crates/jsonwebtoken) crate.

View File

@ -20,13 +20,14 @@
//! "k": "Wpj30SfkzM_m0Sa_B2NqNw", //! "k": "Wpj30SfkzM_m0Sa_B2NqNw",
//! "alg": "HS256" //! "alg": "HS256"
//! }"#; //! }"#;
//! let jwk: jwk::JsonWebKey = jwt_str.parse().unwrap(); //! let the_jwk: jwk::JsonWebKey = jwt_str.parse().unwrap();
//! println!("{:#?}", jwk); // looks like `jwt_str` but with reordered fields. //! println!("{:#?}", the_jwk); // looks like `jwt_str` but with reordered fields.
//! ``` //! ```
//! //!
//! ### Using with other crates //! ### Using with other crates
//! //!
//! ``` //! ```
//! #[cfg(all(feature = "generate", feature = "jwt-convert"))] {
//! extern crate jsonwebtoken as jwt; //! extern crate jsonwebtoken as jwt;
//! extern crate jsonwebkey as jwk; //! extern crate jsonwebkey as jwk;
//! //!
@ -36,18 +37,17 @@
//! let mut my_jwk = jwk::JsonWebKey::new(jwk::Key::generate_p256()); //! let mut my_jwk = jwk::JsonWebKey::new(jwk::Key::generate_p256());
//! my_jwk.set_algorithm(jwk::Algorithm::ES256); //! my_jwk.set_algorithm(jwk::Algorithm::ES256);
//! //!
//! let encoding_key = jwt::EncodingKey::from_ec_der(&my_jwk.key.to_der().unwrap()); //! let alg: jwt::Algorithm = my_jwk.algorithm.unwrap().into();
//! let token = jwt::encode( //! let token = jwt::encode(
//! &jwt::Header::new(my_jwk.algorithm.unwrap().into()), //! &jwt::Header::new(alg),
//! &TokenClaims {}, //! &TokenClaims {},
//! &encoding_key, //! &my_jwk.key.to_encoding_key(),
//! ).unwrap(); //! ).unwrap();
//! //!
//! let public_pem = my_jwk.key.to_public().unwrap().to_pem().unwrap(); //! let mut validation = jwt::Validation::new(alg);
//! let decoding_key = jwt::DecodingKey::from_ec_pem(public_pem.as_bytes()).unwrap();
//! let mut validation = jwt::Validation::new(my_jwk.algorithm.unwrap().into());
//! validation.validate_exp = false; //! validation.validate_exp = false;
//! jwt::decode::<TokenClaims>(&token, &decoding_key, &validation).unwrap(); //! jwt::decode::<TokenClaims>(&token, &my_jwk.key.to_decoding_key(), &validation).unwrap();
//! }
//! ``` //! ```
//! //!
//! ## Features //! ## Features
@ -65,13 +65,15 @@ mod key_ops;
mod tests; mod tests;
mod utils; mod utils;
use std::borrow::Cow;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub use byte_array::ByteArray; pub use byte_array::ByteArray;
pub use byte_vec::ByteVec; pub use byte_vec::ByteVec;
pub use key_ops::KeyOps; pub use key_ops::KeyOps;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct JsonWebKey { pub struct JsonWebKey {
#[serde(flatten)] #[serde(flatten)]
pub key: Box<Key>, pub key: Box<Key>,
@ -196,12 +198,12 @@ impl Key {
!self.is_private() !self.is_private()
} }
/// Returns the public part of this key, if it's symmetric. /// Returns the public part of this key (symmetric keys have no public parts).
pub fn to_public(&self) -> Option<Self> { pub fn to_public(&self) -> Option<Cow<Self>> {
if self.is_public() { if self.is_public() {
return Some(self.clone()); return Some(Cow::Borrowed(self));
} }
Some(match self { Some(Cow::Owned(match self {
Self::Symmetric { .. } => return None, Self::Symmetric { .. } => return None,
Self::EC { Self::EC {
curve: Curve::P256 { x, y, .. }, curve: Curve::P256 { x, y, .. },
@ -216,19 +218,19 @@ impl Key {
public: public.clone(), public: public.clone(),
private: None, private: None,
}, },
}) }))
} }
/// If this key is asymmetric, encodes it as PKCS#8. /// If this key is asymmetric, encodes it as PKCS#8.
#[cfg(feature = "convert")] #[cfg(feature = "pkcs-convert")]
pub fn to_der(&self) -> Result<Vec<u8>, PkcsConvertError> { pub fn try_to_der(&self) -> Result<Vec<u8>, ConversionError> {
use num_bigint::BigUint; use num_bigint::BigUint;
use yasna::{models::ObjectIdentifier, DERWriter, DERWriterSeq, Tag}; use yasna::{models::ObjectIdentifier, DERWriter, DERWriterSeq, Tag};
use crate::utils::pkcs8; use crate::utils::pkcs8;
if let Self::Symmetric { .. } = self { if let Self::Symmetric { .. } = self {
return Err(PkcsConvertError::NotAsymmetric); return Err(ConversionError::NotAsymmetric);
} }
Ok(match self { Ok(match self {
@ -253,8 +255,7 @@ impl Key {
Some(private_point) => { Some(private_point) => {
pkcs8::write_private(oids, |writer: &mut DERWriterSeq| { pkcs8::write_private(oids, |writer: &mut DERWriterSeq| {
writer.next().write_i8(1); // version writer.next().write_i8(1); // version
use std::array::FixedSizeArray; writer.next().write_bytes(&**private_point);
writer.next().write_bytes(private_point.as_slice());
// The following tagged value is optional. OpenSSL produces it, // The following tagged value is optional. OpenSSL produces it,
// but many tools, including jwt.io and `jsonwebtoken`, don't like it, // but many tools, including jwt.io and `jsonwebtoken`, don't like it,
// so we don't include it. // so we don't include it.
@ -308,7 +309,7 @@ impl Key {
qi: Some(_), qi: Some(_),
}, },
) => pkcs8::write_private(oids, |writer| write_private(writer, private)), ) => pkcs8::write_private(oids, |writer| write_private(writer, private)),
Some(_) => return Err(PkcsConvertError::MissingRsaParams), Some(_) => return Err(ConversionError::MissingRsaParams),
None => pkcs8::write_public(oids, |writer| { None => pkcs8::write_public(oids, |writer| {
let body = let body =
yasna::construct_der(|writer| writer.write_sequence(write_public)); yasna::construct_der(|writer| writer.write_sequence(write_public));
@ -320,11 +321,18 @@ impl Key {
}) })
} }
/// Unwrapping `try_to_der`.
/// Panics if the key is not asymmetric or there are missing RSA components.
#[cfg(feature = "pkcs-convert")]
pub fn to_der(&self) -> Vec<u8> {
self.try_to_der().unwrap()
}
/// If this key is asymmetric, encodes it as PKCS#8 with PEM armoring. /// If this key is asymmetric, encodes it as PKCS#8 with PEM armoring.
#[cfg(feature = "convert")] #[cfg(feature = "pkcs-convert")]
pub fn to_pem(&self) -> Result<String, PkcsConvertError> { pub fn try_to_pem(&self) -> Result<String, ConversionError> {
use std::fmt::Write; use std::fmt::Write;
let der_b64 = base64::encode(self.to_der()?); let der_b64 = base64::encode(self.try_to_der()?);
let key_ty = if self.is_private() { let key_ty = if self.is_private() {
"PRIVATE" "PRIVATE"
} else { } else {
@ -332,6 +340,7 @@ impl Key {
}; };
let mut pem = String::new(); let mut pem = String::new();
writeln!(&mut pem, "-----BEGIN {} KEY-----", key_ty).unwrap(); writeln!(&mut pem, "-----BEGIN {} KEY-----", key_ty).unwrap();
//^ re: `unwrap`, if writing to a string fails, we've got bigger issues.
const MAX_LINE_LEN: usize = 64; const MAX_LINE_LEN: usize = 64;
for i in (0..der_b64.len()).step_by(MAX_LINE_LEN) { for i in (0..der_b64.len()).step_by(MAX_LINE_LEN) {
writeln!( writeln!(
@ -345,6 +354,13 @@ impl Key {
Ok(pem) Ok(pem)
} }
/// Unwrapping `try_to_pem`.
/// Panics if the key is not asymmetric or there are missing RSA components.
#[cfg(feature = "pkcs-convert")]
pub fn to_pem(&self) -> String {
self.try_to_pem().unwrap()
}
/// Generates a new symmetric key with the specified number of bits. /// Generates a new symmetric key with the specified number of bits.
/// Best used with one of the HS algorithms (e.g., HS256). /// Best used with one of the HS algorithms (e.g., HS256).
#[cfg(feature = "generate")] #[cfg(feature = "generate")]
@ -473,16 +489,62 @@ pub enum Algorithm {
ES256, ES256,
} }
#[cfg(any(test, feature = "jsonwebtoken"))] #[cfg(feature = "jwt-convert")]
impl Into<jsonwebtoken::Algorithm> for Algorithm { const _IMPL_JWT_CONVERSIONS: () = {
use jsonwebtoken as jwt;
impl Into<jwt::Algorithm> for Algorithm {
fn into(self) -> jsonwebtoken::Algorithm { fn into(self) -> jsonwebtoken::Algorithm {
match self { match self {
Self::HS256 => jsonwebtoken::Algorithm::HS256, Self::HS256 => jwt::Algorithm::HS256,
Self::ES256 => jsonwebtoken::Algorithm::ES256, Self::ES256 => jwt::Algorithm::ES256,
Self::RS256 => jsonwebtoken::Algorithm::RS256, Self::RS256 => jwt::Algorithm::RS256,
} }
} }
} }
impl Key {
/// Returns an `EncodingKey` if the key is private.
pub fn try_to_encoding_key(&self) -> Result<jwt::EncodingKey, ConversionError> {
if self.is_public() {
return Err(ConversionError::NotPrivate);
}
Ok(match self {
Self::Symmetric { key } => jwt::EncodingKey::from_secret(key),
// The following two conversion will not panic, as we've ensured that the keys
// are private and tested that the successful output of `try_to_pem` is valid.
Self::EC { .. } => {
jwt::EncodingKey::from_ec_pem(self.try_to_pem()?.as_bytes()).unwrap()
}
Self::RSA { .. } => {
jwt::EncodingKey::from_rsa_pem(self.try_to_pem()?.as_bytes()).unwrap()
}
})
}
/// Unwrapping `try_to_encoding_key`. Panics if the key is public.
pub fn to_encoding_key(&self) -> jwt::EncodingKey {
self.try_to_encoding_key().unwrap()
}
pub fn to_decoding_key(&self) -> jwt::DecodingKey<'static> {
match self {
Self::Symmetric { key } => jwt::DecodingKey::from_secret(key).into_static(),
Self::EC { .. } => {
// The following will not panic: all EC JWKs have public components due to
// typing. PEM conversion will always succeed, for the same reason.
// Hence, jwt::DecodingKey shall have no issue with de-converting.
jwt::DecodingKey::from_ec_pem(self.to_public().unwrap().to_pem().as_bytes())
.unwrap()
.into_static()
}
Self::RSA { .. } => jwt::DecodingKey::from_rsa_pem(self.to_pem().as_bytes())
.unwrap()
.into_static(),
}
}
}
};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
@ -497,10 +559,14 @@ pub enum Error {
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum PkcsConvertError { pub enum ConversionError {
#[error("encoding RSA JWK as PKCS#8 requires specifing all of p, q, dp, dq, qi")] #[error("encoding RSA JWK as PKCS#8 requires specifing all of p, q, dp, dq, qi")]
MissingRsaParams, MissingRsaParams,
#[error("a symmetric key can not be encoded using PKCS#8")] #[error("a symmetric key can not be encoded using PKCS#8")]
NotAsymmetric, NotAsymmetric,
#[cfg(feature = "jwt-convert")]
#[error("a public key cannot be converted to a `jsonwebtoken::EncodingKey`")]
NotPrivate,
} }

View File

@ -86,7 +86,7 @@ fn serialize_es256() {
); );
} }
#[cfg(feature = "generate")] #[cfg(all(feature = "jwt-convert", feature = "generate"))]
#[test] #[test]
fn generate_p256() { fn generate_p256() {
extern crate jsonwebtoken as jwt; extern crate jsonwebtoken as jwt;
@ -97,7 +97,7 @@ fn generate_p256() {
let mut the_jwk = JsonWebKey::new(Key::generate_p256()); let mut the_jwk = JsonWebKey::new(Key::generate_p256());
the_jwk.set_algorithm(Algorithm::ES256).unwrap(); the_jwk.set_algorithm(Algorithm::ES256).unwrap();
let encoding_key = jwt::EncodingKey::from_ec_der(&the_jwk.key.to_der().unwrap()); let encoding_key = jwt::EncodingKey::from_ec_der(&the_jwk.key.to_der());
let token = jwt::encode( let token = jwt::encode(
&jwt::Header::new(the_jwk.algorithm.unwrap().into()), &jwt::Header::new(the_jwk.algorithm.unwrap().into()),
&TokenClaims {}, &TokenClaims {},
@ -107,7 +107,7 @@ fn generate_p256() {
let mut validation = jwt::Validation::new(the_jwk.algorithm.unwrap().into()); let mut validation = jwt::Validation::new(the_jwk.algorithm.unwrap().into());
validation.validate_exp = false; validation.validate_exp = false;
let public_pem = the_jwk.key.to_public().unwrap().to_pem().unwrap(); let public_pem = the_jwk.key.to_public().unwrap().to_pem();
let decoding_key = jwt::DecodingKey::from_ec_pem(public_pem.as_bytes()).unwrap(); let decoding_key = jwt::DecodingKey::from_ec_pem(public_pem.as_bytes()).unwrap();
jwt::decode::<TokenClaims>(&token, &decoding_key, &validation).unwrap(); jwt::decode::<TokenClaims>(&token, &decoding_key, &validation).unwrap();
} }
@ -290,14 +290,14 @@ fn mismatched_algorithm() {
); );
} }
#[cfg(feature = "convert")] #[cfg(feature = "pkcs-convert")]
#[test] #[test]
fn p256_private_to_pem() { fn p256_private_to_pem() {
// generated using mkjwk, converted using node-jwk-to-pem, verified using openssl // generated using mkjwk, converted using node-jwk-to-pem, verified using openssl
let jwk = JsonWebKey::from_str(P256_JWK_FIXTURE).unwrap(); let jwk = JsonWebKey::from_str(P256_JWK_FIXTURE).unwrap();
#[rustfmt::skip] #[rustfmt::skip]
assert_eq!( assert_eq!(
jwk.key.to_pem().unwrap(), jwk.key.to_pem(),
"-----BEGIN PRIVATE KEY----- "-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgZoKQ9j4dhIBlMRVr MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgZoKQ9j4dhIBlMRVr
v+QG8P/T9sutv3/95eio9MtpgKihRANCAARA4wea/3q1WUm/642qmucNIoiPkCIt v+QG8P/T9sutv3/95eio9MtpgKihRANCAARA4wea/3q1WUm/642qmucNIoiPkCIt
@ -307,13 +307,13 @@ NcpGiZdidq/Q3U42GaB53LWrRBOjQqypl0HSST5zc2RF/JwZmXXtwGOJ
); );
} }
#[cfg(feature = "convert")] #[cfg(feature = "pkcs-convert")]
#[test] #[test]
fn p256_public_to_pem() { fn p256_public_to_pem() {
let jwk = JsonWebKey::from_str(P256_JWK_FIXTURE).unwrap(); let jwk = JsonWebKey::from_str(P256_JWK_FIXTURE).unwrap();
#[rustfmt::skip] #[rustfmt::skip]
assert_eq!( assert_eq!(
jwk.key.to_public().unwrap().to_pem().unwrap(), jwk.key.to_public().unwrap().to_pem(),
"-----BEGIN PUBLIC KEY----- "-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQOMHmv96tVlJv+uNqprnDSKIj5Ai MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQOMHmv96tVlJv+uNqprnDSKIj5Ai
LTXKRomXYnav0N1ONhmgedy1q0QTo0KsqZdB0kk+c3NkRfycGZl17cBjiQ== LTXKRomXYnav0N1ONhmgedy1q0QTo0KsqZdB0kk+c3NkRfycGZl17cBjiQ==
@ -322,13 +322,13 @@ LTXKRomXYnav0N1ONhmgedy1q0QTo0KsqZdB0kk+c3NkRfycGZl17cBjiQ==
); );
} }
#[cfg(feature = "convert")] #[cfg(feature = "pkcs-convert")]
#[test] #[test]
fn rsa_private_to_pem() { fn rsa_private_to_pem() {
let jwk = JsonWebKey::from_str(RSA_JWK_FIXTURE).unwrap(); let jwk = JsonWebKey::from_str(RSA_JWK_FIXTURE).unwrap();
#[rustfmt::skip] #[rustfmt::skip]
assert_eq!( assert_eq!(
jwk.key.to_pem().unwrap(), jwk.key.to_pem(),
"-----BEGIN PRIVATE KEY----- "-----BEGIN PRIVATE KEY-----
MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEApCzbcd9kjvg5rfGH MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEApCzbcd9kjvg5rfGH
dEMWnXo49zbB6FLQ+m0B0BvVp0aojVWYa0xujC+ZP7ZhxByPxyc2PazwFJJi9ivZ dEMWnXo49zbB6FLQ+m0B0BvVp0aojVWYa0xujC+ZP7ZhxByPxyc2PazwFJJi9ivZ
@ -343,13 +343,13 @@ J2lmylxUG0M=
); );
} }
#[cfg(feature = "convert")] #[cfg(feature = "pkcs-convert")]
#[test] #[test]
fn rsa_public_to_pem() { fn rsa_public_to_pem() {
let jwk = JsonWebKey::from_str(RSA_JWK_FIXTURE).unwrap(); let jwk = JsonWebKey::from_str(RSA_JWK_FIXTURE).unwrap();
#[rustfmt::skip] #[rustfmt::skip]
assert_eq!( assert_eq!(
jwk.key.to_public().unwrap().to_pem().unwrap(), jwk.key.to_public().unwrap().to_pem(),
"-----BEGIN PUBLIC KEY----- "-----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKQs23HfZI74Oa3xh3RDFp16OPc2wehS MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKQs23HfZI74Oa3xh3RDFp16OPc2wehS
0PptAdAb1adGqI1VmGtMbowvmT+2YcQcj8cnNj2s8BSSYvYr2f4IEcMCAwEAAQ== 0PptAdAb1adGqI1VmGtMbowvmT+2YcQcj8cnNj2s8BSSYvYr2f4IEcMCAwEAAQ==

View File

@ -31,7 +31,7 @@ pub fn deserialize_base64<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D:
}) })
} }
#[cfg(feature = "convert")] #[cfg(feature = "pkcs-convert")]
pub mod pkcs8 { pub mod pkcs8 {
use yasna::{ use yasna::{
models::{ObjectIdentifier, TaggedDerValue}, models::{ObjectIdentifier, TaggedDerValue},