jwt: added key rsa key generation, import/export, jwk as PS256

This commit is contained in:
Philippe Loctaux 2023-03-12 13:59:14 +01:00
parent e1ec84f7c6
commit 44506422e9
14 changed files with 334 additions and 1 deletions

View file

@ -9,3 +9,5 @@ thiserror = "1"
chrono = "0.4.23" chrono = "0.4.23"
sqlx = "0.5.13" sqlx = "0.5.13"
url = "2.3.1" url = "2.3.1"
serde = "1"
serde_json = "1"

View file

@ -7,4 +7,4 @@ edition = "2021"
thiserror = { workspace = true } thiserror = { workspace = true }
nanoid = "0.4.0" nanoid = "0.4.0"
nanoid-dictionary = "0.4.3" nanoid-dictionary = "0.4.3"
serde = { version = "1", features = ["derive"] } serde = { workspace = true, features = ["derive"] }

71
crates/id/src/key.rs Normal file
View file

@ -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<str> for KeyID {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl FromStr for KeyID {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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);
}
}

View file

@ -1,3 +1,4 @@
mod key;
mod user; mod user;
// error // error
@ -9,4 +10,5 @@ pub enum Error {
Invalid(&'static str), Invalid(&'static str),
} }
pub use key::KeyID;
pub use user::UserID; pub use user::UserID;

15
crates/jwt/Cargo.toml Normal file
View file

@ -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" }

17
crates/jwt/src/error.rs Normal file
View file

@ -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),
}

13
crates/jwt/src/jwk.rs Normal file
View file

@ -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>,
}

19
crates/jwt/src/key.rs Normal file
View file

@ -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))
}

View file

@ -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<RsaPrivateKey>) -> Self {
Self {
id: id.clone(),
key: key.into_inner(),
}
}
pub fn to_der(&self) -> Result<Zeroizing<Vec<u8>>, Error> {
Ok(self.key.to_pkcs8_der()?.to_bytes())
}
pub fn from_der(der: &[u8], id: &KeyID) -> Result<Self, Error> {
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());
}
}

View file

@ -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<RsaPublicKey>) -> Self {
Self {
id: id.clone(),
key: key.into_inner(),
}
}
pub fn to_der(&self) -> Result<Vec<u8>, Error> {
Ok(self.key.to_pkcs1_der()?.to_vec())
}
pub fn from_der(der: &[u8], id: &KeyID) -> Result<Self, Error> {
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);
}
}

11
crates/jwt/src/lib.rs Normal file
View file

@ -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};

25
crates/jwt/src/token.rs Normal file
View file

@ -0,0 +1,25 @@
use crate::Error;
use jwt_compact::UntrustedToken;
pub fn parse(token: &str) -> Result<UntrustedToken, Error> {
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());
}
}

Binary file not shown.

Binary file not shown.