Skip to content

Commit

Permalink
feat: Enable supporting key binding
Browse files Browse the repository at this point in the history
Signed-off-by: Abdulbois <[email protected]>
  • Loading branch information
Abdulbois committed Dec 14, 2023
1 parent 9d52b9c commit 6cc38df
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 41 deletions.
30 changes: 21 additions & 9 deletions src/holder.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use std::collections::HashMap;
use std::str::FromStr;
use std::time;
use jsonwebtoken::{Algorithm, EncodingKey, Header};
use serde_json::{Map, Value};

use jsonwebtoken::EncodingKey;
use serde_json::{json, Map, Value};

use crate::{COMBINED_SERIALIZATION_FORMAT_SEPARATOR, DEFAULT_SIGNING_ALG, SD_DIGESTS_KEY, SD_LIST_PREFIX};
use crate::{COMBINED_SERIALIZATION_FORMAT_SEPARATOR, DEFAULT_SIGNING_ALG, KB_DIGEST_KEY, SD_DIGESTS_KEY, SD_LIST_PREFIX};
use crate::SDJWTCommon;
use crate::utils::create_base64_encoded_hash;

pub struct SDJWTHolder {
sd_jwt_engine: SDJWTCommon,
Expand Down Expand Up @@ -184,18 +185,29 @@ impl SDJWTHolder {
sign_alg: Option<String>,
) {
let _alg = sign_alg.unwrap_or_else(|| DEFAULT_SIGNING_ALG.to_string());

self.key_binding_jwt_header.insert("alg".to_string(), _alg.into());
// Set key-binding fields
self.key_binding_jwt_header.insert("alg".to_string(), _alg.clone().into());
self.key_binding_jwt_header.insert("typ".to_string(), crate::KB_JWT_TYP_HEADER.into());

self.key_binding_jwt_payload.insert("nonce".to_string(), nonce.into());
self.key_binding_jwt_payload.insert("aud".to_string(), aud.into());
let timestamp = time::SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap().as_secs();
self.key_binding_jwt_payload.insert("iat".to_string(), timestamp.into());
self._set_key_binding_digest_key();
// Create key-binding jwt
let mut header = Header::new(Algorithm::from_str(_alg.as_str()).unwrap());
header.typ = Some(crate::KB_JWT_TYP_HEADER.into());
self.serialized_key_binding_jwt = jsonwebtoken::encode(&header, &self.key_binding_jwt_payload, &_holder_key).unwrap();
}

let _payload = json!(self.key_binding_jwt_payload);
fn _set_key_binding_digest_key(&mut self) {
let mut combined: Vec<&str> = Vec::with_capacity(self.hs_disclosures.len() + 1);
combined.push(&self.serialized_sd_jwt);
combined.extend(self.hs_disclosures.iter().map(|s| s.as_str()));
let combined = combined.join(COMBINED_SERIALIZATION_FORMAT_SEPARATOR);

//FIXME jsonwebtoken signature self.serialized_key_binding_jwt = payload.sign_with_key(holder_key).unwrap();
let _sd_hash = create_base64_encoded_hash(combined);
let _sd_hash = serde_json::to_value(&_sd_hash).unwrap();
self.key_binding_jwt_payload.insert(KB_DIGEST_KEY.to_owned(), _sd_hash);
}
}

Expand Down
15 changes: 7 additions & 8 deletions src/issuer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ use std::str::FromStr;
use std::vec::Vec;

use jsonwebtoken::{Algorithm, EncodingKey, Header};
use jsonwebtoken::jwk::Jwk;
use rand::Rng;
use serde_json::{json, Map as SJMap, Map};
use serde_json::Value;

use crate::{COMBINED_SERIALIZATION_FORMAT_SEPARATOR, DEFAULT_DIGEST_ALG, DIGEST_ALG_KEY,
SD_DIGESTS_KEY, SD_LIST_PREFIX, DEFAULT_SIGNING_ALG,
SDJWTCommon, SDJWTHasSDClaimException};
use crate::{COMBINED_SERIALIZATION_FORMAT_SEPARATOR, DEFAULT_DIGEST_ALG, DIGEST_ALG_KEY, SD_DIGESTS_KEY, SD_LIST_PREFIX, DEFAULT_SIGNING_ALG, SDJWTCommon, SDJWTHasSDClaimException, CNF_KEY, JWK_KEY};
use crate::disclosure::SDJWTDisclosure;

