From 44506422e95f28dc1ae8bc7182d8141b29e1c18e Mon Sep 17 00:00:00 2001 From: Philippe Loctaux Date: Sun, 12 Mar 2023 13:59:14 +0100 Subject: [PATCH] jwt: added key rsa key generation, import/export, jwk as PS256 --- Cargo.toml | 2 + crates/id/Cargo.toml | 2 +- crates/id/src/key.rs | 71 ++++++++++++++++++++++++ crates/id/src/lib.rs | 2 + crates/jwt/Cargo.toml | 15 +++++ crates/jwt/src/error.rs | 17 ++++++ crates/jwt/src/jwk.rs | 13 +++++ crates/jwt/src/key.rs | 19 +++++++ crates/jwt/src/key/private.rs | 66 ++++++++++++++++++++++ crates/jwt/src/key/public.rs | 92 +++++++++++++++++++++++++++++++ crates/jwt/src/lib.rs | 11 ++++ crates/jwt/src/token.rs | 25 +++++++++ crates/jwt/tests/private_key.der | Bin 0 -> 1793 bytes crates/jwt/tests/public_key.der | Bin 0 -> 398 bytes 14 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 crates/id/src/key.rs create mode 100644 crates/jwt/Cargo.toml create mode 100644 crates/jwt/src/error.rs create mode 100644 crates/jwt/src/jwk.rs create mode 100644 crates/jwt/src/key.rs create mode 100644 crates/jwt/src/key/private.rs create mode 100644 crates/jwt/src/key/public.rs create mode 100644 crates/jwt/src/lib.rs create mode 100644 crates/jwt/src/token.rs create mode 100644 crates/jwt/tests/private_key.der create mode 100644 crates/jwt/tests/public_key.der diff --git a/Cargo.toml b/Cargo.toml index 891ebae..15fa5d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,5 @@ thiserror = "1" chrono = "0.4.23" sqlx = "0.5.13" url = "2.3.1" +serde = "1" +serde_json = "1" \ No newline at end of file diff --git a/crates/id/Cargo.toml b/crates/id/Cargo.toml index 1157e8b..e94ccff 100644 --- a/crates/id/Cargo.toml +++ b/crates/id/Cargo.toml @@ -7,4 +7,4 @@ edition = "2021" thiserror = { workspace = true } nanoid = "0.4.0" nanoid-dictionary = "0.4.3" -serde = { version = "1", features = ["derive"] } \ No newline at end of file +serde = { workspace = true, features = ["derive"] } diff --git a/crates/id/src/key.rs b/crates/id/src/key.rs new file mode 100644 index 0000000..e9f0e2b --- /dev/null +++ b/crates/id/src/key.rs @@ -0,0 +1,71 @@ +use super::Error; +use nanoid::nanoid; +use nanoid_dictionary::ALPHANUMERIC; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +const LENGTH: usize = 30; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct KeyID(pub String); + +impl Display for KeyID { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Default for KeyID { + fn default() -> Self { + Self(nanoid!(LENGTH, ALPHANUMERIC)) + } +} + +impl AsRef for KeyID { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +impl FromStr for KeyID { + type Err = Error; + + fn from_str(s: &str) -> Result { + if s.len() == LENGTH && s.chars().all(|c| ALPHANUMERIC.contains(&c)) { + Ok(Self(s.to_string())) + } else { + Err(Error::Invalid("Key")) + } + } +} + +#[cfg(test)] +mod tests { + use super::{KeyID, LENGTH}; + use std::str::FromStr; + + #[test] + fn invalid_length() { + assert!(KeyID::from_str("test").is_err()); + } + + #[test] + fn invalid_characters() { + let value = "abcdef!!!!!!@#$%^&*()_++zyxwvq"; + assert_eq!(value.len(), LENGTH); + + assert!(KeyID::from_str(value).is_err()); + } + + #[test] + fn valid() { + let value = "abcdefghijklmnopqrstuvwxyzabcd"; + assert_eq!(value.len(), LENGTH); + let id = KeyID::from_str(value); + assert!(id.is_ok()); + + let id = id.unwrap(); + assert_eq!(id.0, value); + } +} diff --git a/crates/id/src/lib.rs b/crates/id/src/lib.rs index 46e215d..c427f20 100644 --- a/crates/id/src/lib.rs +++ b/crates/id/src/lib.rs @@ -1,3 +1,4 @@ +mod key; mod user; // error @@ -9,4 +10,5 @@ pub enum Error { Invalid(&'static str), } +pub use key::KeyID; pub use user::UserID; diff --git a/crates/jwt/Cargo.toml b/crates/jwt/Cargo.toml new file mode 100644 index 0000000..1a2b7d4 --- /dev/null +++ b/crates/jwt/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "jwt" +version = "0.0.0" +edition = "2021" + +[dependencies] +jwt-compact = { version = "0.6.0", features = ["with_rsa"] } +thiserror = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +rand = "0.8.5" +rsa = "0.7.2" + +# local crates +id = { path = "../id" } diff --git a/crates/jwt/src/error.rs b/crates/jwt/src/error.rs new file mode 100644 index 0000000..ef1bf5f --- /dev/null +++ b/crates/jwt/src/error.rs @@ -0,0 +1,17 @@ +// error +#[derive(thiserror::Error)] +// the rest +#[derive(Debug)] +pub enum Error { + #[error("Failed to generate key: `{0}`")] + Generation(#[from] rsa::errors::Error), + + #[error("Failed to handle private key: `{0}`")] + Private(#[from] rsa::pkcs8::Error), + + #[error("Failed to handle public key: `{0}`")] + Public(#[from] rsa::pkcs1::Error), + + #[error("Failed to parse JWT: `{0}`")] + JwtParsing(#[from] jwt_compact::ParseError), +} diff --git a/crates/jwt/src/jwk.rs b/crates/jwt/src/jwk.rs new file mode 100644 index 0000000..279fb58 --- /dev/null +++ b/crates/jwt/src/jwk.rs @@ -0,0 +1,13 @@ +use jwt_compact::jwk::JsonWebKey as JsonWebKeyBase; +use serde::Serialize; +use std::borrow::Cow; + +#[derive(Debug, Serialize)] +pub struct JsonWebKey<'a> { + #[serde(flatten)] + pub base: JsonWebKeyBase<'a>, + #[serde(rename = "kid")] + pub key_id: &'a str, + #[serde(rename = "alg")] + pub algorithm: Cow<'a, str>, +} diff --git a/crates/jwt/src/key.rs b/crates/jwt/src/key.rs new file mode 100644 index 0000000..29f1365 --- /dev/null +++ b/crates/jwt/src/key.rs @@ -0,0 +1,19 @@ +use crate::Error; +use id::KeyID; +use jwt_compact::alg::{ModulusBits, Rsa}; + +mod private; +mod public; + +pub use private::PrivateKey; +pub use public::PublicKey; + +pub fn generate(id: &KeyID) -> Result<(PrivateKey, PublicKey), Error> { + let mut rng = rand::thread_rng(); + let key_combo = Rsa::generate(&mut rng, ModulusBits::ThreeKibibytes)?; + + let private = PrivateKey::new(id, key_combo.0); + let public = PublicKey::new(id, key_combo.1); + + Ok((private, public)) +} diff --git a/crates/jwt/src/key/private.rs b/crates/jwt/src/key/private.rs new file mode 100644 index 0000000..c85bb62 --- /dev/null +++ b/crates/jwt/src/key/private.rs @@ -0,0 +1,66 @@ +use crate::Error; +use id::KeyID; +use jwt_compact::alg::{RsaPrivateKey, StrongKey}; +use rsa::pkcs8::der::zeroize::Zeroizing; +use rsa::pkcs8::{DecodePrivateKey, EncodePrivateKey}; + +pub struct PrivateKey { + id: KeyID, + key: RsaPrivateKey, +} + +impl PrivateKey { + pub fn new(id: &KeyID, key: StrongKey) -> Self { + Self { + id: id.clone(), + key: key.into_inner(), + } + } + + pub fn to_der(&self) -> Result>, Error> { + Ok(self.key.to_pkcs8_der()?.to_bytes()) + } + + pub fn from_der(der: &[u8], id: &KeyID) -> Result { + Ok(Self { + id: id.clone(), + key: RsaPrivateKey::from_pkcs8_der(der)?, + }) + } +} + +#[cfg(test)] +mod tests { + use crate::PrivateKey; + use id::KeyID; + + #[test] + fn generate() { + let key_id = KeyID::default(); + let keys = crate::generate(&key_id); + assert!(keys.is_ok()); + + let private_key = keys.unwrap().0; + assert_eq!(private_key.id, key_id); + } + + #[test] + fn import_export() { + let key_id = KeyID::default(); + // Import premade key + let premade_der = include_bytes!("../../tests/private_key.der"); + let private_key = PrivateKey::from_der(premade_der, &key_id); + + // Make sure import is ok + assert!(private_key.is_ok()); + let private_key = private_key.unwrap(); + + // Export private key + let exported_der = private_key.to_der(); + assert!(exported_der.is_ok()); + let exported_der = exported_der.unwrap(); + + // Make sure output matches + assert_eq!(premade_der.as_slice(), exported_der.as_slice()); + } +} diff --git a/crates/jwt/src/key/public.rs b/crates/jwt/src/key/public.rs new file mode 100644 index 0000000..1cf928e --- /dev/null +++ b/crates/jwt/src/key/public.rs @@ -0,0 +1,92 @@ +use crate::jwk::JsonWebKey; +use crate::Error; +use id::KeyID; +use jwt_compact::alg::{Rsa, RsaPublicKey, StrongKey}; +use jwt_compact::jwk::JsonWebKey as JsonWebKeyBase; +use jwt_compact::Algorithm; +use rsa::pkcs1::{DecodeRsaPublicKey, EncodeRsaPublicKey}; + +pub struct PublicKey { + id: KeyID, + key: RsaPublicKey, +} + +impl PublicKey { + pub fn new(id: &KeyID, key: StrongKey) -> Self { + Self { + id: id.clone(), + key: key.into_inner(), + } + } + + pub fn to_der(&self) -> Result, Error> { + Ok(self.key.to_pkcs1_der()?.to_vec()) + } + + pub fn from_der(der: &[u8], id: &KeyID) -> Result { + Ok(Self { + id: id.clone(), + key: RsaPublicKey::from_pkcs1_der(der)?, + }) + } + + pub fn jwk(&self) -> JsonWebKey { + JsonWebKey { + base: JsonWebKeyBase::from(&self.key), + key_id: self.id.as_ref(), + algorithm: Rsa::ps256().name(), + } + } +} + +#[cfg(test)] +mod tests { + use crate::PublicKey; + use id::KeyID; + use std::str::FromStr; + + #[test] + fn generate() { + let key_id = KeyID::default(); + let keys = crate::generate(&key_id); + assert!(keys.is_ok()); + + let public_key = keys.unwrap().1; + assert_eq!(public_key.id, key_id); + } + + #[test] + fn import_export() { + let key_id = KeyID::default(); + // Import premade key + let premade_der = include_bytes!("../../tests/public_key.der"); + let public_key = PublicKey::from_der(premade_der, &key_id); + + // Make sure import is ok + assert!(public_key.is_ok()); + let public_key = public_key.unwrap(); + + // Export public key + let exported_der = public_key.to_der(); + assert!(exported_der.is_ok()); + let exported_der = exported_der.unwrap(); + + // Make sure output matches + assert_eq!(premade_der.as_slice(), exported_der.as_slice()); + } + + #[test] + fn jwk() { + let key_id = KeyID::from_str("SgTG8ulMHAp5UsGWuCclw36zWsdEo5").unwrap(); + // Import premade key + let premade_der = include_bytes!("../../tests/public_key.der"); + let public_key = PublicKey::from_der(premade_der, &key_id).unwrap(); + + let generated_jwk = serde_json::to_string(&public_key.jwk()); + assert!(generated_jwk.is_ok()); + + let jwk = r#"{"kty":"RSA","n":"3os0j_kNfdTHJVQ-eMYXyRBWIqsrJDdELxLAh3_WlOZtsBwiGVNnpHQm9cRB63Un9UJpYGbWz38emglXc8bHPrUArDl-K_5ioDlbh7hAaz3rZ6b8LDIPUO-jYICdxBdv1THXSWbTEistZF1TYsXg7G4xrxiFKnZLBNaSgJrKOAY8AUNWuby-vZKr5X9e3SG7kvPsUITyqSmDz4ZTCj4QScx4O9gyqz1_UEBxTRSKcpS82YzAo2Byo5avRWesiGoaxs8lNv0QJ22IY1KVoROv3hHFeFEcg3D4NTfFG2Cd8d1OMXfILhtFnQZbt5ZxIG9SCOfirn32-9OtoLemKlgSq0gbLf6t1OK12LK6mIJ78pphlnhHdvHeJ75PV6c2lS2Wwd75NYBJzhIojG4U4Lbpe7T_NDFaxExry_7V5oxX8tbb-OzJnuPOQRR0H5uOBjdVo7i5vjnDKOTDpro3XPQjBbIBkABhDdU2FcXkEbl8_ByyYZZni7ekzGrVSJB_vxvv","e":"AQAB","kid":"SgTG8ulMHAp5UsGWuCclw36zWsdEo5","alg":"PS256"}"#; + let generated_jwk = generated_jwk.unwrap(); + assert_eq!(jwk, generated_jwk); + } +} diff --git a/crates/jwt/src/lib.rs b/crates/jwt/src/lib.rs new file mode 100644 index 0000000..b6241fa --- /dev/null +++ b/crates/jwt/src/lib.rs @@ -0,0 +1,11 @@ +extern crate core; + +mod error; +mod jwk; +mod key; +mod token; + +/// Exports +pub use error::Error; +pub use key::generate; +pub use key::{PrivateKey, PublicKey}; diff --git a/crates/jwt/src/token.rs b/crates/jwt/src/token.rs new file mode 100644 index 0000000..a683fc6 --- /dev/null +++ b/crates/jwt/src/token.rs @@ -0,0 +1,25 @@ +use crate::Error; +use jwt_compact::UntrustedToken; + +pub fn parse(token: &str) -> Result { + Ok(UntrustedToken::new(token)?) +} + +#[cfg(test)] +mod tests { + #[test] + fn parse_valid() { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; + let token = super::parse(jwt); + + assert!(token.is_ok()); + } + + #[test] + fn parse_invalid() { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; + let token = super::parse(jwt); + + assert!(token.is_err()); + } +} diff --git a/crates/jwt/tests/private_key.der b/crates/jwt/tests/private_key.der new file mode 100644 index 0000000000000000000000000000000000000000..ff0c311fdb7591426e7d3935ce6a7257d1c5b439 GIT binary patch literal 1793 zcmXqLV*AU)$Y8+B#;Mij(e|B}k&%&=fu)Jr(ZvPMHa5*Us0=&EgC% zK6c!0E5jPgI_-Z+3oN7CcQ|C*zD{5EN5_ai;Qiu+hPg+?^RF6S_e{Giq^+A08=Q3X z!J9n8^%AXGW!@~;CN<1DWx-~{=p43l&%V8rRzIzeyQ{c+(&sk;EuU6uHlJ?`=CTv; zJX2wP!)Uc_eSkxuuSi$Xlsz|l4lGV6T0CvNYxv0QKI;kpbnW(!Hz(&kKIbS>B0sy2%{+ARj-C4~4{JO*ylj_w%okD}9xoXNWCF`<6H^m`^|Mg~TvCdP&!#T~q9lgwq~?N}D1 z?upZRoI7p(>r`o<>isNVZAE`>(DgM~a+C9uxXq*o$*wYYIHfG)6Q6HN-28Wr>y_Zm zp5M#o@B1Ljyt4h+2A8Vtq(vVW*md6bYzfiWza!Dita-ZM?yU@7$-7&k1o>y=-g)kD zXrWKv-ANZ1*b-0g7Jt~WImm4>)D-Du&d&2Fs+9UfdU2|s*zui1CMM9XF87@wbJ z*%$Z!NOej4b>~Fi7`KSPpjz)80+(;h_6;hD|9XmTo5V(5>1j1FSFejNdlqcV%HPUj zc24?{7RS{B%ihaEP3=dujTSvvl^oY`yY=@9&3})V9SjQjJmYyf)4%3%{ZrsIIZs8+qpocQl#i=1uC4`X#t*v2BW8?{6QyqGKQA|32`&xS6#`W|vCZ^*vz@ zS<9KkrbfqY%4W`1IB_n(dBVOWTzfB;tUi2kf`(GK#nE>+)~(8)ylc+~W&VjTMVoo< z)csE5S?5|E8ldNRZ&`I~XL_@2QtH9;H|-^v8V@o&oV4bsa@*~m8y~dJv*u=9x6aPh z)fL>99`pE|LrZK?^tn$-`!D+y*u5?M81!UA-|ZvO*4az4`(+%Dmaa2+tIphXJJ{^F zV1VrT&biJ4iQN{>8{Kr9;sld^cFw!rGe6g&J9X31oXfA;o_97Zn6Z7Lo9XE_%S$x5 znEM~r<*iO`F)!1Tm3nqkdCP`ZJq*W$7!Yv-(RRv@z4c7z znxZ}DsXZ(Q=Ihxi-EVZi{8|4%Q}wr=!yrHW>3@6ql&+`>a#}o^cCd3< z;;xGmHG>T&rp<_7b|=+%HqVMb!Yz_>Zoj!~V|_(YbW^zG)w%$8XURshU)ekK{cemwhpN?G=NrK9$NG$>)0>v^WWaz zl=W<3Vt6w7tj%Y+yBlx*Hd%G<@~Q09^SmY*bAuSAeiWzbDJ;{gswg?m_w|IK$f~yO z37T7%9R3n|=}~|2Wu~xQ6=lzMSGcA`oV=dF5pBlyxPMae|C_-#J_=o_IV-rSZSslA z;HNM5C z3zb@AzMt{ls!5xbBR;+@Y79M*42p(RYA4S&n;+~-z5eE;Q0fF7@i~_TyGQr literal 0 HcmV?d00001 diff --git a/crates/jwt/tests/public_key.der b/crates/jwt/tests/public_key.der new file mode 100644 index 0000000000000000000000000000000000000000..1fc360f1adbd6e713f7eeb7f5912a0c4dc42afdf GIT binary patch literal 398 zcmXqLV(emSVr*o%*KN}OlehNDan%sJieus@1;Uh8Ypa;M=nEZaufI0sS?&fICCT9Q zB_(QKk2tYf^ksi^jQ9iI zFHUHfdqh0{s^N9dw97);x+$^2Nk<>N$unFp(W+JE&2nv0!>m&lY&ML}VLSKi+dFCX z)B3o(in}L$eiP90X{BcK`LGGI28JdbQMk6bF=5b;)J5b)7HDD zuj$B=I(A;w?5}`&Zbx#^)P=(9?+G5Q2$X3q_+e^(R61eq$Gd)pi0{( KXJTe#U<3fYhQT-h literal 0 HcmV?d00001