mirror of
				https://github.com/BitskiCo/jwk-rs
				synced 2025-10-31 01:04:43 +00:00 
			
		
		
		
	Add jwt and panicking conversions
This commit is contained in:
		| @@ -24,7 +24,8 @@ yasna = { version = "0.3", optional = true, features = ["num-bigint"] } | ||||
| zeroize = { version = "1.1", features = ["zeroize_derive"] } | ||||
|  | ||||
| [features] | ||||
| convert = ["num-bigint", "yasna"] | ||||
| pkcs-convert = ["num-bigint", "yasna"] | ||||
| jwt-convert = ["pkcs-convert", "jsonwebtoken"] | ||||
| generate = ["p256", "rand"] | ||||
|  | ||||
| [dev-dependencies] | ||||
|   | ||||
							
								
								
									
										27
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								README.md
									
									
									
									
									
								
							| @@ -34,13 +34,16 @@ let jwt_str = r#"{ | ||||
|    "k": "Wpj30SfkzM_m0Sa_B2NqNw", | ||||
|    "alg": "HS256" | ||||
| }"#; | ||||
| let jwk: jwk::JsonWebKey = jwt_str.parse().unwrap(); | ||||
| println!("{:#?}", jwk); // looks like `jwt_str` but with reordered fields. | ||||
| let the_jwk: jwk::JsonWebKey = jwt_str.parse().unwrap(); | ||||
| println!("{:#?}", the_jwk); // looks like `jwt_str` but with reordered fields. | ||||
| ``` | ||||
|  | ||||
| ### Using with other crates | ||||
|  | ||||
| *Note:* The following example requires the `jwt-convert` feature. | ||||
|  | ||||
| ```rust | ||||
| #[cfg(all(feature = "generate", feature = "jwt-convert"))] { | ||||
| extern crate jsonwebtoken as jwt; | ||||
| extern crate jsonwebkey as jwk; | ||||
|  | ||||
| @@ -50,24 +53,24 @@ 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 alg: jwt::Algorithm = my_jwk.algorithm.unwrap().into(); | ||||
| let token = jwt::encode( | ||||
|     &jwt::Header::new(my_jwk.algorithm.unwrap().into()), | ||||
|     &jwt::Header::new(alg), | ||||
|     &TokenClaims {}, | ||||
|     &encoding_key, | ||||
|     &my_jwk.key.to_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()); | ||||
| let mut validation = jwt::Validation::new(alg); | ||||
| 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 | ||||
|  | ||||
| * `convert` - enables `Key::{to_der, to_pem}`. | ||||
|               This pulls in the [yasna](https://crates.io/crates/yasna) crate. | ||||
| * `pkcs-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. | ||||
| * `jwt-convert` - enables conversions to types in the | ||||
|                   [jsonwebtoken](https://crates.io/crates/jsonwebtoken) crate. | ||||
|   | ||||
							
								
								
									
										132
									
								
								src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										132
									
								
								src/lib.rs
									
									
									
									
									
								
							| @@ -20,13 +20,14 @@ | ||||
| //!    "k": "Wpj30SfkzM_m0Sa_B2NqNw", | ||||
| //!    "alg": "HS256" | ||||
| //! }"#; | ||||
| //! let jwk: jwk::JsonWebKey = jwt_str.parse().unwrap(); | ||||
| //! println!("{:#?}", jwk); // looks like `jwt_str` but with reordered fields. | ||||
| //! let the_jwk: jwk::JsonWebKey = jwt_str.parse().unwrap(); | ||||
| //! println!("{:#?}", the_jwk); // looks like `jwt_str` but with reordered fields. | ||||
| //! ``` | ||||
| //! | ||||
| //! ### Using with other crates | ||||
| //! | ||||
| //! ``` | ||||
| //! #[cfg(all(feature = "generate", feature = "jwt-convert"))] { | ||||
| //! extern crate jsonwebtoken as jwt; | ||||
| //! extern crate jsonwebkey as jwk; | ||||
| //! | ||||
| @@ -36,18 +37,17 @@ | ||||
| //! 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 alg: jwt::Algorithm = my_jwk.algorithm.unwrap().into(); | ||||
| //! let token = jwt::encode( | ||||
| //!     &jwt::Header::new(my_jwk.algorithm.unwrap().into()), | ||||
| //!     &jwt::Header::new(alg), | ||||
| //!     &TokenClaims {}, | ||||
| //!     &encoding_key, | ||||
| //!     &my_jwk.key.to_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()); | ||||
| //! let mut validation = jwt::Validation::new(alg); | ||||
| //! 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 | ||||
| @@ -65,13 +65,15 @@ mod key_ops; | ||||
| mod tests; | ||||
| mod utils; | ||||
|  | ||||
| use std::borrow::Cow; | ||||
|  | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| pub use byte_array::ByteArray; | ||||
| pub use byte_vec::ByteVec; | ||||
| pub use key_ops::KeyOps; | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] | ||||
| #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | ||||
| pub struct JsonWebKey { | ||||
|     #[serde(flatten)] | ||||
|     pub key: Box<Key>, | ||||
| @@ -196,12 +198,12 @@ impl Key { | ||||
|         !self.is_private() | ||||
|     } | ||||
|  | ||||
|     /// Returns the public part of this key, if it's symmetric. | ||||
|     pub fn to_public(&self) -> Option<Self> { | ||||
|     /// Returns the public part of this key (symmetric keys have no public parts). | ||||
|     pub fn to_public(&self) -> Option<Cow<Self>> { | ||||
|         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::EC { | ||||
|                 curve: Curve::P256 { x, y, .. }, | ||||
| @@ -216,19 +218,19 @@ impl Key { | ||||
|                 public: public.clone(), | ||||
|                 private: None, | ||||
|             }, | ||||
|         }) | ||||
|         })) | ||||
|     } | ||||
|  | ||||
|     /// If this key is asymmetric, encodes it as PKCS#8. | ||||
|     #[cfg(feature = "convert")] | ||||
|     pub fn to_der(&self) -> Result<Vec<u8>, PkcsConvertError> { | ||||
|     #[cfg(feature = "pkcs-convert")] | ||||
|     pub fn try_to_der(&self) -> Result<Vec<u8>, ConversionError> { | ||||
|         use num_bigint::BigUint; | ||||
|         use yasna::{models::ObjectIdentifier, DERWriter, DERWriterSeq, Tag}; | ||||
|  | ||||
|         use crate::utils::pkcs8; | ||||
|  | ||||
|         if let Self::Symmetric { .. } = self { | ||||
|             return Err(PkcsConvertError::NotAsymmetric); | ||||
|             return Err(ConversionError::NotAsymmetric); | ||||
|         } | ||||
|  | ||||
|         Ok(match self { | ||||
| @@ -253,8 +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()); | ||||
|                             writer.next().write_bytes(&**private_point); | ||||
|                             // The following tagged value is optional. OpenSSL produces it, | ||||
|                             // but many tools, including jwt.io and `jsonwebtoken`, don't like it, | ||||
|                             // so we don't include it. | ||||
| @@ -308,7 +309,7 @@ impl Key { | ||||
|                             qi: Some(_), | ||||
|                         }, | ||||
|                     ) => 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| { | ||||
|                         let body = | ||||
|                             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. | ||||
|     #[cfg(feature = "convert")] | ||||
|     pub fn to_pem(&self) -> Result<String, PkcsConvertError> { | ||||
|     #[cfg(feature = "pkcs-convert")] | ||||
|     pub fn try_to_pem(&self) -> Result<String, ConversionError> { | ||||
|         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() { | ||||
|             "PRIVATE" | ||||
|         } else { | ||||
| @@ -332,6 +340,7 @@ impl Key { | ||||
|         }; | ||||
|         let mut pem = String::new(); | ||||
|         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; | ||||
|         for i in (0..der_b64.len()).step_by(MAX_LINE_LEN) { | ||||
|             writeln!( | ||||
| @@ -345,6 +354,13 @@ impl Key { | ||||
|         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. | ||||
|     /// Best used with one of the HS algorithms (e.g., HS256). | ||||
|     #[cfg(feature = "generate")] | ||||
| @@ -473,16 +489,62 @@ pub enum Algorithm { | ||||
|     ES256, | ||||
| } | ||||
|  | ||||
| #[cfg(any(test, feature = "jsonwebtoken"))] | ||||
| impl Into<jsonwebtoken::Algorithm> for Algorithm { | ||||
|     fn into(self) -> jsonwebtoken::Algorithm { | ||||
|         match self { | ||||
|             Self::HS256 => jsonwebtoken::Algorithm::HS256, | ||||
|             Self::ES256 => jsonwebtoken::Algorithm::ES256, | ||||
|             Self::RS256 => jsonwebtoken::Algorithm::RS256, | ||||
| #[cfg(feature = "jwt-convert")] | ||||
| const _IMPL_JWT_CONVERSIONS: () = { | ||||
|     use jsonwebtoken as jwt; | ||||
|  | ||||
|     impl Into<jwt::Algorithm> for Algorithm { | ||||
|         fn into(self) -> jsonwebtoken::Algorithm { | ||||
|             match self { | ||||
|                 Self::HS256 => jwt::Algorithm::HS256, | ||||
|                 Self::ES256 => jwt::Algorithm::ES256, | ||||
|                 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)] | ||||
| pub enum Error { | ||||
| @@ -497,10 +559,14 @@ pub enum 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")] | ||||
|     MissingRsaParams, | ||||
|  | ||||
|     #[error("a symmetric key can not be encoded using PKCS#8")] | ||||
|     NotAsymmetric, | ||||
|  | ||||
|     #[cfg(feature = "jwt-convert")] | ||||
|     #[error("a public key cannot be converted to a `jsonwebtoken::EncodingKey`")] | ||||
|     NotPrivate, | ||||
| } | ||||
|   | ||||
							
								
								
									
										22
									
								
								src/tests.rs
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								src/tests.rs
									
									
									
									
									
								
							| @@ -86,7 +86,7 @@ fn serialize_es256() { | ||||
|     ); | ||||
| } | ||||
|  | ||||
| #[cfg(feature = "generate")] | ||||
| #[cfg(all(feature = "jwt-convert", feature = "generate"))] | ||||
| #[test] | ||||
| fn generate_p256() { | ||||
|     extern crate jsonwebtoken as jwt; | ||||
| @@ -97,7 +97,7 @@ fn generate_p256() { | ||||
|     let mut the_jwk = JsonWebKey::new(Key::generate_p256()); | ||||
|     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( | ||||
|         &jwt::Header::new(the_jwk.algorithm.unwrap().into()), | ||||
|         &TokenClaims {}, | ||||
| @@ -107,7 +107,7 @@ fn generate_p256() { | ||||
|  | ||||
|     let mut validation = jwt::Validation::new(the_jwk.algorithm.unwrap().into()); | ||||
|     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(); | ||||
|     jwt::decode::<TokenClaims>(&token, &decoding_key, &validation).unwrap(); | ||||
| } | ||||
| @@ -290,14 +290,14 @@ fn mismatched_algorithm() { | ||||
|     ); | ||||
| } | ||||
|  | ||||
| #[cfg(feature = "convert")] | ||||
| #[cfg(feature = "pkcs-convert")] | ||||
| #[test] | ||||
| fn p256_private_to_pem() { | ||||
|     // generated using mkjwk, converted using node-jwk-to-pem, verified using openssl | ||||
|     let jwk = JsonWebKey::from_str(P256_JWK_FIXTURE).unwrap(); | ||||
|     #[rustfmt::skip] | ||||
|     assert_eq!( | ||||
|         jwk.key.to_pem().unwrap(), | ||||
|         jwk.key.to_pem(), | ||||
| "-----BEGIN PRIVATE KEY----- | ||||
| MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgZoKQ9j4dhIBlMRVr | ||||
| v+QG8P/T9sutv3/95eio9MtpgKihRANCAARA4wea/3q1WUm/642qmucNIoiPkCIt | ||||
| @@ -307,13 +307,13 @@ NcpGiZdidq/Q3U42GaB53LWrRBOjQqypl0HSST5zc2RF/JwZmXXtwGOJ | ||||
|     ); | ||||
| } | ||||
|  | ||||
| #[cfg(feature = "convert")] | ||||
| #[cfg(feature = "pkcs-convert")] | ||||
| #[test] | ||||
| fn p256_public_to_pem() { | ||||
|     let jwk = JsonWebKey::from_str(P256_JWK_FIXTURE).unwrap(); | ||||
|     #[rustfmt::skip] | ||||
|     assert_eq!( | ||||
|         jwk.key.to_public().unwrap().to_pem().unwrap(), | ||||
|         jwk.key.to_public().unwrap().to_pem(), | ||||
| "-----BEGIN PUBLIC KEY----- | ||||
| MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQOMHmv96tVlJv+uNqprnDSKIj5Ai | ||||
| LTXKRomXYnav0N1ONhmgedy1q0QTo0KsqZdB0kk+c3NkRfycGZl17cBjiQ== | ||||
| @@ -322,13 +322,13 @@ LTXKRomXYnav0N1ONhmgedy1q0QTo0KsqZdB0kk+c3NkRfycGZl17cBjiQ== | ||||
|     ); | ||||
| } | ||||
|  | ||||
| #[cfg(feature = "convert")] | ||||
| #[cfg(feature = "pkcs-convert")] | ||||
| #[test] | ||||
| fn rsa_private_to_pem() { | ||||
|     let jwk = JsonWebKey::from_str(RSA_JWK_FIXTURE).unwrap(); | ||||
|     #[rustfmt::skip] | ||||
|     assert_eq!( | ||||
|         jwk.key.to_pem().unwrap(), | ||||
|         jwk.key.to_pem(), | ||||
| "-----BEGIN PRIVATE KEY----- | ||||
| MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEApCzbcd9kjvg5rfGH | ||||
| dEMWnXo49zbB6FLQ+m0B0BvVp0aojVWYa0xujC+ZP7ZhxByPxyc2PazwFJJi9ivZ | ||||
| @@ -343,13 +343,13 @@ J2lmylxUG0M= | ||||
|     ); | ||||
| } | ||||
|  | ||||
| #[cfg(feature = "convert")] | ||||
| #[cfg(feature = "pkcs-convert")] | ||||
| #[test] | ||||
| fn rsa_public_to_pem() { | ||||
|     let jwk = JsonWebKey::from_str(RSA_JWK_FIXTURE).unwrap(); | ||||
|     #[rustfmt::skip] | ||||
|     assert_eq!( | ||||
|         jwk.key.to_public().unwrap().to_pem().unwrap(), | ||||
|         jwk.key.to_public().unwrap().to_pem(), | ||||
| "-----BEGIN PUBLIC KEY----- | ||||
| MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKQs23HfZI74Oa3xh3RDFp16OPc2wehS | ||||
| 0PptAdAb1adGqI1VmGtMbowvmT+2YcQcj8cnNj2s8BSSYvYr2f4IEcMCAwEAAQ== | ||||
|   | ||||
| @@ -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 { | ||||
|     use yasna::{ | ||||
|         models::{ObjectIdentifier, TaggedDerValue}, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Nick Hynes
					Nick Hynes