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