pub struct SDJWTIssuer {
Expand All @@ -20,7 +19,7 @@ pub struct SDJWTIssuer {

// input data
issuer_key: EncodingKey,
holder_key: Option<EncodingKey>,
holder_key: Option<Jwk>,

// internal fields
inner: SDJWTCommon,
Expand Down Expand Up @@ -106,7 +105,7 @@ impl SDJWTIssuer {
user_claims: Value,
mut sd_strategy: SDJWTClaimsStrategy,
issuer_key: EncodingKey,
holder_key: Option<EncodingKey>,
holder_key: Option<Jwk>,
sign_alg: Option<String>,
add_decoy_claims: bool,
serialization_format: String,
Expand Down Expand Up @@ -154,9 +153,9 @@ impl SDJWTIssuer {
self.sd_jwt_payload.append(&mut always_revealed_claims);

if let Some(holder_key) = &self.holder_key {
let _ = holder_key;
unimplemented!("holder key is not supported for issuance");
//TODO public? self.sd_jwt_payload.insert("cnf".to_string(), json!({"jwk": holder_key.export_public(true)}));
self.sd_jwt_payload
.entry(CNF_KEY)
.or_insert_with(|| json!({JWK_KEY: holder_key}));
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod disclosure;
pub mod holder;
pub mod issuer;
pub mod verifier;
pub mod utils;

pub const DEFAULT_SIGNING_ALG: &str = "ES256";
const SD_DIGESTS_KEY: &str = "_sd";
Expand All @@ -19,10 +20,13 @@ pub const DEFAULT_DIGEST_ALG: &str = "sha-256";
const SD_LIST_PREFIX: &str = "...";
const _SD_JWT_TYP_HEADER: &str = "sd+jwt";
const KB_JWT_TYP_HEADER: &str = "kb+jwt";
const KB_DIGEST_KEY: &str = "_sd_hash";
const JWS_KEY_DISCLOSURES: &str = "disclosures";
const JWS_KEY_KB_JWT: &str = "kb_jwt";
pub const COMBINED_SERIALIZATION_FORMAT_SEPARATOR: &str = "~";
const JWT_SEPARATOR: &str = ".";
const CNF_KEY: &str = "cnf";
const JWK_KEY: &str = "jwk";

#[derive(Debug)]
pub(crate) struct SDJWTHasSDClaimException(String);
Expand Down
11 changes: 11 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use base64::Engine;
use base64::engine::general_purpose;
use sha2::Digest;

pub fn create_base64_encoded_hash(data: String) -> String {
let mut hasher = sha2::Sha256::new();
hasher.update(data);
let hash = hasher.finalize();

general_purpose::URL_SAFE_NO_PAD.encode(&hash)
}
82 changes: 76 additions & 6 deletions src/verifier.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
use std::collections::HashMap;
use std::option::Option;
use std::str::FromStr;
use std::string::String;
use std::vec::Vec;

use jsonwebtoken::{Algorithm, DecodingKey, Header, Validation};
use jsonwebtoken::jwk::Jwk;
use log::debug;
use serde_json::{Map, Value};

use crate::{DEFAULT_DIGEST_ALG, DEFAULT_SIGNING_ALG, DIGEST_ALG_KEY, SD_DIGESTS_KEY, SDJWTCommon};
use crate::{CNF_KEY, COMBINED_SERIALIZATION_FORMAT_SEPARATOR, DEFAULT_DIGEST_ALG, DEFAULT_SIGNING_ALG, DIGEST_ALG_KEY, JWK_KEY, KB_DIGEST_KEY, KB_JWT_TYP_HEADER, SD_DIGESTS_KEY, SDJWTCommon};
use crate::utils::create_base64_encoded_hash;

type KeyResolver = dyn Fn(&str, &Header) -> DecodingKey;

pub struct SDJWTVerifier {
sd_jwt_engine: SDJWTCommon,

sd_jwt_payload: Map<String, Value>,
_holder_public_key_payload: Option<HashMap<String, Value>>,
_holder_public_key_payload: Option<Map<String, Value>>,
duplicate_hash_check: Vec<String>,
pub verified_claims: Value,

Expand Down Expand Up @@ -72,13 +73,82 @@ impl SDJWTVerifier {
let _ = sign_alg; //FIXME check algo

self.sd_jwt_payload = claims;
self._holder_public_key_payload = if let Some(holder_public_key_payload) =
self.sd_jwt_payload.get(CNF_KEY).and_then(Value::as_object)
{
Some(holder_public_key_payload.clone())
} else {
None
};

Ok(())
}

fn verify_key_binding_jwt(&mut self, expected_aud: String, expected_nonce: String, sign_alg: Option<&str>) -> Result<(), String> {
let (_, _, _) = (expected_aud, expected_nonce, sign_alg);
unimplemented!("JWT KB");
let sign_alg = sign_alg.unwrap_or(DEFAULT_SIGNING_ALG);
let holder_public_key_payload_jwk = match &self._holder_public_key_payload {
None => { return Err("No holder public key in SD-JWT".to_string()); }
Some(payload) => {
if let Some(jwk) = payload.get(JWK_KEY) {
jwk.clone()
} else {
return Err("The holder_public_key_payload is malformed. It doesn't contain the claim jwk".to_string());
}
}
};
let pubkey: DecodingKey = match serde_json::from_value::<Jwk>(holder_public_key_payload_jwk) {
Ok(jwk) => {
if let Ok(pubkey) = DecodingKey::from_jwk(&jwk) {
pubkey
} else {
return Err("Cannot parse DecodingKey from json".to_string());
}
},
Err(_) => {
return Err("Cannot parse JWK from json".to_string());
}
};
let key_binding_jwt = match &self.sd_jwt_engine.unverified_input_key_binding_jwt {
Some(payload) => {
let mut validation = Validation::new(Algorithm::from_str(sign_alg).unwrap());
validation.set_audience(&[expected_aud.as_str()]);
validation.set_required_spec_claims(&["aud"]);

let jwt = jsonwebtoken::decode::<Map<String, Value>>(
payload.as_str(),
&pubkey,
&validation,
).unwrap();

jwt
}
None => {
return Err("Cannot parse Key Binding JWK from json".to_string());
}
};
if key_binding_jwt.header.typ != Some(KB_JWT_TYP_HEADER.to_string()) {
return Err("Invalid header type".to_string());
}
if key_binding_jwt.claims.get("nonce") != Some(&Value::String(expected_nonce)) {
return Err("Invalid nonce in KB-JWT".to_string());
}
if self.sd_jwt_engine.serialization_format == "compact" {
let _sd_hash = self._get_key_binding_digest_hash();
if key_binding_jwt.claims.get(KB_DIGEST_KEY) != Some(&Value::String(_sd_hash)) {
return Err("Invalid digest in KB-JWT".to_string());
}
}

Ok(())
}

fn _get_key_binding_digest_hash(&mut self) -> String {
let mut combined: Vec<&str> = Vec::with_capacity(self.sd_jwt_engine.input_disclosures.len() + 1);
combined.push(self.sd_jwt_engine.unverified_sd_jwt.as_ref().unwrap().as_str());
combined.extend(self.sd_jwt_engine.input_disclosures.iter().map(|s| s.as_str()));
let combined = combined.join(COMBINED_SERIALIZATION_FORMAT_SEPARATOR);

create_base64_encoded_hash(combined)
}

fn extract_sd_claims(&mut self) -> Result<Value, String> {
Expand Down
41 changes: 30 additions & 11 deletions tests/demos.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use crate::utils::fixtures::{
ADDRESS_CLAIMS, ADDRESS_ONLY_STRUCTURED_JSONPATH, ADDRESS_ONLY_STRUCTURED_ONE_OPEN_JSONPATH,
ARRAYED_CLAIMS, ARRAYED_CLAIMS_JSONPATH, COMPLEX_EIDAS_CLAIMS, COMPLEX_EIDAS_JSONPATH,
HOLDER_KEY, ISSUER_KEY, ISSUER_PUBLIC_KEY, NESTED_ARRAY_CLAIMS, NESTED_ARRAY_JSONPATH,
W3C_VC_CLAIMS, W3C_VC_JSONPATH,
HOLDER_JWK_KEY, HOLDER_KEY, ISSUER_KEY, ISSUER_PUBLIC_KEY, NESTED_ARRAY_CLAIMS,
NESTED_ARRAY_JSONPATH, W3C_VC_CLAIMS, W3C_VC_JSONPATH,
};
use jsonwebtoken::jwk::Jwk;
use jsonwebtoken::{DecodingKey, EncodingKey};
use rstest::{fixture, rstest};
use sd_jwt_rs::issuer::SDJWTClaimsStrategy;
Expand All @@ -20,10 +21,16 @@ fn issuer_key() -> EncodingKey {
EncodingKey::from_ec_pem(private_issuer_bytes).unwrap()
}

fn holder_jwk() -> Option<Jwk> {
let jwk: Jwk = serde_json::from_str(HOLDER_JWK_KEY).unwrap();
Some(jwk)
}

#[allow(unused)]
fn holder_key() -> EncodingKey {
fn holder_key() -> Option<EncodingKey> {
let private_issuer_bytes = HOLDER_KEY.as_bytes();
EncodingKey::from_ec_pem(private_issuer_bytes).unwrap()
let key = EncodingKey::from_ec_pem(private_issuer_bytes).unwrap();
Some(key)
}

fn _address_claims() -> serde_json::Value {
Expand Down Expand Up @@ -243,11 +250,17 @@ fn w3c_vc<'a>() -> (
}

#[allow(unused)]
fn presentation_metadata() -> (Option<String>, Option<String>, Option<EncodingKey>) {
fn presentation_metadata() -> (
Option<String>,
Option<String>,
Option<EncodingKey>,
Option<Jwk>,
) {
(
Some("1234567890".to_owned()),
Some("https://verifier.example.org".to_owned()),
Some(holder_key()),
holder_key(),
holder_jwk(),
)
}

Expand All @@ -268,23 +281,24 @@ fn demo_positive_cases(
Map<String, Value>,
usize,
),
#[values((None, None, None), /*presentation_metadata()*/)] presentation_metadata: (
#[values((None, None, None, None), presentation_metadata())] presentation_metadata: (
Option<String>,
Option<String>,
Option<EncodingKey>,
Option<Jwk>,
),
#[values("compact".to_string())] format: String,
#[values(None, Some(DEFAULT_SIGNING_ALG.to_owned()))] sign_algo: Option<String>,
#[values(true, false)] add_decoy: bool,
) {
let (user_claims, strategy, holder_disclosed_claims, number_of_revealed_sds) = data;
let (nonce, aud, holder_key) = presentation_metadata;
let (nonce, aud, holder_key, holder_jwk) = presentation_metadata;
// Issuer issues SD-JWT
let sd_jwt = SDJWTIssuer::issue_sd_jwt(
user_claims.clone(),
strategy,
issuer_key,
holder_key.clone(),
holder_jwk.clone(),
sign_algo.clone(),
add_decoy,
format.clone(),
Expand Down Expand Up @@ -312,9 +326,14 @@ fn demo_positive_cases(

let intersected_parts: HashSet<_> = issued_parts.intersection(&revealed_parts).collect();
// Compare that number of disclosed parts are equal
assert_eq!(intersected_parts.len(), revealed_parts.len());
let mut revealed_parts_number = revealed_parts.len();
if holder_jwk.is_some() {
// Remove KB
revealed_parts_number -= 1;
}
assert_eq!(intersected_parts.len(), revealed_parts_number);
// here `+1` means adding issued jwt part also
assert_eq!(number_of_revealed_sds + 1, revealed_parts.len());
assert_eq!(number_of_revealed_sds + 1, revealed_parts_number);
// Verify presentation
let _verified = SDJWTVerifier::new(
presentation.clone(),
Expand Down
13 changes: 6 additions & 7 deletions tests/utils/fixtures.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
pub const ISSUER_KEY: &str = "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgUr2bNKuBPOrAaxsR\nnbSH6hIhmNTxSGXshDSUD1a1y7ihRANCAARvbx3gzBkyPDz7TQIbjF+ef1IsxUwz\nX1KWpmlVv+421F7+c1sLqGk4HUuoVeN8iOoAcE547pJhUEJyf5Asc6pP\n-----END PRIVATE KEY-----\n";
pub const ISSUER_PUBLIC_KEY: &str = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEb28d4MwZMjw8+00CG4xfnn9SLMVM\nM19SlqZpVb/uNtRe/nNbC6hpOB1LqFXjfIjqAHBOeO6SYVBCcn+QLHOqTw==\n-----END PUBLIC KEY-----\n";
pub const HOLDER_KEY: &str = "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg5K5SCos8zf9zRemG\nGUl6yfok+/NiiryNZsvANWMhF+KhRANCAARMIARHX1m+7c4cXiPhbi99JWgcg/Ug\nuKUOWzu8J4Z6Z2cY4llm2TEBh1VilUOIW0iIq7FX7nnAhOreI0/Rdh2U\n-----END PRIVATE KEY-----\n";
pub const HOLDER_JWK_KEY: &str = r#"{
"kty": "EC",
"crv": "P-256",
"x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc",
"y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ"
}"#;
pub const ADDRESS_CLAIMS: &str = r#"{
"sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
"iss": "https://example.com/issuer",
Expand Down Expand Up @@ -125,13 +131,6 @@ pub const W3C_VC_CLAIMS: &str = r#"{
"jti": "http://example.com/credentials/3732",
"iat": 1683000000,
"exp": 1883000000,
"cnf": {
"jwk": {
"kty": "RSA",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e": "AQAB"
}
},
"type": "IdentityCredential",
"credentialSubject": {
"given_name": "John",
Expand Down

0 comments on commit 6cc38df

Please sign in to comment.