Compare commits

...

5 Commits

Author SHA1 Message Date
4984027a29 Refactor API
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-04-20 13:48:00 +02:00
4e8797d68d Improve API 2024-04-20 13:36:41 +02:00
9f56be95de Add Renovate configuration
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-20 13:24:36 +02:00
21c7c94b10 Add Drone configuration
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-20 13:21:05 +02:00
e947417b0b Update crate metadata 2024-04-20 13:19:52 +02:00
6 changed files with 129 additions and 58 deletions

16
.drone.yml Normal file
View File

@@ -0,0 +1,16 @@
---
kind: pipeline
type: docker
name: default
steps:
- name: cargo_check
image: rust
commands:
- rustup component add clippy
- cargo clippy --all-features -- -D warnings
- cargo clippy -- -D warnings
- cargo test --all-features

2
Cargo.lock generated
View File

@@ -40,7 +40,7 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]] [[package]]
name = "basic-jwt" name = "basic-jwt"
version = "0.1.0" version = "0.2.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"elliptic-curve", "elliptic-curve",

View File

@@ -1,7 +1,13 @@
[package] [package]
name = "basic-jwt" name = "basic-jwt"
version = "0.1.0" version = "0.2.0"
edition = "2021" edition = "2021"
authors = ["Pierre Hubert <pierre.git@communiquons.org>"]
description = "Basic JWT signing and verification library"
license = "MIT"
repository = "https://gitea.communiquons.org/pierre/basic-jwt"
keywords = ["jwt"]
readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -9,6 +15,6 @@ edition = "2021"
rand = "0.8.5" rand = "0.8.5"
serde = { version = "1.0.198", features = ["derive"] } serde = { version = "1.0.198", features = ["derive"] }
anyhow = "1.0.82" anyhow = "1.0.82"
elliptic-curve = { version = "0.13.8", features = ["pkcs8","pem" ] } elliptic-curve = { version = "0.13.8", features = ["pkcs8", "pem"] }
p384 = { version = "0.13.0", features = ["ecdsa", "pkcs8", "pem"] } p384 = { version = "0.13.0", features = ["ecdsa", "pkcs8", "pem"] }
jsonwebtoken = "9.3.0" jsonwebtoken = "9.3.0"

View File

@@ -10,13 +10,16 @@ Basic usage:
```rust ```rust
let claims = ...; // note : claims must be serializable let claims = ...; // note : claims must be serializable
// Generate a key pair. Public and private key are both serializable // Generate a key pair. Private and public key are both serializable
let (pub_key, priv_key) = generate_ec384_keypair().unwrap(); let priv_key = JWTPrivateKey::generate_ec384_signing_key().unwrap();
let pub_key = priv_key.to_public_key().unwrap();
// Create a JWT for the given claims (note: standard claims: sub, iss, ...) are not // Create a JWT for the given claims (note: standard claims: sub, iss, ...) are not
// automatically added if they are missing // automatically added if they are missing
let jwt = sign_jwt(&priv_key, &claims).expect("Failed to sign JWT!"); let jwt = priv_key.sign_jwt(&claims).expect("Failed to sign JWT!");
// Validate signed JWT // Validate signed JWT
let claims_out = validate_jwt::<Claims>(&pub_key, &jwt).expect("Failed to validate JWT!"); let claims_out = pub_key
.validate_jwt::<Claims>(&jwt)
.expect("Failed to validate JWT!");
``` ```

10
renovate.json Normal file
View File

@@ -0,0 +1,10 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"packageRules": [
{
"matchUpdateTypes": ["major", "minor", "patch"],
"automerge": true
}
]
}

View File

