Skip to content

Commit

Permalink
Enable SDs in array type claims.
Browse files Browse the repository at this point in the history
Enable issuing and disclosing SDs in array type claims;
enable unpacking array type of SDs, minor fixes.

Signed-off-by: Abdulbois <[email protected]>
  • Loading branch information
Abdulbois authored and jovfer committed Dec 12, 2023
1 parent 95325c8 commit 89c05e9
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 11 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/disclosure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
};
Expand Down
78 changes: 74 additions & 4 deletions src/holder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -131,6 +132,24 @@ impl SDJWTHolder {
hash_to_disclosure
}

fn select_disclosures_from_disclosed_list(&self, sd_jwt_claims: &Vec<Value>, claims_to_disclose: &Vec<Value>) -> Vec<String> {
let mut hash_to_disclosure: Vec<String> = 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,
Expand All @@ -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;
Expand Down Expand Up @@ -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);

}
}
23 changes: 21 additions & 2 deletions src/issuer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand Down
65 changes: 62 additions & 3 deletions src/verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
};

Expand All @@ -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)?;
}

Expand Down Expand Up @@ -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};

Expand Down Expand Up @@ -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());
}
}

0 comments on commit 89c05e9

Please sign in to comment.