mirror of
				https://github.com/BitskiCo/jwk-rs
				synced 2025-10-31 01:04:43 +00:00 
			
		
		
		
	Add docs
This commit is contained in:
		| @@ -1,6 +1,6 @@ | ||||
| [package] | ||||
| name = "jsonwebkey" | ||||
| version = "0.0.2" | ||||
| version = "0.0.3" | ||||
| authors = ["Nick Hynes <nhynes@nhynes.com>"] | ||||
| 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" | ||||
|   | ||||
							
								
								
									
										58
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								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::<TokenClaims>(&token, &decoding_key, &validation).unwrap(); | ||||
| ``` | ||||
|  | ||||
| ## Features | ||||
|   | ||||
| @@ -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<const N: usize>(pub [u8; N]); | ||||
|   | ||||
| @@ -9,6 +9,7 @@ use zeroize::Zeroize; | ||||
|  | ||||
| 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)] | ||||
| #[zeroize(drop)] | ||||
| pub struct ByteVec(pub Vec<u8>); | ||||
|   | ||||
| @@ -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<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> { | ||||
|                 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), | ||||
| ); | ||||
|   | ||||
							
								
								
									
										91
									
								
								src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										91
									
								
								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::<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_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<String>, | ||||
|  | ||||
|     #[serde(default, rename = "alg", skip_serializing_if = "Option::is_none")] | ||||
|     pub algorithm: Option<JsonWebAlgorithm>, | ||||
|     pub algorithm: Option<Algorithm>, | ||||
| } | ||||
|  | ||||
| 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<ByteArray<32>>, | ||||
|         /// 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<ByteVec>, | ||||
|     /// Second factor Chinese Remainder Theorem (CRT) exponent. | ||||
|     /// Second factor CRT exponent. | ||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] | ||||
|     pub dq: Option<ByteVec>, | ||||
|     /// 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<jsonwebtoken::Algorithm> for JsonWebAlgorithm { | ||||
| impl Into<jsonwebtoken::Algorithm> for Algorithm { | ||||
|     fn into(self) -> jsonwebtoken::Algorithm { | ||||
|         match self { | ||||
|             Self::HS256 => jsonwebtoken::Algorithm::HS256, | ||||
|   | ||||
| @@ -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), | ||||
|         } | ||||
|     ); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Nick Hynes
					Nick Hynes