mirror of
				https://github.com/BitskiCo/jwk-rs
				synced 2025-10-30 16:54:45 +00:00 
			
		
		
		
	Add PEM conversion
This commit is contained in:
		| @@ -1,6 +1,6 @@ | ||||
| [package] | ||||
| name = "jsonwebkey" | ||||
| version = "0.0.1" | ||||
| version = "0.0.2" | ||||
| authors = ["Nick Hynes <nhynes@nhynes.com>"] | ||||
| description = "JSON Web Key (JWK) (de)serialization, generation, and conversion." | ||||
| readme = "README.md" | ||||
| @@ -17,4 +17,8 @@ serde = { version = "1.0", features = ["derive"] } | ||||
| serde_json = "1.0" | ||||
| syn = { version = "1.0", features = ["full"] } # required to parse const generics | ||||
| thiserror = "1.0" | ||||
| yasna = { version = "0.3", optional = true } | ||||
| zeroize = { version = "1.1", features = ["zeroize_derive"] } | ||||
|  | ||||
| [features] | ||||
| conversion = ["yasna"] | ||||
|   | ||||
							
								
								
									
										35
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,10 +1,37 @@ | ||||
| # jsonwebkey | ||||
|  | ||||
| **JSON Web Key (JWK) (de)serialization, generation, and conversion.** | ||||
| *[JSON Web Key (JWK)](https://tools.ietf.org/html/rfc7517#section-4.3) (de)serialization, generation, and conversion.* | ||||
|  | ||||
| This library aims to be [spec](https://tools.ietf.org/html/rfc7517#section-4.3) compliant and secure. | ||||
| Note: requires rustc nightly >= 1.45 for conveniences around fixed-size arrays. | ||||
|  | ||||
| **Goals** | ||||
|  | ||||
| tl;dr: get keys into a format that can be used by other crates; be as safe as possible while doing so. | ||||
|  | ||||
| Features: | ||||
| - [x] 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)) | ||||
| - [x] Conversion to PEM for interop with existing JWT libraries (e.g., [jsonwebtoken](https://crates.io/crates/jsonwebtoken)) | ||||
| - [ ] Key generation (particularly for testing) | ||||
|  | ||||
| **Non-goals** | ||||
|  | ||||
| * be a fully-featured JOSE framework | ||||
|  | ||||
| ## Example | ||||
|  | ||||
| ```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(); | ||||
| } | ||||
| ``` | ||||
|   | ||||
| @@ -9,7 +9,7 @@ use zeroize::{Zeroize, Zeroizing}; | ||||
|  | ||||
| use crate::utils::{deserialize_base64, serialize_base64}; | ||||
|  | ||||
| #[derive(Zeroize, Deref, AsRef, From)] | ||||
| #[derive(Clone, Zeroize, Deref, AsRef, From)] | ||||
| #[zeroize(drop)] | ||||
| pub struct ByteArray<const N: usize>(pub [u8; N]); | ||||
|  | ||||
|   | ||||
| @@ -9,7 +9,7 @@ use zeroize::Zeroize; | ||||
|  | ||||
| use crate::utils::{deserialize_base64, serialize_base64}; | ||||
|  | ||||
| #[derive(PartialEq, Eq, Zeroize, Deref, AsRef, From)] | ||||
| #[derive(Clone, PartialEq, Eq, Zeroize, Deref, AsRef, From)] | ||||
| #[zeroize(drop)] | ||||
| pub struct ByteVec(pub Vec<u8>); | ||||
|  | ||||
|   | ||||
							
								
								
									
										199
									
								
								src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										199
									
								
								src/lib.rs
									
									
									
									
									
								
							| @@ -18,7 +18,7 @@ pub use key_ops::KeyOps; | ||||
| #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] | ||||
| pub struct JsonWebKey { | ||||
|     #[serde(flatten)] | ||||
|     pub key_type: Box<KeyType>, | ||||
|     pub key: Box<Key>, | ||||
|  | ||||
|     #[serde(default, rename = "use", skip_serializing_if = "Option::is_none")] | ||||
|     pub key_use: Option<KeyUse>, | ||||
| @@ -46,12 +46,12 @@ impl std::str::FromStr for JsonWebKey { | ||||
|  | ||||
|         // Validate alg. | ||||
|         use JsonWebAlgorithm::*; | ||||
|         use KeyType::*; | ||||
|         use Key::*; | ||||
|         let alg = match &jwk.algorithm { | ||||
|             Some(alg) => alg, | ||||
|             None => return Ok(jwk), | ||||
|         }; | ||||
|         match (alg, &*jwk.key_type) { | ||||
|         match (alg, &*jwk.key) { | ||||
|             ( | ||||
|                 ES256, | ||||
|                 EC { | ||||
| @@ -75,9 +75,9 @@ impl std::fmt::Display for JsonWebKey { | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] | ||||
| #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | ||||
| #[serde(tag = "kty")] | ||||
| pub enum KeyType { | ||||
| pub enum Key { | ||||
|     EC { | ||||
|         #[serde(flatten)] | ||||
|         params: Curve, | ||||
| @@ -95,7 +95,188 @@ pub enum KeyType { | ||||
|     }, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] | ||||
| impl Key { | ||||
|     /// Returns true iff this key only contains private components (i.e. a private asymmetric | ||||
|     /// key or a symmetric key). | ||||
|     fn is_private(&self) -> bool { | ||||
|         match self { | ||||
|             Self::Symmetric { .. } | ||||
|             | Self::EC { | ||||
|                 params: Curve::P256 { d: Some(_), .. }, | ||||
|                 .. | ||||
|             } | ||||
|             | Self::RSA { | ||||
|                 private: Some(_), .. | ||||
|             } => true, | ||||
|             _ => false, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Returns true iff this key only contains non-private components. | ||||
|     pub fn is_public(&self) -> bool { | ||||
|         !self.is_private() | ||||
|     } | ||||
|  | ||||
|     /// Returns the public part of this key, if it's symmetric. | ||||
|     pub fn to_public(&self) -> Option<Self> { | ||||
|         if self.is_public() { | ||||
|             return Some(self.clone()); | ||||
|         } | ||||
|         Some(match self { | ||||
|             Self::Symmetric { .. } => return None, | ||||
|             Self::EC { | ||||
|                 params: Curve::P256 { x, y, .. }, | ||||
|             } => Self::EC { | ||||
|                 params: Curve::P256 { | ||||
|                     x: x.clone(), | ||||
|                     y: y.clone(), | ||||
|                     d: None, | ||||
|                 }, | ||||
|             }, | ||||
|             Self::RSA { public, .. } => Self::RSA { | ||||
|                 public: public.clone(), | ||||
|                 private: None, | ||||
|             }, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     /// If this key is asymmetric, encodes it as PKCS#8. | ||||
|     #[cfg(feature = "conversion")] | ||||
|     pub fn to_der(&self) -> Option<Vec<u8>> { | ||||
|         use yasna::{models::ObjectIdentifier, DERWriter, DERWriterSeq, Tag}; | ||||
|  | ||||
|         if let Self::Symmetric { .. } = self { | ||||
|             return None; | ||||
|         } | ||||
|         Some(yasna::construct_der(|writer| match self { | ||||
|             Self::EC { | ||||
|                 params: Curve::P256 { d, x, y }, | ||||
|             } => { | ||||
|                 let write_curve_oid = |writer: DERWriter| { | ||||
|                     writer.write_oid(&ObjectIdentifier::from_slice(&[ | ||||
|                         1, 2, 840, 10045, 3, 1, 7, // prime256v1 | ||||
|                     ])); | ||||
|                 }; | ||||
|                 let write_public = |writer: DERWriter| { | ||||
|                     let public_bytes: Vec<u8> = [0x04 /* uncompressed */] | ||||
|                         .iter() | ||||
|                         .chain(x.iter()) | ||||
|                         .chain(y.iter()) | ||||
|                         .copied() | ||||
|                         .collect(); | ||||
|                     writer.write_bitvec_bytes(&public_bytes, 8 * (32 * 2 + 1)); | ||||
|                 }; | ||||
|                 writer.write_sequence(|writer| { | ||||
|                     match d { | ||||
|                         Some(private_point) => { | ||||
|                             writer.next().write_i8(1); // version | ||||
|                             writer.next().write_bytes(private_point.as_ref()); | ||||
|                             writer.next().write_tagged(Tag::context(0), |writer| { | ||||
|                                 write_curve_oid(writer); | ||||
|                             }); | ||||
|                             writer.next().write_tagged(Tag::context(1), |writer| { | ||||
|                                 write_public(writer); | ||||
|                             }); | ||||
|                         } | ||||
|                         None => { | ||||
|                             writer.next().write_sequence(|writer| { | ||||
|                                 writer.next().write_oid(&ObjectIdentifier::from_slice(&[ | ||||
|                                     1, 2, 840, 10045, 2, 1, // ecPublicKey | ||||
|                                 ])); | ||||
|                                 write_curve_oid(writer.next()); | ||||
|                             }); | ||||
|                             write_public(writer.next()); | ||||
|                         } | ||||
|                     }; | ||||
|                 }); | ||||
|             } | ||||
|             Self::RSA { public, private } => { | ||||
|                 let write_alg_id = |writer: &mut DERWriterSeq| { | ||||
|                     writer.next().write_oid(&ObjectIdentifier::from_slice(&[ | ||||
|                         1, 2, 840, 113549, 1, 1, 1, // rsaEncryption | ||||
|                     ])); | ||||
|                     writer.next().write_null(); // parameters | ||||
|                 }; | ||||
|                 let write_public = |writer: &mut DERWriterSeq| { | ||||
|                     writer.next().write_bytes(&*public.n); | ||||
|                     writer.next().write_u32(PUBLIC_EXPONENT); | ||||
|                 }; | ||||
|                 writer.write_sequence(|writer| { | ||||
|                     match private { | ||||
|                         Some(private) => { | ||||
|                             writer.next().write_i8(0); // version | ||||
|                             writer.next().write_sequence(|writer| { | ||||
|                                 write_alg_id(writer); | ||||
|                             }); | ||||
|                             writer | ||||
|                                 .next() | ||||
|                                 .write_tagged(yasna::tags::TAG_OCTETSTRING, |writer| { | ||||
|                                     writer.write_sequence(|writer| { | ||||
|                                         writer.next().write_i8(0); // version | ||||
|                                         write_public(writer); | ||||
|                                         writer.next().write_bytes(&private.d); | ||||
|                                         if let Some(p) = &private.p { | ||||
|                                             writer.next().write_bytes(p); | ||||
|                                         } | ||||
|                                         if let Some(q) = &private.q { | ||||
|                                             writer.next().write_bytes(q); | ||||
|                                         } | ||||
|                                         if let Some(dp) = &private.dp { | ||||
|                                             writer.next().write_bytes(dp); | ||||
|                                         } | ||||
|                                         if let Some(dq) = &private.dq { | ||||
|                                             writer.next().write_bytes(dq); | ||||
|                                         } | ||||
|                                         if let Some(qi) = &private.qi { | ||||
|                                             writer.next().write_bytes(qi); | ||||
|                                         } | ||||
|                                     }); | ||||
|                                 }); | ||||
|                         } | ||||
|                         None => { | ||||
|                             write_alg_id(writer); | ||||
|                             writer | ||||
|                                 .next() | ||||
|                                 .write_tagged(yasna::tags::TAG_BITSTRING, |writer| { | ||||
|                                     writer.write_sequence(|writer| { | ||||
|                                         write_public(writer); | ||||
|                                     }) | ||||
|                                 }); | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|             Self::Symmetric { .. } => unreachable!("checked above"), | ||||
|         })) | ||||
|     } | ||||
|  | ||||
|     /// If this key is asymmetric, encodes it as PKCS#8 with PEM armoring. | ||||
|     #[cfg(feature = "conversion")] | ||||
|     pub fn to_pem(&self) -> Option<String> { | ||||
|         use std::fmt::Write; | ||||
|         let der_b64 = base64::encode(self.to_der()?); | ||||
|         let key_ty = if self.is_private() { | ||||
|             "PRIVATE" | ||||
|         } else { | ||||
|             "PUBLIC" | ||||
|         }; | ||||
|         let mut pem = String::new(); | ||||
|         writeln!(&mut pem, "-----BEGIN {} KEY-----", key_ty).unwrap(); | ||||
|         const MAX_LINE_LEN: usize = 64; | ||||
|         for i in (0..der_b64.len()).step_by(MAX_LINE_LEN) { | ||||
|             writeln!( | ||||
|                 &mut pem, | ||||
|                 "{}", | ||||
|                 &der_b64[i..std::cmp::min(i + MAX_LINE_LEN, der_b64.len())] | ||||
|             ) | ||||
|             .unwrap(); | ||||
|         } | ||||
|         writeln!(&mut pem, "-----END {} KEY-----", key_ty).unwrap(); | ||||
|         Some(pem) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | ||||
| #[serde(tag = "crv")] | ||||
| pub enum Curve { | ||||
|     /// prime256v1 | ||||
| @@ -109,7 +290,7 @@ pub enum Curve { | ||||
|     }, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] | ||||
| #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | ||||
| pub struct RsaPublic { | ||||
|     /// Public exponent. Must be 65537. | ||||
|     pub e: PublicExponent, | ||||
| @@ -120,7 +301,7 @@ pub struct RsaPublic { | ||||
| const PUBLIC_EXPONENT: u32 = 65537; | ||||
| const PUBLIC_EXPONENT_B64: &str = "AQAB"; // little-endian, strip zeros | ||||
| const PUBLIC_EXPONENT_B64_PADDED: &str = "AQABAA=="; | ||||
| #[derive(Debug, PartialEq, Eq)] | ||||
| #[derive(Clone, Copy, Debug, PartialEq, Eq)] | ||||
| pub struct PublicExponent; | ||||
|  | ||||
| impl Serialize for PublicExponent { | ||||
| @@ -143,7 +324,7 @@ impl<'de> Deserialize<'de> for PublicExponent { | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] | ||||
| #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | ||||
| pub struct RsaPrivate { | ||||
|     /// Private exponent. | ||||
|     pub d: ByteVec, | ||||
|   | ||||
							
								
								
									
										34
									
								
								src/tests.rs
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								src/tests.rs
									
									
									
									
									
								
							| @@ -19,7 +19,7 @@ fn deserialize_es256() { | ||||
|     assert_eq!( | ||||
|         jwk, | ||||
|         JsonWebKey { | ||||
|             key_type: box KeyType::EC { | ||||
|             key: box Key::EC { | ||||
|                 // The parameters were decoded using a 10-liner Rust script. | ||||
|                 params: Curve::P256 { | ||||
|                     d: Some( | ||||
| @@ -53,7 +53,7 @@ fn deserialize_es256() { | ||||
| #[test] | ||||
| fn serialize_es256() { | ||||
|     let jwk = JsonWebKey { | ||||
|         key_type: box KeyType::EC { | ||||
|         key: box Key::EC { | ||||
|             params: Curve::P256 { | ||||
|                 d: None, | ||||
|                 x: [1u8; 32].into(), | ||||
| @@ -83,7 +83,7 @@ fn deserialize_hs256() { | ||||
|     assert_eq!( | ||||
|         jwk, | ||||
|         JsonWebKey { | ||||
|             key_type: box KeyType::Symmetric { | ||||
|             key: box Key::Symmetric { | ||||
|                 // The parameters were decoded using a 10-liner Rust script. | ||||
|                 key: vec![180, 3, 141, 233].into(), | ||||
|             }, | ||||
| @@ -98,7 +98,7 @@ fn deserialize_hs256() { | ||||
| #[test] | ||||
| fn serialize_hs256() { | ||||
|     let jwk = JsonWebKey { | ||||
|         key_type: box KeyType::Symmetric { | ||||
|         key: box Key::Symmetric { | ||||
|             key: vec![42; 16].into(), | ||||
|         }, | ||||
|         key_id: None, | ||||
| @@ -130,7 +130,7 @@ fn deserialize_rs256() { | ||||
|     assert_eq!( | ||||
|         jwk, | ||||
|         JsonWebKey { | ||||
|             key_type: box KeyType::RSA { | ||||
|             key: box Key::RSA { | ||||
|                 public: RsaPublic { | ||||
|                     e: PublicExponent, | ||||
|                     n: vec![ | ||||
| @@ -201,7 +201,7 @@ fn deserialize_rs256() { | ||||
| #[test] | ||||
| fn serialize_rs256() { | ||||
|     let jwk = JsonWebKey { | ||||
|         key_type: box KeyType::RSA { | ||||
|         key: box Key::RSA { | ||||
|             public: RsaPublic { | ||||
|                 e: PublicExponent, | ||||
|                 n: vec![105, 183, 62].into(), | ||||
| @@ -261,3 +261,25 @@ fn mismatched_algorithm() { | ||||
|         }"# | ||||
|     ); | ||||
| } | ||||
|  | ||||
| #[cfg(feature = "conversion")] | ||||
| #[test] | ||||
| fn es256_to_pem() { | ||||
|     let jwk_str = r#"{ | ||||
|         "kty": "EC", | ||||
|         "d": "ZoKQ9j4dhIBlMRVrv-QG8P_T9sutv3_95eio9MtpgKg", | ||||
|         "crv": "P-256", | ||||
|         "x": "QOMHmv96tVlJv-uNqprnDSKIj5AiLTXKRomXYnav0N0", | ||||
|         "y": "TjYZoHnctatEE6NCrKmXQdJJPnNzZEX8nBmZde3AY4k" | ||||
|     }"#; | ||||
|     let jwk = JsonWebKey::from_str(jwk_str).unwrap(); | ||||
|     #[rustfmt::skip] | ||||
|     assert_eq!( | ||||
|         base64::encode(jwk.key.to_pem().unwrap()), | ||||
| "-----BEGIN PRIVATE KEY----- | ||||
| MHcCAQEEIGaCkPY+HYSAZTEVa7/kBvD/0/bLrb9//eXoqPTLaYCooAoGCCqGSM49 | ||||
| AwEHoUQDQgAEQOMHmv96tVlJv+uNqprnDSKIj5AiLTXKRomXYnav0N1ONhmgedy1 | ||||
| q0QTo0KsqZdB0kk+c3NkRfycGZl17cBjiQ== | ||||
| -----END PRIVATE KEY-----" | ||||
|     ); | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Nick Hynes
					Nick Hynes