Skip to content

Commit

Permalink
Merge pull request #9 from robjsliwa/torc
Browse files Browse the repository at this point in the history
Migrate from jsonwebtoken to jwt-rustcrypto and Miscellaneous Improvements
  • Loading branch information
robjsliwa authored Oct 21, 2024
2 parents d7292f1 + 2516a82 commit 1a0612f
Show file tree
Hide file tree
Showing 19 changed files with 222 additions and 308 deletions.
35 changes: 31 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ name: Rust
on:
push:
branches: [ "main" ]
tags:
- 'release_v*'
pull_request:
branches: [ "main" ]

Expand All @@ -16,11 +18,36 @@ jobs:

steps:
- uses: actions/checkout@v3
- name: Install wasm32 target
run: rustup target add wasm32-unknown-unknown
- name: Check code formatting
run: cargo fmt
- name: Run linter
run: cargo clippy
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- name: Build without Ring
run: cargo build --no-default-features --features "noring"
- name: Run tests without Ring
run: cargo test --no-default-features --features "noring"
- name: Build WASM
run: cargo build --target wasm32-unknown-unknown --release

publish:

if: startsWith(github.ref, 'refs/tags/release_v')

runs-on: ubuntu-latest
needs: build

steps:
- uses: actions/checkout@v4

- name: Install stable Rust
run: rustup install stable

- name: Verify cargo package
run: cargo package

- name: Publish to crates.io
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
run: cargo publish
18 changes: 4 additions & 14 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "sdjwt"
version = "0.7.3"
version = "0.8.0"
authors = ["Rob Sliwa <robjsliwa@gmail.com>"]
license = "MIT"
readme = "README.md"
Expand All @@ -17,19 +17,9 @@ serde_yaml = "0.9.30"
thiserror = "1.0.51"
rand = "0.8.5"
base64 = "0.21.5"
sha2 = "0.10.8"
chrono = "0.4.31"
rsa = "0.8"
getrandom = { version = "0.2.12", features = ["js"] }

jsonwebtoken-rustcrypto = { version = "1.2.0", optional = true }
serde_plain = { version = "1.0.2", optional = true }

jsonwebtoken = { version = "9.2.0", optional = true }

[features]
default = ["ring"]
ring = ["jsonwebtoken"]
noring = ["jsonwebtoken-rustcrypto", "serde_plain"]
sha2 = "0.10.8"
jwt-rustcrypto = "0.2.0"
rsa = "0.9.6"

