1
0
mirror of https://github.com/BitskiCo/jwk-rs synced 2024-11-24 12:59:22 +00:00
This commit is contained in:
Nick Hynes 2020-07-13 23:51:09 +00:00
parent 30ceb84639
commit bca7f0a3ca
No known key found for this signature in database
GPG Key ID: 5B3463E9F1D73C83
7 changed files with 140 additions and 55 deletions

View File

@ -1,6 +1,6 @@
[package] [package]
name = "jsonwebkey" name = "jsonwebkey"
version = "0.0.2" version = "0.0.3"
authors = ["Nick Hynes <nhynes@nhynes.com>"] authors = ["Nick Hynes <nhynes@nhynes.com>"]
description = "JSON Web Key (JWK) (de)serialization, generation, and conversion." description = "JSON Web Key (JWK) (de)serialization, generation, and conversion."
readme = "README.md" readme = "README.md"
@ -15,7 +15,6 @@ derive_more = "0.99"
jsonwebtoken = { version = "7.2", optional = true } jsonwebtoken = { version = "7.2", optional = true }
num-bigint = { version = "0.2", optional = true } num-bigint = { version = "0.2", optional = true }
p256 = { version = "0.3", optional = true } p256 = { version = "0.3", optional = true }
paste = "0.1"
rand = { version = "0.7", optional = true } rand = { version = "0.7", optional = true }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"

View File

@ -8,32 +8,56 @@ Note: requires rustc nightly >= 1.45 for conveniences around fixed-size arrays.
tl;dr: get keys into a format that can be used by other crates; be as safe as possible while doing so. tl;dr: get keys into a format that can be used by other crates; be as safe as possible while doing so.
- [x] Serialization and deserialization of _Required_ and _Recommended_ key types (HS256, RS256, ES256) - Serialization and deserialization of _Required_ and _Recommended_ key types (HS256, RS256, ES256)
- [x] Conversion to PEM for interop with existing JWT libraries (e.g., [jsonwebtoken](https://crates.io/crates/jsonwebtoken)) - Conversion to PEM for interop with existing JWT libraries (e.g., [jsonwebtoken](https://crates.io/crates/jsonwebtoken))
- [ ] Key generation (particularly for testing) - Key generation (particularly useful for testing)
**Non-goals** **Non-goals**
* be a fully-featured JOSE framework - be a fully-featured JOSE framework
## Example ## Examples
### Deserializing from JSON
```rust
extern crate jsonwebkey as jwk;
// Generated using https://mkjwk.org/.
let jwt_str = r#"{
"kty": "oct",
"use": "sig",
"kid": "my signing key",
"k": "Wpj30SfkzM_m0Sa_B2NqNw",
"alg": "HS256"
}"#;
let jwk: jwk::JsonWebKey = jwt_str.parse().unwrap();
println!("{:#?}", jwk); // looks like `jwt_str` but with reordered fields.
```
### Using with other crates
```rust ```rust
extern crate jsonwebtoken as jwt; extern crate jsonwebtoken as jwt;
extern crate jsonwebkey as jwk; extern crate jsonwebkey as jwk;
fn main() { #[derive(serde::Serialize, serde::Deserialize)]
let jwk_str = r#"{ struct TokenClaims {}
"kty": "EC",
"d": "ZoKQ9j4dhIBlMRVrv-QG8P_T9sutv3_95eio9MtpgKg", let mut my_jwk = jwk::JsonWebKey::new(jwk::Key::generate_p256());
"crv": "P-256", my_jwk.set_algorithm(jwk::Algorithm::ES256);
"x": "QOMHmv96tVlJv-uNqprnDSKIj5AiLTXKRomXYnav0N0",
"y": "TjYZoHnctatEE6NCrKmXQdJJPnNzZEX8nBmZde3AY4k" let encoding_key = jwt::EncodingKey::from_ec_der(&my_jwk.key.to_der().unwrap());
}"#; let token = jwt::encode(
let jwk = jwk::JsonWebKey::from_str(jwk_str).unwrap(); &jwt::Header::new(my_jwk.algorithm.unwrap().into()),
let encoding_key = jwk::EncodingKey::from_ec_der(jwk.to_der().unwrap()); &TokenClaims {},
let token = jwt::encode(&jwt::Header::default(), &() /* claims */, encoding_key).unwrap(); &encoding_key,
} ).unwrap();
let public_pem = my_jwk.key.to_public().unwrap().to_pem().unwrap();
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;
jwt::decode::<TokenClaims>(&token, &decoding_key, &validation).unwrap();
``` ```
## Features ## Features

View File

