jwt: added key rsa key generation, import/export, jwk as PS256
This commit is contained in:
parent
e1ec84f7c6
commit
44506422e9
14 changed files with 334 additions and 1 deletions
|
|
@ -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"
|
||||||
|
|
@ -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
71
crates/id/src/key.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
15
crates/jwt/Cargo.toml
Normal 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
17
crates/jwt/src/error.rs
Normal 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
13
crates/jwt/src/jwk.rs
Normal 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
19
crates/jwt/src/key.rs
Normal 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))
|
||||||
|
}
|
||||||
66
crates/jwt/src/key/private.rs
Normal file
66
crates/jwt/src/key/private.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
92
crates/jwt/src/key/public.rs
Normal file
92
crates/jwt/src/key/public.rs
Normal 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
11
crates/jwt/src/lib.rs
Normal 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
25
crates/jwt/src/token.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
crates/jwt/tests/private_key.der
Normal file
BIN
crates/jwt/tests/private_key.der
Normal file
Binary file not shown.
BIN
crates/jwt/tests/public_key.der
Normal file
BIN
crates/jwt/tests/public_key.der
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue