diff --git a/README.md b/README.md index 51aa402..f0942dd 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Please refer to the list, unchecked items are in progress and will be supported - [x] CI pipelines for Ubuntu - [ ] Support of multiple hash / sign algorithms - [ ] JWT Key Binding support -- [ ] Selective disclosure of arrays elements +- [x] Selective disclosure of arrays elements - [ ] Extended error handling - [ ] Extended CI/CD diff --git a/src/disclosure.rs b/src/disclosure.rs index b176f72..a1f81c4 100644 --- a/src/disclosure.rs +++ b/src/disclosure.rs @@ -16,7 +16,7 @@ impl SDJWTDisclosure { let raw_b64 = SDJWTCommon::base64url_encode(data.as_bytes()); (data, raw_b64) } else { - let data = format!("[{}, {}]", salt, value_str); + let data = format!(r#"["{}", {}]"#, salt, value_str); let raw_b64 = SDJWTCommon::base64url_encode(data.as_bytes()); (data, raw_b64) }; diff --git a/src/holder.rs b/src/holder.rs index 79f7f9f..a1c938c 100644 --- a/src/holder.rs +++ b/src/holder.rs @@ -4,7 +4,7 @@ use std::time; use jsonwebtoken::EncodingKey; use serde_json::{json, Map, Value}; -use crate::{COMBINED_SERIALIZATION_FORMAT_SEPARATOR, DEFAULT_SIGNING_ALG, SD_DIGESTS_KEY}; +use crate::{COMBINED_SERIALIZATION_FORMAT_SEPARATOR, DEFAULT_SIGNING_ALG, SD_DIGESTS_KEY, SD_LIST_PREFIX}; use crate::SDJWTCommon; pub struct SDJWTHolder { @@ -100,12 +100,13 @@ impl SDJWTHolder { let disclosure = self.sd_jwt_engine.hash_to_decoded_disclosure[digest].as_array().unwrap(); (disclosure[1].as_str().unwrap(), (&disclosure[2], digest)) }).collect(); //TODO split to 2 maps - for (key_to_disclose, value_to_disclose) in claims_to_disclose { match value_to_disclose { Value::Null | Value::Bool(true) | Value::Number(_) | Value::String(_) => { /* disclose without children */ } - Value::Array(_) => { - unimplemented!() + Value::Array(arr_to_disclose) => { + if let Some(arr) = sd_jwt_claims.get(&key_to_disclose).and_then(Value::as_array) { + hash_to_disclosure.append(&mut self.select_disclosures_from_disclosed_list(&arr, &arr_to_disclose)) + } } Value::Object(next_disclosure) if (!next_disclosure.is_empty()) => { let next_sd_jwt_claims = if let Some(next) = sd_jwt_claims.get(&key_to_disclose).and_then(Value::as_object) { @@ -131,6 +132,24 @@ impl SDJWTHolder { hash_to_disclosure } + fn select_disclosures_from_disclosed_list(&self, sd_jwt_claims: &Vec, claims_to_disclose: &Vec) -> Vec { + let mut hash_to_disclosure: Vec = Vec::new(); + for (claim_to_disclose, claim) in claims_to_disclose.iter().zip(sd_jwt_claims) { + match (claim_to_disclose, claim) { + (Value::Bool(true), Value::Object(claim)) => { + if let Some(Value::String(digest)) = claim.get(SD_LIST_PREFIX) { + hash_to_disclosure.push(self.sd_jwt_engine.hash_to_disclosure[digest].to_owned()); + } + } + (Value::Array(new_claims_to_disclose), Value::Array(claim)) => { + self.select_disclosures_from_disclosed_list(claim, new_claims_to_disclose); + } + _ => {} + } + } + + return hash_to_disclosure; + } fn create_key_binding_jwt( &mut self, nonce: String, @@ -156,6 +175,7 @@ impl SDJWTHolder { #[cfg(test)] mod tests { + use std::collections::HashSet; use jsonwebtoken::EncodingKey; use serde_json::{json, Map, Value}; use crate::issuer::SDJWTClaimsStrategy; @@ -213,4 +233,54 @@ mod tests { let expected = parts.join(COMBINED_SERIALIZATION_FORMAT_SEPARATOR); assert_eq!(expected, presentation); } + + #[test] + fn create_presentation_for_arrayed_disclosures() { + let mut user_claims = json!( + { + "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", + "name": "Bois", + "addresses": [ + { + "street_address": "Schulstr. 12", + "locality": "Schulpforta", + "region": "Sachsen-Anhalt", + "country": "DE" + }, + { + "street_address": "456 Main St", + "locality": "Anytown", + "region": "NY", + "country": "US" + } + ], + "nationalities": [ + "US", + "CA" + ] + } + ); + let strategy = SDJWTClaimsStrategy::Partial(vec!["$.name", "$.addresses[1]", "$.addresses[1].country", "$.nationalities[0]"]); + + let private_issuer_bytes = PRIVATE_ISSUER_PEM.as_bytes(); + let issuer_key = EncodingKey::from_ec_pem(private_issuer_bytes).unwrap(); + let sd_jwt = SDJWTIssuer::issue_sd_jwt(user_claims.clone(), strategy, issuer_key, None, None, false, "compact".to_owned()); + // Choose what to reveal + user_claims["addresses"] = Value::Array(vec![Value::Bool(true), Value::Bool(false)]); + user_claims["nationalities"] = Value::Array(vec![Value::Bool(true), Value::Bool(true)]); + + let issued = sd_jwt.serialized_sd_jwt.clone(); + println!("{}", issued); + let presentation = SDJWTHolder::new(sd_jwt.serialized_sd_jwt, "compact".to_ascii_lowercase()).create_presentation(user_claims.as_object().unwrap().clone(), None, None, None, None); + println!("{}", presentation); + let mut issued_parts: HashSet<&str> = issued.split(COMBINED_SERIALIZATION_FORMAT_SEPARATOR).collect(); + issued_parts.remove(""); + + let mut revealed_parts: HashSet<&str> = presentation.split(COMBINED_SERIALIZATION_FORMAT_SEPARATOR).collect(); + revealed_parts.remove(""); + + let union: HashSet<_> = issued_parts.intersection(&revealed_parts).collect(); + assert_eq!(union.len(), 3); + + } } \ No newline at end of file diff --git a/src/issuer.rs b/src/issuer.rs index e3d5d09..be90224 100644 --- a/src/issuer.rs +++ b/src/issuer.rs @@ -69,7 +69,26 @@ impl<'a> SDJWTClaimsStrategy<'a> { Self::Full => Self::Full, Self::Partial(sd_keys) => { let next_sd_keys = sd_keys.iter().filter_map(|str| { - str.strip_prefix(key).and_then(|str| str.strip_prefix('.')) + str.strip_prefix(key).as_mut().and_then(|claim| { + if let Some(next_claim) = claim.strip_prefix('.') { + Some(next_claim) + } else if let Some(next_claim) = claim.strip_prefix('[').and_then(|str| str.strip_suffix(']')) { + Some(next_claim) + } else { + // Removes "[", "]" symbols form "index" and returns "next_claim" as "index.remained_claims.." + // For example: [0].street -> 0.street + if let Some(remainder) = claim.strip_prefix('[') { + *claim = remainder; + let remainder: Vec<&str> = claim.splitn(2, ']').collect(); + //FIXME Change to safe impl + *claim = remainder.join("").leak(); + + Some(claim) + } else { + None + } + } + }) }).collect(); Self::Partial(next_sd_keys) } @@ -170,7 +189,7 @@ impl SDJWTIssuer { let subtree = self.create_sd_claims(object, strategy_for_child); if sd_strategy.sd_for_key(&key) { - let disclosure = SDJWTDisclosure::new(Some(key.to_owned()), subtree, &self.inner); + let disclosure = SDJWTDisclosure::new(None, subtree, &self.inner); claims.push(json!({ SD_LIST_PREFIX: disclosure.hash})); self.all_disclosures.push(disclosure); } else { diff --git a/src/verifier.rs b/src/verifier.rs index ca8f69e..26d64ae 100644 --- a/src/verifier.rs +++ b/src/verifier.rs @@ -96,7 +96,18 @@ impl SDJWTVerifier { Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => { return Ok(sd_jwt_claims.to_owned()); } - Value::Array(_) => { unimplemented!() } + Value::Array(arr) => { + if arr.is_empty() { + return Err("Array of disclosed claims cannot be empty".to_string()); + } + + let mut claims = vec![]; + for value in arr { + let claim = self.unpack_disclosed_claims(value).unwrap(); + claims.push(claim); + } + return Ok(serde_json::to_value(claims).unwrap()); + } Value::Object(obj) => { obj } }; @@ -109,7 +120,7 @@ impl SDJWTVerifier { } } - if let Some(digest_of_disclosures) = nested_sd_jwt_claims[SD_DIGESTS_KEY].as_array() { + if let Some(Value::Array(digest_of_disclosures)) = nested_sd_jwt_claims.get(SD_DIGESTS_KEY) { self.unpack_from_digests(&mut disclosed_claims, digest_of_disclosures)?; } @@ -150,7 +161,7 @@ impl SDJWTVerifier { #[cfg(test)] mod tests { use jsonwebtoken::{DecodingKey, EncodingKey}; - use serde_json::json; + use serde_json::{json, Value}; use crate::issuer::SDJWTClaimsStrategy; use crate::{SDJWTHolder, SDJWTIssuer, SDJWTVerifier}; @@ -183,4 +194,52 @@ mod tests { }), None, None, "compact".to_owned()).unwrap().verified_claims; assert_eq!(user_claims, verified_claims); } + + #[test] + fn verify_arrayed_presentation() { + let user_claims = json!( + { + "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", + "name": "Bois", + "iss": "https://example.com/issuer", + "iat": 1683000000, + "exp": 1883000000, + "addresses": [ + { + "street_address": "Schulstr. 12", + "locality": "Schulpforta", + "region": "Sachsen-Anhalt", + "country": "DE" + }, + { + "street_address": "456 Main St", + "locality": "Anytown", + "region": "NY", + "country": "US" + } + ], + "nationalities": [ + "US", + "CA" + ] + } + ); + let private_issuer_bytes = PRIVATE_ISSUER_PEM.as_bytes(); + let issuer_key = EncodingKey::from_ec_pem(private_issuer_bytes).unwrap(); + let strategy = SDJWTClaimsStrategy::Partial(vec!["$.name", "$.addresses[1]", "$.addresses[1].country", "$.nationalities[0]"]); + let sd_jwt = SDJWTIssuer::issue_sd_jwt(user_claims.clone(), strategy, issuer_key, None, None, false, "compact".to_owned()); + + let mut claims_to_disclose = user_claims.clone(); + claims_to_disclose["addresses"] = Value::Array(vec![Value::Bool(true), Value::Bool(true)]); + claims_to_disclose["nationalities"] = Value::Array(vec![Value::Bool(true), Value::Bool(true)]); + let presentation = SDJWTHolder::new(sd_jwt.serialized_sd_jwt.clone(), "compact".to_owned()).create_presentation(claims_to_disclose.as_object().unwrap().clone(), None, None, None, None); + + let verified_claims = SDJWTVerifier::new(presentation.clone(), Box::new(|_, _| { + let public_issuer_bytes = PUBLIC_ISSUER_PEM.as_bytes(); + DecodingKey::from_ec_pem(public_issuer_bytes).unwrap() + }), None, None, "compact".to_owned()).unwrap().verified_claims; + + assert_eq!(verified_claims["addresses"][0].to_owned(), user_claims["addresses"][0].to_owned()); + assert!(!verified_claims["addresses"][1].to_owned().get("...").unwrap().to_string().is_empty()); + } } \ No newline at end of file