@ -9,6 +9,7 @@ use zeroize::{Zeroize, Zeroizing};
use crate::utils::{deserialize_base64, serialize_base64}; use crate::utils::{deserialize_base64, serialize_base64};
/// A zeroizing-on-drop container for a `[u8; N]` that deserializes from base64.
#[derive(Clone, Zeroize, Deref, AsRef, From)] #[derive(Clone, Zeroize, Deref, AsRef, From)]
#[zeroize(drop)] #[zeroize(drop)]
pub struct ByteArray<const N: usize>(pub [u8; N]); pub struct ByteArray<const N: usize>(pub [u8; N]);

View File

@ -9,6 +9,7 @@ use zeroize::Zeroize;
use crate::utils::{deserialize_base64, serialize_base64}; use crate::utils::{deserialize_base64, serialize_base64};
/// A zeroizing-on-drop container for a `Vec<u8>` that deserializes from base64.
#[derive(Clone, PartialEq, Eq, Zeroize, Deref, AsRef, From)] #[derive(Clone, PartialEq, Eq, Zeroize, Deref, AsRef, From)]
#[zeroize(drop)] #[zeroize(drop)]
pub struct ByteVec(pub Vec<u8>); pub struct ByteVec(pub Vec<u8>);

View File

@ -4,13 +4,11 @@ use serde::{
}; };
macro_rules! impl_key_ops { macro_rules! impl_key_ops {
($(($key_op:ident, $i:literal)),+,) => { ($(($key_op:ident, $const_name:ident, $i:literal)),+,) => {
paste::item! { bitflags::bitflags! {
bitflags::bitflags! { #[derive(Default)]
#[derive(Default)] pub struct KeyOps: u16 {
pub struct KeyOps: u16 { $(const $const_name = $i;)*
$(const [<$key_op:upper>] = $i;)*
}
} }
} }
@ -18,7 +16,7 @@ macro_rules! impl_key_ops {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> { fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
let mut seq = s.serialize_seq(Some(self.bits().count_ones() as usize))?; let mut seq = s.serialize_seq(Some(self.bits().count_ones() as usize))?;
$( $(
if self.contains(paste::expr! { KeyOps::[<$key_op:upper>] }) { if self.contains(KeyOps::$const_name) {
seq.serialize_element(stringify!($key_op))?; seq.serialize_element(stringify!($key_op))?;
} }
)+ )+
@ -33,7 +31,7 @@ macro_rules! impl_key_ops {
for op_str in op_strs { for op_str in op_strs {
$( $(
if op_str == stringify!($key_op) { if op_str == stringify!($key_op) {
ops |= paste::expr! { KeyOps::[<$key_op:upper>] }; ops |= KeyOps::$const_name;
continue; continue;
} }
)+ )+
@ -47,12 +45,12 @@ macro_rules! impl_key_ops {
#[rustfmt::skip] #[rustfmt::skip]
impl_key_ops!( impl_key_ops!(
(sign, 0b00000001), (sign, SIGN, 0b00000001),
(verify, 0b00000010), (verify, VERIFY, 0b00000010),
(encrypt, 0b00000100), (encrypt, ENCRYPT, 0b00000100),
(decrypt, 0b00001000), (decrypt, DECRYPT, 0b00001000),
(wrapKey, 0b00010000), (wrapKey, WRAP_KEY, 0b00010000),
(unwrapKey, 0b00100000), (unwrapKey, UNWRAP_KEY, 0b00100000),
(deriveKey, 0b01000000), (deriveKey, DERIVE_KEY, 0b01000000),
(deriveBits, 0b10000000), (deriveBits, DERIVE_BITS, 0b10000000),
); );

View File

@ -1,6 +1,65 @@
#![allow(incomplete_features)] #![allow(incomplete_features)]
#![feature(box_syntax, const_generics, fixed_size_array)] #![feature(box_syntax, const_generics, fixed_size_array)]
//! # jsonwebkey
//!
//! *[JSON Web Key (JWK)](https://tools.ietf.org/html/rfc7517#section-4.3) (de)serialization, generation, and conversion.*
//!
//! **Note**: this crate requires Rust nightly >= 1.45 because it uses
//! `feature(const_generics, fixed_size_array)` to enable statically-checked key lengths.
//!
//! ## Examples
//!
//! ### Deserializing from JSON
//!
//! ```
//! extern crate jsonwebkey as jwk;
//! // Generated using https://mkjwk.org/.
//! let jwt_str = r#"{
//! "kty": "oct",
//! "use": "sig",
//! "kid": "my signing key",
//! "k": "Wpj30SfkzM_m0Sa_B2NqNw",
//! "alg": "HS256"
//! }"#;
//! let jwk: jwk::JsonWebKey = jwt_str.parse().unwrap();
//! println!("{:#?}", jwk); // looks like `jwt_str` but with reordered fields.
//! ```
//!
//! ### Using with other crates
//!
//! ```
//! extern crate jsonwebtoken as jwt;
//! extern crate jsonwebkey as jwk;
//!
//! #[derive(serde::Serialize, serde::Deserialize)]
//! struct TokenClaims {}
//!
//! let mut my_jwk = jwk::JsonWebKey::new(jwk::Key::generate_p256());
//! my_jwk.set_algorithm(jwk::Algorithm::ES256);
//!
//! let encoding_key = jwt::EncodingKey::from_ec_der(&my_jwk.key.to_der().unwrap());
//! let token = jwt::encode(
//! &jwt::Header::new(my_jwk.algorithm.unwrap().into()),
//! &TokenClaims {},
//! &encoding_key,
//! ).unwrap();
//!
//! let public_pem = my_jwk.key.to_public().unwrap().to_pem().unwrap();
//! 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;
//! jwt::decode::<TokenClaims>(&token, &decoding_key, &validation).unwrap();
//! ```
//!
//! ## Features
//!
//! * `convert` - enables `Key::{to_der, to_pem}`.
//! This pulls in the [yasna](https://crates.io/crates/yasna) crate.
//! * `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.
//! * `jsonwebtoken` - enables conversions to types in the [jsonwebtoken](https://crates.io/crates/jsonwebtoken) crate.
mod byte_array; mod byte_array;
mod byte_vec; mod byte_vec;
mod key_ops; mod key_ops;
@ -8,10 +67,7 @@ mod key_ops;
mod tests; mod tests;
mod utils; mod utils;
use std::array::FixedSizeArray;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use zeroize::Zeroize;
pub use byte_array::ByteArray; pub use byte_array::ByteArray;
pub use byte_vec::ByteVec; pub use byte_vec::ByteVec;
@ -32,7 +88,7 @@ pub struct JsonWebKey {
pub key_id: Option<String>, pub key_id: Option<String>,
#[serde(default, rename = "alg", skip_serializing_if = "Option::is_none")] #[serde(default, rename = "alg", skip_serializing_if = "Option::is_none")]
pub algorithm: Option<JsonWebAlgorithm>, pub algorithm: Option<Algorithm>,
} }
impl JsonWebKey { impl JsonWebKey {
@ -46,7 +102,7 @@ impl JsonWebKey {
} }
} }
pub fn set_algorithm(&mut self, alg: JsonWebAlgorithm) -> Result<(), Error> { pub fn set_algorithm(&mut self, alg: Algorithm) -> Result<(), Error> {
Self::validate_algorithm(alg, &*self.key)?; Self::validate_algorithm(alg, &*self.key)?;
self.algorithm = Some(alg); self.algorithm = Some(alg);
Ok(()) Ok(())
@ -56,8 +112,8 @@ impl JsonWebKey {
Ok(serde_json::from_slice(bytes.as_ref())?) Ok(serde_json::from_slice(bytes.as_ref())?)
} }
fn validate_algorithm(alg: JsonWebAlgorithm, key: &Key) -> Result<(), Error> { fn validate_algorithm(alg: Algorithm, key: &Key) -> Result<(), Error> {
use JsonWebAlgorithm::*; use Algorithm::*;
use Key::*; use Key::*;
match (alg, key) { match (alg, key) {
( (
@ -199,6 +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.as_slice()); 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,
@ -333,28 +390,32 @@ impl Key {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "crv")] #[serde(tag = "crv")]
pub enum Curve { pub enum Curve {
/// prime256v1 /// Parameters of the prime256v1 (P256) curve.
#[serde(rename = "P-256")] #[serde(rename = "P-256")]
P256 { P256 {
/// Private point. /// The private scalar.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
d: Option<ByteArray<32>>, d: Option<ByteArray<32>>,
/// The curve point x coordinate.
x: ByteArray<32>, x: ByteArray<32>,
/// The curve point y coordinate.
y: ByteArray<32>, y: ByteArray<32>,
}, },
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RsaPublic { pub struct RsaPublic {
/// Public exponent. Must be 65537. /// The standard public exponent, 65537.
pub e: PublicExponent, pub e: PublicExponent,
/// Modulus, p*q. /// The modulus, p*q.
pub n: ByteVec, pub n: ByteVec,
} }
const PUBLIC_EXPONENT: u32 = 65537; const PUBLIC_EXPONENT: u32 = 65537;
const PUBLIC_EXPONENT_B64: &str = "AQAB"; // little-endian, strip zeros const PUBLIC_EXPONENT_B64: &str = "AQAB"; // little-endian, strip zeros
const PUBLIC_EXPONENT_B64_PADDED: &str = "AQABAA=="; const PUBLIC_EXPONENT_B64_PADDED: &str = "AQABAA==";
/// The standard RSA public exponent, 65537.
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PublicExponent; pub struct PublicExponent;
@ -391,7 +452,7 @@ pub struct RsaPrivate {
/// First factor Chinese Remainder Theorem (CRT) exponent. /// First factor Chinese Remainder Theorem (CRT) exponent.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub dp: Option<ByteVec>, pub dp: Option<ByteVec>,
/// Second factor Chinese Remainder Theorem (CRT) exponent. /// Second factor CRT exponent.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub dq: Option<ByteVec>, pub dq: Option<ByteVec>,
/// First CRT coefficient. /// First CRT coefficient.
@ -407,15 +468,15 @@ pub enum KeyUse {
Encryption, Encryption,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Zeroize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum JsonWebAlgorithm { pub enum Algorithm {
HS256, HS256,
RS256, RS256,
ES256, ES256,
} }
#[cfg(any(test, feature = "jsonwebtoken"))] #[cfg(any(test, feature = "jsonwebtoken"))]
impl Into<jsonwebtoken::Algorithm> for JsonWebAlgorithm { impl Into<jsonwebtoken::Algorithm> for Algorithm {
fn into(self) -> jsonwebtoken::Algorithm { fn into(self) -> jsonwebtoken::Algorithm {
match self { match self {
Self::HS256 => jsonwebtoken::Algorithm::HS256, Self::HS256 => jsonwebtoken::Algorithm::HS256,

View File

@ -24,6 +24,7 @@ static RSA_JWK_FIXTURE: &str = r#"{
"qi": "adhQHH8IGXFfLEMnZ5t_TeCp5zgSwQktJ2lmylxUG0M", "qi": "adhQHH8IGXFfLEMnZ5t_TeCp5zgSwQktJ2lmylxUG0M",
"dp": "qVnLiKeoSG_Olz17OGBGd4a2sqVFnrjh_51wuaQDdTk", "dp": "qVnLiKeoSG_Olz17OGBGd4a2sqVFnrjh_51wuaQDdTk",
"dq": "GL_Ec6xYg2z1FRfyyGyU1lgf0BJFTZcfNI8ISIN5ssE", "dq": "GL_Ec6xYg2z1FRfyyGyU1lgf0BJFTZcfNI8ISIN5ssE",
"key_ops": ["wrapKey"],
"n": "pCzbcd9kjvg5rfGHdEMWnXo49zbB6FLQ-m0B0BvVp0aojVWYa0xujC-ZP7ZhxByPxyc2PazwFJJi9ivZ_ggRww" "n": "pCzbcd9kjvg5rfGHdEMWnXo49zbB6FLQ-m0B0BvVp0aojVWYa0xujC-ZP7ZhxByPxyc2PazwFJJi9ivZ_ggRww"
}"#; }"#;
@ -56,7 +57,7 @@ fn deserialize_es256() {
.into(), .into(),
}, },
}, },
algorithm: Some(JsonWebAlgorithm::ES256), algorithm: Some(Algorithm::ES256),
key_id: Some("a key".into()), key_id: Some("a key".into()),
key_ops: KeyOps::empty(), key_ops: KeyOps::empty(),
key_use: Some(KeyUse::Encryption), key_use: Some(KeyUse::Encryption),
@ -94,7 +95,7 @@ fn generate_p256() {
struct TokenClaims {} struct TokenClaims {}
let mut the_jwk = JsonWebKey::new(Key::generate_p256()); let mut the_jwk = JsonWebKey::new(Key::generate_p256());
the_jwk.set_algorithm(JsonWebAlgorithm::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().unwrap());
let token = jwt::encode( let token = jwt::encode(
@ -127,7 +128,7 @@ fn deserialize_hs256() {
// The parameters were decoded using a 10-liner Rust script. // The parameters were decoded using a 10-liner Rust script.
key: vec![180, 3, 141, 233].into(), key: vec![180, 3, 141, 233].into(),
}, },
algorithm: Some(JsonWebAlgorithm::HS256), algorithm: Some(Algorithm::HS256),
key_id: None, key_id: None,
key_ops: KeyOps::SIGN | KeyOps::VERIFY, key_ops: KeyOps::SIGN | KeyOps::VERIFY,
key_use: None, key_use: None,
@ -219,7 +220,7 @@ fn deserialize_rs256() {
}, },
algorithm: None, algorithm: None,
key_id: None, key_id: None,
key_ops: KeyOps::empty(), key_ops: KeyOps::WRAP_KEY,
key_use: Some(KeyUse::Encryption), key_use: Some(KeyUse::Encryption),
} }
); );