Skip to content

Commit

Permalink
Merge pull request #11 from robjsliwa/wasmbindgen
Browse files Browse the repository at this point in the history
Exposing SD-JWT Functionality for WebAssembly
  • Loading branch information
robjsliwa authored Oct 30, 2024
2 parents e7f1c04 + 89a19f0 commit 60954a8
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 18 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/target
/Cargo.lock
.vscode/
.vscode/
/pkg
/.cargo
12 changes: 9 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "sdjwt"
version = "0.8.0"
version = "0.8.1"
authors = ["Rob Sliwa <robjsliwa@gmail.com>"]
license = "MIT"
readme = "README.md"
Expand All @@ -19,7 +19,13 @@ rand = "0.8.5"
base64 = "0.21.5"
chrono = "0.4.31"
sha2 = "0.10.8"
jwt-rustcrypto = "0.2.0"
jwt-rustcrypto = "0.2.1"
rsa = "0.9.6"
wasm-bindgen = "0.2.95"
serde-wasm-bindgen = "0.6.5"

[dev-dependencies]
[lib]
crate-type = ["cdylib", "rlib"]

[profile.release]
lto = true
14 changes: 8 additions & 6 deletions src/algorithm/hashalgorithm.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
use crate::Error;
use base64::Engine;
use rand::{thread_rng, Rng};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256, Sha384, Sha512};
use std::convert::TryFrom;
use std::fmt;

