From 6cc38df7c3ed45d545009ab21a85ce157638363f Mon Sep 17 00:00:00 2001 From: Abdulbois Date: Thu, 14 Dec 2023 13:36:49 +0500 Subject: [PATCH] feat: Enable supporting key binding Signed-off-by: Abdulbois --- src/holder.rs | 30 ++++++++++----- src/issuer.rs | 15 ++++---- src/lib.rs | 4 ++ src/utils.rs | 11 ++++++ src/verifier.rs | 82 ++++++++++++++++++++++++++++++++++++++--- tests/demos.rs | 41 +++++++++++++++------ tests/utils/fixtures.rs | 13 +++---- 7 files changed, 155 insertions(+), 41 deletions(-) create mode 100644 src/utils.rs diff --git a/src/holder.rs b/src/holder.rs index c444a89..b81dbd7 100644 --- a/src/holder.rs +++ b/src/holder.rs @@ -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, @@ -184,18 +185,29 @@ impl SDJWTHolder { sign_alg: Option, ) { 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); } } diff --git a/src/issuer.rs b/src/issuer.rs index 0107c51..7c3d3c5 100644 --- a/src/issuer.rs +++ b/src/issuer.rs @@ -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 { @@ -20,7 +19,7 @@ pub struct SDJWTIssuer { // input data issuer_key: EncodingKey, - holder_key: Option, + holder_key: Option, // internal fields inner: SDJWTCommon, @@ -106,7 +105,7 @@ impl SDJWTIssuer { user_claims: Value, mut sd_strategy: SDJWTClaimsStrategy, issuer_key: EncodingKey, - holder_key: Option, + holder_key: Option, sign_alg: Option, add_decoy_claims: bool, serialization_format: String, @@ -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})); } } diff --git a/src/lib.rs b/src/lib.rs index 186efe0..f94448a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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"; @@ -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); diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..daa104c --- /dev/null +++ b/src/utils.rs @@ -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) +} \ No newline at end of file diff --git a/src/verifier.rs b/src/verifier.rs index 1282d62..c648d89 100644 --- a/src/verifier.rs +++ b/src/verifier.rs @@ -1,13 +1,14 @@ -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; @@ -15,7 +16,7 @@ pub struct SDJWTVerifier { sd_jwt_engine: SDJWTCommon, sd_jwt_payload: Map, - _holder_public_key_payload: Option>, + _holder_public_key_payload: Option>, duplicate_hash_check: Vec, pub verified_claims: Value, @@ -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::(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::>( + 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 { diff --git a/tests/demos.rs b/tests/demos.rs index ef31d11..e366838 100644 --- a/tests/demos.rs +++ b/tests/demos.rs @@ -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; @@ -20,10 +21,16 @@ fn issuer_key() -> EncodingKey { EncodingKey::from_ec_pem(private_issuer_bytes).unwrap() } +fn holder_jwk() -> Option { + let jwk: Jwk = serde_json::from_str(HOLDER_JWK_KEY).unwrap(); + Some(jwk) +} + #[allow(unused)] -fn holder_key() -> EncodingKey { +fn holder_key() -> Option { 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 { @@ -243,11 +250,17 @@ fn w3c_vc<'a>() -> ( } #[allow(unused)] -fn presentation_metadata() -> (Option, Option, Option) { +fn presentation_metadata() -> ( + Option, + Option, + Option, + Option, +) { ( Some("1234567890".to_owned()), Some("https://verifier.example.org".to_owned()), - Some(holder_key()), + holder_key(), + holder_jwk(), ) } @@ -268,23 +281,24 @@ fn demo_positive_cases( Map, usize, ), - #[values((None, None, None), /*presentation_metadata()*/)] presentation_metadata: ( + #[values((None, None, None, None), presentation_metadata())] presentation_metadata: ( Option, Option, Option, + Option, ), #[values("compact".to_string())] format: String, #[values(None, Some(DEFAULT_SIGNING_ALG.to_owned()))] sign_algo: Option, #[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(), @@ -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(), diff --git a/tests/utils/fixtures.rs b/tests/utils/fixtures.rs index ee43795..25857f4 100644 --- a/tests/utils/fixtures.rs +++ b/tests/utils/fixtures.rs @@ -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", @@ -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",