From bca7f0a3ca6be32ee85990626dfc77c694306add Mon Sep 17 00:00:00 2001 From: Nick Hynes Date: Mon, 13 Jul 2020 23:51:09 +0000 Subject: [PATCH] Add docs --- Cargo.toml | 3 +- README.md | 58 +++++++++++++++++++++--------- src/byte_array.rs | 1 + src/byte_vec.rs | 1 + src/key_ops.rs | 32 ++++++++--------- src/lib.rs | 91 +++++++++++++++++++++++++++++++++++++++-------- src/tests.rs | 9 ++--- 7 files changed, 140 insertions(+), 55 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2995776..9d39cc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jsonwebkey" -version = "0.0.2" +version = "0.0.3" authors = ["Nick Hynes "] description = "JSON Web Key (JWK) (de)serialization, generation, and conversion." readme = "README.md" @@ -15,7 +15,6 @@ derive_more = "0.99" jsonwebtoken = { version = "7.2", optional = true } num-bigint = { version = "0.2", optional = true } p256 = { version = "0.3", optional = true } -paste = "0.1" rand = { version = "0.7", optional = true } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/README.md b/README.md index 24a5844..72325e7 100644 --- a/README.md +++ b/README.md @@ -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. -- [x] 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)) -- [ ] Key generation (particularly for testing) +- Serialization and deserialization of _Required_ and _Recommended_ key types (HS256, RS256, ES256) +- Conversion to PEM for interop with existing JWT libraries (e.g., [jsonwebtoken](https://crates.io/crates/jsonwebtoken)) +- Key generation (particularly useful for testing) **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 extern crate jsonwebtoken as jwt; extern crate jsonwebkey as jwk; -fn main() { - let jwk_str = r#"{ - "kty": "EC", - "d": "ZoKQ9j4dhIBlMRVrv-QG8P_T9sutv3_95eio9MtpgKg", - "crv": "P-256", - "x": "QOMHmv96tVlJv-uNqprnDSKIj5AiLTXKRomXYnav0N0", - "y": "TjYZoHnctatEE6NCrKmXQdJJPnNzZEX8nBmZde3AY4k" - }"#; - let jwk = jwk::JsonWebKey::from_str(jwk_str).unwrap(); - let encoding_key = jwk::EncodingKey::from_ec_der(jwk.to_der().unwrap()); - let token = jwt::encode(&jwt::Header::default(), &() /* claims */, encoding_key).unwrap(); -} +#[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::(&token, &decoding_key, &validation).unwrap(); ``` ## Features diff --git a/src/byte_array.rs b/src/byte_array.rs index 5cded5d..8c81573 100644 --- a/src/byte_array.rs +++ b/src/byte_array.rs @@ -9,6 +9,7 @@ use zeroize::{Zeroize, Zeroizing}; 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)] #[zeroize(drop)] pub struct ByteArray(pub [u8; N]); diff --git a/src/byte_vec.rs b/src/byte_vec.rs index ba054ae..fa86cd5 100644 --- a/src/byte_vec.rs +++ b/src/byte_vec.rs @@ -9,6 +9,7 @@ use zeroize::Zeroize; use crate::utils::{deserialize_base64, serialize_base64}; +/// A zeroizing-on-drop container for a `Vec` that deserializes from base64. #[derive(Clone, PartialEq, Eq, Zeroize, Deref, AsRef, From)] #[zeroize(drop)] pub struct ByteVec(pub Vec); diff --git a/src/key_ops.rs b/src/key_ops.rs index 5bd331e..2ab2d2e 100644 --- a/src/key_ops.rs +++ b/src/key_ops.rs @@ -4,13 +4,11 @@ use serde::{ }; macro_rules! impl_key_ops { - ($(($key_op:ident, $i:literal)),+,) => { - paste::item! { - bitflags::bitflags! { - #[derive(Default)] - pub struct KeyOps: u16 { - $(const [<$key_op:upper>] = $i;)* - } + ($(($key_op:ident, $const_name:ident, $i:literal)),+,) => { + bitflags::bitflags! { + #[derive(Default)] + pub struct KeyOps: u16 { + $(const $const_name = $i;)* } } @@ -18,7 +16,7 @@ macro_rules! impl_key_ops { fn serialize(&self, s: S) -> Result { 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))?; } )+ @@ -33,7 +31,7 @@ macro_rules! impl_key_ops { for op_str in op_strs { $( if op_str == stringify!($key_op) { - ops |= paste::expr! { KeyOps::[<$key_op:upper>] }; + ops |= KeyOps::$const_name; continue; } )+ @@ -47,12 +45,12 @@ macro_rules! impl_key_ops { #[rustfmt::skip] impl_key_ops!( - (sign, 0b00000001), - (verify, 0b00000010), - (encrypt, 0b00000100), - (decrypt, 0b00001000), - (wrapKey, 0b00010000), - (unwrapKey, 0b00100000), - (deriveKey, 0b01000000), - (deriveBits, 0b10000000), + (sign, SIGN, 0b00000001), + (verify, VERIFY, 0b00000010), + (encrypt, ENCRYPT, 0b00000100), + (decrypt, DECRYPT, 0b00001000), + (wrapKey, WRAP_KEY, 0b00010000), + (unwrapKey, UNWRAP_KEY, 0b00100000), + (deriveKey, DERIVE_KEY, 0b01000000), + (deriveBits, DERIVE_BITS, 0b10000000), ); diff --git a/src/lib.rs b/src/lib.rs index cf0adba..d04c845 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,65 @@ #![allow(incomplete_features)] #![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::(&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_vec; mod key_ops; @@ -8,10 +67,7 @@ mod key_ops; mod tests; mod utils; -use std::array::FixedSizeArray; - use serde::{Deserialize, Serialize}; -use zeroize::Zeroize; pub use byte_array::ByteArray; pub use byte_vec::ByteVec; @@ -32,7 +88,7 @@ pub struct JsonWebKey { pub key_id: Option, #[serde(default, rename = "alg", skip_serializing_if = "Option::is_none")] - pub algorithm: Option, + pub algorithm: Option, } 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.algorithm = Some(alg); Ok(()) @@ -56,8 +112,8 @@ impl JsonWebKey { Ok(serde_json::from_slice(bytes.as_ref())?) } - fn validate_algorithm(alg: JsonWebAlgorithm, key: &Key) -> Result<(), Error> { - use JsonWebAlgorithm::*; + fn validate_algorithm(alg: Algorithm, key: &Key) -> Result<(), Error> { + use Algorithm::*; use Key::*; match (alg, key) { ( @@ -199,6 +255,7 @@ impl Key { Some(private_point) => { pkcs8::write_private(oids, |writer: &mut DERWriterSeq| { writer.next().write_i8(1); // version + use std::array::FixedSizeArray; writer.next().write_bytes(private_point.as_slice()); // The following tagged value is optional. OpenSSL produces 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)] #[serde(tag = "crv")] pub enum Curve { - /// prime256v1 + /// Parameters of the prime256v1 (P256) curve. #[serde(rename = "P-256")] P256 { - /// Private point. + /// The private scalar. #[serde(skip_serializing_if = "Option::is_none")] d: Option>, + /// The curve point x coordinate. x: ByteArray<32>, + /// The curve point y coordinate. y: ByteArray<32>, }, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct RsaPublic { - /// Public exponent. Must be 65537. + /// The standard public exponent, 65537. pub e: PublicExponent, - /// Modulus, p*q. + /// The modulus, p*q. pub n: ByteVec, } const PUBLIC_EXPONENT: u32 = 65537; const PUBLIC_EXPONENT_B64: &str = "AQAB"; // little-endian, strip zeros const PUBLIC_EXPONENT_B64_PADDED: &str = "AQABAA=="; + +/// The standard RSA public exponent, 65537. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct PublicExponent; @@ -391,7 +452,7 @@ pub struct RsaPrivate { /// First factor Chinese Remainder Theorem (CRT) exponent. #[serde(default, skip_serializing_if = "Option::is_none")] pub dp: Option, - /// Second factor Chinese Remainder Theorem (CRT) exponent. + /// Second factor CRT exponent. #[serde(default, skip_serializing_if = "Option::is_none")] pub dq: Option, /// First CRT coefficient. @@ -407,15 +468,15 @@ pub enum KeyUse { Encryption, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Zeroize)] -pub enum JsonWebAlgorithm { +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum Algorithm { HS256, RS256, ES256, } #[cfg(any(test, feature = "jsonwebtoken"))] -impl Into for JsonWebAlgorithm { +impl Into for Algorithm { fn into(self) -> jsonwebtoken::Algorithm { match self { Self::HS256 => jsonwebtoken::Algorithm::HS256, diff --git a/src/tests.rs b/src/tests.rs index 6e4b270..ad2f3eb 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -24,6 +24,7 @@ static RSA_JWK_FIXTURE: &str = r#"{ "qi": "adhQHH8IGXFfLEMnZ5t_TeCp5zgSwQktJ2lmylxUG0M", "dp": "qVnLiKeoSG_Olz17OGBGd4a2sqVFnrjh_51wuaQDdTk", "dq": "GL_Ec6xYg2z1FRfyyGyU1lgf0BJFTZcfNI8ISIN5ssE", + "key_ops": ["wrapKey"], "n": "pCzbcd9kjvg5rfGHdEMWnXo49zbB6FLQ-m0B0BvVp0aojVWYa0xujC-ZP7ZhxByPxyc2PazwFJJi9ivZ_ggRww" }"#; @@ -56,7 +57,7 @@ fn deserialize_es256() { .into(), }, }, - algorithm: Some(JsonWebAlgorithm::ES256), + algorithm: Some(Algorithm::ES256), key_id: Some("a key".into()), key_ops: KeyOps::empty(), key_use: Some(KeyUse::Encryption), @@ -94,7 +95,7 @@ fn generate_p256() { struct TokenClaims {} 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 token = jwt::encode( @@ -127,7 +128,7 @@ fn deserialize_hs256() { // The parameters were decoded using a 10-liner Rust script. key: vec![180, 3, 141, 233].into(), }, - algorithm: Some(JsonWebAlgorithm::HS256), + algorithm: Some(Algorithm::HS256), key_id: None, key_ops: KeyOps::SIGN | KeyOps::VERIFY, key_use: None, @@ -219,7 +220,7 @@ fn deserialize_rs256() { }, algorithm: None, key_id: None, - key_ops: KeyOps::empty(), + key_ops: KeyOps::WRAP_KEY, key_use: Some(KeyUse::Encryption), } );