pub(crate) fn generate_salt(len: usize) -> String {
let mut salt = vec![0u8; len];
thread_rng().fill(&mut salt[..]);
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(salt)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HashAlgorithm {
SHA256,
SHA384,
SHA512,
}

impl ToString for HashAlgorithm {
fn to_string(&self) -> String {
impl fmt::Display for HashAlgorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HashAlgorithm::SHA256 => "sha-256".to_string(),
HashAlgorithm::SHA384 => "sha-384".to_string(),
HashAlgorithm::SHA512 => "sha-512".to_string(),
HashAlgorithm::SHA256 => write!(f, "sha-256"),
HashAlgorithm::SHA384 => write!(f, "sha-384"),
HashAlgorithm::SHA512 => write!(f, "sha-512"),
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/disclosure.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use crate::algorithm::{base64_hash, generate_salt, HashAlgorithm};
use crate::error::Error;
use base64::Engine;
use serde::{Deserialize, Serialize};
use serde_json::Value;

const ARRAY_DISCLOSURE_LEN: usize = 2;
const OBJECT_DISCLOSURE_LEN: usize = 3;

#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Disclosure {
disclosure: String,
digest: String,
Expand Down
3 changes: 2 additions & 1 deletion src/disclosure_path.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::Disclosure;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisclosurePath {
pub path: String,
pub disclosure: Disclosure,
Expand Down
13 changes: 13 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use jwt_rustcrypto::Error as JwtError;
use serde_json::Error as SerdeError;
use thiserror::Error;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::JsValue;

#[derive(Error, Debug)]
pub enum Error {
Expand Down Expand Up @@ -42,4 +44,15 @@ pub enum Error {
YamlError(#[from] serde_yaml::Error),
#[error("YAML invalid sd tag: {0}")]
YamlInvalidSDTag(String),

#[cfg(target_arch = "wasm32")]
#[error("WasmJsValueConversionFailed {0}")]
WasmJsValueConversionFailed(#[from] serde_wasm_bindgen::Error),
}

#[cfg(target_arch = "wasm32")]
impl From<Error> for JsValue {
fn from(val: Error) -> Self {
JsValue::from_str(&val.to_string())
}
}
173 changes: 173 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,176 @@
#[cfg(target_arch = "wasm32")]
use serde::{Deserialize, Serialize};
#[cfg(target_arch = "wasm32")]
use serde_json::Value;
#[cfg(target_arch = "wasm32")]
use serde_wasm_bindgen::to_value;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;

// #[cfg(target_arch = "wasm32")]
// #[wasm_bindgen]
// extern "C" {
// #[wasm_bindgen(js_namespace = console)]
// fn log(value: &str);
// }

#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub struct SdJwtIssuer {}

#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
impl SdJwtIssuer {
#[wasm_bindgen(constructor)]
pub fn new() -> SdJwtIssuer {
SdJwtIssuer {}
}

pub fn encode(
&self,
claims: &str,
signing_key: &str,
algorithm: &str,
) -> Result<String, JsValue> {
let (claims, tagged_paths) = parse_yaml(claims)?;
let encoding_key = match algorithm {
"RS256" | "RS384" | "RS512" | "PS256" | "PS384" | "PS512" => {
KeyForEncoding::from_rsa_pem(signing_key.as_bytes())?
}
"ES256" | "ES384" | "ES512" => KeyForEncoding::from_ec_pem(signing_key.as_bytes())?,
_ => return Err(JsValue::from_str("Unsupported algorithm")),
};
let issuer_sd_jwt = crate::issuer::Issuer::new(claims.clone())?
.iter_disclosable(tagged_paths.iter())
.encode(&encoding_key)?;
Ok(issuer_sd_jwt)
}
}

#[cfg(target_arch = "wasm32")]
impl Default for SdJwtIssuer {
fn default() -> Self {
Self::new()
}
}

#[cfg(target_arch = "wasm32")]
#[derive(Serialize, Deserialize)]
pub struct DecodedIssuerJwt {
pub header: Value,
pub updated_claims: Value,
pub disclosure_paths: Vec<DisclosurePath>,
}

#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub struct SdJwtHolder {}

#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
impl SdJwtHolder {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
SdJwtHolder {}
}

#[wasm_bindgen]
pub fn verify(
&self,
encoded_issuer_jwt: &str,
public_key: &str,
algorithm: &str,
) -> Result<JsValue, JsValue> {
let decoding_key = match algorithm {
"RS256" | "RS384" | "RS512" => KeyForDecoding::from_rsa_pem(public_key.as_bytes())?,
"ES256" | "ES384" | "ES512" => KeyForDecoding::from_ec_pem(public_key.as_bytes())?,
_ => return Err(JsValue::from_str("Unsupported algorithm")),
};
let validation = Validation::default().without_expiry();
let (header, decoded_claims, disclosure_paths) =
Holder::verify(encoded_issuer_jwt, &decoding_key, &validation)?;
let decoded_issuer_jwt = DecodedIssuerJwt {
header,
updated_claims: decoded_claims,
disclosure_paths,
};
Ok(to_value(&decoded_issuer_jwt)?)
}

#[wasm_bindgen]
pub fn presentation(
&self,
encoded_issuer_jwt: &str,
redacted_paths: Vec<String>,
) -> Result<String, JsValue> {
let mut presentation = Holder::presentation(encoded_issuer_jwt)?;
let _ = redacted_paths
.iter()
.try_for_each::<_, Result<(), Error>>(|path| {
presentation.redact(path)?;
Ok(())
});

Ok(presentation.build()?)
}
}

#[cfg(target_arch = "wasm32")]
impl Default for SdJwtHolder {
fn default() -> Self {
Self::new()
}
}

#[cfg(target_arch = "wasm32")]
#[derive(Debug, Serialize, Deserialize)]
pub struct DecodedHolderJwt {
pub header: Value,
pub restored_claims: Value,
}

#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub struct SdJwtVerifier {}

#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
impl SdJwtVerifier {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
SdJwtVerifier {}
}

#[wasm_bindgen]
pub fn verify(
&self,
holder_presentation_sdjwt: &str,
public_key: &str,
algorithm: &str,
) -> Result<JsValue, JsValue> {
let decoding_key = match algorithm {
"RS256" | "RS384" | "RS512" => KeyForDecoding::from_rsa_pem(public_key.as_bytes())?,
"ES256" | "ES384" | "ES512" => KeyForDecoding::from_ec_pem(public_key.as_bytes())?,
_ => return Err(JsValue::from_str("Unsupported algorithm")),
};
let validation = Validation::default().without_expiry();
let (header, restored_claims) =
Verifier::verify(holder_presentation_sdjwt, &decoding_key, &validation, &None)?;
let decoded_holder_jwt = DecodedHolderJwt {
header,
restored_claims,
};
Ok(to_value(&decoded_holder_jwt)?)
}
}

#[cfg(target_arch = "wasm32")]
impl Default for SdJwtVerifier {
fn default() -> Self {
Self::new()
}
}

pub mod algorithm;
pub mod decoding;
pub(crate) mod decoy;
Expand Down
3 changes: 0 additions & 3 deletions src/validation.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// This is based on https://github.com/Keats/jsonwebtoken/blob/master/src/validation.rs and is used
// to provide facade for underlying JWT library set the validation parameters for the JWT.

use crate::Algorithm;
use std::collections::HashSet;

Expand Down
4 changes: 1 addition & 3 deletions src/verifier.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{
base64_hash, decode, sd_jwt_parts,
utils::{drop_kb, remove_digests, restore_disclosures},
Error, HashAlgorithm, Jwk, KeyForDecoding, Validation,
Error, HashAlgorithm, KeyForDecoding, Validation,
};
use base64::Engine;
use serde_json::Value;
Expand Down Expand Up @@ -63,8 +63,6 @@ impl Verifier {
));
}

let _ = Jwk::from_value(claims["cnf"].clone())?;

let hash_alg = match HashAlgorithm::try_from(claims["_sd_alg"].as_str().ok_or(
Error::SDJWTRejected("Issuer SD JWT must contain _sd_alg claim".to_string()),
)?) {
Expand Down

0 comments on commit 60954a8

Please sign in to comment.