@@ -5,41 +5,54 @@ use p384::pkcs8::{EncodePrivateKey, LineEnding};
use rand::rngs::OsRng; use rand::rngs::OsRng;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
use std::str::FromStr;
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Eq, PartialEq)] #[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Eq, PartialEq)]
#[serde(tag = "alg")] #[serde(tag = "alg")]
pub enum TokenPubKey { pub enum JWTPublicKey {
/// ECDSA with SHA2-384 variant /// ECDSA with SHA2-384 variant
ES384 { r#pub: String }, ES384 {
#[serde(rename = "pub")]
public: String,
},
} }
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
#[serde(tag = "alg")] #[serde(tag = "alg")]
pub enum TokenPrivKey { pub enum JWTPrivateKey {
ES384 { r#priv: String }, ES384 { r#priv: String },
} }
/// Generate a new ES384 keypair impl JWTPrivateKey {
pub fn generate_ec384_keypair() -> anyhow::Result<(TokenPubKey, TokenPrivKey)> { /// Generate a new ES384 signing key
pub fn generate_ec384_signing_key() -> anyhow::Result<Self> {
let signing_key = SigningKey::random(&mut OsRng); let signing_key = SigningKey::random(&mut OsRng);
let priv_pem = signing_key let priv_pem = signing_key
.to_pkcs8_der()? .to_pkcs8_der()?
.to_pem("PRIVATE KEY", LineEnding::LF)? .to_pem("PRIVATE KEY", LineEnding::LF)?
.to_string(); .to_string();
Ok(Self::ES384 { r#priv: priv_pem })
}
/// Get associated public key
pub fn to_public_key(&self) -> anyhow::Result<JWTPublicKey> {
match self {
JWTPrivateKey::ES384 { r#priv } => {
let signing_key = SigningKey::from_str(r#priv)?;
let pub_key = VerifyingKey::from(signing_key); let pub_key = VerifyingKey::from(signing_key);
let pub_pem = pub_key.to_public_key_pem(LineEnding::LF)?; let pub_pem = pub_key.to_public_key_pem(LineEnding::LF)?;
Ok(( Ok(JWTPublicKey::ES384 { public: pub_pem })
TokenPubKey::ES384 { r#pub: pub_pem }, }
TokenPrivKey::ES384 { r#priv: priv_pem }, }
)) }
}
/// Sign JWT with a private key /// Sign a JWT
pub fn sign_jwt<C: Serialize>(key: &TokenPrivKey, claims: &C) -> anyhow::Result<String> { pub fn sign_jwt<C: Serialize>(&self, claims: &C) -> anyhow::Result<String> {
match key { match self {
TokenPrivKey::ES384 { r#priv } => { JWTPrivateKey::ES384 { r#priv } => {
let encoding_key = EncodingKey::from_ec_pem(r#priv.as_bytes())?; let encoding_key = EncodingKey::from_ec_pem(r#priv.as_bytes())?;
Ok(jsonwebtoken::encode( Ok(jsonwebtoken::encode(
@@ -49,25 +62,28 @@ pub fn sign_jwt<C: Serialize>(key: &TokenPrivKey, claims: &C) -> anyhow::Result<
)?) )?)
} }
} }
}
} }
/// Validate a given JWT impl JWTPublicKey {
pub fn validate_jwt<E: DeserializeOwned>(key: &TokenPubKey, token: &str) -> anyhow::Result<E> { /// Validate a given JWT
match key { pub fn validate_jwt<E: DeserializeOwned>(&self, jwt: &str) -> anyhow::Result<E> {
TokenPubKey::ES384 { r#pub } => { match self {
let decoding_key = DecodingKey::from_ec_pem(r#pub.as_bytes())?; JWTPublicKey::ES384 { public } => {
let decoding_key = DecodingKey::from_ec_pem(public.as_bytes())?;
let validation = Validation::new(Algorithm::ES384); let validation = Validation::new(Algorithm::ES384);
Ok(jsonwebtoken::decode::<E>(token, &decoding_key, &validation)?.claims) Ok(jsonwebtoken::decode::<E>(jwt, &decoding_key, &validation)?.claims)
}
} }
} }
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{generate_ec384_keypair, sign_jwt, validate_jwt};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use crate::JWTPrivateKey;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
fn time() -> u64 { fn time() -> u64 {
@@ -94,47 +110,67 @@ mod test {
#[test] #[test]
fn jwt_encode_sign_verify_valid() { fn jwt_encode_sign_verify_valid() {
let (pub_key, priv_key) = generate_ec384_keypair().unwrap(); let priv_key = JWTPrivateKey::generate_ec384_signing_key().unwrap();
let pub_key = priv_key.to_public_key().unwrap();
let claims = Claims::default(); let claims = Claims::default();
let jwt = sign_jwt(&priv_key, &claims).expect("Failed to sign JWT!"); let jwt = priv_key.sign_jwt(&claims).expect("Failed to sign JWT!");
let claims_out = validate_jwt::<Claims>(&pub_key, &jwt).expect("Failed to validate JWT!"); let claims_out = pub_key
.validate_jwt::<Claims>(&jwt)
.expect("Failed to validate JWT!");
assert_eq!(claims, claims_out) assert_eq!(claims, claims_out)
} }
#[test] #[test]
fn jwt_encode_sign_verify_invalid_key() { fn jwt_encode_sign_verify_invalid_key() {
let (_pub_key, priv_key) = generate_ec384_keypair().unwrap(); let priv_key = JWTPrivateKey::generate_ec384_signing_key().unwrap();
let (pub_key_2, _priv_key_2) = generate_ec384_keypair().unwrap(); let pub_key_2 = JWTPrivateKey::generate_ec384_signing_key()
.unwrap()
.to_public_key()
.unwrap();
let claims = Claims::default(); let claims = Claims::default();
let jwt = sign_jwt(&priv_key, &claims).expect("Failed to sign JWT!"); let jwt = priv_key.sign_jwt(&claims).expect("Failed to sign JWT!");
validate_jwt::<Claims>(&pub_key_2, &jwt).expect_err("JWT should not have validated!"); pub_key_2
.validate_jwt::<Claims>(&jwt)
.expect_err("JWT should not have validated!");
} }
#[test] #[test]
fn jwt_verify_random_string() { fn jwt_verify_random_string() {
let (pub_key, _priv_key) = generate_ec384_keypair().unwrap(); let priv_key = JWTPrivateKey::generate_ec384_signing_key().unwrap();
validate_jwt::<Claims>(&pub_key, "random_string") let pub_key = priv_key.to_public_key().unwrap();
pub_key
.validate_jwt::<Claims>("random_string")
.expect_err("JWT should not have validated!"); .expect_err("JWT should not have validated!");
} }
#[test] #[test]
fn jwt_expired() { fn jwt_expired() {
let (pub_key, priv_key) = generate_ec384_keypair().unwrap(); let priv_key = JWTPrivateKey::generate_ec384_signing_key().unwrap();
let pub_key = priv_key.to_public_key().unwrap();
let claims = Claims { let claims = Claims {
exp: time() - 100, exp: time() - 100,
..Default::default() ..Default::default()
}; };
let jwt = sign_jwt(&priv_key, &claims).expect("Failed to sign JWT!"); let jwt = priv_key.sign_jwt(&claims).expect("Failed to sign JWT!");
validate_jwt::<Claims>(&pub_key, &jwt).expect_err("JWT should not have validated!"); pub_key
.validate_jwt::<Claims>(&jwt)
.expect_err("JWT should not have validated!");
} }
#[test] #[test]
fn jwt_invalid_signature() { fn jwt_invalid_signature() {
let (pub_key, priv_key) = generate_ec384_keypair().unwrap(); let priv_key = JWTPrivateKey::generate_ec384_signing_key().unwrap();
let pub_key = priv_key.to_public_key().unwrap();
let claims = Claims::default(); let claims = Claims::default();
let jwt = sign_jwt(&priv_key, &claims).expect("Failed to sign JWT!"); let jwt = priv_key.sign_jwt(&claims).expect("Failed to sign JWT!");
validate_jwt::<Claims>(&pub_key, &format!("{jwt}bad")) pub_key
.validate_jwt::<Claims>(&format!("{jwt}bad"))
.expect_err("JWT should not have validated!"); .expect_err("JWT should not have validated!");
} }
} }