diff --git a/Cargo.toml b/Cargo.toml index aea4bfe..75539f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ lazy_static = { version = "1.4", optional = true } log = "0.4" rand = "0.8" serde = { version = "1.0.193", features = ["derive"] } -serde_json = "1.0" +serde_json = { version = "1.0.113", features = ["preserve_order"] } sha2 = "0.10" thiserror = "1.0.51" strum = { version = "0.25", default-features = false, features = ["std", "derive"] } diff --git a/README.md b/README.md index 067c7d0..1ab78c1 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ cargo test ``` ### Interoperability testing tool -Coming soon (planned for v0.0.7) +See [Generate tool README](./generate/README.md) document. ## External Dependencies diff --git a/generate/Cargo.toml b/generate/Cargo.toml new file mode 100644 index 0000000..76bdffd --- /dev/null +++ b/generate/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "sd-jwt-generate" +version = "0.1.0" +edition = "2021" +authors = ["Abdulbois Tursunov ", "Alexander Sukhachev "] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "4.4.10", features = ["derive"] } +serde = { version = "1.0.193", features = ["derive"] } +serde_yaml = "0.9.27" +serde_json = { version = "1.0.113", features = ["preserve_order"] } +jsonwebtoken = "9.1" +sd-jwt-rs = {path = "./..", features = ["mock_salts"]} \ No newline at end of file diff --git a/generate/README.md b/generate/README.md new file mode 100644 index 0000000..3dd3bfa --- /dev/null +++ b/generate/README.md @@ -0,0 +1,117 @@ +# SD-JWT Interop tool + +This tool is used to verify interoperability between the `sd-jwt-rust` and `sd-jwt-python` implementations of the [IETF SD-JWT specification](https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/). + +## How does the Interop tool work? + +The main idea is to generate data structures (SDJWT/presentation/verified claims) using both implementations and compare them. + +The `sd-jwt-python` is used to generate artifacts based on input data (`specification.yml`) and store them as files. +The interop tool (based on `sd-jwt-rust`) is used to generate artifacts using the same specification file, load artifacts stored in files by `sd-jwt-python` and compare them. The interop tool doesn't store any files on filesystem. + +There are some factors that make impossible to compare data due to non-equivalence data generated by different implementations: + +- Using random 'salt' in each run that make results different even though they are generated by the same implementation. +- Not equivalent json-serialized strings (different number of spaces) generated under the hood of the different implementations. +- Using 'decoy' digests in the SD-JWT payload. + +In order to reach reproducibility and equivalence of the values generated by both implementations it is required to use the same input data (issuer private key, user claims, etc.) and to get rid of some non-deterministic values during data generating (values of 'salt', for example). + +### Deterministic 'salt' + +In order to make it possible to get reproducible result each run it's required to use deterministic values of 'salt' used in internal algorithms. The `sd-jwt-python` project implements such behavior for test purposes. + +In order to use the same set of 'salt' values by the `sd-jwt-rust` project Python-implementation stores values in the `claims_vs_salts.json` file as artifact. The Interop tool loads values from the file and use it instead of random generated values (see the `mock_salts` feature). + + +### Similar json serialization + +In order to have the same json-strings used under the hood of the both implementations there is some code that gets rid of different number of spaces: + +```rust + value_str = value_str + .replace(":[", ": [") + .replace(',', ", ") + .replace("\":", "\": ") + .replace("\": ", "\": "); +``` + +### 'Decoy' SD items + +In order to make it possible to compare `SD-JWT` payloads that contains [decoy](https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html#name-decoy-digests) it was decided to detect and remove all `decoy` items from payloads and then compare them. + + +## How to use the interop tool? + +1. Install the prerequisites +2. Clone and build the `sd-jwt-rust` project +3. Clone and build the `sd-jwt-python` project +4. Generate artifacts using the `sd-jwt-python` project +5. Run the interop tool + +### Install the prerequisites + +In order to be able to build both implementations it is required to setup following tools: + +- `Rust`/`cargo` +- `poetry` + + +### Clone and build the `sd-jwt-rust` project + +```shell +git clone git@github.com:openwallet-foundation-labs/sd-jwt-rust.git +cd sd-jwt-rust/generate +cargo build +``` + + +### Clone and build the `sd-jwt-python` project + +Once the project repo is cloned to local directory it is necessary to apply special patch. +This patch is required to have some additional files as artifacts generated by the `sd-jwt-python` project. + +Files: + +- `claims_vs_salts.json` file contains values of so called 'salt' that have been used during `SDJWT` issuance. +- `issuer_key.pem` file contains the issuer's private key. +- `issuer_public_key.pem` file contains the issuer's public key. +- `holder_key.pem` file contains the holder's private key. + +The files are used to make it possible for this tool to generate the same values of artifacts (SDJWT payload/SDJWT claims/presentation/verified claims) that are generated by `sd-jwt-python`. + + +```shell +git clone git@github.com:openwallet-foundation-labs/sd-jwt-python.git +cd sd-jwt-python + +# apply the patch +git apply ../sd-jwt-rust/generate/sd_jwt_python.patch + +# build +poetry install && poetry build +``` + + + +### Generate artifacts using the `sd-jwt-python` project + +```shell +pushd sd-jwt-python/tests/testcases && poetry run ../../src/sd_jwt/bin/generate.py -- example && popd +pushd sd-jwt-python/examples && poetry run ../src/sd_jwt/bin/generate.py -- example && popd +``` + + +### Run the interop tool + +```shell +cd sd-jwt-rust/generate +sd_jwt_py="../../sd-jwt-python" +for cases_dir in $sd_jwt_py/examples $sd_jwt_py/tests/testcases; do + for test_case_dir in $(ls $cases_dir); do + if [[ -d $cases_dir/$test_case_dir ]]; then + ./target/debug/sd-jwt-generate -p $cases_dir/$test_case_dir + fi + done +done +``` diff --git a/generate/sd_jwt_python.patch b/generate/sd_jwt_python.patch new file mode 100644 index 0000000..5692bb0 --- /dev/null +++ b/generate/sd_jwt_python.patch @@ -0,0 +1,97 @@ +diff --git a/.gitignore b/.gitignore +index 1874e26..72ff453 100644 +--- a/.gitignore ++++ b/.gitignore +@@ -157,7 +157,7 @@ cython_debug/ + # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore + # and can be added to the global gitignore or merged into this file. For a more nuclear + # option (not recommended) you can uncomment the following to ignore the entire idea folder. +-#.idea/ ++.idea/ + + + # Ignore output of test cases except for specification.yml +diff --git a/pyproject.toml b/pyproject.toml +index 4294e64..47c9281 100644 +--- a/pyproject.toml ++++ b/pyproject.toml +@@ -12,7 +12,7 @@ jwcrypto = ">=1.3.1" + pyyaml = ">=5.4" + + [tool.poetry.group.dev.dependencies] +-flake8 = "^6.0.0" ++# flake8 = "^6.0.0" + black = "^23.3.0" + + [build-system] +diff --git a/src/sd_jwt/bin/generate.py b/src/sd_jwt/bin/generate.py +index ad00641..d0299ea 100755 +--- a/src/sd_jwt/bin/generate.py ++++ b/src/sd_jwt/bin/generate.py +@@ -105,12 +105,36 @@ def generate_test_case_data(settings: Dict, testcase_path: Path, type: str): + + # Write the test case data to the directory of the test case + ++ claims_vs_salts = [] ++ for disclosure in sdjwt_at_issuer.ii_disclosures: ++ claims_vs_salts.append(disclosure.salt) ++ + _artifacts = { + "user_claims": ( + remove_sdobj_wrappers(testcase["user_claims"]), + "User Claims", + "json", + ), ++ "issuer_key": ( ++ demo_keys["issuer_key"].export_to_pem(True, None).decode("utf-8"), ++ "Issuer private key", ++ "pem", ++ ), ++ "issuer_public_key": ( ++ demo_keys["issuer_public_key"].export_to_pem(False, None).decode("utf-8"), ++ "Issuer public key", ++ "pem", ++ ), ++ "holder_key": ( ++ demo_keys["holder_key"].export_to_pem(True, None).decode("utf-8"), ++ "Issuer private key", ++ "pem", ++ ), ++ "claims_vs_salts": ( ++ claims_vs_salts, ++ "Claims with Salts", ++ "json", ++ ), + "sd_jwt_payload": ( + sdjwt_at_issuer.sd_jwt_payload, + "Payload of the SD-JWT", +diff --git a/src/sd_jwt/disclosure.py b/src/sd_jwt/disclosure.py +index a9727c4..d1f983a 100644 +--- a/src/sd_jwt/disclosure.py ++++ b/src/sd_jwt/disclosure.py +@@ -15,11 +15,11 @@ class SDJWTDisclosure: + self._hash() + + def _hash(self): +- salt = self.issuer._generate_salt() ++ self._salt = self.issuer._generate_salt() + if self.key is None: +- data = [salt, self.value] ++ data = [self._salt, self.value] + else: +- data = [salt, self.key, self.value] ++ data = [self._salt, self.key, self.value] + + self._json = dumps(data).encode("utf-8") + +@@ -30,6 +30,10 @@ class SDJWTDisclosure: + def hash(self): + return self._hash + ++ @property ++ def salt(self): ++ return self._salt ++ + @property + def b64(self): + return self._raw_b64 diff --git a/generate/src/error.rs b/generate/src/error.rs new file mode 100644 index 0000000..615f23c --- /dev/null +++ b/generate/src/error.rs @@ -0,0 +1,296 @@ +#![allow(unused)] + +use std::error::Error as StdError; +use std::fmt::{self, Display, Formatter}; +use std::result::Result as StdResult; + +pub type Result = std::result::Result; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ErrorKind { + Input, + IOError, + DataNotEqual, +} + +impl ErrorKind { + #[must_use] + pub const fn as_str(&self) -> &'static str { + match self { + Self::Input => "Input error", + Self::IOError => "IO error", + Self::DataNotEqual => "Data not equal error", + } + } +} + +impl Display for ErrorKind { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// The standard crate error type +#[derive(Debug)] +pub struct Error { + kind: ErrorKind, + pub cause: Option>, + pub message: Option, + // backtrace (when supported) +} + +impl Error { + pub fn from_msg>(kind: ErrorKind, msg: T) -> Self { + Self { + kind, + cause: None, + message: Some(msg.into()), + } + } + + pub fn from_opt_msg>(kind: ErrorKind, msg: Option) -> Self { + Self { + kind, + cause: None, + message: msg.map(Into::into), + } + } + + #[must_use] + #[inline] + pub const fn kind(&self) -> ErrorKind { + self.kind + } + + #[must_use] + pub fn with_cause>>(mut self, err: T) -> Self { + self.cause = Some(err.into()); + self + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match (self.kind, &self.message) { + (ErrorKind::Input, None) => write!(f, "{:?}", self.kind), + (ErrorKind::Input, Some(msg)) => f.write_str(msg), + (kind, None) => write!(f, "{kind}"), + (kind, Some(msg)) => write!(f, "{kind}: {msg}"), + }?; + if let Some(ref source) = self.cause { + write!(f, " [{source}]")?; + } + Ok(()) + } +} + +impl StdError for Error { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + self.cause + .as_ref() + .map(|err| unsafe { std::mem::transmute(&**err) }) + } +} + +impl PartialEq for Error { + fn eq(&self, other: &Self) -> bool { + self.kind == other.kind && self.message == other.message + } +} + +impl From for Error { + fn from(kind: ErrorKind) -> Self { + Self { + kind, + cause: None, + message: None, + } + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Self::from(ErrorKind::IOError).with_cause(err) + } +} + +impl From for Error { + fn from(err: serde_json::Error) -> Self { + Self::from(ErrorKind::Input).with_cause(err) + } +} + +impl From for Error { + fn from(err: serde_yaml::Error) -> Self { + Self::from(ErrorKind::Input).with_cause(err) + } +} + +impl From<(ErrorKind, M)> for Error +where + M: fmt::Display + Send + Sync + 'static, +{ + fn from((kind, msg): (ErrorKind, M)) -> Self { + Self::from_msg(kind, msg.to_string()) + } +} + +macro_rules! err_msg { + () => { + $crate::error::Error::from($crate::error::ErrorKind::Input) + }; + ($kind:ident) => { + $crate::error::Error::from($crate::error::ErrorKind::$kind) + }; + ($kind:ident, $($args:tt)+) => { + $crate::error::Error::from_msg($crate::error::ErrorKind::$kind, format!($($args)+)) + }; + ($($args:tt)+) => { + $crate::error::Error::from_msg($crate::error::ErrorKind::Input, format!($($args)+)) + }; +} + +macro_rules! err_map { + ($($params:tt)*) => { + |err| err_msg!($($params)*).with_cause(err) + }; +} + +pub trait ResultExt { + fn map_err_string(self) -> StdResult; + fn map_input_err(self, mapfn: F) -> Result + where + F: FnOnce() -> M, + M: fmt::Display + Send + Sync + 'static; + fn with_err_msg(self, kind: ErrorKind, msg: M) -> Result + where + M: fmt::Display + Send + Sync + 'static; + fn with_input_err(self, msg: M) -> Result + where + M: fmt::Display + Send + Sync + 'static; +} + +impl ResultExt for StdResult +where + E: std::error::Error + Send + Sync + 'static, +{ + fn map_err_string(self) -> StdResult { + self.map_err(|err| err.to_string()) + } + + fn map_input_err(self, mapfn: F) -> Result + where + F: FnOnce() -> M, + M: fmt::Display + Send + Sync + 'static, + { + self.map_err(|err| Error::from_msg(ErrorKind::Input, mapfn().to_string()).with_cause(err)) + } + + fn with_err_msg(self, kind: ErrorKind, msg: M) -> Result + where + M: fmt::Display + Send + Sync + 'static, + { + self.map_err(|err| Error::from_msg(kind, msg.to_string()).with_cause(err)) + } + + #[inline] + fn with_input_err(self, msg: M) -> Result + where + M: fmt::Display + Send + Sync + 'static, + { + self.map_err(|err| Error::from_msg(ErrorKind::Input, msg.to_string()).with_cause(err)) + } +} + +type DynError = Box; + +macro_rules! define_error { + ($name:tt, $short:expr, $doc:tt) => { + #[derive(Debug, Error)] + #[doc=$doc] + pub struct $name { + pub context: Option, + pub source: Option, + } + + impl $name { + pub fn from_msg>(msg: T) -> Self { + Self::from(msg.into()) + } + + pub fn from_err(err: E) -> Self + where + E: StdError + Send + Sync + 'static, + { + Self { + context: None, + source: Some(Box::new(err) as DynError), + } + } + + pub fn from_msg_err(msg: M, err: E) -> Self + where + M: Into, + E: StdError + Send + Sync + 'static, + { + Self { + context: Some(msg.into()), + source: Some(Box::new(err) as DynError), + } + } + } + + impl From<&str> for $name { + fn from(context: &str) -> Self { + Self { + context: Some(context.to_owned()), + source: None, + } + } + } + + impl From for $name { + fn from(context: String) -> Self { + Self { + context: Some(context), + source: None, + } + } + } + + impl From> for $name { + fn from(context: Option) -> Self { + Self { + context, + source: None, + } + } + } + + impl From<(M, E)> for $name + where + M: Into, + E: StdError + Send + Sync + 'static, + { + fn from((context, err): (M, E)) -> Self { + Self::from_msg_err(context, err) + } + } + + impl From<$name> for String { + fn from(s: $name) -> Self { + s.to_string() + } + } + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, $short)?; + match self.context { + Some(ref context) => write!(f, ": {}", context), + None => Ok(()), + } + } + } + }; +} \ No newline at end of file diff --git a/generate/src/main.rs b/generate/src/main.rs new file mode 100644 index 0000000..80f617c --- /dev/null +++ b/generate/src/main.rs @@ -0,0 +1,276 @@ +mod error; +mod types; +mod utils; + +use jsonwebtoken::jwk::Jwk; + +use crate::error::{Error, ErrorKind, Result}; +use crate::utils::funcs::{parse_sdjwt_paylod, load_salts}; +use clap::Parser; +use jsonwebtoken::{EncodingKey, DecodingKey}; +use sd_jwt_rs::issuer::{ClaimsForSelectiveDisclosureStrategy, SDJWTIssuer}; +use sd_jwt_rs::holder::SDJWTHolder; +use sd_jwt_rs::verifier::SDJWTVerifier; +use sd_jwt_rs::SDJWTSerializationFormat; +use serde_json::{Number, Value}; +use std::path::PathBuf; +use types::cli::{Cli, GenerateType}; +use types::settings::Settings; +use types::specification::Specification; + +const ISSUER_KEY_PEM_FILE_NAME: &str = "issuer_key.pem"; +const ISSUER_PUBLIC_KEY_PEM_FILE_NAME: &str = "issuer_public_key.pem"; +// const HOLDER_KEY_PEM_FILE_NAME: &str = "holder_key.pem"; +const SETTINGS_FILE_NAME: &str = "settings.yml"; +const SPECIFICATION_FILE_NAME: &str = "specification.yml"; +const SALTS_FILE_NAME: &str = "claims_vs_salts.json"; +const SD_JWT_FILE_NAME_TEMPLATE: &str = "sd_jwt_issuance"; +const VERIFIED_CLAIMS_FILE_NAME: &str = "verified_contents.json"; + +fn main() { + let args = Cli::parse(); + + println!("type_: {:?}, paths: {:?}", args.type_.clone(), args.paths); + + let basedir = std::env::current_dir().expect("Unable to get current directory"); + let spec_directories = get_specification_paths(&args, basedir).unwrap(); + + for mut directory in spec_directories { + println!("Generating data for '{:?}'", directory); + let settings = get_settings(&directory.parent().unwrap().join("..").join(SETTINGS_FILE_NAME)); + let specs = Specification::from(&directory); + + // Remove specification.yaml from path + directory.pop(); + + generate_and_check(&directory, &settings, specs, args.type_.clone()).unwrap(); + } +} + +fn generate_and_check( + directory: &PathBuf, + settings: &Settings, + specs: Specification, + _: GenerateType, +) -> Result<()> { + let decoy = specs.add_decoy_claims.unwrap_or(false); + let serialization_format; + let stored_sd_jwt_file_path; + + match &specs.serialization_format { + Some(format) if format == "json" => { + serialization_format = SDJWTSerializationFormat::JSON; + stored_sd_jwt_file_path = directory.join(format!("{SD_JWT_FILE_NAME_TEMPLATE}.json")); + }, + Some(format) if format == "compact" => { + serialization_format = SDJWTSerializationFormat::Compact; + stored_sd_jwt_file_path = directory.join(format!("{SD_JWT_FILE_NAME_TEMPLATE}.txt")); + }, + None => { + println!("using default serialization format: Compact"); + serialization_format = SDJWTSerializationFormat::Compact; + stored_sd_jwt_file_path = directory.join(format!("{SD_JWT_FILE_NAME_TEMPLATE}.txt")); + }, + Some(format) => { + panic!("unsupported format: {format}"); + }, + }; + + let sd_jwt = issue_sd_jwt(directory, &specs, settings, serialization_format.clone(), decoy)?; + let presentation = create_presentation(&sd_jwt, serialization_format.clone(), &specs.holder_disclosed_claims)?; + + // Verify presentation + let verified_claims = verify_presentation(directory, &presentation, serialization_format.clone())?; + + let loaded_sd_jwt = load_sd_jwt(&stored_sd_jwt_file_path)?; + + let loaded_sdjwt_paylod = parse_sdjwt_paylod(&loaded_sd_jwt.replace('\n', ""), &serialization_format, decoy)?; + let issued_sdjwt_paylod = parse_sdjwt_paylod(&sd_jwt, &serialization_format, decoy)?; + + compare_jwt_payloads(&loaded_sdjwt_paylod, &issued_sdjwt_paylod)?; + + let loaded_verified_claims_content = load_sd_jwt(&directory.join(VERIFIED_CLAIMS_FILE_NAME))?; + let loaded_verified_claims = parse_verified_claims(&loaded_verified_claims_content)?; + + compare_verified_claims(&loaded_verified_claims, &verified_claims)?; + + Ok(()) +} + +fn issue_sd_jwt( + directory: &PathBuf, + specs: &Specification, + settings: &Settings, + serialization_format: SDJWTSerializationFormat, + decoy: bool +) -> Result { + let issuer_key = get_key(&directory.join(ISSUER_KEY_PEM_FILE_NAME)); + + let mut user_claims = specs.user_claims.claims_to_json_value()?; + let claims_obj = user_claims.as_object_mut().expect("must be an object"); + + if !claims_obj.contains_key("iss") { + claims_obj.insert(String::from("iss"), Value::String(settings.identifiers.issuer.clone())); + } + + if !claims_obj.contains_key("iat") { + let iat = settings.iat.expect("'iat' value must be provided by settings.yml"); + claims_obj.insert(String::from("iat"), Value::Number(Number::from(iat))); + } + + if !claims_obj.contains_key("exp") { + let exp = settings.exp.expect("'expt' value must be provided by settings.yml"); + claims_obj.insert(String::from("exp"), Value::Number(Number::from(exp))); + } + + let sd_claims_jsonpaths = specs.user_claims.sd_claims_to_jsonpath()?; + + let strategy = + ClaimsForSelectiveDisclosureStrategy::Custom(sd_claims_jsonpaths.iter().map(String::as_str).collect()); + + let jwk: Option = if specs.key_binding.unwrap_or(false) { + let jwk: Jwk = serde_yaml::from_value(settings.key_settings.holder_key.clone()).unwrap(); + Some(jwk) + } else { + None + }; + + let mut issuer = SDJWTIssuer::new(issuer_key, Some(String::from("ES256"))); + let sd_jwt = issuer.issue_sd_jwt( + user_claims, + strategy, + jwk, + decoy, + serialization_format) + .unwrap(); + + Ok(sd_jwt) +} + +fn create_presentation( + sd_jwt: &str, + serialization_format: SDJWTSerializationFormat, + disclosed_claims: &serde_json::Map +) -> Result { + let mut holder = SDJWTHolder::new(sd_jwt.to_string(), serialization_format).unwrap(); + + let presentation = holder + .create_presentation( + disclosed_claims.clone(), + None, + None, + None, + None + ).unwrap(); + + Ok(presentation) +} + +fn verify_presentation( + directory: &PathBuf, + presentation: &str, + serialization_format: SDJWTSerializationFormat +) -> Result { + let pub_key_path = directory.clone().join(ISSUER_PUBLIC_KEY_PEM_FILE_NAME); + + let _verified = SDJWTVerifier::new( + presentation.to_string(), + Box::new(move |_, _| { + let key = std::fs::read(&pub_key_path).expect("Failed to read file"); + DecodingKey::from_ec_pem(&key).expect("Unable to create EncodingKey") + }), + None, + None, + serialization_format, + ).unwrap(); + + Ok(_verified.verified_claims) +} + +fn parse_verified_claims(content: &str) -> Result { + let json_value: Value = serde_json::from_str(content)?; + + // TODO: check if the json_value is json object + Ok(json_value) +} + +fn load_sd_jwt(path: &PathBuf) -> Result { + let content = std::fs::read_to_string(path)?; + Ok(content) +} + +fn compare_jwt_payloads(loaded_payload: &Value, issued_payload: &Value) -> Result<()> { + if issued_payload.eq(loaded_payload) { + println!("\nJWT payloads are equal"); + } else { + eprintln!("\nJWT payloads are NOT equal"); + + println!("Issued SD-JWT \n {:#?}", issued_payload); + println!("Loaded SD-JWT \n {:#?}", loaded_payload); + + return Err(Error::from_msg(ErrorKind::DataNotEqual, "JWT payloads are different")); + } + + Ok(()) +} + +fn compare_verified_claims(loaded_claims: &Value, verified_claims: &Value) -> Result<()> { + if loaded_claims.eq(verified_claims) { + println!("Verified claims are equal",); + } else { + eprintln!("Verified claims are NOT equal"); + + println!("Issued verified claims \n {:#?}", verified_claims); + println!("Loaded verified claims \n {:#?}", loaded_claims); + + return Err(Error::from_msg(ErrorKind::DataNotEqual, "verified claims are different")); + } + + Ok(()) +} + +fn get_key(path: &PathBuf) -> EncodingKey { + let key = std::fs::read(path).expect("Failed to read file"); + + EncodingKey::from_ec_pem(&key).expect("Unable to create EncodingKey") +} + +fn get_settings(path: &PathBuf) -> Settings { + println!("settings.yaml - {:?}", path); + + Settings::from(path) +} + +fn get_specification_paths(args: &Cli, basedir: PathBuf) -> Result> { + let glob: Vec; + if args.paths.is_empty() { + glob = basedir + .read_dir()? + .filter_map(|entry| { + if let Ok(entry) = entry { + let path = entry.path(); + if path.is_dir() && path.join(SPECIFICATION_FILE_NAME).exists() { + // load_salts(&path).map_err(|err| Error::from_msg(ErrorKind::IOError, err.to_string()))?; + load_salts(&path.join(SALTS_FILE_NAME)).unwrap(); + return Some(path.join(SPECIFICATION_FILE_NAME)); + } + } + None + }) + .collect(); + } else { + glob = args + .paths + .iter() + .map(|d| { + // load_salts(&path).map_err(|err| Error::from_msg(ErrorKind::IOError, err.to_string()))?; + load_salts(&d.join(SALTS_FILE_NAME)).unwrap(); + basedir.join(d).join(SPECIFICATION_FILE_NAME) + }) + .collect(); + } + + println!("specification.yaml files - {:?}", glob); + + Ok(glob) +} diff --git a/generate/src/types/cli.rs b/generate/src/types/cli.rs new file mode 100644 index 0000000..49b7fc3 --- /dev/null +++ b/generate/src/types/cli.rs @@ -0,0 +1,20 @@ +use clap::Parser; +use serde::Serialize; + +#[derive(Parser)] +pub struct Cli { + /// The type to generate + #[arg(short, value_enum, default_value_t = GenerateType::Example)] + pub type_: GenerateType, + /// The paths to the directories where specification.yaml file is located + #[arg(short, value_delimiter = ' ', num_args = 0.., require_equals = false)] + pub paths: Vec, +} + + +#[derive(clap::ValueEnum, Clone, Debug, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum GenerateType { + Example, + TestCase, +} \ No newline at end of file diff --git a/generate/src/types/mod.rs b/generate/src/types/mod.rs new file mode 100644 index 0000000..8bad8a4 --- /dev/null +++ b/generate/src/types/mod.rs @@ -0,0 +1,3 @@ +pub mod settings; +pub mod specification; +pub mod cli; \ No newline at end of file diff --git a/generate/src/types/settings.rs b/generate/src/types/settings.rs new file mode 100644 index 0000000..dfd9f1e --- /dev/null +++ b/generate/src/types/settings.rs @@ -0,0 +1,91 @@ +use std::path::PathBuf; +use serde::{Deserialize, Serialize}; +use serde_yaml::Value; + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct KeySettings { + pub key_size: i32, + pub kty: String, + pub issuer_key: Key, + pub holder_key: Value, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct Key { + pub kty: String, + pub d: String, + pub crv: String, + pub x: String, + pub y: String, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct Identifiers { + pub issuer: String, + pub verifier: String, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct Settings { + pub identifiers: Identifiers, + pub key_settings: KeySettings, + pub key_binding_nonce: String, + pub expiry_seconds: Option, + pub random_seed: Option, + pub iat: Option, + pub exp: Option, +} + +impl From<&PathBuf> for Settings { + fn from(path: &PathBuf) -> Self { + let contents = std::fs::read_to_string(path) + .expect("Failed to read settings file"); + + let settings: Settings = serde_yaml::from_str(&contents) + .expect("Failed to parse YAML"); + + settings + } +} + +#[cfg(test)] +mod tests { + use crate::types::settings::Settings; + + #[test] + fn test_test_settings() { + let yaml_str = r#" + identifiers: + issuer: "https://example.com/issuer" + verifier: "https://example.com/verifier" + + key_settings: + key_size: 256 + kty: "EC" + issuer_key: + kty: "EC" + d: "Ur2bNKuBPOrAaxsRnbSH6hIhmNTxSGXshDSUD1a1y7g" + crv: "P-256" + x: "b28d4MwZMjw8-00CG4xfnn9SLMVMM19SlqZpVb_uNtQ" + y: "Xv5zWwuoaTgdS6hV43yI6gBwTnjukmFQQnJ_kCxzqk8" + holder_key: + kty: "EC" + d: "5K5SCos8zf9zRemGGUl6yfok-_NiiryNZsvANWMhF-I" + crv: "P-256" + x: "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc" + y: "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ" + + key_binding_nonce: "1234567890" + + expiry_seconds: 86400000 + random_seed: 0 + iat: 1683000000 + exp: 1883000000 + "#; + + let settings: Settings = serde_yaml::from_str(yaml_str).unwrap(); + println!("{:#?}", settings); + assert_eq!(settings.identifiers.issuer, "https://example.com/issuer"); + } +} + diff --git a/generate/src/types/specification.rs b/generate/src/types/specification.rs new file mode 100644 index 0000000..88d36fd --- /dev/null +++ b/generate/src/types/specification.rs @@ -0,0 +1,190 @@ +use crate::utils::generate::generate_jsonpath_from_tagged_values; +use serde::{Deserialize, Serialize}; +use serde_yaml::Value; +use std::path::PathBuf; +use crate::error::Result; + +const SD_TAG: &str = "!sd"; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Default)] +pub struct Specification { + pub user_claims: UserClaims, + pub holder_disclosed_claims: serde_json::Map, + pub add_decoy_claims: Option, + pub key_binding: Option, + pub serialization_format: Option, +} + +impl Specification { + fn update_disclosed_claims(&mut self) { + // not to transform top-level empty object + if self.holder_disclosed_claims.is_empty() { + return; + } + + let res = replace_empty_items(&serde_json::Value::Object(self.holder_disclosed_claims.clone())); + self.holder_disclosed_claims = res.as_object().unwrap().clone(); + } +} + +fn replace_empty_items(m: &serde_json::Value) -> serde_json::Value { + match m { + serde_json::Value::Array(arr) if (arr.is_empty()) => { + serde_json::Value::Bool(false) + } + serde_json::Value::Object(obj) if (obj.is_empty()) => { + serde_json::Value::Bool(false) + } + serde_json::Value::Array(arr) => { + let mut result = Vec::new(); + + for value in arr { + result.push(replace_empty_items(value)); + } + + serde_json::Value::Array(result) + } + serde_json::Value::Object(obj) => { + let mut result = serde_json::Map::new(); + + for (key, value) in obj { + result.insert(key.clone(), replace_empty_items(value)); + } + + serde_json::Value::Object(result) + } + _ => { + m.clone() + } + } +} + +impl From<&str> for Specification { + fn from(value: &str) -> Self { + let mut result = serde_yaml::from_str(value).unwrap_or(Specification::default()); + result.update_disclosed_claims(); + result + } +} + +impl From<&PathBuf> for Specification { + fn from(path: &PathBuf) -> Self { + let contents = std::fs::read_to_string(path).expect("Failed to read specification file"); + + let mut spec: Specification = serde_yaml::from_str(&contents).expect("Failed to parse YAML"); + + spec.update_disclosed_claims(); + + spec + } +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Default)] +pub struct UserClaims(Value); + +impl UserClaims { + pub fn claims_to_json_value(&self) -> Result { + let filtered_value = _remove_tags(&self.0); + let json_value: serde_json::Value = + serde_yaml::from_value(filtered_value).expect("Failed to convert serde_json::Value"); + + Ok(json_value) + } + + pub fn sd_claims_to_jsonpath(&self) -> Result> { + let path = "".to_string(); + let mut paths = Vec::new(); + let mut claims = self.0.clone(); + + let _ = generate_jsonpath_from_tagged_values(&mut claims, path, &mut paths); + + Ok(paths) + } +} + +fn _validate(value: &Value) -> Result<()> { + match value { + Value::String(_) | Value::Bool(_) | Value::Number(_) => Ok(()), + Value::Tagged(tag) => { + if tag.tag == SD_TAG { + _validate(&tag.value) + } else { + panic!( + "Unsupported tag {:?} in claim-name, only !sd tag is supported", + tag.tag + ); + } + } + Value::Sequence(list) => { + for v in list { + _validate(v)?; + } + + Ok(()) + } + Value::Mapping(map) => { + for (key, value) in map { + _validate(key)?; + _validate(value)?; + } + + Ok(()) + } + + _ => { + panic!("Unsupported type for claim-name, it can be only string or tagged"); + } + } +} + +fn _remove_tags(original: &Value) -> Value { + match original { + Value::Tagged(tag) => _remove_tags(&tag.value), + Value::Mapping(map) => { + let mut filtered_map = serde_yaml::Mapping::new(); + + for (key, value) in map.iter() { + match key { + Value::Tagged(tag) => { + let filtered_value = _remove_tags(value); + + filtered_map.insert(tag.value.clone(), filtered_value); + } + Value::Null => {} + _ => { + let filtered_value = _remove_tags(value); + filtered_map.insert(key.clone(), filtered_value); + } + } + } + + Value::Mapping(filtered_map) + } + Value::Sequence(seq) => { + let filtered_seq: Vec = seq.iter().map(_remove_tags).collect(); + + Value::Sequence(filtered_seq) + } + other => other.clone(), + } +} +#[cfg(test)] +mod tests { + use crate::types::specification::Specification; + + #[test] + fn test_specification() { + let yaml_str = r#" + user_claims: + sub: 6c5c0a49-b589-431d-bae7-219122a9ec2c + !sd address: + street_address: Schulstr. 12 + !sd street_address1: Schulstr. 12 + + holder_disclosed_claims: {} + "#; + + let spec = Specification::from(yaml_str); + println!("{:?}", spec.user_claims.claims_to_json_value().unwrap()) + } +} diff --git a/generate/src/utils/funcs.rs b/generate/src/utils/funcs.rs new file mode 100644 index 0000000..3dff777 --- /dev/null +++ b/generate/src/utils/funcs.rs @@ -0,0 +1,117 @@ +use std::collections::HashSet; +use std::path::PathBuf; + +use serde_json::Value; +use sd_jwt_rs::SDJWTSerializationFormat; +use sd_jwt_rs::utils::{base64_hash, base64url_decode}; +use sd_jwt_rs::utils::SALTS; +use crate::error::{Error, ErrorKind, Result}; + + +pub fn parse_sdjwt_paylod( + sd_jwt: &str, + serialization_format: &SDJWTSerializationFormat, + remove_decoy: bool +) -> Result { + + match serialization_format { + SDJWTSerializationFormat::JSON => { + parse_payload_json(sd_jwt, remove_decoy) + }, + SDJWTSerializationFormat::Compact => { + parse_payload_compact(sd_jwt, remove_decoy) + } + } +} + +fn parse_payload_json(sd_jwt: &str, remove_decoy: bool) -> Result { + let v: serde_json::Value = serde_json::from_str(sd_jwt).unwrap(); + + let disclosures = v.as_object().unwrap().get("disclosures").unwrap(); + + let mut hashes: HashSet = HashSet::new(); + + for disclosure in disclosures.as_array().unwrap() { + let hash = base64_hash(disclosure.as_str().unwrap().replace(' ', "").as_bytes()); + hashes.insert(hash.clone()); + } + + let ddd = v.as_object().unwrap().get("payload").unwrap().as_str().unwrap().replace(' ', ""); + let payload = base64url_decode(&ddd).unwrap(); + + let payload: serde_json::Value = serde_json::from_slice(&payload).unwrap(); + + if remove_decoy { + return Ok(remove_decoy_items(&payload, &hashes)); + } + + Ok(payload) +} + +fn parse_payload_compact(sd_jwt: &str, remove_decoy: bool) -> Result { + let mut disclosures: Vec = sd_jwt + .split('~') + .filter(|s| !s.is_empty()) + .map(String::from) + .collect(); + + let payload = disclosures.remove(0); + + let payload: Vec<_> = payload.split('.').collect(); + let payload = String::from(payload[1]); + + let mut hashes: HashSet = HashSet::new(); + + for disclosure in disclosures { + let hash = base64_hash(disclosure.as_bytes()); + hashes.insert(hash.clone()); + } + + let payload = base64url_decode(&payload).unwrap(); + + let payload: serde_json::Value = serde_json::from_slice(&payload).unwrap(); + + if remove_decoy { + return Ok(remove_decoy_items(&payload, &hashes)); + } + + Ok(payload) +} + +fn remove_decoy_items(payload: &Value, hashes: &HashSet) -> Value { + let mut map: serde_json::Map = serde_json::Map::new(); + + for (key, val) in payload.as_object().unwrap() { + if key == "_sd" { + let v1: Vec<_> = val.as_array().unwrap().iter() + .filter(|item| hashes.contains(item.as_str().unwrap())).cloned() + .collect(); + + let filtered_array = serde_json::Value::Array(v1); + map.insert(key.clone(), filtered_array); + } else if val.is_object() { + let filtered_object = remove_decoy_items(val, hashes); + map.insert(key.clone(), filtered_object); + } else { + map.insert(key.clone(), val.clone()); + } + } + + Value::Object(map) +} + +pub fn load_salts(path: &PathBuf) -> Result<()> { + let json_data = std::fs::read_to_string(path) + .map_err(|e| Error::from_msg(ErrorKind::IOError, e.to_string()))?; + let salts: Vec = serde_json::from_str(&json_data)?; + + { + let mut s = SALTS.lock().unwrap(); + + for salt in salts.iter() { + s.push_back(salt.clone()); + } + } + + Ok(()) +} diff --git a/generate/src/utils/generate.rs b/generate/src/utils/generate.rs new file mode 100644 index 0000000..a98cecc --- /dev/null +++ b/generate/src/utils/generate.rs @@ -0,0 +1,65 @@ +use serde_yaml::Value; +use crate::error::Result; + +#[allow(unused)] +pub fn generate_jsonpath_from_tagged_values( + yaml: &Value, + mut path: String, + paths: &mut Vec, +) -> Result<()> { + + if path.is_empty() { + path.push('$'); + } + + match yaml { + Value::Mapping(map) => { + for (key, value) in map { + // Handle nested + + let mut subpath: String; + + match key { + Value::Tagged(tagged) => { + subpath = format!("{}.{}", &path, tagged.value.as_str().unwrap()); + paths.push(subpath.clone()); + generate_jsonpath_from_tagged_values(value, subpath, paths); + } + Value::String(s) => { + subpath = format!("{}.{}", &path, &s); + generate_jsonpath_from_tagged_values(value, subpath, paths); + } + _ => {} + } + } + } + Value::Sequence(seq) => { + for (idx, value) in seq.iter().enumerate() { + + let mut subpath = format!("{}.[{}]", &path, idx); + generate_jsonpath_from_tagged_values(value, subpath, paths); + } + } + Value::Tagged(tagged) => { + // TODO: handle other value types (int/bool/etc) + + match &tagged.value { + Value::Mapping(m) => { + paths.push(path.clone()); + generate_jsonpath_from_tagged_values(&tagged.value, path.clone(), paths); + } + Value::Sequence(s) => { + paths.push(path.clone()); + generate_jsonpath_from_tagged_values(&tagged.value, path.clone(), paths); + } + _ => { + paths.push(path.clone()); + } + } + + } + _ => {} + } + + Ok(()) +} diff --git a/generate/src/utils/mod.rs b/generate/src/utils/mod.rs new file mode 100644 index 0000000..6b1d26e --- /dev/null +++ b/generate/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod generate; +pub mod funcs; \ No newline at end of file diff --git a/src/disclosure.rs b/src/disclosure.rs index 079fbfa..5c0ea65 100644 --- a/src/disclosure.rs +++ b/src/disclosure.rs @@ -1,4 +1,10 @@ -use crate::utils::{base64_hash, base64url_encode, generate_salt}; +use crate::utils::{base64_hash, base64url_encode}; +#[cfg(not(feature = "mock_salts"))] +use crate::utils::generate_salt; +#[cfg(feature = "mock_salts")] +use crate::utils::generate_salt_mock; +use serde_json::Value; + #[derive(Debug)] pub(crate) struct SDJWTDisclosure { @@ -8,19 +14,31 @@ pub(crate) struct SDJWTDisclosure { impl SDJWTDisclosure { pub(crate) fn new(key: Option, value: V) -> Self where V: ToString { - let salt = generate_salt(key.clone()); + #[cfg(not(feature = "mock_salts"))] + let salt = generate_salt(); let mut value_str = value.to_string(); - value_str = value_str.replace(":[", ": [").replace(',', ", "); - let (_data, raw_b64) = if let Some(key) = &key { //TODO remove data? - let data = format!(r#"["{}", "{}", {}]"#, salt, key, value_str); - let raw_b64 = base64url_encode(data.as_bytes()); - (data, raw_b64) + + #[cfg(feature = "mock_salts")] + let salt = { + value_str = value_str + .replace(":[", ": [") + .replace(',', ", ") + .replace("\":", "\": ") + .replace("\": ", "\": "); + generate_salt_mock() + }; + + if !value_str.is_ascii() { + value_str = escape_unicode_chars(&value_str); + } + + let data = if let Some(key) = &key { + format!(r#"["{}", {}, {}]"#, salt, escape_json(key), value_str) } else { - let data = format!(r#"["{}", {}]"#, salt, value_str); - let raw_b64 = base64url_encode(data.as_bytes()); - (data, raw_b64) + format!(r#"["{}", {}]"#, salt, value_str) }; + let raw_b64 = base64url_encode(data.as_bytes()); let hash = base64_hash(raw_b64.as_bytes()); Self { @@ -30,6 +48,33 @@ impl SDJWTDisclosure { } } +fn escape_unicode_chars(s: &str) -> String { + let mut result = String::new(); + + for c in s.chars() { + if c.is_ascii() { + result.push(c); + } else { + let esc_c = c.escape_unicode().to_string(); + + let esc_c_new = match esc_c.chars().count() { + 6 => esc_c.replace("\\u{", "\\u00").replace('}', ""), // example: \u{de} + 7 => esc_c.replace("\\u{", "\\u0").replace('}', ""), // example: \u{980} + 8 => esc_c.replace("\\u{", "\\u").replace('}', ""), // example: \u{23f0} + _ => {panic!("unexpected value")} + }; + + result.push_str(&esc_c_new); + } + } + + result +} + +fn escape_json(s: &str) -> String { + Value::String(String::from(s)).to_string() +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/holder.rs b/src/holder.rs index 0f93ebb..3ddb2c6 100644 --- a/src/holder.rs +++ b/src/holder.rs @@ -433,10 +433,10 @@ mod tests { let mut parts: Vec<&str> = issued .split(COMBINED_SERIALIZATION_FORMAT_SEPARATOR) .collect(); + parts.remove(5); parts.remove(4); parts.remove(3); parts.remove(2); - parts.remove(1); let expected = parts.join(COMBINED_SERIALIZATION_FORMAT_SEPARATOR); assert_eq!(expected, presentation); } diff --git a/src/issuer.rs b/src/issuer.rs index 3ac501a..c156259 100644 --- a/src/issuer.rs +++ b/src/issuer.rs @@ -32,9 +32,9 @@ pub struct SDJWTIssuer { // internal fields inner: SDJWTCommon, all_disclosures: Vec, - pub sd_jwt_payload: SJMap, - pub signed_sd_jwt: String, - pub serialized_sd_jwt: String, + sd_jwt_payload: SJMap, + signed_sd_jwt: String, + serialized_sd_jwt: String, } /// ClaimsForSelectiveDisclosureStrategy is used to determine which claims can be selectively disclosed later by the holder. @@ -196,7 +196,7 @@ impl SDJWTIssuer { let always_revealed_root_keys = vec!["iss", "iat", "exp"]; let mut always_revealed_claims: Map = always_revealed_root_keys .into_iter() - .filter_map(|key| claims_obj_ref.remove_entry(key)) + .filter_map(|key| claims_obj_ref.shift_remove_entry(key)) .collect(); self.sd_jwt_payload = self @@ -252,6 +252,10 @@ impl SDJWTIssuer { sd_strategy: ClaimsForSelectiveDisclosureStrategy, ) -> Value { let mut claims = SJMap::new(); + + // to have the first key "_sd" in the ordered map + claims.insert(SD_DIGESTS_KEY.to_owned(), Value::Null); + let mut sd_claims = Vec::new(); for (key, value) in user_claims.iter() { @@ -281,6 +285,8 @@ impl SDJWTIssuer { SD_DIGESTS_KEY.to_owned(), Value::Array(sd_claims.into_iter().map(Value::String).collect()), ); + } else { + claims.shift_remove(SD_DIGESTS_KEY); } Value::Object(claims) @@ -353,7 +359,7 @@ impl SDJWTIssuer { } fn create_decoy_claim_entry(&mut self) -> String { - let digest = base64_hash(generate_salt(None).as_bytes()).to_string(); + let digest = base64_hash(generate_salt().as_bytes()).to_string(); digest } } diff --git a/src/utils.rs b/src/utils.rs index 4cd14fc..76880a1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -12,14 +12,15 @@ use rand::RngCore; use serde_json::Value; use sha2::Digest; #[cfg(feature = "mock_salts")] -use std::{collections::HashMap, sync::Mutex}; +use std::{collections::VecDeque, sync::Mutex}; #[cfg(feature = "mock_salts")] lazy_static! { - pub static ref SALTS: Mutex> = Mutex::new(HashMap::new()); + pub static ref SALTS: Mutex> = Mutex::new(VecDeque::new()); } -pub(crate) fn base64_hash(data: &[u8]) -> String { +#[doc(hidden)] +pub fn base64_hash(data: &[u8]) -> String { let mut hasher = sha2::Sha256::new(); hasher.update(data); let hash = hasher.finalize(); @@ -31,28 +32,25 @@ pub(crate) fn base64url_encode(data: &[u8]) -> String { general_purpose::URL_SAFE_NO_PAD.encode(data) } -pub(crate) fn base64url_decode(b64data: &str) -> Result> { +#[doc(hidden)] +pub fn base64url_decode(b64data: &str) -> Result> { general_purpose::URL_SAFE_NO_PAD .decode(b64data) .map_err(|e| Error::DeserializationError(e.to_string())) } -pub(crate) fn generate_salt(_key_for_predefined_salt: Option) -> String { - - #[cfg(feature = "mock_salts")] - { - let map = SALTS.lock().unwrap(); - if let Some(salt) = _key_for_predefined_salt.and_then(|key| map.get(&key)) { - //FIXME better mock approach - return salt.clone() - } - } - +pub(crate) fn generate_salt() -> String { let mut buf = [0u8; 16]; ThreadRng::default().fill_bytes(&mut buf); base64url_encode(&buf) } +#[cfg(feature = "mock_salts")] +pub(crate) fn generate_salt_mock() -> String { + let mut salts = SALTS.lock().unwrap(); + return salts.pop_front().expect("SALTS is empty"); +} + pub(crate) fn jwt_payload_decode(b64data: &str) -> Result> { serde_json::from_str( &String::from_utf8( diff --git a/src/verifier.rs b/src/verifier.rs index 748ae53..7b27752 100644 --- a/src/verifier.rs +++ b/src/verifier.rs @@ -231,15 +231,15 @@ impl SDJWTVerifier { fn unpack_disclosed_claims(&mut self, sd_jwt_claims: &Value) -> Result { match sd_jwt_claims { Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => { - return Ok(sd_jwt_claims.to_owned()); + Ok(sd_jwt_claims.to_owned()) } Value::Array(arr) => { - return self.unpack_disclosed_claims_in_array(arr); + self.unpack_disclosed_claims_in_array(arr) } Value::Object(obj) => { - return self.unpack_disclosed_claims_in_object(obj); + self.unpack_disclosed_claims_in_object(obj) } - }; + } } fn unpack_disclosed_claims_in_array(&mut self, arr: &Vec) -> Result { @@ -263,8 +263,8 @@ impl SDJWTVerifier { let digest = obj.get(SD_LIST_PREFIX).unwrap(); let disclosed_claim = self.unpack_from_digest(digest)?; - if disclosed_claim.is_some() { - claims.push(disclosed_claim.unwrap()); + if let Some(disclosed_claim) = disclosed_claim { + claims.push(disclosed_claim); } }, _ => { @@ -273,7 +273,7 @@ impl SDJWTVerifier { }, } } - return Ok(Value::Array(claims)); + Ok(Value::Array(claims)) } fn unpack_disclosed_claims_in_object(&mut self, nested_sd_jwt_claims: &Map) -> Result {