[dev-dependencies]
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ Add the following to Cargo.toml:
sdjwt = "0.7.0"
```

# Supported Algorithms

The library supports the following signing and verifying algorithms:

- **HMAC**: `HS256`, `HS384`, `HS512`
- **RSA**: `RS256`, `RS384`, `RS512`, `PS256`, `PS384`, `PS512`
- **ECDSA**: `ES256`, `ES256K`, `ES384`, `ES512`

# How to use

There are three use cases: Issuer, Holder, and Verifier.
Expand Down
3 changes: 2 additions & 1 deletion src/algorithm/algorithm.rs → src/algorithm/algorithms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ pub enum Algorithm {
HS384,
HS512,
ES256,
ES256K,
ES384,
ES512,
#[default]
RS256,
RS384,
RS512,
PS256,
PS384,
PS512,
EdDSA,
}
4 changes: 2 additions & 2 deletions src/algorithm/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
pub mod algorithm;
pub mod algorithms;
pub mod hashalgorithm;

pub use algorithm::*;
pub use algorithms::*;
pub use hashalgorithm::*;
148 changes: 47 additions & 101 deletions src/decoding.rs
Original file line number Diff line number Diff line change
@@ -1,104 +1,70 @@
use crate::{Algorithm, Error, Validation};
#[cfg(feature = "noring")]
use base64::Engine;
#[cfg(feature = "ring")]
use jsonwebtoken::{
decode as jwt_decode, Algorithm as JwtAlgorithm, DecodingKey, Validation as JwtValidation,
use jwt_rustcrypto::{
decode as jwt_decode, Algorithm as JwtAlgorithm, ValidationOptions as JwtValidation,
VerifyingKey,
};

#[cfg(feature = "noring")]
use jsonwebtoken_rustcrypto::{
decode as jwt_decode, Algorithm as JwtAlgorithm, DecodingKey, Validation as JwtValidation,
};
#[cfg(feature = "noring")]
use rsa::PublicKeyParts;
#[cfg(feature = "noring")]
use rsa::{pkcs1::DecodeRsaPublicKey, RsaPublicKey};

use serde_json::Value;

#[derive(Clone)]
pub struct KeyForDecoding {
key: DecodingKey,
key: VerifyingKey,
}

impl KeyForDecoding {
#[cfg(feature = "ring")]
pub fn from_secret(secret: &[u8]) -> Self {
KeyForDecoding {
key: DecodingKey::from_secret(secret),
key: VerifyingKey::from_secret(secret),
}
}

#[cfg(feature = "ring")]
pub fn from_base64_secret(secret: &str) -> Result<Self, Error> {
Ok(KeyForDecoding {
key: DecodingKey::from_base64_secret(secret)?,
key: VerifyingKey::from_base64_secret(secret)?,
})
}

#[cfg(feature = "ring")]
pub fn from_rsa_pem(key: &[u8]) -> Result<Self, Error> {
Ok(KeyForDecoding {
key: DecodingKey::from_rsa_pem(key)?,
key: VerifyingKey::from_rsa_pem(key)?,
})
}

#[cfg(feature = "noring")]
pub fn from_rsa_pem(key: &[u8]) -> Result<Self, Error> {
let rsa_key = RsaPublicKey::from_pkcs1_pem(std::str::from_utf8(key)?)?;

let modulus =
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(rsa_key.n().to_bytes_be());
let exponent =
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(rsa_key.e().to_bytes_be());

Self::from_rsa_components(&modulus, &exponent)
}

pub fn from_rsa_components(modulus: &str, exponent: &str) -> Result<Self, Error> {
pub fn from_rsa_components(modulus: &[u8], exponent: &[u8]) -> Result<Self, Error> {
Ok(KeyForDecoding {
key: DecodingKey::from_rsa_components(modulus, exponent)?,
key: VerifyingKey::from_rsa_components(modulus, exponent)?,
})
}

#[cfg(feature = "ring")]
pub fn from_ec_pem(key: &[u8]) -> Result<Self, Error> {
Ok(KeyForDecoding {
key: DecodingKey::from_ec_pem(key)?,
key: VerifyingKey::from_ec_pem(key)?,
})
}

#[cfg(feature = "ring")]
pub fn from_ed_pem(key: &[u8]) -> Result<Self, Error> {
// pub fn from_ed_pem(key: &[u8]) -> Result<Self, Error> {
// Ok(KeyForDecoding {
// key: DecodingKey::from_ed_pem(key)?,
// })
// }

pub fn from_rsa_der(der: &[u8]) -> Result<Self, Error> {
Ok(KeyForDecoding {
key: DecodingKey::from_ed_pem(key)?,
key: VerifyingKey::from_rsa_der(der)?,
})
}

#[cfg(feature = "ring")]
pub fn from_rsa_der(der: &[u8]) -> Self {
KeyForDecoding {
key: DecodingKey::from_rsa_der(der),
}
}

#[cfg(feature = "ring")]
pub fn from_ec_der(der: &[u8]) -> Self {
KeyForDecoding {
key: DecodingKey::from_ec_der(der),
}
pub fn from_ec_der(der: &[u8]) -> Result<Self, Error> {
Ok(KeyForDecoding {
key: VerifyingKey::from_ec_der(der)?,
})
}

#[cfg(feature = "ring")]
pub fn from_ed_der(der: &[u8]) -> Self {
KeyForDecoding {
key: DecodingKey::from_ed_der(der),
}
pub fn from_ed_der(der: &[u8]) -> Result<Self, Error> {
Ok(KeyForDecoding {
key: VerifyingKey::from_ed_der(der)?,
})
}
}

#[cfg(feature = "ring")]
fn build_validation(validation: &Validation) -> JwtValidation {
let mut valid = JwtValidation::new(match validation.algorithms {
Algorithm::HS256 => JwtAlgorithm::HS256,
Expand All @@ -108,48 +74,20 @@ fn build_validation(validation: &Validation) -> JwtValidation {
Algorithm::RS384 => JwtAlgorithm::RS384,
Algorithm::RS512 => JwtAlgorithm::RS512,
Algorithm::ES256 => JwtAlgorithm::ES256,
Algorithm::ES256K => JwtAlgorithm::ES256K,
Algorithm::ES384 => JwtAlgorithm::ES384,
Algorithm::ES512 => JwtAlgorithm::ES512,
Algorithm::PS256 => JwtAlgorithm::PS256,
Algorithm::PS384 => JwtAlgorithm::PS384,
Algorithm::PS512 => JwtAlgorithm::PS512,
Algorithm::EdDSA => JwtAlgorithm::EdDSA,
});

valid.required_spec_claims = validation.required_spec_claims.clone();
valid.required_claims = validation.required_spec_claims.clone();
valid.leeway = validation.leeway;
valid.validate_exp = validation.validate_exp;
valid.validate_nbf = validation.validate_nbf;
valid.validate_aud = validation.validate_aud;
valid.aud = validation.aud.clone();
valid.iss = validation.iss.clone();
valid.sub = validation.sub.clone();
valid
}

#[cfg(feature = "noring")]
fn build_validation(validation: &Validation) -> JwtValidation {
let mut valid = JwtValidation::new(match validation.algorithms {
Algorithm::HS256 => JwtAlgorithm::HS256,
Algorithm::HS384 => JwtAlgorithm::HS384,
Algorithm::HS512 => JwtAlgorithm::HS512,
Algorithm::RS256 => JwtAlgorithm::RS256,
Algorithm::RS384 => JwtAlgorithm::RS384,
Algorithm::RS512 => JwtAlgorithm::RS512,
Algorithm::ES256 => JwtAlgorithm::ES256,
Algorithm::ES384 => JwtAlgorithm::ES384,
Algorithm::PS256 => JwtAlgorithm::PS256,
Algorithm::PS384 => JwtAlgorithm::PS384,
Algorithm::PS512 => JwtAlgorithm::PS512,
// Algorithm::EdDSA => JwtAlgorithm::EdDSA,
_ => unimplemented!(),
});

valid.leeway = validation.leeway;
valid.validate_exp = validation.validate_exp;
valid.validate_nbf = validation.validate_nbf;
valid.aud = validation.aud.clone();
valid.iss = validation.iss.clone();
valid.sub = validation.sub.clone();
valid.audiences = validation.aud.clone();
valid.issuer = validation.iss.clone();
valid
}

Expand All @@ -161,7 +99,7 @@ pub fn decode(
let validation = build_validation(validation);
let token_data = jwt_decode(token, &key.key, &validation)?;
let header: Value = serde_json::from_str(&serde_json::to_string(&token_data.header)?)?;
Ok((header, token_data.claims))
Ok((header, token_data.payload))
}

pub fn sd_jwt_parts(serialized_jwt: &str) -> (String, Vec<String>, Option<String>) {
Expand All @@ -187,10 +125,12 @@ pub fn sd_jwt_parts(serialized_jwt: &str) -> (String, Vec<String>, Option<String
mod tests {
use super::*;
use chrono::{Duration, Utc};
use rand::rngs::OsRng;
use rsa::pkcs1::EncodeRsaPublicKey;
use rsa::pkcs1v15::SigningKey;
use rsa::pkcs8::EncodePrivateKey;
use rsa::pkcs8::EncodePublicKey;
use rsa::signature::Keypair;
use rsa::{RsaPrivateKey, RsaPublicKey};
use sha2::Sha256;

const TEST_CLAIMS: &str = r#"{
"sub": "user_42",
Expand All @@ -214,28 +154,34 @@ mod tests {
}"#;

fn keys() -> (RsaPrivateKey, RsaPublicKey) {
let mut rng = OsRng;
let mut rng = rand::thread_rng();

let bits = 2048;
let private_key = RsaPrivateKey::new(&mut rng, bits).unwrap();
let public_key = RsaPublicKey::from(&private_key);
let private_key = RsaPrivateKey::new(&mut rng, bits).expect("failed to generate a key");
let signing_key = SigningKey::<Sha256>::new(private_key.clone());
let public_key = signing_key.verifying_key().into();

(private_key, public_key)
}

fn convert_to_pem(private_key: RsaPrivateKey, public_key: RsaPublicKey) -> (String, String) {
(
private_key
.to_pkcs8_pem(rsa::pkcs8::LineEnding::CR)
.to_pkcs8_pem(rsa::pkcs8::LineEnding::LF)
.unwrap()
.to_string(),
public_key.to_pkcs1_pem(rsa::pkcs1::LineEnding::CR).unwrap(),
public_key
.to_public_key_pem(rsa::pkcs1::LineEnding::LF)
.unwrap(),
)
}

#[test]
fn test_basic_decode() -> Result<(), Error> {
let (priv_key, pub_key) = keys();
let (issuer_private_key, issuer_public_key) = convert_to_pem(priv_key, pub_key);
println!("issuer_private_key: {:?}", issuer_private_key);
println!("issuer_public_key: {:?}", issuer_public_key);
let mut claims: Value = serde_json::from_str(TEST_CLAIMS).unwrap();
let now = Utc::now();
let expiration = now + Duration::minutes(5);
Expand Down
7 changes: 3 additions & 4 deletions src/decoy.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::algorithm::{base64_hash, HashAlgorithm};
use crate::Error;
use rand::{Rng, SeedableRng, rngs::StdRng};
use rand::{rngs::StdRng, Rng, SeedableRng};

#[derive(Debug, Clone)]
pub struct Decoy {
Expand All @@ -18,7 +18,7 @@ impl Decoy {
}
}

pub fn build(self) -> Result<Decoy,Error> {
pub fn build(self) -> Result<Decoy, Error> {
let seed: [u8; 32] = rand::random();
let mut rng = StdRng::from_seed(seed);
let random_number: u32 = rng.gen();
Expand Down Expand Up @@ -58,5 +58,4 @@ mod tests {
let decoy2 = Decoy::new().build().unwrap();
assert_ne!(decoy1.digest(), decoy2.digest());
}

}
}
Loading

0 comments on commit 1a0612f

Please sign in to comment.