From 5c4a8fc66fce9c3aedd9658f667b28db79be2cc0 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab <31316147+abdulmth@users.noreply.github.com> Date: Mon, 8 Jan 2024 14:26:50 +0100 Subject: [PATCH] Upgrade to draft version 07 (#9) --- README.md | 2 +- src/decoder.rs | 54 ++++++++++++++++++++++++++++++++++- src/disclosure.rs | 8 +++--- src/error.rs | 3 ++ src/hasher.rs | 4 +-- src/key_binding_jwt_claims.rs | 5 ++-- tests/api_test.rs | 2 +- 7 files changed, 66 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8fba9d7..2eebcc2 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ # SD-JWT Reference implementation -Rust implementation of the [Selective Disclosure for JWTs (SD-JWT) **version 06**](https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-06.html) +Rust implementation of the [Selective Disclosure for JWTs (SD-JWT) **version 07**](https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html) ## Overview diff --git a/src/decoder.rs b/src/decoder.rs index e535125..404a42a 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -84,6 +84,12 @@ impl SdObjectDecoder { // Decode the object recursively. let mut decoded = self.decode_object(object, &disclosures_map, &mut processed_digests)?; + if processed_digests.len() != disclosures.len() { + return Err(crate::Error::UnusedDisclosures( + disclosures.len().saturating_sub(processed_digests.len()), + )); + } + // Remove `_sd_alg` in case it exists. decoded.remove(SD_ALG); Ok(decoded) @@ -139,6 +145,7 @@ impl SdObjectDecoder { if output.contains_key(&claim_name) { return Err(Error::ClaimCollisionError(claim_name)); } + processed_digests.push(digest_str.clone()); let recursively_decoded = match disclosure.claim_value { Value::Array(ref sub_arr) => Value::Array(self.decode_array(sub_arr, disclosures, processed_digests)?), @@ -199,11 +206,11 @@ impl SdObjectDecoder { if processed_digests.contains(&digest_in_array) { return Err(Error::DuplicateDigestError(digest_in_array)); } - if let Some(disclosure) = disclosures.get(&digest_in_array) { if disclosure.claim_name.is_some() { return Err(Error::InvalidDisclosure("array length must be 2".to_string())); } + processed_digests.push(digest_in_array.clone()); // Recursively decoded the disclosed values. let recursively_decoded = match disclosure.claim_value { Value::Array(ref sub_arr) => { @@ -245,6 +252,7 @@ impl Default for SdObjectDecoder { #[cfg(test)] mod test { + use crate::Disclosure; use crate::Error; use crate::SdObjectDecoder; use crate::SdObjectEncoder; @@ -281,4 +289,48 @@ mod test { let decoded = decoder.decode(encoder.object(), &vec![]).unwrap(); assert!(decoded.get("_sd_alg").is_none()); } + + #[test] + fn duplicate_digest() { + let object = json!({ + "id": "did:value", + }); + let mut encoder = SdObjectEncoder::try_from(object).unwrap(); + let dislosure: Disclosure = encoder.conceal(&["id"], Some("test".to_string())).unwrap(); + // 'obj' contains digest of `id` twice. + let obj = json!({ + "_sd":[ + "mcKLMnXQdCM0gJ5l4Hb6ignpVgCw4SfienkI8vFgpjE", + "mcKLMnXQdCM0gJ5l4Hb6ignpVgCw4SfienkI8vFgpjE" + ] + } + ); + let decoder = SdObjectDecoder::new_with_sha256(); + let result = decoder.decode(obj.as_object().unwrap(), &vec![dislosure.to_string()]); + assert!(matches!(result.err().unwrap(), crate::Error::DuplicateDigestError(_))); + } + + #[test] + fn unused_disclosure() { + let object = json!({ + "id": "did:value", + "tst": "tst-value" + }); + let mut encoder = SdObjectEncoder::try_from(object).unwrap(); + let disclosure_1: Disclosure = encoder.conceal(&["id"], Some("test".to_string())).unwrap(); + let disclosure_2: Disclosure = encoder.conceal(&["tst"], Some("test".to_string())).unwrap(); + // 'obj' contains only the digest of `id`. + let obj = json!({ + "_sd":[ + "mcKLMnXQdCM0gJ5l4Hb6ignpVgCw4SfienkI8vFgpjE", + ] + } + ); + let decoder = SdObjectDecoder::new_with_sha256(); + let result = decoder.decode( + obj.as_object().unwrap(), + &vec![disclosure_1.to_string(), disclosure_2.to_string()], + ); + assert!(matches!(result.err().unwrap(), crate::Error::UnusedDisclosures(1))); + } } diff --git a/src/disclosure.rs b/src/disclosure.rs index 7cc32c3..ac325ad 100644 --- a/src/disclosure.rs +++ b/src/disclosure.rs @@ -10,7 +10,7 @@ use std::fmt::Display; /// Represents an elements constructing a disclosure. /// Object properties and array elements disclosures are supported. /// -/// See: https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-06.html#name-disclosures +/// See: https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html#name-disclosures #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Disclosure { /// The salt value. @@ -69,7 +69,7 @@ impl Disclosure { if decoded.len() == 2 { Ok(Self { salt: decoded - .get(0) + .first() .ok_or(Error::InvalidDisclosure("invalid salt".to_string()))? .as_str() .ok_or(Error::InvalidDisclosure( @@ -87,7 +87,7 @@ impl Disclosure { } else if decoded.len() == 3 { Ok(Self { salt: decoded - .get(0) + .first() .ok_or(Error::InvalidDisclosure("invalid salt".to_string()))? .as_str() .ok_or(Error::InvalidDisclosure( @@ -140,7 +140,7 @@ mod test { use super::Disclosure; // Test values from: - // https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-05.html#appendix-A.2-7 + // https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html#appendix-A.2-7 #[test] fn test_parsing() { let disclosure = Disclosure::new( diff --git a/src/error.rs b/src/error.rs index f67f942..b4abe8c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -39,4 +39,7 @@ pub enum Error { #[error("salt size must be greater than or equal to 16")] InvalidSaltSize, + + #[error("the validation ended with {0} unused disclosure(s)")] + UnusedDisclosures(usize), } diff --git a/src/hasher.rs b/src/hasher.rs index 6c1e6b9..65c8807 100644 --- a/src/hasher.rs +++ b/src/hasher.rs @@ -15,7 +15,7 @@ pub const SHA_ALG_NAME: &str = "sha-256"; /// /// Implementations of this trait are expected only for algorithms listed in /// the IANA "Named Information Hash Algorithm" registry. -/// See [Hash Function Claim](https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-06.html#name-hash-function-claim) +/// See [Hash Function Claim](https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html#name-hash-function-claim) pub trait Hasher: Sync + Send { /// Digests input to produce unique fixed-size hash value in bytes. fn digest(&self, input: &[u8]) -> Vec; @@ -60,7 +60,7 @@ impl Hasher for Sha256Hasher { } } -// Some test values taken from https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-05.html#name-hashing-disclosures +// Some test values taken from https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html#name-disclosures #[cfg(test)] mod test { use crate::Hasher; diff --git a/src/key_binding_jwt_claims.rs b/src/key_binding_jwt_claims.rs index e8dcdf5..cc0ce4a 100644 --- a/src/key_binding_jwt_claims.rs +++ b/src/key_binding_jwt_claims.rs @@ -11,13 +11,12 @@ use crate::Hasher; use serde::Deserialize; use serde::Serialize; -/// +/// Claims set for key binding JWT. #[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] pub struct KeyBindingJwtClaims { pub iat: i64, pub aud: String, pub nonce: String, - #[serde(rename = "_sd_hash")] pub sd_hash: String, #[serde(flatten)] pub properties: BTreeMap, @@ -27,7 +26,7 @@ impl KeyBindingJwtClaims { pub const KB_JWT_HEADER_TYP: &'static str = " kb+jwt"; /// Creates a new [`KeyBindingJwtClaims`]. - /// When `issued_at` is left as None, it will automatically default to the current time + /// When `issued_at` is left as None, it will automatically default to the current time. /// /// # Panic /// When `issued_at` is set to `None` and the system returns time earlier than `SystemTime::UNIX_EPOCH`. diff --git a/tests/api_test.rs b/tests/api_test.rs index 981a4b2..7671f19 100644 --- a/tests/api_test.rs +++ b/tests/api_test.rs @@ -18,7 +18,7 @@ use sd_jwt_payload::SdObjectEncoder; #[test] fn test_complex_structure() { - // Values taken from https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-06.html#appendix-A.2 + // Values taken from https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html#appendix-A.2 let object = json!({ "verified_claims": { "verification": {