From ce6c96206844eb2d418fd732f761cdc94db42538 Mon Sep 17 00:00:00 2001 From: Tristan Miller Date: Mon, 11 Sep 2023 14:40:21 -0600 Subject: [PATCH 01/16] Initial commit of ssi-sd-jwt --- Cargo.toml | 1 + ssi-sd-jwt/Cargo.toml | 23 ++++ ssi-sd-jwt/src/decode.rs | 197 +++++++++++++++++++++++++++++++ ssi-sd-jwt/src/digest.rs | 63 ++++++++++ ssi-sd-jwt/src/encode.rs | 177 +++++++++++++++++++++++++++ ssi-sd-jwt/src/lib.rs | 47 ++++++++ ssi-sd-jwt/src/verify.rs | 196 ++++++++++++++++++++++++++++++ ssi-sd-jwt/tests/decode.rs | 135 +++++++++++++++++++++ ssi-sd-jwt/tests/full_pathway.rs | 120 +++++++++++++++++++ 9 files changed, 959 insertions(+) create mode 100644 ssi-sd-jwt/Cargo.toml create mode 100644 ssi-sd-jwt/src/decode.rs create mode 100644 ssi-sd-jwt/src/digest.rs create mode 100644 ssi-sd-jwt/src/encode.rs create mode 100644 ssi-sd-jwt/src/lib.rs create mode 100644 ssi-sd-jwt/src/verify.rs create mode 100644 ssi-sd-jwt/tests/decode.rs create mode 100644 ssi-sd-jwt/tests/full_pathway.rs diff --git a/Cargo.toml b/Cargo.toml index 65f7e4b0f..51ac8675f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,7 @@ members = [ "ssi-dids", "ssi-jws", "ssi-jwt", + "ssi-sd-jwt", "ssi-tzkey", "ssi-ssh", "ssi-ldp", diff --git a/ssi-sd-jwt/Cargo.toml b/ssi-sd-jwt/Cargo.toml new file mode 100644 index 000000000..25d5c251b --- /dev/null +++ b/ssi-sd-jwt/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "ssi-sd-jwt" +version = "0.1.0" +edition = "2021" +authors = ["Spruce Systems, Inc."] +license = "Apache-2.0" +description = "Implementation of SD-JWT for the ssi library." +repository = "https://github.com/spruceid/ssi/" +documentation = "https://docs.rs/ssi-sd-jwt/" + +[dependencies] +jose-b64 = { version = "0.1", features = ["json"] } +rand = { version = "0.8" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sha2 = "0.10" +ssi-jwk = { path = "../ssi-jwk", version = "0.1" } +ssi-jws = { path = "../ssi-jws", version = "0.1" } +ssi-jwt = { path = "../ssi-jwt", version = "0.1" } +thiserror = "1.0" + +[dev-dependencies] +hex-literal = "0.4.1" diff --git a/ssi-sd-jwt/src/decode.rs b/ssi-sd-jwt/src/decode.rs new file mode 100644 index 000000000..a01f5b4ed --- /dev/null +++ b/ssi-sd-jwt/src/decode.rs @@ -0,0 +1,197 @@ +use serde::de::DeserializeOwned; +use serde::Deserialize; +use ssi_jwk::JWK; +use ssi_jwt::NumericDate; +use std::collections::BTreeMap; + +use crate::verify::{DecodedDisclosure, DisclosureKind}; +use crate::*; + +#[derive(Debug, Deserialize, PartialEq)] +pub struct ValidityClaims { + pub nbf: Option, + pub iat: Option, + pub exp: Option, +} + +pub fn decode_verify( + jwt: &str, + key: &JWK, + disclosures: &[&str], +) -> Result<(ValidityClaims, Claims), Error> { + let mut payload_claims: serde_json::Value = ssi_jwt::decode_verify(jwt, key)?; + + let validity_claims: ValidityClaims = serde_json::from_value(payload_claims.clone())?; + + let sd_alg = sd_alg(&payload_claims)?; + let _ = payload_claims + .as_object_mut() + .unwrap() + .remove(SD_ALG_CLAIM_NAME); + + let mut disclosures = translate_to_in_progress_disclosures(disclosures, sd_alg)?; + + visit_claims(&mut payload_claims, &mut disclosures)?; + + Ok((validity_claims, serde_json::from_value(payload_claims)?)) +} + +fn sd_alg(claims: &serde_json::Value) -> Result { + let alg_name = claims[SD_ALG_CLAIM_NAME] + .as_str() + .ok_or(Error::MissingSdAlg)?; + + SdAlg::try_from(alg_name) +} + +fn translate_to_in_progress_disclosures( + disclosures: &[&str], + sd_alg: SdAlg, +) -> Result, Error> { + let disclosure_vec: Result, Error> = disclosures + .iter() + .map(|disclosure| InProgressDisclosure::new(disclosure, sd_alg)) + .collect(); + + let disclosure_vec = disclosure_vec?; + + let mut disclosure_map = BTreeMap::new(); + for disclosure in disclosure_vec { + let prev = disclosure_map.insert(disclosure.hash.clone(), disclosure); + + if prev.is_some() { + return Err(Error::MultipleDisclosuresWithSameHash); + } + } + + Ok(disclosure_map) +} + +struct InProgressDisclosure { + decoded: DecodedDisclosure, + hash: String, + found: bool, +} + +impl InProgressDisclosure { + fn new(disclosure: &str, sd_alg: SdAlg) -> Result { + Ok(InProgressDisclosure { + decoded: DecodedDisclosure::new(disclosure)?, + hash: hash_encoded_disclosure(sd_alg, disclosure), + found: false, + }) + } +} + +fn visit_claims( + payload_claims: &mut serde_json::Value, + disclosures: &mut BTreeMap, +) -> Result<(), Error> { + let payload_claims = match payload_claims.as_object_mut() { + Some(obj) => obj, + None => return Ok(()), + }; + + // Visit children + for (_, child_claim) in payload_claims.iter_mut() { + visit_claims(child_claim, disclosures)? + } + + // Process _sd claim + let new_claims = if let Some(sd) = payload_claims[SD_CLAIM_NAME].as_array() { + decode_sd_claims(sd, disclosures)? + } else { + vec![] + }; + + if payload_claims.contains_key(SD_CLAIM_NAME) { + payload_claims.remove(SD_CLAIM_NAME); + } + + for (new_claim_name, mut new_claim_value) in new_claims { + visit_claims(&mut new_claim_value, disclosures)?; + + let prev = payload_claims.insert(new_claim_name, new_claim_value); + + if prev.is_some() { + return Err(Error::DisclosureClaimCollidesWithJwtClaim); + } + } + + // Process array claims + for (_, item) in payload_claims.iter_mut() { + if let Some(array) = item.as_array_mut() { + let new_array_items = decode_array_claims(array, disclosures)?; + *array = new_array_items; + } + } + + Ok(()) +} + +fn decode_sd_claims( + sd_claims: &Vec, + disclosures: &mut BTreeMap, +) -> Result, Error> { + let mut found_disclosures = vec![]; + for disclosure_hash in sd_claims { + let disclosure_hash = disclosure_hash.as_str().ok_or(Error::SdClaimNotString)?; + + if let Some(in_progress_disclosure) = disclosures.get_mut(disclosure_hash) { + if in_progress_disclosure.found { + return Err(Error::DisclosureUsedMultipleTimes); + } + in_progress_disclosure.found = true; + match in_progress_disclosure.decoded.kind { + DisclosureKind::ArrayItem(_) => { + return Err(Error::ArrayDisclosureWhenExpectingProperty) + } + DisclosureKind::Property { + ref name, + ref value, + } => found_disclosures.push((name.clone(), value.clone())), + } + } + } + + Ok(found_disclosures) +} + +fn decode_array_claims( + array: &[serde_json::Value], + disclosures: &mut BTreeMap, +) -> Result, Error> { + let mut new_items = vec![]; + for item in array.iter() { + if let Some(hash) = array_item_is_disclosure(item) { + if let Some(in_progress_disclosure) = disclosures.get_mut(hash) { + if in_progress_disclosure.found { + return Err(Error::DisclosureUsedMultipleTimes); + } + in_progress_disclosure.found = true; + match in_progress_disclosure.decoded.kind { + DisclosureKind::ArrayItem(ref value) => { + new_items.push(value.clone()); + } + DisclosureKind::Property { .. } => { + return Err(Error::PropertyDisclosureWhenExpectingArray) + } + } + } + } else { + new_items.push(item.clone()); + } + } + + Ok(new_items) +} + +fn array_item_is_disclosure(item: &serde_json::Value) -> Option<&str> { + let obj = item.as_object()?; + + if obj.len() != 1 { + return None; + } + + obj.get(ARRAY_CLAIM_ITEM_PROPERTY_NAME)?.as_str() +} diff --git a/ssi-sd-jwt/src/digest.rs b/ssi-sd-jwt/src/digest.rs new file mode 100644 index 000000000..23aff5fb6 --- /dev/null +++ b/ssi-sd-jwt/src/digest.rs @@ -0,0 +1,63 @@ +use jose_b64::base64ct::{Base64UrlUnpadded, Encoding}; +use sha2::Digest; + +use crate::Error; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum SdAlg { + Sha256, +} + +impl SdAlg { + const SHA256_STR: &'static str = "sha-256"; +} + +impl SdAlg { + pub fn to_str(&self) -> &'static str { + match self { + SdAlg::Sha256 => Self::SHA256_STR, + } + } +} + +impl TryFrom<&str> for SdAlg { + type Error = Error; + + fn try_from(value: &str) -> Result { + Ok(match value { + Self::SHA256_STR => SdAlg::Sha256, + other => return Err(Error::UnknownSdAlg(other.to_owned())), + }) + } +} + +impl From for &'static str { + fn from(value: SdAlg) -> Self { + value.to_str() + } +} + +pub fn hash_encoded_disclosure(digest_algo: SdAlg, disclosure: &str) -> String { + match digest_algo { + SdAlg::Sha256 => { + let digest = sha2::Sha256::digest(disclosure.as_bytes()); + Base64UrlUnpadded::encode_string(&digest) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_disclosure_hashing() { + assert_eq!( + hash_encoded_disclosure( + SdAlg::Sha256, + "WyI2cU1RdlJMNWhhaiIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0" + ), + "uutlBuYeMDyjLLTpf6Jxi7yNkEF35jdyWMn9U7b_RYY", + ); + } +} diff --git a/ssi-sd-jwt/src/encode.rs b/ssi-sd-jwt/src/encode.rs new file mode 100644 index 000000000..680f3a8b0 --- /dev/null +++ b/ssi-sd-jwt/src/encode.rs @@ -0,0 +1,177 @@ +use jose_b64::base64ct::{Base64UrlUnpadded, Encoding}; +use rand::{CryptoRng, Rng}; +use serde::Serialize; +use ssi_jwk::{Algorithm, JWK}; + +use crate::*; + +fn encode_disclosure_with_salt( + salt: &str, + claim_name: Option<&str>, + claim_value: &ClaimValue, +) -> Result { + let disclosure = match claim_name { + Some(claim_name) => serde_json::json!([salt, claim_name, claim_value]), + None => serde_json::json!([salt, claim_value]), + }; + + let json_bytes = jose_b64::serde::Json::::new(disclosure)?; + + Ok(Base64UrlUnpadded::encode_string(json_bytes.as_ref())) +} + +pub fn encode_disclosure_with_rng( + rng: &mut Rand, + claim_name: Option<&str>, + claim_value: &ClaimValue, +) -> Result { + // TODO: link to rfc wrt suggested bit size of salt + const DEFAULT_SALT_SIZE: usize = 128 / 8; + + let mut salt_bytes = [0u8; DEFAULT_SALT_SIZE]; + + rng.fill_bytes(&mut salt_bytes); + + let salt = Base64UrlUnpadded::encode_string(&salt_bytes); + + encode_disclosure_with_salt(&salt, claim_name, claim_value) +} + +pub fn encode_disclosure( + claim_name: Option<&str>, + claim_value: &ClaimValue, +) -> Result { + let mut rng = rand::rngs::OsRng {}; + encode_disclosure_with_rng(&mut rng, claim_name, claim_value) +} + +pub fn encode_sign( + algorithm: Algorithm, + base_claims: &Claims, + key: &JWK, + sd_alg: SdAlg, + disclosures: Vec, +) -> Result<(String, Vec), Error> { + let mut base_claims_json = serde_json::to_value(base_claims)?; + + let post_encoded_disclosures: Result, Error> = disclosures + .iter() + .map(|disclosure| { + let encoded = disclosure.encode()?; + let hash = hash_encoded_disclosure(sd_alg, &encoded); + Ok(PostEncodedDisclosure { + encoded, + hash, + unencoded: disclosure.clone(), + }) + }) + .collect(); + + let post_encoded_disclosures = post_encoded_disclosures?; + + { + let base_claims_obj = base_claims_json + .as_object_mut() + .ok_or(Error::EncodedAsNonObject)?; + + let prev_sd_alg = base_claims_obj.insert( + SD_ALG_CLAIM_NAME.to_owned(), + serde_json::json!(sd_alg.to_str()), + ); + + if prev_sd_alg.is_some() { + return Err(Error::EncodedClaimsContainsReservedProperty); + } + + let mut sd_claim = vec![]; + + for disclosure in post_encoded_disclosures.iter() { + match disclosure.unencoded { + UnencodedDisclosure::Claim(ref claim_name, _) => { + sd_claim.push(serde_json::Value::String(disclosure.hash.clone())); + base_claims_obj.remove(claim_name); + } + UnencodedDisclosure::ArrayItem(ref claim_name, _) => { + if !base_claims_obj.contains_key(claim_name) { + let _ = base_claims_obj.insert(claim_name.clone(), serde_json::json!([])); + } + + let array = base_claims_obj.get_mut(claim_name).unwrap(); + let array = array.as_array_mut().ok_or(Error::ExpectedArray)?; + + array.push(serde_json::json!({ARRAY_CLAIM_ITEM_PROPERTY_NAME: disclosure.hash.clone()})); + } + } + } + + let prev_sd = + base_claims_obj.insert(SD_CLAIM_NAME.to_owned(), serde_json::Value::Array(sd_claim)); + + if prev_sd.is_some() { + return Err(Error::EncodedClaimsContainsReservedProperty); + } + } + + let jwt = ssi_jwt::encode_sign(algorithm, &base_claims_json, key)?; + + Ok((jwt, post_encoded_disclosures)) +} + +#[derive(Clone, Debug)] +pub enum UnencodedDisclosure { + Claim(String, serde_json::Value), + ArrayItem(String, serde_json::Value), +} + +impl UnencodedDisclosure { + pub fn claim_value_as_ref(&self) -> &serde_json::Value { + match self { + UnencodedDisclosure::ArrayItem(_, value) => value, + UnencodedDisclosure::Claim(_, value) => value, + } + } + + pub fn encoded_claim_name(&self) -> Option<&str> { + match self { + UnencodedDisclosure::Claim(name, _) => Some(name), + UnencodedDisclosure::ArrayItem(_, _) => None, + } + } + + pub fn encode(&self) -> Result { + encode_disclosure(self.encoded_claim_name(), self.claim_value_as_ref()) + } +} + +#[derive(Debug)] +pub struct PostEncodedDisclosure { + pub encoded: String, + pub hash: String, + pub unencoded: UnencodedDisclosure, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_encode_disclosure() { + assert_eq!( + encode_disclosure_with_salt( + "_26bc4LT-ac6q2KI6cBW5es", + Some("family_name"), + &"Möbius".to_owned(), + ) + .unwrap(), + "WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsImZhbWlseV9uYW1lIiwiTcO2Yml1cyJd", + ) + } + + #[test] + fn test_encode_array_disclosure() { + assert_eq!( + encode_disclosure_with_salt("nPuoQnkRFq3BIeAm7AnXFA", None, &"DE".to_owned()).unwrap(), + "WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwiREUiXQ" + ) + } +} diff --git a/ssi-sd-jwt/src/lib.rs b/ssi-sd-jwt/src/lib.rs new file mode 100644 index 000000000..57503e293 --- /dev/null +++ b/ssi-sd-jwt/src/lib.rs @@ -0,0 +1,47 @@ +mod decode; +pub(crate) mod digest; +pub(crate) mod encode; +pub(crate) mod verify; + +pub use decode::{decode_verify, ValidityClaims}; +pub use digest::{hash_encoded_disclosure, SdAlg}; +pub use encode::{encode_disclosure, encode_disclosure_with_rng, encode_sign, UnencodedDisclosure}; +pub use verify::verify_sd_disclosures_array; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("JWT is missing _sd_alg property")] + MissingSdAlg, + #[error("Unknown value of _sd_alg {0}")] + UnknownSdAlg(String), + #[error("Multiple disclosures given with the same hash")] + MultipleDisclosuresWithSameHash, + #[error("An _sd claim wasn't a string")] + SdClaimNotString, + #[error("A disclosure claim would collid with an existing JWT claim")] + DisclosureClaimCollidesWithJwtClaim, + #[error("A disclosure didn't have the correct array length")] + DisclosureArrayLength, + #[error("A disclosure didn't contain the right types for elements")] + DisclosureHasWrongType, + #[error("A single disclosure was used multiple times")] + DisclosureUsedMultipleTimes, + #[error("Found an array item disclosure when expecting a property type")] + ArrayDisclosureWhenExpectingProperty, + #[error("Found a property type disclosure when expecting an array item")] + PropertyDisclosureWhenExpectingArray, + #[error("The base claims to encode did not become a JSON object")] + EncodedAsNonObject, + #[error("The base claims to encode contained a property reserved by SD-JWT")] + EncodedClaimsContainsReservedProperty, + #[error("A property for an array sd claim was not an array")] + ExpectedArray, + #[error(transparent)] + JWS(#[from] ssi_jws::Error), + #[error(transparent)] + JsonSerialization(#[from] serde_json::Error), +} + +const SD_CLAIM_NAME: &str = "_sd"; +const SD_ALG_CLAIM_NAME: &str = "_sd_alg"; +const ARRAY_CLAIM_ITEM_PROPERTY_NAME: &str = "..."; diff --git a/ssi-sd-jwt/src/verify.rs b/ssi-sd-jwt/src/verify.rs new file mode 100644 index 000000000..241e65d42 --- /dev/null +++ b/ssi-sd-jwt/src/verify.rs @@ -0,0 +1,196 @@ +use jose_b64::base64ct::{Base64UrlUnpadded, Encoding}; + +use crate::digest::{hash_encoded_disclosure, SdAlg}; +use crate::Error; + +#[derive(Debug, PartialEq)] +pub struct DecodedDisclosure { + pub salt: String, + pub kind: DisclosureKind, +} + +#[derive(Debug, PartialEq)] +pub enum DisclosureKind { + Property { + name: String, + value: serde_json::Value, + }, + ArrayItem(serde_json::Value), +} + +impl DecodedDisclosure { + pub fn new(encoded: &str) -> Result { + let bytes = Base64UrlUnpadded::decode_vec(encoded).unwrap(); + let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); + + match json { + serde_json::Value::Array(values) => match values.len() { + 3 => validate_property_disclosure(&values), + 2 => validate_array_item_disclosure(&values), + _ => Err(Error::DisclosureArrayLength), + }, + _ => todo!("handle other json: {:?}", json), + } + } +} + +fn validate_property_disclosure(values: &[serde_json::Value]) -> Result { + let salt = values[0].as_str().ok_or(Error::DisclosureHasWrongType)?; + + let name = values[1].as_str().ok_or(Error::DisclosureHasWrongType)?; + + Ok(DecodedDisclosure { + salt: salt.to_owned(), + kind: DisclosureKind::Property { + name: name.to_owned(), + value: values[2].clone(), + }, + }) +} + +fn validate_array_item_disclosure( + values: &[serde_json::Value], +) -> Result { + let salt = values[0].as_str().ok_or(Error::DisclosureHasWrongType)?; + + Ok(DecodedDisclosure { + salt: salt.to_owned(), + kind: DisclosureKind::ArrayItem(values[1].clone()), + }) +} + +pub fn verify_sd_disclosures_array( + digest_algo: SdAlg, + disclosures: &[&str], + sd_claim: &[&str], +) -> Result { + let mut verfied_claims = serde_json::Map::new(); + + for disclosure in disclosures { + let disclosure_hash = hash_encoded_disclosure(digest_algo, disclosure); + + if !disclosure_hash_exists_in_sd_claims(&disclosure_hash, sd_claim) { + continue; + } + + let decoded = DecodedDisclosure::new(disclosure)?; + + match decoded.kind { + DisclosureKind::Property { name, value } => { + let orig = verfied_claims.insert(name, value); + + if let Some(orig) = orig { + todo!( + "handle multiple claims with the same property name: {:?}", + orig, + ) + } + } + DisclosureKind::ArrayItem(_) => { + todo!("array item disclouse in sd claims: {:?}", decoded) + } + } + } + + Ok(serde_json::Value::Object(verfied_claims)) +} + +fn disclosure_hash_exists_in_sd_claims(disclosure_hash: &str, sd_claim: &[&str]) -> bool { + // Todo: Yeah, this is O(N^2) since it's embedded in the for loop in + // verify_disclosures(). I'm expecting small values of N for sd_claim + // where it's just easier to check them rather than + // going through the rigmarole of adding them to map structure beforehand. + // Validate this though. + for sd_claim_item in sd_claim { + // Todo: Does this need to be constant time? I can't think of a reason + // given that sd_claims are ostensibly public anyway, but probably + // should just to be safe. + if &disclosure_hash == sd_claim_item { + return true; + } + } + + false +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_verify_disclosures() { + const DISCLOSURES: [&str; 7] = [ + "WyJyU0x1em5oaUxQQkRSWkUxQ1o4OEtRIiwgInN1YiIsICJqb2huX2RvZV80MiJd", + "WyJhYTFPYmdlUkJnODJudnpMYnRQTklRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd", + "WyI2VWhsZU5HUmJtc0xDOFRndTh2OFdnIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd", + "WyJ2S0t6alFSOWtsbFh2OWVkNUJ1ZHZRIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ", + "WyJVZEVmXzY0SEN0T1BpZDRFZmhPQWNRIiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ", + "WyJOYTNWb0ZGblZ3MjhqT0FyazdJTlZnIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0", + "WyJkQW9mNHNlZTFGdDBXR2dHanVjZ2pRIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0", + ]; + + const SD_CLAIM: [&str; 7] = [ + "5nXy0Z3QiEba1V1lJzeKhAOGQXFlKLIWCLlhf_O-cmo", + "9gZhHAhV7LZnOFZq_q7Fh8rzdqrrNM-hRWsVOlW3nuw", + "S-JPBSkvqliFv1__thuXt3IzX5B_ZXm4W2qs4BoNFrA", + "bviw7pWAkbzI078ZNVa_eMZvk0tdPa5w2o9R3Zycjo4", + "o-LBCDrFF6tC9ew1vAlUmw6Y30CHZF5jOUFhpx5mogI", + "pzkHIM9sv7oZH6YKDsRqNgFGLpEKIj3c5G6UKaTsAjQ", + "rnAzCT6DTy4TsX9QCDv2wwAE4Ze20uRigtVNQkA52X0", + ]; + + let expected_claims: serde_json::Value = serde_json::json!({ + "sub": "john_doe_42", + "given_name": "John", + "family_name": "Doe", + "email": "johndoe@example.com", + "phone_number": "+1-202-555-0101", + "address": {"street_address": "123 Main St", "locality": "Anytown", "region": "Anystate", "country": "US"}, + "birthdate": "1940-01-01" + }); + + let verified_claims = + verify_sd_disclosures_array(SdAlg::Sha256, &DISCLOSURES, &SD_CLAIM).unwrap(); + + assert_eq!(verified_claims, expected_claims) + } + + #[test] + fn test_verify_subset_of_disclosures() { + const DISCLOSURES: [&str; 2] = [ + "WyJhYTFPYmdlUkJnODJudnpMYnRQTklRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd", + "WyI2VWhsZU5HUmJtc0xDOFRndTh2OFdnIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd", + ]; + + const SD_CLAIM: [&str; 7] = [ + "5nXy0Z3QiEba1V1lJzeKhAOGQXFlKLIWCLlhf_O-cmo", + "9gZhHAhV7LZnOFZq_q7Fh8rzdqrrNM-hRWsVOlW3nuw", + "S-JPBSkvqliFv1__thuXt3IzX5B_ZXm4W2qs4BoNFrA", + "bviw7pWAkbzI078ZNVa_eMZvk0tdPa5w2o9R3Zycjo4", + "o-LBCDrFF6tC9ew1vAlUmw6Y30CHZF5jOUFhpx5mogI", + "pzkHIM9sv7oZH6YKDsRqNgFGLpEKIj3c5G6UKaTsAjQ", + "rnAzCT6DTy4TsX9QCDv2wwAE4Ze20uRigtVNQkA52X0", + ]; + + let expected_claims: serde_json::Value = serde_json::json!({ + "given_name": "John", + "family_name": "Doe", + }); + + let verified_claims = + verify_sd_disclosures_array(SdAlg::Sha256, &DISCLOSURES, &SD_CLAIM).unwrap(); + + assert_eq!(verified_claims, expected_claims) + } + + #[test] + fn decode_array_disclosure() { + assert_eq!( + DecodedDisclosure { + salt: "nPuoQnkRFq3BIeAm7AnXFA".to_owned(), + kind: DisclosureKind::ArrayItem(serde_json::json!("DE")) + }, + DecodedDisclosure::new("WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIkRFIl0").unwrap() + ) + } +} diff --git a/ssi-sd-jwt/tests/decode.rs b/ssi-sd-jwt/tests/decode.rs new file mode 100644 index 000000000..fc5b99e3e --- /dev/null +++ b/ssi-sd-jwt/tests/decode.rs @@ -0,0 +1,135 @@ +use serde::{Deserialize, Serialize}; +use ssi_jwk::{Algorithm, JWK}; +use ssi_jwt::NumericDate; +use ssi_sd_jwt::{decode_verify, ValidityClaims}; + +#[derive(Debug, Default, Deserialize, Serialize, PartialEq)] +struct ExampleClaims { + sub: Option, + given_name: Option, + family_name: Option, + email: Option, + phone_number: Option, + phone_number_verified: Option, + address: Option, + birthdate: Option, + updated_at: Option, + nationalities: Option>, +} + +#[derive(Debug, Default, Deserialize, Serialize, PartialEq)] +struct AddressClaim { + street_address: Option, + locality: Option, + region: Option, + country: Option, +} + +fn test_key() -> JWK { + serde_json::from_value(serde_json::json!({ + "kty": "EC", + "d": "oYVImrMZjUclmWuhqa6bjzqGx5HFkbx76_00oWUHiLw", + "use": "sig", + "crv": "P-256", + "kid": "rpaXW8yADRnS2150CdsMtftwxtzSiVTV9bgHHG86v-E", + "x": "UX7TC8uQ9sn06c3DxXy1Ua5V9BK-cb9fQfukVrCLD8s", + "y": "yNXRKOnwBMTx536uajfNHklxpG9bAbdLlmVn6-XuK0Q", + "alg": "ES256" + })) + .unwrap() +} + +fn test_standard_sd_jwt() -> String { + let key = test_key(); + let claims = serde_json::json!({ + "_sd": [ + "CrQe7S5kqBAHt-nMYXgc6bdt2SH5aTY1sU_M-PgkjPI", + "JzYjH4svliH0R3PyEMfeZu6Jt69u5qehZo7F7EPYlSE", + "PorFbpKuVu6xymJagvkFsFXAbRoc2JGlAUA2BA4o7cI", + "TGf4oLbgwd5JQaHyKVQZU9UdGE0w5rtDsrZzfUaomLo", + "XQ_3kPKt1XyX7KANkqVR6yZ2Va5NrPIvPYbyMvRKBMM", + "XzFrzwscM6Gn6CJDc6vVK8BkMnfG8vOSKfpPIZdAfdE", + "gbOsI4Edq2x2Kw-w5wPEzakob9hV1cRD0ATN3oQL9JM", + "jsu9yVulwQQlhFlM_3JlzMaSFzglhQG0DpfayQwLUK4" + ], + "iss": "https://example.com/issuer", + "iat": 1683000000, + "exp": 1883000000, + "sub": "user_42", + "nationalities": [ + { "...": "pFndjkZ_VCzmyTa6UjlZo3dh-ko8aIKQc9DlGzhaVYo" }, + { "...": "7Cf6JkPudry3lcbwHgeZ8khAv1U1OSlerP0VkBJrWZ0" } + ], + "_sd_alg": "sha-256" + }); + + ssi_jwt::encode_sign(Algorithm::ES256, &claims, &key).unwrap() +} + +// *Claim email*: +// * SHA-256 Hash: JzYjH4svliH0R3PyEMfeZu6Jt69u5qehZo7F7EPYlSE +// * Disclosure: +// WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VA +// ZXhhbXBsZS5jb20iXQ +// * Contents: ["6Ij7tM-a5iVPGboS5tmvVA", "email", +// "johndoe@example.com"] +const EMAIL_DISCLOSURE: &'static str = + "WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ"; + +// *Array Entry*: +// * SHA-256 Hash: 7Cf6JkPudry3lcbwHgeZ8khAv1U1OSlerP0VkBJrWZ0 +// * Disclosure: +// WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIkRFIl0 +// * Contents: ["nPuoQnkRFq3BIeAm7AnXFA", "DE"] +const NATIONALITY_DE_DISCLOSURE: &'static str = "WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIkRFIl0"; + +#[test] +fn decode_single() { + let (_, claims) = + decode_verify::(&test_standard_sd_jwt(), &test_key(), &[EMAIL_DISCLOSURE]) + .unwrap(); + + assert_eq!( + claims, + ExampleClaims { + sub: Some("user_42".to_owned()), + email: Some("johndoe@example.com".to_owned()), + nationalities: Some(vec![]), + ..Default::default() + }, + ) +} + +#[test] +fn decode_single_array_item() { + let (_, claims) = decode_verify::( + &test_standard_sd_jwt(), + &test_key(), + &[NATIONALITY_DE_DISCLOSURE], + ) + .unwrap(); + + assert_eq!( + claims, + ExampleClaims { + sub: Some("user_42".to_owned()), + nationalities: Some(vec!["DE".to_owned()]), + ..Default::default() + }, + ) +} + +#[test] +fn validitity_decodes() { + let (validity, _) = + decode_verify::(&test_standard_sd_jwt(), &test_key(), &[]).unwrap(); + + assert_eq!( + validity, + ValidityClaims { + nbf: None, + iat: Some(NumericDate::try_from_seconds(1683000000.0).unwrap()), + exp: Some(NumericDate::try_from_seconds(1883000000.0).unwrap()), + } + ) +} diff --git a/ssi-sd-jwt/tests/full_pathway.rs b/ssi-sd-jwt/tests/full_pathway.rs new file mode 100644 index 000000000..f9130fea8 --- /dev/null +++ b/ssi-sd-jwt/tests/full_pathway.rs @@ -0,0 +1,120 @@ +use serde::{Deserialize, Serialize}; +use ssi_jwk::{Algorithm, JWK}; +use ssi_sd_jwt::*; + +fn test_key() -> JWK { + serde_json::from_value(serde_json::json!({ + "kty": "EC", + "d": "oYVImrMZjUclmWuhqa6bjzqGx5HFkbx76_00oWUHiLw", + "use": "sig", + "crv": "P-256", + "kid": "rpaXW8yADRnS2150CdsMtftwxtzSiVTV9bgHHG86v-E", + "x": "UX7TC8uQ9sn06c3DxXy1Ua5V9BK-cb9fQfukVrCLD8s", + "y": "yNXRKOnwBMTx536uajfNHklxpG9bAbdLlmVn6-XuK0Q", + "alg": "ES256" + })) + .unwrap() +} + +#[test] +fn full_pathway_regular_claim() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct BaseClaims { + sub: String, + disclosure0: Option, + disclosure1: Option, + } + + let base_claims = BaseClaims { + sub: "user".to_owned(), + disclosure0: None, + disclosure1: None, + }; + + let (jwt, disclosures) = encode_sign( + Algorithm::ES256, + &base_claims, + &test_key(), + SdAlg::Sha256, + vec![ + UnencodedDisclosure::Claim("disclosure0".to_owned(), serde_json::json!("value0")), + UnencodedDisclosure::Claim("disclosure1".to_owned(), serde_json::json!("value1")), + ], + ) + .unwrap(); + + let (_, full_jwt_claims) = decode_verify::( + &jwt, + &test_key(), + &[&disclosures[0].encoded, &disclosures[1].encoded], + ) + .unwrap(); + + assert_eq!( + BaseClaims { + sub: "user".to_owned(), + disclosure0: Some("value0".to_owned()), + disclosure1: Some("value1".to_owned()), + }, + full_jwt_claims, + ); + + let (_, one_sd_claim) = + decode_verify::(&jwt, &test_key(), &[&disclosures[1].encoded]).unwrap(); + + assert_eq!( + BaseClaims { + sub: "user".to_owned(), + disclosure0: None, + disclosure1: Some("value1".to_owned()) + }, + one_sd_claim, + ); +} + +#[test] +fn full_pathway_array() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct BaseClaims { + sub: String, + array_disclosure: Vec, + } + + let base_claims = BaseClaims { + sub: "user".to_owned(), + array_disclosure: vec![], + }; + + let (jwt, disclosures) = encode_sign( + Algorithm::ES256, + &base_claims, + &test_key(), + SdAlg::Sha256, + vec![ + UnencodedDisclosure::ArrayItem( + "array_disclosure".to_owned(), + serde_json::json!("value0"), + ), + UnencodedDisclosure::ArrayItem( + "array_disclosure".to_owned(), + serde_json::json!("value1"), + ), + ], + ) + .unwrap(); + + let (_, full_jwt_claims) = decode_verify::( + &jwt, + &test_key(), + &[&disclosures[0].encoded, &disclosures[1].encoded], + ) + .unwrap(); + + assert_eq!( + BaseClaims { + sub: "user".to_owned(), + array_disclosure: vec!["value0".to_owned(), "value1".to_owned()], + }, + full_jwt_claims + ); +} From edaff1eb0e7f00310e618ab8a881864b33a5db51 Mon Sep 17 00:00:00 2001 From: Tristan Miller Date: Wed, 27 Sep 2023 09:01:15 -0600 Subject: [PATCH 02/16] Expose construction API --- ssi-sd-jwt/src/decode.rs | 32 ++- ssi-sd-jwt/src/encode.rs | 57 ++++- ssi-sd-jwt/src/lib.rs | 20 +- ssi-sd-jwt/src/serialized.rs | 117 ++++++++++ ssi-sd-jwt/tests/full_pathway.rs | 113 ++++++++- ssi-sd-jwt/tests/rfc_examples.rs | 380 +++++++++++++++++++++++++++++++ 6 files changed, 695 insertions(+), 24 deletions(-) create mode 100644 ssi-sd-jwt/src/serialized.rs create mode 100644 ssi-sd-jwt/tests/rfc_examples.rs diff --git a/ssi-sd-jwt/src/decode.rs b/ssi-sd-jwt/src/decode.rs index a01f5b4ed..76da750c8 100644 --- a/ssi-sd-jwt/src/decode.rs +++ b/ssi-sd-jwt/src/decode.rs @@ -4,6 +4,7 @@ use ssi_jwk::JWK; use ssi_jwt::NumericDate; use std::collections::BTreeMap; +use crate::serialized::deserialize_string_format; use crate::verify::{DecodedDisclosure, DisclosureKind}; use crate::*; @@ -14,6 +15,16 @@ pub struct ValidityClaims { pub exp: Option, } +pub fn decode_verify_string_format( + serialized: &str, + key: &JWK, +) -> Result<(ValidityClaims, Claims), Error> { + let deserialized = + deserialize_string_format(serialized).ok_or(Error::UnableToDeserializeStringFormat)?; + + decode_verify(deserialized.jwt, key, &deserialized.disclosures) +} + pub fn decode_verify( jwt: &str, key: &JWK, @@ -33,6 +44,12 @@ pub fn decode_verify( visit_claims(&mut payload_claims, &mut disclosures)?; + for (_, disclosure) in disclosures { + if !disclosure.found { + return Err(Error::UnusedDisclosure); + } + } + Ok((validity_claims, serde_json::from_value(payload_claims)?)) } @@ -67,6 +84,7 @@ fn translate_to_in_progress_disclosures( Ok(disclosure_map) } +#[derive(Debug)] struct InProgressDisclosure { decoded: DecodedDisclosure, hash: String, @@ -98,8 +116,8 @@ fn visit_claims( } // Process _sd claim - let new_claims = if let Some(sd) = payload_claims[SD_CLAIM_NAME].as_array() { - decode_sd_claims(sd, disclosures)? + let new_claims = if let Some(sd_claims) = payload_claims.get(SD_CLAIM_NAME) { + decode_sd_claims(sd_claims, disclosures)? } else { vec![] }; @@ -121,7 +139,12 @@ fn visit_claims( // Process array claims for (_, item) in payload_claims.iter_mut() { if let Some(array) = item.as_array_mut() { - let new_array_items = decode_array_claims(array, disclosures)?; + let mut new_array_items = decode_array_claims(array, disclosures)?; + + for item in new_array_items.iter_mut() { + visit_claims(item, disclosures)?; + } + *array = new_array_items; } } @@ -130,9 +153,10 @@ fn visit_claims( } fn decode_sd_claims( - sd_claims: &Vec, + sd_claims: &serde_json::Value, disclosures: &mut BTreeMap, ) -> Result, Error> { + let sd_claims = sd_claims.as_array().ok_or(Error::SdPropertyNotArray)?; let mut found_disclosures = vec![]; for disclosure_hash in sd_claims { let disclosure_hash = disclosure_hash.as_str().ok_or(Error::SdClaimNotString)?; diff --git a/ssi-sd-jwt/src/encode.rs b/ssi-sd-jwt/src/encode.rs index 680f3a8b0..15c38964f 100644 --- a/ssi-sd-jwt/src/encode.rs +++ b/ssi-sd-jwt/src/encode.rs @@ -37,7 +37,7 @@ pub fn encode_disclosure_with_rng( encode_disclosure_with_salt(&salt, claim_name, claim_value) } -pub fn encode_disclosure( +fn encode_disclosure( claim_name: Option<&str>, claim_value: &ClaimValue, ) -> Result { @@ -45,13 +45,34 @@ pub fn encode_disclosure( encode_disclosure_with_rng(&mut rng, claim_name, claim_value) } +pub fn encode_property_disclosure( + sd_alg: SdAlg, + claim_name: &str, + claim_value: &ClaimValue, +) -> Result { + let encoded = encode_disclosure(Some(claim_name), claim_value)?; + let hash = hash_encoded_disclosure(sd_alg, &encoded); + + Ok(Disclosure { encoded, hash }) +} + +pub fn encode_array_disclosure( + sd_alg: SdAlg, + claim_value: &ClaimValue, +) -> Result { + let encoded = encode_disclosure(None, claim_value)?; + let hash = hash_encoded_disclosure(sd_alg, &encoded); + + Ok(Disclosure { encoded, hash }) +} + pub fn encode_sign( algorithm: Algorithm, base_claims: &Claims, key: &JWK, sd_alg: SdAlg, disclosures: Vec, -) -> Result<(String, Vec), Error> { +) -> Result<(String, Vec), Error> { let mut base_claims_json = serde_json::to_value(base_claims)?; let post_encoded_disclosures: Result, Error> = disclosures @@ -59,7 +80,7 @@ pub fn encode_sign( .map(|disclosure| { let encoded = disclosure.encode()?; let hash = hash_encoded_disclosure(sd_alg, &encoded); - Ok(PostEncodedDisclosure { + Ok(FullDisclosure { encoded, hash, unencoded: disclosure.clone(), @@ -87,7 +108,7 @@ pub fn encode_sign( for disclosure in post_encoded_disclosures.iter() { match disclosure.unencoded { - UnencodedDisclosure::Claim(ref claim_name, _) => { + UnencodedDisclosure::Property(ref claim_name, _) => { sd_claim.push(serde_json::Value::String(disclosure.hash.clone())); base_claims_obj.remove(claim_name); } @@ -119,21 +140,41 @@ pub fn encode_sign( #[derive(Clone, Debug)] pub enum UnencodedDisclosure { - Claim(String, serde_json::Value), + Property(String, serde_json::Value), ArrayItem(String, serde_json::Value), } impl UnencodedDisclosure { + pub fn new_property, Value: Serialize>( + name: S, + value: &Value, + ) -> Result { + Ok(UnencodedDisclosure::Property( + name.as_ref().to_owned(), + serde_json::to_value(value)?, + )) + } + + pub fn new_array_item, Value: Serialize>( + parent: S, + value: &Value, + ) -> Result { + Ok(UnencodedDisclosure::ArrayItem( + parent.as_ref().to_owned(), + serde_json::to_value(value)?, + )) + } + pub fn claim_value_as_ref(&self) -> &serde_json::Value { match self { UnencodedDisclosure::ArrayItem(_, value) => value, - UnencodedDisclosure::Claim(_, value) => value, + UnencodedDisclosure::Property(_, value) => value, } } pub fn encoded_claim_name(&self) -> Option<&str> { match self { - UnencodedDisclosure::Claim(name, _) => Some(name), + UnencodedDisclosure::Property(name, _) => Some(name), UnencodedDisclosure::ArrayItem(_, _) => None, } } @@ -144,7 +185,7 @@ impl UnencodedDisclosure { } #[derive(Debug)] -pub struct PostEncodedDisclosure { +pub struct FullDisclosure { pub encoded: String, pub hash: String, pub unencoded: UnencodedDisclosure, diff --git a/ssi-sd-jwt/src/lib.rs b/ssi-sd-jwt/src/lib.rs index 57503e293..52a6dc59d 100644 --- a/ssi-sd-jwt/src/lib.rs +++ b/ssi-sd-jwt/src/lib.rs @@ -1,15 +1,21 @@ mod decode; pub(crate) mod digest; pub(crate) mod encode; +pub(crate) mod serialized; pub(crate) mod verify; -pub use decode::{decode_verify, ValidityClaims}; +pub use decode::{decode_verify, decode_verify_string_format, ValidityClaims}; pub use digest::{hash_encoded_disclosure, SdAlg}; -pub use encode::{encode_disclosure, encode_disclosure_with_rng, encode_sign, UnencodedDisclosure}; +pub use encode::{ + encode_array_disclosure, encode_property_disclosure, encode_sign, UnencodedDisclosure, +}; +pub use serialized::{deserialize_string_format, serialize_string_format}; pub use verify::verify_sd_disclosures_array; #[derive(thiserror::Error, Debug)] pub enum Error { + #[error("Unable to deserialize string format of concatenated tildes")] + UnableToDeserializeStringFormat, #[error("JWT is missing _sd_alg property")] MissingSdAlg, #[error("Unknown value of _sd_alg {0}")] @@ -18,6 +24,8 @@ pub enum Error { MultipleDisclosuresWithSameHash, #[error("An _sd claim wasn't a string")] SdClaimNotString, + #[error("And _sd property was not an array type")] + SdPropertyNotArray, #[error("A disclosure claim would collid with an existing JWT claim")] DisclosureClaimCollidesWithJwtClaim, #[error("A disclosure didn't have the correct array length")] @@ -36,6 +44,8 @@ pub enum Error { EncodedClaimsContainsReservedProperty, #[error("A property for an array sd claim was not an array")] ExpectedArray, + #[error("A disclosure was not used during decoding")] + UnusedDisclosure, #[error(transparent)] JWS(#[from] ssi_jws::Error), #[error(transparent)] @@ -45,3 +55,9 @@ pub enum Error { const SD_CLAIM_NAME: &str = "_sd"; const SD_ALG_CLAIM_NAME: &str = "_sd_alg"; const ARRAY_CLAIM_ITEM_PROPERTY_NAME: &str = "..."; + +#[derive(Debug)] +pub struct Disclosure { + pub encoded: String, + pub hash: String, +} diff --git a/ssi-sd-jwt/src/serialized.rs b/ssi-sd-jwt/src/serialized.rs new file mode 100644 index 000000000..f81aa881f --- /dev/null +++ b/ssi-sd-jwt/src/serialized.rs @@ -0,0 +1,117 @@ +pub fn serialize_string_format(jwt: &str, disclosures: &[&str]) -> String { + let mut serialized = format!("{}~", jwt); + + for disclosure in disclosures { + serialized = format!("{}{}~", serialized, disclosure) + } + + serialized +} + +#[derive(Debug, PartialEq)] +pub struct Deserialized<'a> { + pub jwt: &'a str, + pub disclosures: Vec<&'a str>, +} + +impl<'a> Deserialized<'a> { + pub fn serialize(&self) -> String { + serialize_string_format(self.jwt, &self.disclosures) + } +} + +pub fn deserialize_string_format(serialized: &str) -> Option> { + if !serialized.contains('~') { + return None; + } + let mut items = serialized.split('~'); + let jwt = items.next()?; + + let mut disclosures: Vec<_> = items.collect(); + + // Remove Key Binding JWT + if disclosures.len() > 1 { + disclosures.pop(); + } + + Some(Deserialized { jwt, disclosures }) +} + +#[cfg(test)] +mod tests { + use super::*; + + const ENCODED: &str = concat!( + "eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIkM5aW5wNllvUmFFWFI0Mjd6WUpQN1Fya", + "zFXSF84YmR3T0FfWVVyVW5HUVUiLCAiS3VldDF5QWEwSElRdlluT1ZkNTloY1ZpTzlVZ", + "zZKMmtTZnFZUkJlb3d2RSIsICJNTWxkT0ZGekIyZDB1bWxtcFRJYUdlcmhXZFVfUHBZZ", + "kx2S2hoX2ZfOWFZIiwgIlg2WkFZT0lJMnZQTjQwVjd4RXhad1Z3ejd5Um1MTmNWd3Q1R", + "Ew4Ukx2NGciLCAiWTM0em1JbzBRTExPdGRNcFhHd2pCZ0x2cjE3eUVoaFlUMEZHb2ZSL", + "WFJRSIsICJmeUdwMFdUd3dQdjJKRFFsbjFsU2lhZW9iWnNNV0ExMGJRNTk4OS05RFRzI", + "iwgIm9tbUZBaWNWVDhMR0hDQjB1eXd4N2ZZdW8zTUhZS08xNWN6LVJaRVlNNVEiLCAic", + "zBCS1lzTFd4UVFlVTh0VmxsdE03TUtzSVJUckVJYTFQa0ptcXhCQmY1VSJdLCAiaXNzI", + "jogImh0dHBzOi8vZXhhbXBsZS5jb20vaXNzdWVyIiwgImlhdCI6IDE2ODMwMDAwMDAsI", + "CJleHAiOiAxODgzMDAwMDAwLCAiYWRkcmVzcyI6IHsiX3NkIjogWyI2YVVoelloWjdTS", + "jFrVm1hZ1FBTzN1MkVUTjJDQzFhSGhlWnBLbmFGMF9FIiwgIkF6TGxGb2JrSjJ4aWF1c", + "FJFUHlvSnotOS1OU2xkQjZDZ2pyN2ZVeW9IemciLCAiUHp6Y1Z1MHFiTXVCR1NqdWxmZ", + "Xd6a2VzRDl6dXRPRXhuNUVXTndrclEtayIsICJiMkRrdzBqY0lGOXJHZzhfUEY4WmN2b", + "mNXN3p3Wmo1cnlCV3ZYZnJwemVrIiwgImNQWUpISVo4VnUtZjlDQ3lWdWIyVWZnRWs4a", + "nZ2WGV6d0sxcF9KbmVlWFEiLCAiZ2xUM2hyU1U3ZlNXZ3dGNVVEWm1Xd0JUdzMyZ25Vb", + "GRJaGk4aEdWQ2FWNCIsICJydkpkNmlxNlQ1ZWptc0JNb0d3dU5YaDlxQUFGQVRBY2k0M", + "G9pZEVlVnNBIiwgInVOSG9XWWhYc1poVkpDTkUyRHF5LXpxdDd0NjlnSkt5NVFhRnY3R", + "3JNWDQiXX0sICJfc2RfYWxnIjogInNoYS0yNTYifQ.rFsowW-KSZe7EITlWsGajR9nnG", + "BLlQ78qgtdGIZg3FZuZnxtapP0H8CUMnffJAwPQJmGnpFpulTkLWHiI1kMmw~WyJHMDJ", + "OU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICJcdTZlMmZcdTUzM2EiXQ~WyJs", + "a2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiSlAiXQ~" + ); + + const JWT: &str = concat!( + "eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIkM5aW5wNllvUmFFWFI0Mjd6WUpQN1Fya", + "zFXSF84YmR3T0FfWVVyVW5HUVUiLCAiS3VldDF5QWEwSElRdlluT1ZkNTloY1ZpTzlVZ", + "zZKMmtTZnFZUkJlb3d2RSIsICJNTWxkT0ZGekIyZDB1bWxtcFRJYUdlcmhXZFVfUHBZZ", + "kx2S2hoX2ZfOWFZIiwgIlg2WkFZT0lJMnZQTjQwVjd4RXhad1Z3ejd5Um1MTmNWd3Q1R", + "Ew4Ukx2NGciLCAiWTM0em1JbzBRTExPdGRNcFhHd2pCZ0x2cjE3eUVoaFlUMEZHb2ZSL", + "WFJRSIsICJmeUdwMFdUd3dQdjJKRFFsbjFsU2lhZW9iWnNNV0ExMGJRNTk4OS05RFRzI", + "iwgIm9tbUZBaWNWVDhMR0hDQjB1eXd4N2ZZdW8zTUhZS08xNWN6LVJaRVlNNVEiLCAic", + "zBCS1lzTFd4UVFlVTh0VmxsdE03TUtzSVJUckVJYTFQa0ptcXhCQmY1VSJdLCAiaXNzI", + "jogImh0dHBzOi8vZXhhbXBsZS5jb20vaXNzdWVyIiwgImlhdCI6IDE2ODMwMDAwMDAsI", + "CJleHAiOiAxODgzMDAwMDAwLCAiYWRkcmVzcyI6IHsiX3NkIjogWyI2YVVoelloWjdTS", + "jFrVm1hZ1FBTzN1MkVUTjJDQzFhSGhlWnBLbmFGMF9FIiwgIkF6TGxGb2JrSjJ4aWF1c", + "FJFUHlvSnotOS1OU2xkQjZDZ2pyN2ZVeW9IemciLCAiUHp6Y1Z1MHFiTXVCR1NqdWxmZ", + "Xd6a2VzRDl6dXRPRXhuNUVXTndrclEtayIsICJiMkRrdzBqY0lGOXJHZzhfUEY4WmN2b", + "mNXN3p3Wmo1cnlCV3ZYZnJwemVrIiwgImNQWUpISVo4VnUtZjlDQ3lWdWIyVWZnRWs4a", + "nZ2WGV6d0sxcF9KbmVlWFEiLCAiZ2xUM2hyU1U3ZlNXZ3dGNVVEWm1Xd0JUdzMyZ25Vb", + "GRJaGk4aEdWQ2FWNCIsICJydkpkNmlxNlQ1ZWptc0JNb0d3dU5YaDlxQUFGQVRBY2k0M", + "G9pZEVlVnNBIiwgInVOSG9XWWhYc1poVkpDTkUyRHF5LXpxdDd0NjlnSkt5NVFhRnY3R", + "3JNWDQiXX0sICJfc2RfYWxnIjogInNoYS0yNTYifQ.rFsowW-KSZe7EITlWsGajR9nnG", + "BLlQ78qgtdGIZg3FZuZnxtapP0H8CUMnffJAwPQJmGnpFpulTkLWHiI1kMmw" + ); + + const DISCLOSURE_0: &str = + "WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICJcdTZlMmZcdTUzM2EiXQ"; + const DISCLOSURE_1: &str = "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiSlAiXQ"; + + #[test] + fn serialize() { + assert_eq!( + serialize_string_format(JWT, &[DISCLOSURE_0, DISCLOSURE_1]), + ENCODED, + ) + } + + #[test] + fn deserialize() { + assert_eq!( + deserialize_string_format(ENCODED), + Some(Deserialized { + jwt: JWT, + disclosures: vec![DISCLOSURE_0, DISCLOSURE_1], + }) + ) + } + + #[test] + fn deserialize_fails_with_emtpy() { + assert_eq!(deserialize_string_format(""), None) + } +} diff --git a/ssi-sd-jwt/tests/full_pathway.rs b/ssi-sd-jwt/tests/full_pathway.rs index f9130fea8..53e40ff3c 100644 --- a/ssi-sd-jwt/tests/full_pathway.rs +++ b/ssi-sd-jwt/tests/full_pathway.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use serde_json::json; use ssi_jwk::{Algorithm, JWK}; use ssi_sd_jwt::*; @@ -37,8 +38,8 @@ fn full_pathway_regular_claim() { &test_key(), SdAlg::Sha256, vec![ - UnencodedDisclosure::Claim("disclosure0".to_owned(), serde_json::json!("value0")), - UnencodedDisclosure::Claim("disclosure1".to_owned(), serde_json::json!("value1")), + UnencodedDisclosure::new_property("disclosure0", &json!("value0")).unwrap(), + UnencodedDisclosure::new_property("disclosure1", &json!("value1")).unwrap(), ], ) .unwrap(); @@ -91,14 +92,8 @@ fn full_pathway_array() { &test_key(), SdAlg::Sha256, vec![ - UnencodedDisclosure::ArrayItem( - "array_disclosure".to_owned(), - serde_json::json!("value0"), - ), - UnencodedDisclosure::ArrayItem( - "array_disclosure".to_owned(), - serde_json::json!("value1"), - ), + UnencodedDisclosure::new_array_item("array_disclosure", &json!("value0")).unwrap(), + UnencodedDisclosure::new_array_item("array_disclosure", &json!("value1")).unwrap(), ], ) .unwrap(); @@ -118,3 +113,101 @@ fn full_pathway_array() { full_jwt_claims ); } + +#[test] +fn nested_claims() { + const SD_ALG: SdAlg = SdAlg::Sha256; + + // Decode types + #[derive(Debug, Deserialize, PartialEq)] + struct InnerNestedClaim { + inner_property: String, + } + + #[derive(Debug, Deserialize, PartialEq)] + struct OuterNestedClaim { + inner: Option, + } + + #[derive(Debug, Deserialize, PartialEq)] + struct Claims { + sub: String, + outer: Option, + } + + // Manually encode + let inner_disclosure = encode_property_disclosure( + SD_ALG, + "inner", + &serde_json::json!({"inner_property": "value"}), + ) + .unwrap(); + + let outer_disclosure = encode_property_disclosure( + SD_ALG, + "outer", + &serde_json::json!({ + "_sd": [ + inner_disclosure.hash + ] + }), + ) + .unwrap(); + + let jwt = ssi_jwt::encode_sign( + Algorithm::ES256, + &serde_json::json!({ + "_sd": [ + outer_disclosure.hash + ], + "_sd_alg": SD_ALG.to_str(), + "sub": "user", + }), + &test_key(), + ) + .unwrap(); + + // No claims provided + let (_, no_sd_claims) = decode_verify::(&jwt, &test_key(), &[]).unwrap(); + assert_eq!( + no_sd_claims, + Claims { + sub: "user".to_owned(), + outer: None, + } + ); + + // Outer provided + let (_, outer_provided) = + decode_verify::(&jwt, &test_key(), &[&outer_disclosure.encoded]).unwrap(); + assert_eq!( + outer_provided, + Claims { + sub: "user".to_owned(), + outer: Some(OuterNestedClaim { inner: None }) + } + ); + + // Inner and outer provided + let (_, inner_and_outer_provided) = decode_verify::( + &jwt, + &test_key(), + &[&outer_disclosure.encoded, &inner_disclosure.encoded], + ) + .unwrap(); + assert_eq!( + inner_and_outer_provided, + Claims { + sub: "user".to_owned(), + outer: Some(OuterNestedClaim { + inner: Some(InnerNestedClaim { + inner_property: "value".to_owned(), + }) + }) + } + ); + + // Inner without outer errors + let result = decode_verify::(&jwt, &test_key(), &[&inner_disclosure.encoded]); + assert!(result.is_err()); +} diff --git a/ssi-sd-jwt/tests/rfc_examples.rs b/ssi-sd-jwt/tests/rfc_examples.rs new file mode 100644 index 000000000..7cc930cd1 --- /dev/null +++ b/ssi-sd-jwt/tests/rfc_examples.rs @@ -0,0 +1,380 @@ +use serde::{Deserialize, Serialize}; +use ssi_sd_jwt::decode_verify; + +fn rfc_a_5_key() -> ssi_jwk::JWK { + serde_json::from_value(serde_json::json!({ + "kty": "EC", + "crv": "P-256", + "x": "b28d4MwZMjw8-00CG4xfnn9SLMVMM19SlqZpVb_uNtQ", + "y": "Xv5zWwuoaTgdS6hV43yI6gBwTnjukmFQQnJ_kCxzqk8" + })) + .unwrap() +} + +#[test] +fn rfc_a_1_example_2_verification() { + #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] + struct Example2Address { + street_address: Option, + locality: Option, + region: Option, + country: Option, + } + + #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] + struct Example2Claims { + sub: Option, + given_name: Option, + family_name: Option, + email: Option, + phone_number: Option, + address: Example2Address, + birthdate: Option, + iss: String, + iat: u32, + exp: u32, + } + + const EXAMPLE_2_JWT: &str = concat!( + "eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIkM5aW5wNllvUmFFWFI0Mjd6WUpQN1Fya", + "zFXSF84YmR3T0FfWVVyVW5HUVUiLCAiS3VldDF5QWEwSElRdlluT1ZkNTloY1ZpTzlVZ", + "zZKMmtTZnFZUkJlb3d2RSIsICJNTWxkT0ZGekIyZDB1bWxtcFRJYUdlcmhXZFVfUHBZZ", + "kx2S2hoX2ZfOWFZIiwgIlg2WkFZT0lJMnZQTjQwVjd4RXhad1Z3ejd5Um1MTmNWd3Q1R", + "Ew4Ukx2NGciLCAiWTM0em1JbzBRTExPdGRNcFhHd2pCZ0x2cjE3eUVoaFlUMEZHb2ZSL", + "WFJRSIsICJmeUdwMFdUd3dQdjJKRFFsbjFsU2lhZW9iWnNNV0ExMGJRNTk4OS05RFRzI", + "iwgIm9tbUZBaWNWVDhMR0hDQjB1eXd4N2ZZdW8zTUhZS08xNWN6LVJaRVlNNVEiLCAic", + "zBCS1lzTFd4UVFlVTh0VmxsdE03TUtzSVJUckVJYTFQa0ptcXhCQmY1VSJdLCAiaXNzI", + "jogImh0dHBzOi8vZXhhbXBsZS5jb20vaXNzdWVyIiwgImlhdCI6IDE2ODMwMDAwMDAsI", + "CJleHAiOiAxODgzMDAwMDAwLCAiYWRkcmVzcyI6IHsiX3NkIjogWyI2YVVoelloWjdTS", + "jFrVm1hZ1FBTzN1MkVUTjJDQzFhSGhlWnBLbmFGMF9FIiwgIkF6TGxGb2JrSjJ4aWF1c", + "FJFUHlvSnotOS1OU2xkQjZDZ2pyN2ZVeW9IemciLCAiUHp6Y1Z1MHFiTXVCR1NqdWxmZ", + "Xd6a2VzRDl6dXRPRXhuNUVXTndrclEtayIsICJiMkRrdzBqY0lGOXJHZzhfUEY4WmN2b", + "mNXN3p3Wmo1cnlCV3ZYZnJwemVrIiwgImNQWUpISVo4VnUtZjlDQ3lWdWIyVWZnRWs4a", + "nZ2WGV6d0sxcF9KbmVlWFEiLCAiZ2xUM2hyU1U3ZlNXZ3dGNVVEWm1Xd0JUdzMyZ25Vb", + "GRJaGk4aEdWQ2FWNCIsICJydkpkNmlxNlQ1ZWptc0JNb0d3dU5YaDlxQUFGQVRBY2k0M", + "G9pZEVlVnNBIiwgInVOSG9XWWhYc1poVkpDTkUyRHF5LXpxdDd0NjlnSkt5NVFhRnY3R", + "3JNWDQiXX0sICJfc2RfYWxnIjogInNoYS0yNTYifQ.rFsowW-KSZe7EITlWsGajR9nnG", + "BLlQ78qgtdGIZg3FZuZnxtapP0H8CUMnffJAwPQJmGnpFpulTkLWHiI1kMmw" + ); + + const SUB_CLAIM_DISCLOSURE: &str = + "WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInN1YiIsICI2YzVjMGE0OS1iNTg5LTQzMWQtYmFlNy0yMTkxMjJhOWVjMmMiXQ"; + const GIVEN_NAME_DISCLOSURE: &str = + "WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImdpdmVuX25hbWUiLCAiXHU1OTJhXHU5MGNlIl0"; + const FAMILY_NAME_DISCLOSURE: &str = + "WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImZhbWlseV9uYW1lIiwgIlx1NWM3MVx1NzUzMCJd"; + const EMAIL_CLAIM_DISCLOSURE: &str = + "WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgImVtYWlsIiwgIlwidW51c3VhbCBlbWFpbCBhZGRyZXNzXCJAZXhhbXBsZS5qcCJd"; + const PHONE_NUMBER_DISCLOSURE: &str = + "WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgInBob25lX251bWJlciIsICIrODEtODAtMTIzNC01Njc4Il0"; + const ADDRESS_STREET_ADDRESS_DISCLOSURES: &str = + "WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgInN0cmVldF9hZGRyZXNzIiwgIlx1Njc3MVx1NGVhY1x1OTBmZFx1NmUyZlx1NTMzYVx1ODI5ZFx1NTE2Y1x1NTcxMlx1ZmYxNFx1NGUwMVx1NzZlZVx1ZmYxMlx1MjIxMlx1ZmYxOCJd"; + const ADDRESS_LOCALITY_DISCLOSURE: &str = + "WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImxvY2FsaXR5IiwgIlx1Njc3MVx1NGVhY1x1OTBmZCJd"; + const ADDRESS_REGION_DISCLOSURE: &str = + "WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICJcdTZlMmZcdTUzM2EiXQ"; + const ADDRESS_COUNTRY_DISCLOSURE: &str = + "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiSlAiXQ"; + const BIRTHDATE_DISCLOSURE: &str = + "WyJ5eXRWYmRBUEdjZ2wyckk0QzlHU29nIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0"; + + // Raw with no disclosures + let (_, no_disclosures) = + decode_verify::(EXAMPLE_2_JWT, &rfc_a_5_key(), &[]).unwrap(); + + assert_eq!( + no_disclosures, + Example2Claims { + address: Default::default(), + iss: "https://example.com/issuer".to_owned(), + iat: 1683000000, + exp: 1883000000, + ..Default::default() + } + ); + + // Top level claim disclosed + let (_, sub_claim_disclosed) = + decode_verify::(EXAMPLE_2_JWT, &rfc_a_5_key(), &[SUB_CLAIM_DISCLOSURE]) + .unwrap(); + + assert_eq!( + sub_claim_disclosed, + Example2Claims { + sub: Some("6c5c0a49-b589-431d-bae7-219122a9ec2c".to_owned()), + address: Default::default(), + iss: "https://example.com/issuer".to_owned(), + iat: 1683000000, + exp: 1883000000, + ..Default::default() + } + ); + + // Address claim disclosed + let (_, address_country_disclosed) = decode_verify::( + EXAMPLE_2_JWT, + &rfc_a_5_key(), + &[ADDRESS_COUNTRY_DISCLOSURE], + ) + .unwrap(); + + assert_eq!( + address_country_disclosed, + Example2Claims { + address: Example2Address { + country: Some("JP".to_owned()), + ..Default::default() + }, + iss: "https://example.com/issuer".to_owned(), + iat: 1683000000, + exp: 1883000000, + ..Default::default() + } + ); + + // All claims disclosed + let (_, all_claims) = decode_verify::( + EXAMPLE_2_JWT, + &rfc_a_5_key(), + &[ + SUB_CLAIM_DISCLOSURE, + GIVEN_NAME_DISCLOSURE, + FAMILY_NAME_DISCLOSURE, + EMAIL_CLAIM_DISCLOSURE, + PHONE_NUMBER_DISCLOSURE, + ADDRESS_STREET_ADDRESS_DISCLOSURES, + ADDRESS_LOCALITY_DISCLOSURE, + ADDRESS_REGION_DISCLOSURE, + ADDRESS_COUNTRY_DISCLOSURE, + BIRTHDATE_DISCLOSURE, + ], + ) + .unwrap(); + + assert_eq!( + all_claims, + Example2Claims { + sub: Some("6c5c0a49-b589-431d-bae7-219122a9ec2c".to_owned()), + given_name: Some("太郎".to_owned()), + family_name: Some("山田".to_owned()), + email: Some("\"unusual email address\"@example.jp".to_owned()), + phone_number: Some("+81-80-1234-5678".to_owned()), + address: Example2Address { + street_address: Some("東京都港区芝公園4丁目2−8".to_owned()), + locality: Some("東京都".to_owned()), + region: Some("港区".to_owned()), + country: Some("JP".to_owned()), + }, + birthdate: Some("1940-01-01".to_owned()), + iss: "https://example.com/issuer".to_owned(), + iat: 1683000000, + exp: 1883000000 + } + ); +} + +#[test] +fn rfc_a_2_example_3_verification() { + const EXAMPLE_3_JWT: &str = concat!( + "eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIi1hU3puSWQ5bVdNOG9jdVFvbENsbHN4V", + "mdncTEtdkhXNE90bmhVdFZtV3ciLCAiSUticllObjN2QTdXRUZyeXN2YmRCSmpERFVfR", + "XZRSXIwVzE4dlRScFVTZyIsICJvdGt4dVQxNG5CaXd6TkozTVBhT2l0T2w5cFZuWE9hR", + "UhhbF94a3lOZktJIl0sICJpc3MiOiAiaHR0cHM6Ly9leGFtcGxlLmNvbS9pc3N1ZXIiL", + "CAiaWF0IjogMTY4MzAwMDAwMCwgImV4cCI6IDE4ODMwMDAwMDAsICJ2ZXJpZmllZF9jb", + "GFpbXMiOiB7InZlcmlmaWNhdGlvbiI6IHsiX3NkIjogWyI3aDRVRTlxU2N2REtvZFhWQ", + "3VvS2ZLQkpwVkJmWE1GX1RtQUdWYVplM1NjIiwgInZUd2UzcmFISUZZZ0ZBM3hhVUQyY", + "U14Rno1b0RvOGlCdTA1cUtsT2c5THciXSwgInRydXN0X2ZyYW1ld29yayI6ICJkZV9hb", + "WwiLCAiZXZpZGVuY2UiOiBbeyIuLi4iOiAidFlKMFREdWN5WlpDUk1iUk9HNHFSTzV2a", + "1BTRlJ4RmhVRUxjMThDU2wzayJ9XX0sICJjbGFpbXMiOiB7Il9zZCI6IFsiUmlPaUNuN", + "l93NVpIYWFka1FNcmNRSmYwSnRlNVJ3dXJSczU0MjMxRFRsbyIsICJTXzQ5OGJicEt6Q", + "jZFYW5mdHNzMHhjN2NPYW9uZVJyM3BLcjdOZFJtc01vIiwgIldOQS1VTks3Rl96aHNBY", + "jlzeVdPNklJUTF1SGxUbU9VOHI4Q3ZKMGNJTWsiLCAiV3hoX3NWM2lSSDliZ3JUQkppL", + "WFZSE5DTHQtdmpoWDFzZC1pZ09mXzlsayIsICJfTy13SmlIM2VuU0I0Uk9IbnRUb1FUO", + "EptTHR6LW1oTzJmMWM4OVhvZXJRIiwgImh2RFhod21HY0pRc0JDQTJPdGp1TEFjd0FNc", + "ERzYVUwbmtvdmNLT3FXTkUiXX19LCAiX3NkX2FsZyI6ICJzaGEtMjU2In0.Xtpp8nvAq", + "22k6wNRiYHGRoRnkn3EBaHdjcaa0sf0sYjCiyZnmSRlxv_C72gRwfVQkSA36ID_I46QS", + "TZvBrgm3g" + ); + + #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] + struct VerificationEvidenceDocumentIssuer { + name: String, + country: String, + } + + #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] + struct VerificationEvidenceDocument { + #[serde(rename = "type")] + _type: String, + issuer: VerificationEvidenceDocumentIssuer, + number: String, + date_of_issuance: String, + date_of_expiry: String, + } + + #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] + struct VerificationEvidence { + #[serde(rename = "type")] + _type: Option, + method: Option, + time: Option, + document: Option, + } + + #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] + struct Verification { + trust_framework: String, + time: Option, + verification_process: Option, + evidence: Vec, + } + + #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] + struct PlaceOfBirth { + country: String, + locality: String, + } + + #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] + struct Address { + locality: String, + postal_code: String, + country: String, + street_address: String, + } + + #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] + struct VerifiedClaimsClaims { + given_name: Option, + family_name: Option, + nationalities: Option>, + birthdate: Option, + place_of_birth: Option, + address: Option
, + } + + #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] + struct VerifiedClaims { + verification: Verification, + claims: VerifiedClaimsClaims, + } + + #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] + struct Example3Claims { + verified_claims: VerifiedClaims, + iss: String, + iat: u32, + exp: u32, + birth_middle_name: Option, + salutation: Option, + msisdn: Option, + } + + const VERIFIED_CLAIMS_TIME_DISCLOSURE: &str = + "WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInRpbWUiLCAiMjAxMi0wNC0yM1QxODoyNVoiXQ"; + const VERIFIED_CLAIMS_VERIFICATION_PROCESS_DISCLOSURE: &str = + "WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgInZlcmlmaWNhdGlvbl9wcm9jZXNzIiwgImYyNGM2Zi02ZDNmLTRlYzUtOTczZS1iMGQ4NTA2ZjNiYzciXQ"; + const VERIFIED_CLAIMS_EVIDENCE_0_TYPE_DISCLOSURE: &str = + "WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgInR5cGUiLCAiZG9jdW1lbnQiXQ"; + const VERIFIED_CLAIMS_EVIDENCE_0_METHOD_DISCLOSURE: &str = + "WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgIm1ldGhvZCIsICJwaXBwIl0"; + const VERIFIED_CLAIMS_EVIDENCE_0_TIME_DISCLOSURE: &str = + "WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgInRpbWUiLCAiMjAxMi0wNC0yMlQxMTozMFoiXQ"; + const VERIFIED_CLAIMS_EVIDENCE_0_DOCUMENT_DISCLOSURE: &str = + "WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImRvY3VtZW50IiwgeyJ0eXBlIjogImlkY2FyZCIsICJpc3N1ZXIiOiB7Im5hbWUiOiAiU3RhZHQgQXVnc2J1cmciLCAiY291bnRyeSI6ICJERSJ9LCAibnVtYmVyIjogIjUzNTU0NTU0IiwgImRhdGVfb2ZfaXNzdWFuY2UiOiAiMjAxMC0wMy0yMyIsICJkYXRlX29mX2V4cGlyeSI6ICIyMDIwLTAzLTIyIn1d"; + const VERIFIED_CLAIMS_EVIDENCE_0_DISCLOSURE: &str = + "WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgeyJfc2QiOiBbIjl3cGpWUFd1RDdQSzBuc1FETDhCMDZsbWRnVjNMVnliaEh5ZFFwVE55TEkiLCAiRzVFbmhPQU9vVTlYXzZRTU52ekZYanBFQV9SYy1BRXRtMWJHX3djYUtJayIsICJJaHdGcldVQjYzUmNacTl5dmdaMFhQYzdHb3doM08ya3FYZUJJc3dnMUI0IiwgIldweFE0SFNvRXRjVG1DQ0tPZURzbEJfZW11Y1lMejJvTzhvSE5yMWJFVlEiXX1d"; + const VERIFIED_CLAIMS_CLAIMS_GIVEN_NAME_DISCLOSURE: &str = + "WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgImdpdmVuX25hbWUiLCAiTWF4Il0"; + const VERIFIED_CLAIMS_CLAIMS_FAMILY_NAME_DISCLOSURE: &str = + "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImZhbWlseV9uYW1lIiwgIk1cdTAwZmNsbGVyIl0"; + const VERIFIED_CLAIMS_CLAIMS_NATIONALITIES_DISCLOSURE: &str = + "WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIm5hdGlvbmFsaXRpZXMiLCBbIkRFIl1d"; + const VERIFIED_CLAIMS_CLAIMS_BIRTHDATE_DISCLOSURE: &str = + "WyI1YlBzMUlxdVpOYTBoa2FGenp6Wk53IiwgImJpcnRoZGF0ZSIsICIxOTU2LTAxLTI4Il0"; + const VERIFIED_CLAIMS_CLAIMS_PLACE_OF_BIRTH_DISCLOSURE: &str = + "WyI1YTJXMF9OcmxFWnpmcW1rXzdQcS13IiwgInBsYWNlX29mX2JpcnRoIiwgeyJjb3VudHJ5IjogIklTIiwgImxvY2FsaXR5IjogIlx1MDBkZXlra3ZhYlx1MDBlNmphcmtsYXVzdHVyIn1d"; + const VERIFIED_CLAIMS_CLAIMS_ADDRESS_DISCLOSURE: &str = + "WyJ5MXNWVTV3ZGZKYWhWZGd3UGdTN1JRIiwgImFkZHJlc3MiLCB7ImxvY2FsaXR5IjogIk1heHN0YWR0IiwgInBvc3RhbF9jb2RlIjogIjEyMzQ0IiwgImNvdW50cnkiOiAiREUiLCAic3RyZWV0X2FkZHJlc3MiOiAiV2VpZGVuc3RyYVx1MDBkZmUgMjIifV0"; + const BIRTH_MIDDLE_NAME_DISCLOSURE: &str = + "WyJIYlE0WDhzclZXM1FEeG5JSmRxeU9BIiwgImJpcnRoX21pZGRsZV9uYW1lIiwgIlRpbW90aGV1cyJd"; + const SALUTATION_DISCLOSURE: &str = + "WyJDOUdTb3VqdmlKcXVFZ1lmb2pDYjFBIiwgInNhbHV0YXRpb24iLCAiRHIuIl0"; + const MSISDN_DISCLOSURE: &str = + "WyJreDVrRjE3Vi14MEptd1V4OXZndnR3IiwgIm1zaXNkbiIsICI0OTEyMzQ1Njc4OSJd"; + + // All Claims + let (_, all_claims) = decode_verify::( + EXAMPLE_3_JWT, + &rfc_a_5_key(), + &[ + VERIFIED_CLAIMS_TIME_DISCLOSURE, + VERIFIED_CLAIMS_VERIFICATION_PROCESS_DISCLOSURE, + VERIFIED_CLAIMS_EVIDENCE_0_TYPE_DISCLOSURE, + VERIFIED_CLAIMS_EVIDENCE_0_METHOD_DISCLOSURE, + VERIFIED_CLAIMS_EVIDENCE_0_TIME_DISCLOSURE, + VERIFIED_CLAIMS_EVIDENCE_0_DOCUMENT_DISCLOSURE, + VERIFIED_CLAIMS_EVIDENCE_0_DISCLOSURE, + VERIFIED_CLAIMS_CLAIMS_GIVEN_NAME_DISCLOSURE, + VERIFIED_CLAIMS_CLAIMS_FAMILY_NAME_DISCLOSURE, + VERIFIED_CLAIMS_CLAIMS_NATIONALITIES_DISCLOSURE, + VERIFIED_CLAIMS_CLAIMS_BIRTHDATE_DISCLOSURE, + VERIFIED_CLAIMS_CLAIMS_PLACE_OF_BIRTH_DISCLOSURE, + VERIFIED_CLAIMS_CLAIMS_ADDRESS_DISCLOSURE, + BIRTH_MIDDLE_NAME_DISCLOSURE, + SALUTATION_DISCLOSURE, + MSISDN_DISCLOSURE, + ], + ) + .unwrap(); + + assert_eq!( + all_claims, + Example3Claims { + verified_claims: VerifiedClaims { + verification: Verification { + trust_framework: "de_aml".to_owned(), + time: Some("2012-04-23T18:25Z".to_owned()), + verification_process: Some("f24c6f-6d3f-4ec5-973e-b0d8506f3bc7".to_owned()), + evidence: vec![VerificationEvidence { + _type: Some("document".to_owned()), + method: Some("pipp".to_owned()), + time: Some("2012-04-22T11:30Z".to_owned()), + document: Some(VerificationEvidenceDocument { + _type: "idcard".to_owned(), + issuer: VerificationEvidenceDocumentIssuer { + name: "Stadt Augsburg".to_owned(), + country: "DE".to_owned(), + }, + number: "53554554".to_owned(), + date_of_issuance: "2010-03-23".to_owned(), + date_of_expiry: "2020-03-22".to_owned(), + }) + }], + }, + claims: VerifiedClaimsClaims { + given_name: Some("Max".to_owned()), + family_name: Some("Müller".to_owned()), + nationalities: Some(vec!["DE".to_owned()]), + birthdate: Some("1956-01-28".to_owned()), + place_of_birth: Some(PlaceOfBirth { + country: "IS".to_owned(), + locality: "Þykkvabæjarklaustur".to_owned(), + }), + address: Some(Address { + locality: "Maxstadt".to_owned(), + postal_code: "12344".to_owned(), + country: "DE".to_owned(), + street_address: "Weidenstraße 22".to_owned(), + }), + }, + }, + iss: "https://example.com/issuer".to_owned(), + iat: 1683000000, + exp: 1883000000, + birth_middle_name: Some("Timotheus".to_owned()), + salutation: Some("Dr.".to_owned()), + msisdn: Some("49123456789".to_owned()), + } + ) +} From 1c81a1b7d307ae567330edaa08c7d98a64075151 Mon Sep 17 00:00:00 2001 From: Tristan Miller Date: Fri, 29 Sep 2023 08:39:50 -0600 Subject: [PATCH 03/16] Remove all todosgr todo ssi-sd-jwt/ --- ssi-sd-jwt/src/lib.rs | 6 ++---- ssi-sd-jwt/src/verify.rs | 19 ++++++++----------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/ssi-sd-jwt/src/lib.rs b/ssi-sd-jwt/src/lib.rs index 52a6dc59d..6080f29a6 100644 --- a/ssi-sd-jwt/src/lib.rs +++ b/ssi-sd-jwt/src/lib.rs @@ -28,10 +28,8 @@ pub enum Error { SdPropertyNotArray, #[error("A disclosure claim would collid with an existing JWT claim")] DisclosureClaimCollidesWithJwtClaim, - #[error("A disclosure didn't have the correct array length")] - DisclosureArrayLength, - #[error("A disclosure didn't contain the right types for elements")] - DisclosureHasWrongType, + #[error("A disclosure is malformed")] + DisclosureMalformed, #[error("A single disclosure was used multiple times")] DisclosureUsedMultipleTimes, #[error("Found an array item disclosure when expecting a property type")] diff --git a/ssi-sd-jwt/src/verify.rs b/ssi-sd-jwt/src/verify.rs index 241e65d42..5397e4e0a 100644 --- a/ssi-sd-jwt/src/verify.rs +++ b/ssi-sd-jwt/src/verify.rs @@ -27,17 +27,17 @@ impl DecodedDisclosure { serde_json::Value::Array(values) => match values.len() { 3 => validate_property_disclosure(&values), 2 => validate_array_item_disclosure(&values), - _ => Err(Error::DisclosureArrayLength), + _ => Err(Error::DisclosureMalformed), }, - _ => todo!("handle other json: {:?}", json), + _ => Err(Error::DisclosureMalformed), } } } fn validate_property_disclosure(values: &[serde_json::Value]) -> Result { - let salt = values[0].as_str().ok_or(Error::DisclosureHasWrongType)?; + let salt = values[0].as_str().ok_or(Error::DisclosureMalformed)?; - let name = values[1].as_str().ok_or(Error::DisclosureHasWrongType)?; + let name = values[1].as_str().ok_or(Error::DisclosureMalformed)?; Ok(DecodedDisclosure { salt: salt.to_owned(), @@ -51,7 +51,7 @@ fn validate_property_disclosure(values: &[serde_json::Value]) -> Result Result { - let salt = values[0].as_str().ok_or(Error::DisclosureHasWrongType)?; + let salt = values[0].as_str().ok_or(Error::DisclosureMalformed)?; Ok(DecodedDisclosure { salt: salt.to_owned(), @@ -79,15 +79,12 @@ pub fn verify_sd_disclosures_array( DisclosureKind::Property { name, value } => { let orig = verfied_claims.insert(name, value); - if let Some(orig) = orig { - todo!( - "handle multiple claims with the same property name: {:?}", - orig, - ) + if orig.is_some() { + return Err(Error::DisclosureUsedMultipleTimes); } } DisclosureKind::ArrayItem(_) => { - todo!("array item disclouse in sd claims: {:?}", decoded) + return Err(Error::ArrayDisclosureWhenExpectingProperty); } } } From 9db1cfeeff1cc9bd7708c38072441d771d37211d Mon Sep 17 00:00:00 2001 From: Tristan Miller Date: Wed, 4 Oct 2023 08:31:19 -0600 Subject: [PATCH 04/16] Re export ssi-sd-jwt in core ssi --- Cargo.toml | 1 + src/lib.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 51ac8675f..4038e99a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,7 @@ ssi-ucan = { path = "./ssi-ucan", version = "0.1" } ssi-vc = { path = "./ssi-vc", version = "0.2.0" } ssi-zcap-ld = { path = "./ssi-zcap-ld", version = "0.1.2" } ssi-caips = { path = "./ssi-caips", version = "0.1", default-features = false } +ssi-sd-jwt = { path = "./ssi-sd-jwt", version = "0.1" } [workspace] members = [ diff --git a/src/lib.rs b/src/lib.rs index 0882fca86..d0646e81c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,6 +64,7 @@ pub use ssi_ldp as ldp; pub use ssi_ldp::eip712; #[deprecated = "Use ssi::ldp::soltx"] pub use ssi_ldp::soltx; +pub use ssi_sd_jwt as sd_jwt; pub use ssi_ssh as ssh; pub use ssi_tzkey as tzkey; pub use ssi_ucan as ucan; From 81d4022d41f0c0ddaf176350c0123c45f4471ca4 Mon Sep 17 00:00:00 2001 From: Tristan Miller Date: Wed, 4 Oct 2023 08:46:01 -0600 Subject: [PATCH 05/16] Rename decode public functions to make 'easy mode' clearer --- ssi-sd-jwt/src/decode.rs | 6 +++--- ssi-sd-jwt/src/lib.rs | 2 +- ssi-sd-jwt/tests/decode.rs | 21 ++++++++++++++------- ssi-sd-jwt/tests/full_pathway.rs | 18 +++++++++++------- ssi-sd-jwt/tests/rfc_examples.rs | 20 ++++++++++++-------- 5 files changed, 41 insertions(+), 26 deletions(-) diff --git a/ssi-sd-jwt/src/decode.rs b/ssi-sd-jwt/src/decode.rs index 76da750c8..ba0c7b471 100644 --- a/ssi-sd-jwt/src/decode.rs +++ b/ssi-sd-jwt/src/decode.rs @@ -15,17 +15,17 @@ pub struct ValidityClaims { pub exp: Option, } -pub fn decode_verify_string_format( +pub fn decode_verify( serialized: &str, key: &JWK, ) -> Result<(ValidityClaims, Claims), Error> { let deserialized = deserialize_string_format(serialized).ok_or(Error::UnableToDeserializeStringFormat)?; - decode_verify(deserialized.jwt, key, &deserialized.disclosures) + decode_verify_disclosure_array(deserialized.jwt, key, &deserialized.disclosures) } -pub fn decode_verify( +pub fn decode_verify_disclosure_array( jwt: &str, key: &JWK, disclosures: &[&str], diff --git a/ssi-sd-jwt/src/lib.rs b/ssi-sd-jwt/src/lib.rs index 6080f29a6..3005c0ac8 100644 --- a/ssi-sd-jwt/src/lib.rs +++ b/ssi-sd-jwt/src/lib.rs @@ -4,7 +4,7 @@ pub(crate) mod encode; pub(crate) mod serialized; pub(crate) mod verify; -pub use decode::{decode_verify, decode_verify_string_format, ValidityClaims}; +pub use decode::{decode_verify, decode_verify_disclosure_array, ValidityClaims}; pub use digest::{hash_encoded_disclosure, SdAlg}; pub use encode::{ encode_array_disclosure, encode_property_disclosure, encode_sign, UnencodedDisclosure, diff --git a/ssi-sd-jwt/tests/decode.rs b/ssi-sd-jwt/tests/decode.rs index fc5b99e3e..26fd32012 100644 --- a/ssi-sd-jwt/tests/decode.rs +++ b/ssi-sd-jwt/tests/decode.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use ssi_jwk::{Algorithm, JWK}; use ssi_jwt::NumericDate; -use ssi_sd_jwt::{decode_verify, ValidityClaims}; +use ssi_sd_jwt::{decode_verify_disclosure_array, ValidityClaims}; #[derive(Debug, Default, Deserialize, Serialize, PartialEq)] struct ExampleClaims { @@ -85,9 +85,12 @@ const NATIONALITY_DE_DISCLOSURE: &'static str = "WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZ #[test] fn decode_single() { - let (_, claims) = - decode_verify::(&test_standard_sd_jwt(), &test_key(), &[EMAIL_DISCLOSURE]) - .unwrap(); + let (_, claims) = decode_verify_disclosure_array::( + &test_standard_sd_jwt(), + &test_key(), + &[EMAIL_DISCLOSURE], + ) + .unwrap(); assert_eq!( claims, @@ -102,7 +105,7 @@ fn decode_single() { #[test] fn decode_single_array_item() { - let (_, claims) = decode_verify::( + let (_, claims) = decode_verify_disclosure_array::( &test_standard_sd_jwt(), &test_key(), &[NATIONALITY_DE_DISCLOSURE], @@ -121,8 +124,12 @@ fn decode_single_array_item() { #[test] fn validitity_decodes() { - let (validity, _) = - decode_verify::(&test_standard_sd_jwt(), &test_key(), &[]).unwrap(); + let (validity, _) = decode_verify_disclosure_array::( + &test_standard_sd_jwt(), + &test_key(), + &[], + ) + .unwrap(); assert_eq!( validity, diff --git a/ssi-sd-jwt/tests/full_pathway.rs b/ssi-sd-jwt/tests/full_pathway.rs index 53e40ff3c..6a3bac29f 100644 --- a/ssi-sd-jwt/tests/full_pathway.rs +++ b/ssi-sd-jwt/tests/full_pathway.rs @@ -44,7 +44,7 @@ fn full_pathway_regular_claim() { ) .unwrap(); - let (_, full_jwt_claims) = decode_verify::( + let (_, full_jwt_claims) = decode_verify_disclosure_array::( &jwt, &test_key(), &[&disclosures[0].encoded, &disclosures[1].encoded], @@ -61,7 +61,8 @@ fn full_pathway_regular_claim() { ); let (_, one_sd_claim) = - decode_verify::(&jwt, &test_key(), &[&disclosures[1].encoded]).unwrap(); + decode_verify_disclosure_array::(&jwt, &test_key(), &[&disclosures[1].encoded]) + .unwrap(); assert_eq!( BaseClaims { @@ -98,7 +99,7 @@ fn full_pathway_array() { ) .unwrap(); - let (_, full_jwt_claims) = decode_verify::( + let (_, full_jwt_claims) = decode_verify_disclosure_array::( &jwt, &test_key(), &[&disclosures[0].encoded, &disclosures[1].encoded], @@ -168,7 +169,8 @@ fn nested_claims() { .unwrap(); // No claims provided - let (_, no_sd_claims) = decode_verify::(&jwt, &test_key(), &[]).unwrap(); + let (_, no_sd_claims) = + decode_verify_disclosure_array::(&jwt, &test_key(), &[]).unwrap(); assert_eq!( no_sd_claims, Claims { @@ -179,7 +181,8 @@ fn nested_claims() { // Outer provided let (_, outer_provided) = - decode_verify::(&jwt, &test_key(), &[&outer_disclosure.encoded]).unwrap(); + decode_verify_disclosure_array::(&jwt, &test_key(), &[&outer_disclosure.encoded]) + .unwrap(); assert_eq!( outer_provided, Claims { @@ -189,7 +192,7 @@ fn nested_claims() { ); // Inner and outer provided - let (_, inner_and_outer_provided) = decode_verify::( + let (_, inner_and_outer_provided) = decode_verify_disclosure_array::( &jwt, &test_key(), &[&outer_disclosure.encoded, &inner_disclosure.encoded], @@ -208,6 +211,7 @@ fn nested_claims() { ); // Inner without outer errors - let result = decode_verify::(&jwt, &test_key(), &[&inner_disclosure.encoded]); + let result = + decode_verify_disclosure_array::(&jwt, &test_key(), &[&inner_disclosure.encoded]); assert!(result.is_err()); } diff --git a/ssi-sd-jwt/tests/rfc_examples.rs b/ssi-sd-jwt/tests/rfc_examples.rs index 7cc930cd1..4e7ea3b5c 100644 --- a/ssi-sd-jwt/tests/rfc_examples.rs +++ b/ssi-sd-jwt/tests/rfc_examples.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use ssi_sd_jwt::decode_verify; +use ssi_sd_jwt::decode_verify_disclosure_array; fn rfc_a_5_key() -> ssi_jwk::JWK { serde_json::from_value(serde_json::json!({ @@ -80,7 +80,8 @@ fn rfc_a_1_example_2_verification() { // Raw with no disclosures let (_, no_disclosures) = - decode_verify::(EXAMPLE_2_JWT, &rfc_a_5_key(), &[]).unwrap(); + decode_verify_disclosure_array::(EXAMPLE_2_JWT, &rfc_a_5_key(), &[]) + .unwrap(); assert_eq!( no_disclosures, @@ -94,9 +95,12 @@ fn rfc_a_1_example_2_verification() { ); // Top level claim disclosed - let (_, sub_claim_disclosed) = - decode_verify::(EXAMPLE_2_JWT, &rfc_a_5_key(), &[SUB_CLAIM_DISCLOSURE]) - .unwrap(); + let (_, sub_claim_disclosed) = decode_verify_disclosure_array::( + EXAMPLE_2_JWT, + &rfc_a_5_key(), + &[SUB_CLAIM_DISCLOSURE], + ) + .unwrap(); assert_eq!( sub_claim_disclosed, @@ -111,7 +115,7 @@ fn rfc_a_1_example_2_verification() { ); // Address claim disclosed - let (_, address_country_disclosed) = decode_verify::( + let (_, address_country_disclosed) = decode_verify_disclosure_array::( EXAMPLE_2_JWT, &rfc_a_5_key(), &[ADDRESS_COUNTRY_DISCLOSURE], @@ -133,7 +137,7 @@ fn rfc_a_1_example_2_verification() { ); // All claims disclosed - let (_, all_claims) = decode_verify::( + let (_, all_claims) = decode_verify_disclosure_array::( EXAMPLE_2_JWT, &rfc_a_5_key(), &[ @@ -304,7 +308,7 @@ fn rfc_a_2_example_3_verification() { "WyJreDVrRjE3Vi14MEptd1V4OXZndnR3IiwgIm1zaXNkbiIsICI0OTEyMzQ1Njc4OSJd"; // All Claims - let (_, all_claims) = decode_verify::( + let (_, all_claims) = decode_verify_disclosure_array::( EXAMPLE_3_JWT, &rfc_a_5_key(), &[ From b72081790197b10c91923415694cfedf593419ac Mon Sep 17 00:00:00 2001 From: Tristan Miller Date: Wed, 4 Oct 2023 09:05:59 -0600 Subject: [PATCH 06/16] Split Error into DecodeError and EncodeError --- ssi-sd-jwt/src/decode.rs | 46 +++++++++++++++++++++------------------ ssi-sd-jwt/src/digest.rs | 6 ++--- ssi-sd-jwt/src/encode.rs | 12 +++++----- ssi-sd-jwt/src/error.rs | 47 ++++++++++++++++++++++++++++++++++++++++ ssi-sd-jwt/src/lib.rs | 40 ++-------------------------------- ssi-sd-jwt/src/verify.rs | 26 ++++++++++++---------- 6 files changed, 97 insertions(+), 80 deletions(-) create mode 100644 ssi-sd-jwt/src/error.rs diff --git a/ssi-sd-jwt/src/decode.rs b/ssi-sd-jwt/src/decode.rs index ba0c7b471..6f415ae03 100644 --- a/ssi-sd-jwt/src/decode.rs +++ b/ssi-sd-jwt/src/decode.rs @@ -18,9 +18,9 @@ pub struct ValidityClaims { pub fn decode_verify( serialized: &str, key: &JWK, -) -> Result<(ValidityClaims, Claims), Error> { - let deserialized = - deserialize_string_format(serialized).ok_or(Error::UnableToDeserializeStringFormat)?; +) -> Result<(ValidityClaims, Claims), DecodeError> { + let deserialized = deserialize_string_format(serialized) + .ok_or(DecodeError::UnableToDeserializeStringFormat)?; decode_verify_disclosure_array(deserialized.jwt, key, &deserialized.disclosures) } @@ -29,7 +29,7 @@ pub fn decode_verify_disclosure_array( jwt: &str, key: &JWK, disclosures: &[&str], -) -> Result<(ValidityClaims, Claims), Error> { +) -> Result<(ValidityClaims, Claims), DecodeError> { let mut payload_claims: serde_json::Value = ssi_jwt::decode_verify(jwt, key)?; let validity_claims: ValidityClaims = serde_json::from_value(payload_claims.clone())?; @@ -46,17 +46,17 @@ pub fn decode_verify_disclosure_array( for (_, disclosure) in disclosures { if !disclosure.found { - return Err(Error::UnusedDisclosure); + return Err(DecodeError::UnusedDisclosure); } } Ok((validity_claims, serde_json::from_value(payload_claims)?)) } -fn sd_alg(claims: &serde_json::Value) -> Result { +fn sd_alg(claims: &serde_json::Value) -> Result { let alg_name = claims[SD_ALG_CLAIM_NAME] .as_str() - .ok_or(Error::MissingSdAlg)?; + .ok_or(DecodeError::MissingSdAlg)?; SdAlg::try_from(alg_name) } @@ -64,8 +64,8 @@ fn sd_alg(claims: &serde_json::Value) -> Result { fn translate_to_in_progress_disclosures( disclosures: &[&str], sd_alg: SdAlg, -) -> Result, Error> { - let disclosure_vec: Result, Error> = disclosures +) -> Result, DecodeError> { + let disclosure_vec: Result, DecodeError> = disclosures .iter() .map(|disclosure| InProgressDisclosure::new(disclosure, sd_alg)) .collect(); @@ -77,7 +77,7 @@ fn translate_to_in_progress_disclosures( let prev = disclosure_map.insert(disclosure.hash.clone(), disclosure); if prev.is_some() { - return Err(Error::MultipleDisclosuresWithSameHash); + return Err(DecodeError::MultipleDisclosuresWithSameHash); } } @@ -92,7 +92,7 @@ struct InProgressDisclosure { } impl InProgressDisclosure { - fn new(disclosure: &str, sd_alg: SdAlg) -> Result { + fn new(disclosure: &str, sd_alg: SdAlg) -> Result { Ok(InProgressDisclosure { decoded: DecodedDisclosure::new(disclosure)?, hash: hash_encoded_disclosure(sd_alg, disclosure), @@ -104,7 +104,7 @@ impl InProgressDisclosure { fn visit_claims( payload_claims: &mut serde_json::Value, disclosures: &mut BTreeMap, -) -> Result<(), Error> { +) -> Result<(), DecodeError> { let payload_claims = match payload_claims.as_object_mut() { Some(obj) => obj, None => return Ok(()), @@ -132,7 +132,7 @@ fn visit_claims( let prev = payload_claims.insert(new_claim_name, new_claim_value); if prev.is_some() { - return Err(Error::DisclosureClaimCollidesWithJwtClaim); + return Err(DecodeError::DisclosureClaimCollidesWithJwtClaim); } } @@ -155,20 +155,24 @@ fn visit_claims( fn decode_sd_claims( sd_claims: &serde_json::Value, disclosures: &mut BTreeMap, -) -> Result, Error> { - let sd_claims = sd_claims.as_array().ok_or(Error::SdPropertyNotArray)?; +) -> Result, DecodeError> { + let sd_claims = sd_claims + .as_array() + .ok_or(DecodeError::SdPropertyNotArray)?; let mut found_disclosures = vec![]; for disclosure_hash in sd_claims { - let disclosure_hash = disclosure_hash.as_str().ok_or(Error::SdClaimNotString)?; + let disclosure_hash = disclosure_hash + .as_str() + .ok_or(DecodeError::SdClaimNotString)?; if let Some(in_progress_disclosure) = disclosures.get_mut(disclosure_hash) { if in_progress_disclosure.found { - return Err(Error::DisclosureUsedMultipleTimes); + return Err(DecodeError::DisclosureUsedMultipleTimes); } in_progress_disclosure.found = true; match in_progress_disclosure.decoded.kind { DisclosureKind::ArrayItem(_) => { - return Err(Error::ArrayDisclosureWhenExpectingProperty) + return Err(DecodeError::ArrayDisclosureWhenExpectingProperty) } DisclosureKind::Property { ref name, @@ -184,13 +188,13 @@ fn decode_sd_claims( fn decode_array_claims( array: &[serde_json::Value], disclosures: &mut BTreeMap, -) -> Result, Error> { +) -> Result, DecodeError> { let mut new_items = vec![]; for item in array.iter() { if let Some(hash) = array_item_is_disclosure(item) { if let Some(in_progress_disclosure) = disclosures.get_mut(hash) { if in_progress_disclosure.found { - return Err(Error::DisclosureUsedMultipleTimes); + return Err(DecodeError::DisclosureUsedMultipleTimes); } in_progress_disclosure.found = true; match in_progress_disclosure.decoded.kind { @@ -198,7 +202,7 @@ fn decode_array_claims( new_items.push(value.clone()); } DisclosureKind::Property { .. } => { - return Err(Error::PropertyDisclosureWhenExpectingArray) + return Err(DecodeError::PropertyDisclosureWhenExpectingArray) } } } diff --git a/ssi-sd-jwt/src/digest.rs b/ssi-sd-jwt/src/digest.rs index 23aff5fb6..39b9093a0 100644 --- a/ssi-sd-jwt/src/digest.rs +++ b/ssi-sd-jwt/src/digest.rs @@ -1,7 +1,7 @@ use jose_b64::base64ct::{Base64UrlUnpadded, Encoding}; use sha2::Digest; -use crate::Error; +use crate::DecodeError; #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum SdAlg { @@ -21,12 +21,12 @@ impl SdAlg { } impl TryFrom<&str> for SdAlg { - type Error = Error; + type Error = DecodeError; fn try_from(value: &str) -> Result { Ok(match value { Self::SHA256_STR => SdAlg::Sha256, - other => return Err(Error::UnknownSdAlg(other.to_owned())), + other => return Err(DecodeError::UnknownSdAlg(other.to_owned())), }) } } diff --git a/ssi-sd-jwt/src/encode.rs b/ssi-sd-jwt/src/encode.rs index 15c38964f..73f2f0887 100644 --- a/ssi-sd-jwt/src/encode.rs +++ b/ssi-sd-jwt/src/encode.rs @@ -72,10 +72,10 @@ pub fn encode_sign( key: &JWK, sd_alg: SdAlg, disclosures: Vec, -) -> Result<(String, Vec), Error> { +) -> Result<(String, Vec), EncodeError> { let mut base_claims_json = serde_json::to_value(base_claims)?; - let post_encoded_disclosures: Result, Error> = disclosures + let post_encoded_disclosures: Result, EncodeError> = disclosures .iter() .map(|disclosure| { let encoded = disclosure.encode()?; @@ -93,7 +93,7 @@ pub fn encode_sign( { let base_claims_obj = base_claims_json .as_object_mut() - .ok_or(Error::EncodedAsNonObject)?; + .ok_or(EncodeError::EncodedAsNonObject)?; let prev_sd_alg = base_claims_obj.insert( SD_ALG_CLAIM_NAME.to_owned(), @@ -101,7 +101,7 @@ pub fn encode_sign( ); if prev_sd_alg.is_some() { - return Err(Error::EncodedClaimsContainsReservedProperty); + return Err(EncodeError::EncodedClaimsContainsReservedProperty); } let mut sd_claim = vec![]; @@ -118,7 +118,7 @@ pub fn encode_sign( } let array = base_claims_obj.get_mut(claim_name).unwrap(); - let array = array.as_array_mut().ok_or(Error::ExpectedArray)?; + let array = array.as_array_mut().ok_or(EncodeError::ExpectedArray)?; array.push(serde_json::json!({ARRAY_CLAIM_ITEM_PROPERTY_NAME: disclosure.hash.clone()})); } @@ -129,7 +129,7 @@ pub fn encode_sign( base_claims_obj.insert(SD_CLAIM_NAME.to_owned(), serde_json::Value::Array(sd_claim)); if prev_sd.is_some() { - return Err(Error::EncodedClaimsContainsReservedProperty); + return Err(EncodeError::EncodedClaimsContainsReservedProperty); } } diff --git a/ssi-sd-jwt/src/error.rs b/ssi-sd-jwt/src/error.rs new file mode 100644 index 000000000..ca4105bad --- /dev/null +++ b/ssi-sd-jwt/src/error.rs @@ -0,0 +1,47 @@ +#[derive(thiserror::Error, Debug)] +pub enum DecodeError { + #[error("Unable to deserialize string format of concatenated tildes")] + UnableToDeserializeStringFormat, + #[error("JWT is missing _sd_alg property")] + MissingSdAlg, + #[error("Unknown value of _sd_alg {0}")] + UnknownSdAlg(String), + #[error("Multiple disclosures given with the same hash")] + MultipleDisclosuresWithSameHash, + #[error("An _sd claim wasn't a string")] + SdClaimNotString, + #[error("And _sd property was not an array type")] + SdPropertyNotArray, + #[error("A disclosure claim would collid with an existing JWT claim")] + DisclosureClaimCollidesWithJwtClaim, + #[error("A disclosure is malformed")] + DisclosureMalformed, + #[error("A single disclosure was used multiple times")] + DisclosureUsedMultipleTimes, + #[error("Found an array item disclosure when expecting a property type")] + ArrayDisclosureWhenExpectingProperty, + #[error("Found a property type disclosure when expecting an array item")] + PropertyDisclosureWhenExpectingArray, + #[error("A disclosure was not used during decoding")] + UnusedDisclosure, + #[error(transparent)] + JWS(#[from] ssi_jws::Error), + #[error(transparent)] + JsonDeserialization(#[from] serde_json::Error), +} + +#[derive(thiserror::Error, Debug)] +pub enum EncodeError { + #[error("The base claims to encode did not become a JSON object")] + EncodedAsNonObject, + #[error("The base claims to encode contained a property reserved by SD-JWT")] + EncodedClaimsContainsReservedProperty, + #[error("A property for an array sd claim was not an array")] + ExpectedArray, + #[error("A disclosure was not used during decoding")] + UnusedDisclosure, + #[error(transparent)] + JWS(#[from] ssi_jws::Error), + #[error(transparent)] + JsonSerialization(#[from] serde_json::Error), +} diff --git a/ssi-sd-jwt/src/lib.rs b/ssi-sd-jwt/src/lib.rs index 3005c0ac8..929fb7c98 100644 --- a/ssi-sd-jwt/src/lib.rs +++ b/ssi-sd-jwt/src/lib.rs @@ -1,6 +1,7 @@ mod decode; pub(crate) mod digest; pub(crate) mod encode; +mod error; pub(crate) mod serialized; pub(crate) mod verify; @@ -9,47 +10,10 @@ pub use digest::{hash_encoded_disclosure, SdAlg}; pub use encode::{ encode_array_disclosure, encode_property_disclosure, encode_sign, UnencodedDisclosure, }; +pub use error::{DecodeError, EncodeError}; pub use serialized::{deserialize_string_format, serialize_string_format}; pub use verify::verify_sd_disclosures_array; -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("Unable to deserialize string format of concatenated tildes")] - UnableToDeserializeStringFormat, - #[error("JWT is missing _sd_alg property")] - MissingSdAlg, - #[error("Unknown value of _sd_alg {0}")] - UnknownSdAlg(String), - #[error("Multiple disclosures given with the same hash")] - MultipleDisclosuresWithSameHash, - #[error("An _sd claim wasn't a string")] - SdClaimNotString, - #[error("And _sd property was not an array type")] - SdPropertyNotArray, - #[error("A disclosure claim would collid with an existing JWT claim")] - DisclosureClaimCollidesWithJwtClaim, - #[error("A disclosure is malformed")] - DisclosureMalformed, - #[error("A single disclosure was used multiple times")] - DisclosureUsedMultipleTimes, - #[error("Found an array item disclosure when expecting a property type")] - ArrayDisclosureWhenExpectingProperty, - #[error("Found a property type disclosure when expecting an array item")] - PropertyDisclosureWhenExpectingArray, - #[error("The base claims to encode did not become a JSON object")] - EncodedAsNonObject, - #[error("The base claims to encode contained a property reserved by SD-JWT")] - EncodedClaimsContainsReservedProperty, - #[error("A property for an array sd claim was not an array")] - ExpectedArray, - #[error("A disclosure was not used during decoding")] - UnusedDisclosure, - #[error(transparent)] - JWS(#[from] ssi_jws::Error), - #[error(transparent)] - JsonSerialization(#[from] serde_json::Error), -} - const SD_CLAIM_NAME: &str = "_sd"; const SD_ALG_CLAIM_NAME: &str = "_sd_alg"; const ARRAY_CLAIM_ITEM_PROPERTY_NAME: &str = "..."; diff --git a/ssi-sd-jwt/src/verify.rs b/ssi-sd-jwt/src/verify.rs index 5397e4e0a..a642c8a51 100644 --- a/ssi-sd-jwt/src/verify.rs +++ b/ssi-sd-jwt/src/verify.rs @@ -1,7 +1,7 @@ use jose_b64::base64ct::{Base64UrlUnpadded, Encoding}; use crate::digest::{hash_encoded_disclosure, SdAlg}; -use crate::Error; +use crate::DecodeError; #[derive(Debug, PartialEq)] pub struct DecodedDisclosure { @@ -19,7 +19,7 @@ pub enum DisclosureKind { } impl DecodedDisclosure { - pub fn new(encoded: &str) -> Result { + pub fn new(encoded: &str) -> Result { let bytes = Base64UrlUnpadded::decode_vec(encoded).unwrap(); let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); @@ -27,17 +27,19 @@ impl DecodedDisclosure { serde_json::Value::Array(values) => match values.len() { 3 => validate_property_disclosure(&values), 2 => validate_array_item_disclosure(&values), - _ => Err(Error::DisclosureMalformed), + _ => Err(DecodeError::DisclosureMalformed), }, - _ => Err(Error::DisclosureMalformed), + _ => Err(DecodeError::DisclosureMalformed), } } } -fn validate_property_disclosure(values: &[serde_json::Value]) -> Result { - let salt = values[0].as_str().ok_or(Error::DisclosureMalformed)?; +fn validate_property_disclosure( + values: &[serde_json::Value], +) -> Result { + let salt = values[0].as_str().ok_or(DecodeError::DisclosureMalformed)?; - let name = values[1].as_str().ok_or(Error::DisclosureMalformed)?; + let name = values[1].as_str().ok_or(DecodeError::DisclosureMalformed)?; Ok(DecodedDisclosure { salt: salt.to_owned(), @@ -50,8 +52,8 @@ fn validate_property_disclosure(values: &[serde_json::Value]) -> Result Result { - let salt = values[0].as_str().ok_or(Error::DisclosureMalformed)?; +) -> Result { + let salt = values[0].as_str().ok_or(DecodeError::DisclosureMalformed)?; Ok(DecodedDisclosure { salt: salt.to_owned(), @@ -63,7 +65,7 @@ pub fn verify_sd_disclosures_array( digest_algo: SdAlg, disclosures: &[&str], sd_claim: &[&str], -) -> Result { +) -> Result { let mut verfied_claims = serde_json::Map::new(); for disclosure in disclosures { @@ -80,11 +82,11 @@ pub fn verify_sd_disclosures_array( let orig = verfied_claims.insert(name, value); if orig.is_some() { - return Err(Error::DisclosureUsedMultipleTimes); + return Err(DecodeError::DisclosureUsedMultipleTimes); } } DisclosureKind::ArrayItem(_) => { - return Err(Error::ArrayDisclosureWhenExpectingProperty); + return Err(DecodeError::ArrayDisclosureWhenExpectingProperty); } } } From 5bfe8af2d9ffabe3b7b68db79f5943ffd3c4fb10 Mon Sep 17 00:00:00 2001 From: Tristan Miller Date: Fri, 13 Oct 2023 09:01:29 -0600 Subject: [PATCH 07/16] Add docs --- ssi-sd-jwt/src/decode.rs | 10 +++++ ssi-sd-jwt/src/digest.rs | 6 +++ ssi-sd-jwt/src/encode.rs | 23 +++++++++++ ssi-sd-jwt/src/error.rs | 42 ++++++++++++++++++- ssi-sd-jwt/src/lib.rs | 14 +++---- ssi-sd-jwt/src/serialized.rs | 4 ++ ssi-sd-jwt/src/verify.rs | 79 ++++++++++++++++-------------------- 7 files changed, 126 insertions(+), 52 deletions(-) diff --git a/ssi-sd-jwt/src/decode.rs b/ssi-sd-jwt/src/decode.rs index 6f415ae03..3c47d42f1 100644 --- a/ssi-sd-jwt/src/decode.rs +++ b/ssi-sd-jwt/src/decode.rs @@ -8,13 +8,21 @@ use crate::serialized::deserialize_string_format; use crate::verify::{DecodedDisclosure, DisclosureKind}; use crate::*; +/// Expiration validity claims that are sampled before expanding selective disclosures #[derive(Debug, Deserialize, PartialEq)] pub struct ValidityClaims { + /// Not Before claim pub nbf: Option, + + /// Issued After claim pub iat: Option, + + /// Expiration claim pub exp: Option, } +/// High level API to decode a fuilly encoded SD-JWT. That is a JWT and selective +/// disclosures seperated by tildes pub fn decode_verify( serialized: &str, key: &JWK, @@ -25,6 +33,8 @@ pub fn decode_verify( decode_verify_disclosure_array(deserialized.jwt, key, &deserialized.disclosures) } +/// Lower level API to decode an SD-JWT that has already been split into it's +/// JWT and disclosure components pub fn decode_verify_disclosure_array( jwt: &str, key: &JWK, diff --git a/ssi-sd-jwt/src/digest.rs b/ssi-sd-jwt/src/digest.rs index 39b9093a0..67d06209e 100644 --- a/ssi-sd-jwt/src/digest.rs +++ b/ssi-sd-jwt/src/digest.rs @@ -3,8 +3,11 @@ use sha2::Digest; use crate::DecodeError; +/// Elements of the _sd_alg claim +#[non_exhaustive] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum SdAlg { + /// SHA-256 Algortim for hashing disclosures Sha256, } @@ -13,6 +16,7 @@ impl SdAlg { } impl SdAlg { + /// String encoding of _sd_alg field pub fn to_str(&self) -> &'static str { match self { SdAlg::Sha256 => Self::SHA256_STR, @@ -37,6 +41,8 @@ impl From for &'static str { } } +/// Lower level API to generate the hash of a given disclosure string already converted +/// into base 64 pub fn hash_encoded_disclosure(digest_algo: SdAlg, disclosure: &str) -> String { match digest_algo { SdAlg::Sha256 => { diff --git a/ssi-sd-jwt/src/encode.rs b/ssi-sd-jwt/src/encode.rs index 73f2f0887..00b86de21 100644 --- a/ssi-sd-jwt/src/encode.rs +++ b/ssi-sd-jwt/src/encode.rs @@ -5,6 +5,16 @@ use ssi_jwk::{Algorithm, JWK}; use crate::*; +/// Disclosure as encoded +#[derive(Debug, PartialEq)] +pub struct Disclosure { + /// Base 64 of disclosure object + pub encoded: String, + + /// Base 64 of hash of disclosure object + pub hash: String, +} + fn encode_disclosure_with_salt( salt: &str, claim_name: Option<&str>, @@ -45,6 +55,7 @@ fn encode_disclosure( encode_disclosure_with_rng(&mut rng, claim_name, claim_value) } +/// Lower level API to create a property style disclosure pub fn encode_property_disclosure( sd_alg: SdAlg, claim_name: &str, @@ -56,6 +67,7 @@ pub fn encode_property_disclosure( Ok(Disclosure { encoded, hash }) } +/// Lower level API to create an array style disclosure pub fn encode_array_disclosure( sd_alg: SdAlg, claim_value: &ClaimValue, @@ -66,6 +78,7 @@ pub fn encode_array_disclosure( Ok(Disclosure { encoded, hash }) } +/// High level API to create most SD-JWTs pub fn encode_sign( algorithm: Algorithm, base_claims: &Claims, @@ -138,13 +151,18 @@ pub fn encode_sign( Ok((jwt, post_encoded_disclosures)) } +/// Represents a disclosure before encoding #[derive(Clone, Debug)] pub enum UnencodedDisclosure { + /// Property style disclosure Property(String, serde_json::Value), + + /// Array style disclosure ArrayItem(String, serde_json::Value), } impl UnencodedDisclosure { + /// Create a new property style UnencodedDisclosure pub fn new_property, Value: Serialize>( name: S, value: &Value, @@ -155,6 +173,7 @@ impl UnencodedDisclosure { )) } + /// Create a new array style UnencodedDisclosure pub fn new_array_item, Value: Serialize>( parent: S, value: &Value, @@ -165,6 +184,7 @@ impl UnencodedDisclosure { )) } + /// Obtain reference to the disclosure's JSON object pub fn claim_value_as_ref(&self) -> &serde_json::Value { match self { UnencodedDisclosure::ArrayItem(_, value) => value, @@ -172,6 +192,8 @@ impl UnencodedDisclosure { } } + /// Obtain reference to the disclosure's name if it is an array style + /// disclosure pub fn encoded_claim_name(&self) -> Option<&str> { match self { UnencodedDisclosure::Property(name, _) => Some(name), @@ -179,6 +201,7 @@ impl UnencodedDisclosure { } } + /// Encode the disclosure into the plaintext base64 string encoding pub fn encode(&self) -> Result { encode_disclosure(self.encoded_claim_name(), self.claim_value_as_ref()) } diff --git a/ssi-sd-jwt/src/error.rs b/ssi-sd-jwt/src/error.rs index ca4105bad..45dafc685 100644 --- a/ssi-sd-jwt/src/error.rs +++ b/ssi-sd-jwt/src/error.rs @@ -1,47 +1,87 @@ +/// Errors in the decode pathway #[derive(thiserror::Error, Debug)] pub enum DecodeError { + /// Unable to deserialize string format of concatenated tildes #[error("Unable to deserialize string format of concatenated tildes")] UnableToDeserializeStringFormat, + + /// JWT is missing _sd_alg property #[error("JWT is missing _sd_alg property")] MissingSdAlg, + + /// Unknown value of _sd_alg #[error("Unknown value of _sd_alg {0}")] UnknownSdAlg(String), + + /// Multiple disclosures given with the same hash #[error("Multiple disclosures given with the same hash")] MultipleDisclosuresWithSameHash, + + /// An _sd claim wasn't a string #[error("An _sd claim wasn't a string")] SdClaimNotString, - #[error("And _sd property was not an array type")] + + /// An _sd property was not an array type + #[error("An _sd property was not an array type")] SdPropertyNotArray, + + /// A disclosure claim would collid with an existing JWT claim #[error("A disclosure claim would collid with an existing JWT claim")] DisclosureClaimCollidesWithJwtClaim, + + /// A disclosure is malformed #[error("A disclosure is malformed")] DisclosureMalformed, + + /// A single disclosure was used multiple times #[error("A single disclosure was used multiple times")] DisclosureUsedMultipleTimes, + + /// Found an array item disclosure when expecting a property type #[error("Found an array item disclosure when expecting a property type")] ArrayDisclosureWhenExpectingProperty, + + /// Found a property type disclosure when expecting an array item #[error("Found a property type disclosure when expecting an array item")] PropertyDisclosureWhenExpectingArray, + + /// A disclosure was not used during decoding #[error("A disclosure was not used during decoding")] UnusedDisclosure, + + /// Bubbled up error from ssi_jws #[error(transparent)] JWS(#[from] ssi_jws::Error), + + /// Bubbled up error from serde_json #[error(transparent)] JsonDeserialization(#[from] serde_json::Error), } +/// Errors in the Encode pathway #[derive(thiserror::Error, Debug)] pub enum EncodeError { + /// The base claims to encode did not become a JSON object #[error("The base claims to encode did not become a JSON object")] EncodedAsNonObject, + + /// The base claims to encode contained a property reserved by SD-JWT #[error("The base claims to encode contained a property reserved by SD-JWT")] EncodedClaimsContainsReservedProperty, + + /// A property for an array sd claim was not an array #[error("A property for an array sd claim was not an array")] ExpectedArray, + + /// A disclosure was not used during decoding #[error("A disclosure was not used during decoding")] UnusedDisclosure, + + /// Bubbled up error from ssi_jws #[error(transparent)] JWS(#[from] ssi_jws::Error), + + /// Bubbled up error from serde_json #[error(transparent)] JsonSerialization(#[from] serde_json::Error), } diff --git a/ssi-sd-jwt/src/lib.rs b/ssi-sd-jwt/src/lib.rs index 929fb7c98..6505cb7dd 100644 --- a/ssi-sd-jwt/src/lib.rs +++ b/ssi-sd-jwt/src/lib.rs @@ -1,3 +1,7 @@ +#![warn(missing_docs)] + +//! SSI library for processing SD-JWTs + mod decode; pub(crate) mod digest; pub(crate) mod encode; @@ -8,18 +12,12 @@ pub(crate) mod verify; pub use decode::{decode_verify, decode_verify_disclosure_array, ValidityClaims}; pub use digest::{hash_encoded_disclosure, SdAlg}; pub use encode::{ - encode_array_disclosure, encode_property_disclosure, encode_sign, UnencodedDisclosure, + encode_array_disclosure, encode_property_disclosure, encode_sign, Disclosure, + UnencodedDisclosure, }; pub use error::{DecodeError, EncodeError}; pub use serialized::{deserialize_string_format, serialize_string_format}; -pub use verify::verify_sd_disclosures_array; const SD_CLAIM_NAME: &str = "_sd"; const SD_ALG_CLAIM_NAME: &str = "_sd_alg"; const ARRAY_CLAIM_ITEM_PROPERTY_NAME: &str = "..."; - -#[derive(Debug)] -pub struct Disclosure { - pub encoded: String, - pub hash: String, -} diff --git a/ssi-sd-jwt/src/serialized.rs b/ssi-sd-jwt/src/serialized.rs index f81aa881f..efc9c98ae 100644 --- a/ssi-sd-jwt/src/serialized.rs +++ b/ssi-sd-jwt/src/serialized.rs @@ -1,3 +1,5 @@ +/// Lower level API to encode a fully encoded SD-JWT given a JWT and disclosure array +/// already fully encoded into their string representations pub fn serialize_string_format(jwt: &str, disclosures: &[&str]) -> String { let mut serialized = format!("{}~", jwt); @@ -20,6 +22,8 @@ impl<'a> Deserialized<'a> { } } +/// Lower level API to seserialize a fully encoded SD-JWT into it's JWT and disclosure +/// components pub fn deserialize_string_format(serialized: &str) -> Option> { if !serialized.contains('~') { return None; diff --git a/ssi-sd-jwt/src/verify.rs b/ssi-sd-jwt/src/verify.rs index a642c8a51..534dbf728 100644 --- a/ssi-sd-jwt/src/verify.rs +++ b/ssi-sd-jwt/src/verify.rs @@ -1,6 +1,5 @@ use jose_b64::base64ct::{Base64UrlUnpadded, Encoding}; -use crate::digest::{hash_encoded_disclosure, SdAlg}; use crate::DecodeError; #[derive(Debug, PartialEq)] @@ -61,60 +60,54 @@ fn validate_array_item_disclosure( }) } -pub fn verify_sd_disclosures_array( - digest_algo: SdAlg, - disclosures: &[&str], - sd_claim: &[&str], -) -> Result { - let mut verfied_claims = serde_json::Map::new(); +#[cfg(test)] +mod tests { + use super::*; - for disclosure in disclosures { - let disclosure_hash = hash_encoded_disclosure(digest_algo, disclosure); + use crate::digest::{hash_encoded_disclosure, SdAlg}; - if !disclosure_hash_exists_in_sd_claims(&disclosure_hash, sd_claim) { - continue; - } + fn verify_sd_disclosures_array( + digest_algo: SdAlg, + disclosures: &[&str], + sd_claim: &[&str], + ) -> Result { + let mut verfied_claims = serde_json::Map::new(); + + for disclosure in disclosures { + let disclosure_hash = hash_encoded_disclosure(digest_algo, disclosure); - let decoded = DecodedDisclosure::new(disclosure)?; + if !disclosure_hash_exists_in_sd_claims(&disclosure_hash, sd_claim) { + continue; + } + + let decoded = DecodedDisclosure::new(disclosure)?; - match decoded.kind { - DisclosureKind::Property { name, value } => { - let orig = verfied_claims.insert(name, value); + match decoded.kind { + DisclosureKind::Property { name, value } => { + let orig = verfied_claims.insert(name, value); - if orig.is_some() { - return Err(DecodeError::DisclosureUsedMultipleTimes); + if orig.is_some() { + return Err(DecodeError::DisclosureUsedMultipleTimes); + } + } + DisclosureKind::ArrayItem(_) => { + return Err(DecodeError::ArrayDisclosureWhenExpectingProperty); } - } - DisclosureKind::ArrayItem(_) => { - return Err(DecodeError::ArrayDisclosureWhenExpectingProperty); } } - } - Ok(serde_json::Value::Object(verfied_claims)) -} - -fn disclosure_hash_exists_in_sd_claims(disclosure_hash: &str, sd_claim: &[&str]) -> bool { - // Todo: Yeah, this is O(N^2) since it's embedded in the for loop in - // verify_disclosures(). I'm expecting small values of N for sd_claim - // where it's just easier to check them rather than - // going through the rigmarole of adding them to map structure beforehand. - // Validate this though. - for sd_claim_item in sd_claim { - // Todo: Does this need to be constant time? I can't think of a reason - // given that sd_claims are ostensibly public anyway, but probably - // should just to be safe. - if &disclosure_hash == sd_claim_item { - return true; - } + Ok(serde_json::Value::Object(verfied_claims)) } - false -} + fn disclosure_hash_exists_in_sd_claims(disclosure_hash: &str, sd_claim: &[&str]) -> bool { + for sd_claim_item in sd_claim { + if &disclosure_hash == sd_claim_item { + return true; + } + } -#[cfg(test)] -mod tests { - use super::*; + false + } #[test] fn test_verify_disclosures() { From 1dc77059a5f6b788299b06b00ee293fee995b44c Mon Sep 17 00:00:00 2001 From: Tristan Miller Date: Fri, 13 Oct 2023 09:19:50 -0600 Subject: [PATCH 08/16] Add doc justifying unwrap() --- ssi-sd-jwt/src/encode.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ssi-sd-jwt/src/encode.rs b/ssi-sd-jwt/src/encode.rs index 00b86de21..c22126bf1 100644 --- a/ssi-sd-jwt/src/encode.rs +++ b/ssi-sd-jwt/src/encode.rs @@ -130,6 +130,8 @@ pub fn encode_sign( let _ = base_claims_obj.insert(claim_name.clone(), serde_json::json!([])); } + // unwrap() justified as id statement above adds claim_name to the map if it + // doesn't previously exist let array = base_claims_obj.get_mut(claim_name).unwrap(); let array = array.as_array_mut().ok_or(EncodeError::ExpectedArray)?; From 37da698e107731a220fac12abb9c7009ff6288c4 Mon Sep 17 00:00:00 2001 From: Tristan Miller Date: Fri, 13 Oct 2023 09:22:32 -0600 Subject: [PATCH 09/16] Missed fmt --- ssi-sd-jwt/src/encode.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ssi-sd-jwt/src/encode.rs b/ssi-sd-jwt/src/encode.rs index c22126bf1..0db5fd49a 100644 --- a/ssi-sd-jwt/src/encode.rs +++ b/ssi-sd-jwt/src/encode.rs @@ -130,7 +130,7 @@ pub fn encode_sign( let _ = base_claims_obj.insert(claim_name.clone(), serde_json::json!([])); } - // unwrap() justified as id statement above adds claim_name to the map if it + // unwrap() justified as id statement above adds claim_name to the map if it // doesn't previously exist let array = base_claims_obj.get_mut(claim_name).unwrap(); let array = array.as_array_mut().ok_or(EncodeError::ExpectedArray)?; From baa8ea3020b0264dcfb10203565345aa8087b5dc Mon Sep 17 00:00:00 2001 From: Tristan Miller Date: Thu, 19 Oct 2023 09:43:12 -0600 Subject: [PATCH 10/16] Spelling and grammar --- ssi-sd-jwt/src/decode.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ssi-sd-jwt/src/decode.rs b/ssi-sd-jwt/src/decode.rs index 3c47d42f1..58a7d8368 100644 --- a/ssi-sd-jwt/src/decode.rs +++ b/ssi-sd-jwt/src/decode.rs @@ -21,8 +21,8 @@ pub struct ValidityClaims { pub exp: Option, } -/// High level API to decode a fuilly encoded SD-JWT. That is a JWT and selective -/// disclosures seperated by tildes +/// High level API to decode a fully encoded SD-JWT. That is a JWT and selective +/// disclosures separated by tildes pub fn decode_verify( serialized: &str, key: &JWK, @@ -33,7 +33,7 @@ pub fn decode_verify( decode_verify_disclosure_array(deserialized.jwt, key, &deserialized.disclosures) } -/// Lower level API to decode an SD-JWT that has already been split into it's +/// Lower level API to decode an SD-JWT that has already been split into its /// JWT and disclosure components pub fn decode_verify_disclosure_array( jwt: &str, From 39612feb6240439e84907fed0fba8e57da66a7e3 Mon Sep 17 00:00:00 2001 From: Tristan Miller Date: Thu, 19 Oct 2023 09:50:04 -0600 Subject: [PATCH 11/16] Remove ValidityClaims concept, concept removed from draft --- ssi-sd-jwt/src/decode.rs | 23 +++-------------------- ssi-sd-jwt/src/lib.rs | 2 +- ssi-sd-jwt/tests/decode.rs | 25 +++---------------------- ssi-sd-jwt/tests/full_pathway.rs | 13 ++++++------- ssi-sd-jwt/tests/rfc_examples.rs | 10 +++++----- 5 files changed, 18 insertions(+), 55 deletions(-) diff --git a/ssi-sd-jwt/src/decode.rs b/ssi-sd-jwt/src/decode.rs index 58a7d8368..db2a97dd7 100644 --- a/ssi-sd-jwt/src/decode.rs +++ b/ssi-sd-jwt/src/decode.rs @@ -1,32 +1,17 @@ use serde::de::DeserializeOwned; -use serde::Deserialize; use ssi_jwk::JWK; -use ssi_jwt::NumericDate; use std::collections::BTreeMap; use crate::serialized::deserialize_string_format; use crate::verify::{DecodedDisclosure, DisclosureKind}; use crate::*; -/// Expiration validity claims that are sampled before expanding selective disclosures -#[derive(Debug, Deserialize, PartialEq)] -pub struct ValidityClaims { - /// Not Before claim - pub nbf: Option, - - /// Issued After claim - pub iat: Option, - - /// Expiration claim - pub exp: Option, -} - /// High level API to decode a fully encoded SD-JWT. That is a JWT and selective /// disclosures separated by tildes pub fn decode_verify( serialized: &str, key: &JWK, -) -> Result<(ValidityClaims, Claims), DecodeError> { +) -> Result { let deserialized = deserialize_string_format(serialized) .ok_or(DecodeError::UnableToDeserializeStringFormat)?; @@ -39,11 +24,9 @@ pub fn decode_verify_disclosure_array( jwt: &str, key: &JWK, disclosures: &[&str], -) -> Result<(ValidityClaims, Claims), DecodeError> { +) -> Result { let mut payload_claims: serde_json::Value = ssi_jwt::decode_verify(jwt, key)?; - let validity_claims: ValidityClaims = serde_json::from_value(payload_claims.clone())?; - let sd_alg = sd_alg(&payload_claims)?; let _ = payload_claims .as_object_mut() @@ -60,7 +43,7 @@ pub fn decode_verify_disclosure_array( } } - Ok((validity_claims, serde_json::from_value(payload_claims)?)) + Ok(serde_json::from_value(payload_claims)?) } fn sd_alg(claims: &serde_json::Value) -> Result { diff --git a/ssi-sd-jwt/src/lib.rs b/ssi-sd-jwt/src/lib.rs index 6505cb7dd..8badf9bc7 100644 --- a/ssi-sd-jwt/src/lib.rs +++ b/ssi-sd-jwt/src/lib.rs @@ -9,7 +9,7 @@ mod error; pub(crate) mod serialized; pub(crate) mod verify; -pub use decode::{decode_verify, decode_verify_disclosure_array, ValidityClaims}; +pub use decode::{decode_verify, decode_verify_disclosure_array}; pub use digest::{hash_encoded_disclosure, SdAlg}; pub use encode::{ encode_array_disclosure, encode_property_disclosure, encode_sign, Disclosure, diff --git a/ssi-sd-jwt/tests/decode.rs b/ssi-sd-jwt/tests/decode.rs index 26fd32012..83704722b 100644 --- a/ssi-sd-jwt/tests/decode.rs +++ b/ssi-sd-jwt/tests/decode.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use ssi_jwk::{Algorithm, JWK}; use ssi_jwt::NumericDate; -use ssi_sd_jwt::{decode_verify_disclosure_array, ValidityClaims}; +use ssi_sd_jwt::decode_verify_disclosure_array; #[derive(Debug, Default, Deserialize, Serialize, PartialEq)] struct ExampleClaims { @@ -85,7 +85,7 @@ const NATIONALITY_DE_DISCLOSURE: &'static str = "WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZ #[test] fn decode_single() { - let (_, claims) = decode_verify_disclosure_array::( + let claims = decode_verify_disclosure_array::( &test_standard_sd_jwt(), &test_key(), &[EMAIL_DISCLOSURE], @@ -105,7 +105,7 @@ fn decode_single() { #[test] fn decode_single_array_item() { - let (_, claims) = decode_verify_disclosure_array::( + let claims = decode_verify_disclosure_array::( &test_standard_sd_jwt(), &test_key(), &[NATIONALITY_DE_DISCLOSURE], @@ -121,22 +121,3 @@ fn decode_single_array_item() { }, ) } - -#[test] -fn validitity_decodes() { - let (validity, _) = decode_verify_disclosure_array::( - &test_standard_sd_jwt(), - &test_key(), - &[], - ) - .unwrap(); - - assert_eq!( - validity, - ValidityClaims { - nbf: None, - iat: Some(NumericDate::try_from_seconds(1683000000.0).unwrap()), - exp: Some(NumericDate::try_from_seconds(1883000000.0).unwrap()), - } - ) -} diff --git a/ssi-sd-jwt/tests/full_pathway.rs b/ssi-sd-jwt/tests/full_pathway.rs index 6a3bac29f..c085469f4 100644 --- a/ssi-sd-jwt/tests/full_pathway.rs +++ b/ssi-sd-jwt/tests/full_pathway.rs @@ -44,7 +44,7 @@ fn full_pathway_regular_claim() { ) .unwrap(); - let (_, full_jwt_claims) = decode_verify_disclosure_array::( + let full_jwt_claims = decode_verify_disclosure_array::( &jwt, &test_key(), &[&disclosures[0].encoded, &disclosures[1].encoded], @@ -60,7 +60,7 @@ fn full_pathway_regular_claim() { full_jwt_claims, ); - let (_, one_sd_claim) = + let one_sd_claim = decode_verify_disclosure_array::(&jwt, &test_key(), &[&disclosures[1].encoded]) .unwrap(); @@ -99,7 +99,7 @@ fn full_pathway_array() { ) .unwrap(); - let (_, full_jwt_claims) = decode_verify_disclosure_array::( + let full_jwt_claims = decode_verify_disclosure_array::( &jwt, &test_key(), &[&disclosures[0].encoded, &disclosures[1].encoded], @@ -169,8 +169,7 @@ fn nested_claims() { .unwrap(); // No claims provided - let (_, no_sd_claims) = - decode_verify_disclosure_array::(&jwt, &test_key(), &[]).unwrap(); + let no_sd_claims = decode_verify_disclosure_array::(&jwt, &test_key(), &[]).unwrap(); assert_eq!( no_sd_claims, Claims { @@ -180,7 +179,7 @@ fn nested_claims() { ); // Outer provided - let (_, outer_provided) = + let outer_provided = decode_verify_disclosure_array::(&jwt, &test_key(), &[&outer_disclosure.encoded]) .unwrap(); assert_eq!( @@ -192,7 +191,7 @@ fn nested_claims() { ); // Inner and outer provided - let (_, inner_and_outer_provided) = decode_verify_disclosure_array::( + let inner_and_outer_provided = decode_verify_disclosure_array::( &jwt, &test_key(), &[&outer_disclosure.encoded, &inner_disclosure.encoded], diff --git a/ssi-sd-jwt/tests/rfc_examples.rs b/ssi-sd-jwt/tests/rfc_examples.rs index 4e7ea3b5c..e1584c3bc 100644 --- a/ssi-sd-jwt/tests/rfc_examples.rs +++ b/ssi-sd-jwt/tests/rfc_examples.rs @@ -79,7 +79,7 @@ fn rfc_a_1_example_2_verification() { "WyJ5eXRWYmRBUEdjZ2wyckk0QzlHU29nIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0"; // Raw with no disclosures - let (_, no_disclosures) = + let no_disclosures = decode_verify_disclosure_array::(EXAMPLE_2_JWT, &rfc_a_5_key(), &[]) .unwrap(); @@ -95,7 +95,7 @@ fn rfc_a_1_example_2_verification() { ); // Top level claim disclosed - let (_, sub_claim_disclosed) = decode_verify_disclosure_array::( + let sub_claim_disclosed = decode_verify_disclosure_array::( EXAMPLE_2_JWT, &rfc_a_5_key(), &[SUB_CLAIM_DISCLOSURE], @@ -115,7 +115,7 @@ fn rfc_a_1_example_2_verification() { ); // Address claim disclosed - let (_, address_country_disclosed) = decode_verify_disclosure_array::( + let address_country_disclosed = decode_verify_disclosure_array::( EXAMPLE_2_JWT, &rfc_a_5_key(), &[ADDRESS_COUNTRY_DISCLOSURE], @@ -137,7 +137,7 @@ fn rfc_a_1_example_2_verification() { ); // All claims disclosed - let (_, all_claims) = decode_verify_disclosure_array::( + let all_claims = decode_verify_disclosure_array::( EXAMPLE_2_JWT, &rfc_a_5_key(), &[ @@ -308,7 +308,7 @@ fn rfc_a_2_example_3_verification() { "WyJreDVrRjE3Vi14MEptd1V4OXZndnR3IiwgIm1zaXNkbiIsICI0OTEyMzQ1Njc4OSJd"; // All Claims - let (_, all_claims) = decode_verify_disclosure_array::( + let all_claims = decode_verify_disclosure_array::( EXAMPLE_3_JWT, &rfc_a_5_key(), &[ From be1e4951daa8b470405afa0ed02b397cab451a97 Mon Sep 17 00:00:00 2001 From: Tristan Miller Date: Thu, 19 Oct 2023 10:11:40 -0600 Subject: [PATCH 12/16] Remove unwrap by combining extract and remove --- ssi-sd-jwt/src/decode.rs | 18 +++++++++--------- ssi-sd-jwt/src/error.rs | 8 ++++++++ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/ssi-sd-jwt/src/decode.rs b/ssi-sd-jwt/src/decode.rs index db2a97dd7..a9e196d5d 100644 --- a/ssi-sd-jwt/src/decode.rs +++ b/ssi-sd-jwt/src/decode.rs @@ -27,11 +27,7 @@ pub fn decode_verify_disclosure_array( ) -> Result { let mut payload_claims: serde_json::Value = ssi_jwt::decode_verify(jwt, key)?; - let sd_alg = sd_alg(&payload_claims)?; - let _ = payload_claims - .as_object_mut() - .unwrap() - .remove(SD_ALG_CLAIM_NAME); + let sd_alg = extract_sd_alg(&mut payload_claims)?; let mut disclosures = translate_to_in_progress_disclosures(disclosures, sd_alg)?; @@ -46,12 +42,16 @@ pub fn decode_verify_disclosure_array( Ok(serde_json::from_value(payload_claims)?) } -fn sd_alg(claims: &serde_json::Value) -> Result { - let alg_name = claims[SD_ALG_CLAIM_NAME] - .as_str() +fn extract_sd_alg(claims: &mut serde_json::Value) -> Result { + let claims = claims.as_object_mut().ok_or(DecodeError::ClaimsWrongType)?; + + let sd_alg_claim = claims + .remove(SD_ALG_CLAIM_NAME) .ok_or(DecodeError::MissingSdAlg)?; - SdAlg::try_from(alg_name) + let sd_alg = sd_alg_claim.as_str().ok_or(DecodeError::SdAlgWrongType)?; + + SdAlg::try_from(sd_alg) } fn translate_to_in_progress_disclosures( diff --git a/ssi-sd-jwt/src/error.rs b/ssi-sd-jwt/src/error.rs index 45dafc685..37cbecf33 100644 --- a/ssi-sd-jwt/src/error.rs +++ b/ssi-sd-jwt/src/error.rs @@ -5,6 +5,10 @@ pub enum DecodeError { #[error("Unable to deserialize string format of concatenated tildes")] UnableToDeserializeStringFormat, + /// JWT payload claims were not a JSON object + #[error("JWT payload claims were not a JSON object")] + ClaimsWrongType, + /// JWT is missing _sd_alg property #[error("JWT is missing _sd_alg property")] MissingSdAlg, @@ -13,6 +17,10 @@ pub enum DecodeError { #[error("Unknown value of _sd_alg {0}")] UnknownSdAlg(String), + /// Type of _sd_alg was not string + #[error("Type of _sd_alg was not string")] + SdAlgWrongType, + /// Multiple disclosures given with the same hash #[error("Multiple disclosures given with the same hash")] MultipleDisclosuresWithSameHash, From f68da70b1eb85980ccdd987b0ca384bca0dfe10d Mon Sep 17 00:00:00 2001 From: Tristan Miller Date: Thu, 19 Oct 2023 10:59:33 -0600 Subject: [PATCH 13/16] Clean up types for disclosure processing --- ssi-sd-jwt/src/decode.rs | 11 ++- ssi-sd-jwt/src/{verify.rs => disclosure.rs} | 0 ssi-sd-jwt/src/lib.rs | 19 ++++- ssi-sd-jwt/src/serialized.rs | 14 +--- ssi-sd-jwt/tests/decode.rs | 14 ++-- ssi-sd-jwt/tests/full_pathway.rs | 61 ++++++++++---- ssi-sd-jwt/tests/rfc_examples.rs | 93 ++++++++++++--------- 7 files changed, 133 insertions(+), 79 deletions(-) rename ssi-sd-jwt/src/{verify.rs => disclosure.rs} (100%) diff --git a/ssi-sd-jwt/src/decode.rs b/ssi-sd-jwt/src/decode.rs index a9e196d5d..471ef2d1e 100644 --- a/ssi-sd-jwt/src/decode.rs +++ b/ssi-sd-jwt/src/decode.rs @@ -2,8 +2,8 @@ use serde::de::DeserializeOwned; use ssi_jwk::JWK; use std::collections::BTreeMap; +use crate::disclosure::{DecodedDisclosure, DisclosureKind}; use crate::serialized::deserialize_string_format; -use crate::verify::{DecodedDisclosure, DisclosureKind}; use crate::*; /// High level API to decode a fully encoded SD-JWT. That is a JWT and selective @@ -15,21 +15,20 @@ pub fn decode_verify( let deserialized = deserialize_string_format(serialized) .ok_or(DecodeError::UnableToDeserializeStringFormat)?; - decode_verify_disclosure_array(deserialized.jwt, key, &deserialized.disclosures) + decode_verify_disclosure_array(deserialized, key) } /// Lower level API to decode an SD-JWT that has already been split into its /// JWT and disclosure components pub fn decode_verify_disclosure_array( - jwt: &str, + deserialized: Deserialized<'_>, key: &JWK, - disclosures: &[&str], ) -> Result { - let mut payload_claims: serde_json::Value = ssi_jwt::decode_verify(jwt, key)?; + let mut payload_claims: serde_json::Value = ssi_jwt::decode_verify(deserialized.jwt, key)?; let sd_alg = extract_sd_alg(&mut payload_claims)?; - let mut disclosures = translate_to_in_progress_disclosures(disclosures, sd_alg)?; + let mut disclosures = translate_to_in_progress_disclosures(&deserialized.disclosures, sd_alg)?; visit_claims(&mut payload_claims, &mut disclosures)?; diff --git a/ssi-sd-jwt/src/verify.rs b/ssi-sd-jwt/src/disclosure.rs similarity index 100% rename from ssi-sd-jwt/src/verify.rs rename to ssi-sd-jwt/src/disclosure.rs diff --git a/ssi-sd-jwt/src/lib.rs b/ssi-sd-jwt/src/lib.rs index 8badf9bc7..49ec746c1 100644 --- a/ssi-sd-jwt/src/lib.rs +++ b/ssi-sd-jwt/src/lib.rs @@ -4,10 +4,10 @@ mod decode; pub(crate) mod digest; +pub(crate) mod disclosure; pub(crate) mod encode; mod error; pub(crate) mod serialized; -pub(crate) mod verify; pub use decode::{decode_verify, decode_verify_disclosure_array}; pub use digest::{hash_encoded_disclosure, SdAlg}; @@ -21,3 +21,20 @@ pub use serialized::{deserialize_string_format, serialize_string_format}; const SD_CLAIM_NAME: &str = "_sd"; const SD_ALG_CLAIM_NAME: &str = "_sd_alg"; const ARRAY_CLAIM_ITEM_PROPERTY_NAME: &str = "..."; + +/// SD-JWT components to be presented for decodindg and validation whtehre coming from +/// a compact representation, eveloping JWT, etc. +#[derive(Debug, PartialEq)] +pub struct Deserialized<'a> { + /// JWT who's claims can be selectively disclosed + pub jwt: &'a str, + /// Disclosures for associated JWT + pub disclosures: Vec<&'a str>, +} + +impl<'a> Deserialized<'a> { + /// Convert Deserialized into a compact serialized format + pub fn compact_serialize(&self) -> String { + serialize_string_format(self.jwt, &self.disclosures) + } +} diff --git a/ssi-sd-jwt/src/serialized.rs b/ssi-sd-jwt/src/serialized.rs index efc9c98ae..034927d3b 100644 --- a/ssi-sd-jwt/src/serialized.rs +++ b/ssi-sd-jwt/src/serialized.rs @@ -1,3 +1,5 @@ +use crate::Deserialized; + /// Lower level API to encode a fully encoded SD-JWT given a JWT and disclosure array /// already fully encoded into their string representations pub fn serialize_string_format(jwt: &str, disclosures: &[&str]) -> String { @@ -10,18 +12,6 @@ pub fn serialize_string_format(jwt: &str, disclosures: &[&str]) -> String { serialized } -#[derive(Debug, PartialEq)] -pub struct Deserialized<'a> { - pub jwt: &'a str, - pub disclosures: Vec<&'a str>, -} - -impl<'a> Deserialized<'a> { - pub fn serialize(&self) -> String { - serialize_string_format(self.jwt, &self.disclosures) - } -} - /// Lower level API to seserialize a fully encoded SD-JWT into it's JWT and disclosure /// components pub fn deserialize_string_format(serialized: &str) -> Option> { diff --git a/ssi-sd-jwt/tests/decode.rs b/ssi-sd-jwt/tests/decode.rs index 83704722b..93a388ad3 100644 --- a/ssi-sd-jwt/tests/decode.rs +++ b/ssi-sd-jwt/tests/decode.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use ssi_jwk::{Algorithm, JWK}; use ssi_jwt::NumericDate; -use ssi_sd_jwt::decode_verify_disclosure_array; +use ssi_sd_jwt::{decode_verify_disclosure_array, Deserialized}; #[derive(Debug, Default, Deserialize, Serialize, PartialEq)] struct ExampleClaims { @@ -86,9 +86,11 @@ const NATIONALITY_DE_DISCLOSURE: &'static str = "WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZ #[test] fn decode_single() { let claims = decode_verify_disclosure_array::( - &test_standard_sd_jwt(), + Deserialized { + jwt: &test_standard_sd_jwt(), + disclosures: vec![EMAIL_DISCLOSURE], + }, &test_key(), - &[EMAIL_DISCLOSURE], ) .unwrap(); @@ -106,9 +108,11 @@ fn decode_single() { #[test] fn decode_single_array_item() { let claims = decode_verify_disclosure_array::( - &test_standard_sd_jwt(), + Deserialized { + jwt: &test_standard_sd_jwt(), + disclosures: vec![NATIONALITY_DE_DISCLOSURE], + }, &test_key(), - &[NATIONALITY_DE_DISCLOSURE], ) .unwrap(); diff --git a/ssi-sd-jwt/tests/full_pathway.rs b/ssi-sd-jwt/tests/full_pathway.rs index c085469f4..983b83916 100644 --- a/ssi-sd-jwt/tests/full_pathway.rs +++ b/ssi-sd-jwt/tests/full_pathway.rs @@ -45,9 +45,11 @@ fn full_pathway_regular_claim() { .unwrap(); let full_jwt_claims = decode_verify_disclosure_array::( - &jwt, + Deserialized { + jwt: &jwt, + disclosures: vec![&disclosures[0].encoded, &disclosures[1].encoded], + }, &test_key(), - &[&disclosures[0].encoded, &disclosures[1].encoded], ) .unwrap(); @@ -60,9 +62,14 @@ fn full_pathway_regular_claim() { full_jwt_claims, ); - let one_sd_claim = - decode_verify_disclosure_array::(&jwt, &test_key(), &[&disclosures[1].encoded]) - .unwrap(); + let one_sd_claim = decode_verify_disclosure_array::( + Deserialized { + jwt: &jwt, + disclosures: vec![&disclosures[1].encoded], + }, + &test_key(), + ) + .unwrap(); assert_eq!( BaseClaims { @@ -100,9 +107,11 @@ fn full_pathway_array() { .unwrap(); let full_jwt_claims = decode_verify_disclosure_array::( - &jwt, + Deserialized { + jwt: &jwt, + disclosures: vec![&disclosures[0].encoded, &disclosures[1].encoded], + }, &test_key(), - &[&disclosures[0].encoded, &disclosures[1].encoded], ) .unwrap(); @@ -169,7 +178,14 @@ fn nested_claims() { .unwrap(); // No claims provided - let no_sd_claims = decode_verify_disclosure_array::(&jwt, &test_key(), &[]).unwrap(); + let no_sd_claims = decode_verify_disclosure_array::( + Deserialized { + jwt: &jwt, + disclosures: vec![], + }, + &test_key(), + ) + .unwrap(); assert_eq!( no_sd_claims, Claims { @@ -179,9 +195,15 @@ fn nested_claims() { ); // Outer provided - let outer_provided = - decode_verify_disclosure_array::(&jwt, &test_key(), &[&outer_disclosure.encoded]) - .unwrap(); + let outer_provided = decode_verify_disclosure_array::( + Deserialized { + jwt: &jwt, + disclosures: vec![&outer_disclosure.encoded], + }, + &test_key(), + ) + .unwrap(); + assert_eq!( outer_provided, Claims { @@ -192,11 +214,14 @@ fn nested_claims() { // Inner and outer provided let inner_and_outer_provided = decode_verify_disclosure_array::( - &jwt, + Deserialized { + jwt: &jwt, + disclosures: vec![&outer_disclosure.encoded, &inner_disclosure.encoded], + }, &test_key(), - &[&outer_disclosure.encoded, &inner_disclosure.encoded], ) .unwrap(); + assert_eq!( inner_and_outer_provided, Claims { @@ -210,7 +235,13 @@ fn nested_claims() { ); // Inner without outer errors - let result = - decode_verify_disclosure_array::(&jwt, &test_key(), &[&inner_disclosure.encoded]); + let result = decode_verify_disclosure_array::( + Deserialized { + jwt: &jwt, + disclosures: vec![&inner_disclosure.encoded], + }, + &test_key(), + ); + assert!(result.is_err()); } diff --git a/ssi-sd-jwt/tests/rfc_examples.rs b/ssi-sd-jwt/tests/rfc_examples.rs index e1584c3bc..b2a5476b1 100644 --- a/ssi-sd-jwt/tests/rfc_examples.rs +++ b/ssi-sd-jwt/tests/rfc_examples.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use ssi_sd_jwt::decode_verify_disclosure_array; +use ssi_sd_jwt::{decode_verify_disclosure_array, Deserialized}; fn rfc_a_5_key() -> ssi_jwk::JWK { serde_json::from_value(serde_json::json!({ @@ -79,9 +79,14 @@ fn rfc_a_1_example_2_verification() { "WyJ5eXRWYmRBUEdjZ2wyckk0QzlHU29nIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0"; // Raw with no disclosures - let no_disclosures = - decode_verify_disclosure_array::(EXAMPLE_2_JWT, &rfc_a_5_key(), &[]) - .unwrap(); + let no_disclosures = decode_verify_disclosure_array::( + Deserialized { + jwt: EXAMPLE_2_JWT, + disclosures: vec![], + }, + &rfc_a_5_key(), + ) + .unwrap(); assert_eq!( no_disclosures, @@ -96,9 +101,11 @@ fn rfc_a_1_example_2_verification() { // Top level claim disclosed let sub_claim_disclosed = decode_verify_disclosure_array::( - EXAMPLE_2_JWT, + Deserialized { + jwt: EXAMPLE_2_JWT, + disclosures: vec![SUB_CLAIM_DISCLOSURE], + }, &rfc_a_5_key(), - &[SUB_CLAIM_DISCLOSURE], ) .unwrap(); @@ -116,9 +123,11 @@ fn rfc_a_1_example_2_verification() { // Address claim disclosed let address_country_disclosed = decode_verify_disclosure_array::( - EXAMPLE_2_JWT, + Deserialized { + jwt: EXAMPLE_2_JWT, + disclosures: vec![ADDRESS_COUNTRY_DISCLOSURE], + }, &rfc_a_5_key(), - &[ADDRESS_COUNTRY_DISCLOSURE], ) .unwrap(); @@ -138,20 +147,22 @@ fn rfc_a_1_example_2_verification() { // All claims disclosed let all_claims = decode_verify_disclosure_array::( - EXAMPLE_2_JWT, + Deserialized { + jwt: EXAMPLE_2_JWT, + disclosures: vec![ + SUB_CLAIM_DISCLOSURE, + GIVEN_NAME_DISCLOSURE, + FAMILY_NAME_DISCLOSURE, + EMAIL_CLAIM_DISCLOSURE, + PHONE_NUMBER_DISCLOSURE, + ADDRESS_STREET_ADDRESS_DISCLOSURES, + ADDRESS_LOCALITY_DISCLOSURE, + ADDRESS_REGION_DISCLOSURE, + ADDRESS_COUNTRY_DISCLOSURE, + BIRTHDATE_DISCLOSURE, + ], + }, &rfc_a_5_key(), - &[ - SUB_CLAIM_DISCLOSURE, - GIVEN_NAME_DISCLOSURE, - FAMILY_NAME_DISCLOSURE, - EMAIL_CLAIM_DISCLOSURE, - PHONE_NUMBER_DISCLOSURE, - ADDRESS_STREET_ADDRESS_DISCLOSURES, - ADDRESS_LOCALITY_DISCLOSURE, - ADDRESS_REGION_DISCLOSURE, - ADDRESS_COUNTRY_DISCLOSURE, - BIRTHDATE_DISCLOSURE, - ], ) .unwrap(); @@ -309,26 +320,28 @@ fn rfc_a_2_example_3_verification() { // All Claims let all_claims = decode_verify_disclosure_array::( - EXAMPLE_3_JWT, + Deserialized { + jwt: EXAMPLE_3_JWT, + disclosures: vec![ + VERIFIED_CLAIMS_TIME_DISCLOSURE, + VERIFIED_CLAIMS_VERIFICATION_PROCESS_DISCLOSURE, + VERIFIED_CLAIMS_EVIDENCE_0_TYPE_DISCLOSURE, + VERIFIED_CLAIMS_EVIDENCE_0_METHOD_DISCLOSURE, + VERIFIED_CLAIMS_EVIDENCE_0_TIME_DISCLOSURE, + VERIFIED_CLAIMS_EVIDENCE_0_DOCUMENT_DISCLOSURE, + VERIFIED_CLAIMS_EVIDENCE_0_DISCLOSURE, + VERIFIED_CLAIMS_CLAIMS_GIVEN_NAME_DISCLOSURE, + VERIFIED_CLAIMS_CLAIMS_FAMILY_NAME_DISCLOSURE, + VERIFIED_CLAIMS_CLAIMS_NATIONALITIES_DISCLOSURE, + VERIFIED_CLAIMS_CLAIMS_BIRTHDATE_DISCLOSURE, + VERIFIED_CLAIMS_CLAIMS_PLACE_OF_BIRTH_DISCLOSURE, + VERIFIED_CLAIMS_CLAIMS_ADDRESS_DISCLOSURE, + BIRTH_MIDDLE_NAME_DISCLOSURE, + SALUTATION_DISCLOSURE, + MSISDN_DISCLOSURE, + ], + }, &rfc_a_5_key(), - &[ - VERIFIED_CLAIMS_TIME_DISCLOSURE, - VERIFIED_CLAIMS_VERIFICATION_PROCESS_DISCLOSURE, - VERIFIED_CLAIMS_EVIDENCE_0_TYPE_DISCLOSURE, - VERIFIED_CLAIMS_EVIDENCE_0_METHOD_DISCLOSURE, - VERIFIED_CLAIMS_EVIDENCE_0_TIME_DISCLOSURE, - VERIFIED_CLAIMS_EVIDENCE_0_DOCUMENT_DISCLOSURE, - VERIFIED_CLAIMS_EVIDENCE_0_DISCLOSURE, - VERIFIED_CLAIMS_CLAIMS_GIVEN_NAME_DISCLOSURE, - VERIFIED_CLAIMS_CLAIMS_FAMILY_NAME_DISCLOSURE, - VERIFIED_CLAIMS_CLAIMS_NATIONALITIES_DISCLOSURE, - VERIFIED_CLAIMS_CLAIMS_BIRTHDATE_DISCLOSURE, - VERIFIED_CLAIMS_CLAIMS_PLACE_OF_BIRTH_DISCLOSURE, - VERIFIED_CLAIMS_CLAIMS_ADDRESS_DISCLOSURE, - BIRTH_MIDDLE_NAME_DISCLOSURE, - SALUTATION_DISCLOSURE, - MSISDN_DISCLOSURE, - ], ) .unwrap(); From e5c4e630cad300399e06a3e5ca41dd4739bd47ca Mon Sep 17 00:00:00 2001 From: Tristan Miller Date: Thu, 19 Oct 2023 11:58:37 -0600 Subject: [PATCH 14/16] Combine validation and extraction --- ssi-sd-jwt/src/decode.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/ssi-sd-jwt/src/decode.rs b/ssi-sd-jwt/src/decode.rs index 471ef2d1e..5f11c422a 100644 --- a/ssi-sd-jwt/src/decode.rs +++ b/ssi-sd-jwt/src/decode.rs @@ -108,16 +108,12 @@ fn visit_claims( } // Process _sd claim - let new_claims = if let Some(sd_claims) = payload_claims.get(SD_CLAIM_NAME) { - decode_sd_claims(sd_claims, disclosures)? + let new_claims = if let Some(sd_claims) = payload_claims.remove(SD_CLAIM_NAME) { + decode_sd_claims(&sd_claims, disclosures)? } else { vec![] }; - if payload_claims.contains_key(SD_CLAIM_NAME) { - payload_claims.remove(SD_CLAIM_NAME); - } - for (new_claim_name, mut new_claim_value) in new_claims { visit_claims(&mut new_claim_value, disclosures)?; From d8ebb5da7ba383edff03d05d46521bd8d8a18051 Mon Sep 17 00:00:00 2001 From: Tristan Miller Date: Thu, 19 Oct 2023 12:12:56 -0600 Subject: [PATCH 15/16] Change uses of jose-b64 to base64 --- ssi-sd-jwt/Cargo.toml | 2 +- ssi-sd-jwt/src/digest.rs | 4 ++-- ssi-sd-jwt/src/disclosure.rs | 7 ++++--- ssi-sd-jwt/src/encode.rs | 8 ++++---- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/ssi-sd-jwt/Cargo.toml b/ssi-sd-jwt/Cargo.toml index 25d5c251b..9f53578da 100644 --- a/ssi-sd-jwt/Cargo.toml +++ b/ssi-sd-jwt/Cargo.toml @@ -9,7 +9,7 @@ repository = "https://github.com/spruceid/ssi/" documentation = "https://docs.rs/ssi-sd-jwt/" [dependencies] -jose-b64 = { version = "0.1", features = ["json"] } +base64 = "0.12" rand = { version = "0.8" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/ssi-sd-jwt/src/digest.rs b/ssi-sd-jwt/src/digest.rs index 67d06209e..34c9e6421 100644 --- a/ssi-sd-jwt/src/digest.rs +++ b/ssi-sd-jwt/src/digest.rs @@ -1,4 +1,4 @@ -use jose_b64::base64ct::{Base64UrlUnpadded, Encoding}; +use base64::URL_SAFE_NO_PAD; use sha2::Digest; use crate::DecodeError; @@ -47,7 +47,7 @@ pub fn hash_encoded_disclosure(digest_algo: SdAlg, disclosure: &str) -> String { match digest_algo { SdAlg::Sha256 => { let digest = sha2::Sha256::digest(disclosure.as_bytes()); - Base64UrlUnpadded::encode_string(&digest) + base64::encode_config(digest, URL_SAFE_NO_PAD) } } } diff --git a/ssi-sd-jwt/src/disclosure.rs b/ssi-sd-jwt/src/disclosure.rs index 534dbf728..843229f95 100644 --- a/ssi-sd-jwt/src/disclosure.rs +++ b/ssi-sd-jwt/src/disclosure.rs @@ -1,4 +1,4 @@ -use jose_b64::base64ct::{Base64UrlUnpadded, Encoding}; +use base64::URL_SAFE_NO_PAD; use crate::DecodeError; @@ -19,8 +19,9 @@ pub enum DisclosureKind { impl DecodedDisclosure { pub fn new(encoded: &str) -> Result { - let bytes = Base64UrlUnpadded::decode_vec(encoded).unwrap(); - let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); + let bytes = base64::decode_config(encoded, URL_SAFE_NO_PAD) + .map_err(|_| DecodeError::DisclosureMalformed)?; + let json: serde_json::Value = serde_json::from_slice(&bytes)?; match json { serde_json::Value::Array(values) => match values.len() { diff --git a/ssi-sd-jwt/src/encode.rs b/ssi-sd-jwt/src/encode.rs index 0db5fd49a..6b89f44a7 100644 --- a/ssi-sd-jwt/src/encode.rs +++ b/ssi-sd-jwt/src/encode.rs @@ -1,4 +1,4 @@ -use jose_b64::base64ct::{Base64UrlUnpadded, Encoding}; +use base64::URL_SAFE_NO_PAD; use rand::{CryptoRng, Rng}; use serde::Serialize; use ssi_jwk::{Algorithm, JWK}; @@ -25,9 +25,9 @@ fn encode_disclosure_with_salt( None => serde_json::json!([salt, claim_value]), }; - let json_bytes = jose_b64::serde::Json::::new(disclosure)?; + let json_string = serde_json::to_string(&disclosure)?; - Ok(Base64UrlUnpadded::encode_string(json_bytes.as_ref())) + Ok(base64::encode_config(json_string, URL_SAFE_NO_PAD)) } pub fn encode_disclosure_with_rng( @@ -42,7 +42,7 @@ pub fn encode_disclosure_with_rng( rng.fill_bytes(&mut salt_bytes); - let salt = Base64UrlUnpadded::encode_string(&salt_bytes); + let salt = base64::encode_config(salt_bytes, URL_SAFE_NO_PAD); encode_disclosure_with_salt(&salt, claim_name, claim_value) } From 376601639992d65eee255a14973bf2999115759e Mon Sep 17 00:00:00 2001 From: Tristan Miller Date: Thu, 19 Oct 2023 12:19:51 -0600 Subject: [PATCH 16/16] Refactor validation to cleaner match statement --- ssi-sd-jwt/src/disclosure.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/ssi-sd-jwt/src/disclosure.rs b/ssi-sd-jwt/src/disclosure.rs index 843229f95..c428dff96 100644 --- a/ssi-sd-jwt/src/disclosure.rs +++ b/ssi-sd-jwt/src/disclosure.rs @@ -24,9 +24,9 @@ impl DecodedDisclosure { let json: serde_json::Value = serde_json::from_slice(&bytes)?; match json { - serde_json::Value::Array(values) => match values.len() { - 3 => validate_property_disclosure(&values), - 2 => validate_array_item_disclosure(&values), + serde_json::Value::Array(values) => match values.as_slice() { + [salt, name, value] => validate_property_disclosure(salt, name, value), + [salt, value] => validate_array_item_disclosure(salt, value), _ => Err(DecodeError::DisclosureMalformed), }, _ => Err(DecodeError::DisclosureMalformed), @@ -35,29 +35,32 @@ impl DecodedDisclosure { } fn validate_property_disclosure( - values: &[serde_json::Value], + salt: &serde_json::Value, + name: &serde_json::Value, + value: &serde_json::Value, ) -> Result { - let salt = values[0].as_str().ok_or(DecodeError::DisclosureMalformed)?; + let salt = salt.as_str().ok_or(DecodeError::DisclosureMalformed)?; - let name = values[1].as_str().ok_or(DecodeError::DisclosureMalformed)?; + let name = name.as_str().ok_or(DecodeError::DisclosureMalformed)?; Ok(DecodedDisclosure { salt: salt.to_owned(), kind: DisclosureKind::Property { name: name.to_owned(), - value: values[2].clone(), + value: value.clone(), }, }) } fn validate_array_item_disclosure( - values: &[serde_json::Value], + salt: &serde_json::Value, + value: &serde_json::Value, ) -> Result { - let salt = values[0].as_str().ok_or(DecodeError::DisclosureMalformed)?; + let salt = salt.as_str().ok_or(DecodeError::DisclosureMalformed)?; Ok(DecodedDisclosure { salt: salt.to_owned(), - kind: DisclosureKind::ArrayItem(values[1].clone()), + kind: DisclosureKind::ArrayItem(value.clone()), }) }