From 721af5edf53dcaa89bdfc22bfce665c576079228 Mon Sep 17 00:00:00 2001 From: Abdulbois Date: Sat, 2 Dec 2023 20:36:22 +0500 Subject: [PATCH] feat: Add generate binary crate Signed-off-by: Abdulbois Signed-off-by: Abdulbois --- .gitignore | 1 + generate/Cargo.toml | 11 +++ generate/src/cli.rs | 20 +++++ generate/src/main.rs | 71 +++++++++++++++++ generate/src/types/mod.rs | 2 + generate/src/types/settings.rs | 77 ++++++++++++++++++ generate/src/types/specification.rs | 119 ++++++++++++++++++++++++++++ src/lib.rs | 2 +- 8 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 generate/Cargo.toml create mode 100644 generate/src/cli.rs create mode 100644 generate/src/main.rs create mode 100644 generate/src/types/mod.rs create mode 100644 generate/src/types/settings.rs create mode 100644 generate/src/types/specification.rs diff --git a/.gitignore b/.gitignore index 6985cf1..a38b19f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb +.idea/ \ No newline at end of file diff --git a/generate/Cargo.toml b/generate/Cargo.toml new file mode 100644 index 0000000..a0c6069 --- /dev/null +++ b/generate/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "sd-jwt-generate" +version = "0.1.0" +edition = "2021" + +# 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" \ No newline at end of file diff --git a/generate/src/cli.rs b/generate/src/cli.rs new file mode 100644 index 0000000..a2af2e4 --- /dev/null +++ b/generate/src/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 specifications.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/main.rs b/generate/src/main.rs new file mode 100644 index 0000000..40fe914 --- /dev/null +++ b/generate/src/main.rs @@ -0,0 +1,71 @@ +mod cli; +mod types; + +use std::path::PathBuf; +use clap::Parser; +use crate::cli::{Cli, GenerateType}; +use crate::types::settings::Settings; + +const SPECIFICATION_FILE_NAME: &str = "specification.yml"; +const SETTINGS_FILE_NAME: &str = "settings.yml"; + +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 settings_file = basedir.join(SETTINGS_FILE_NAME); + + if !settings_file.exists() { + eprintln!("Settings file '{:?}' does not exist.", settings_file); + std::process::exit(1); + } + + let glob: Vec; + if args.paths.is_empty() { + glob = basedir + .read_dir() + .expect("Unable to read directory") + .filter_map(|entry| { + if let Ok(entry) = entry { + let path = entry.path(); + if path.is_dir() && path.join(SPECIFICATION_FILE_NAME).exists() { + return Some(path.join(SPECIFICATION_FILE_NAME)); + } + } + None + }) + .collect(); + } else { + glob = args.paths.iter() + .map(|d| basedir.join(d).join(SPECIFICATION_FILE_NAME)) + .collect(); + } + println!("settings.yaml - {:?}", settings_file); + println!("specification.yaml files - {:?}", glob); + + + let settings = load_yaml_settings(&settings_file); + println!("{:#?}", settings); + + + for case_path in glob { + println!("Generating data for '{:?}'", case_path); + generate_test_case_data(&settings, &case_path, args.type_.clone()); + } +} + +fn generate_test_case_data(settings: &Settings, test_case: &PathBuf, type_: GenerateType) { + todo!() +} + +fn load_yaml_settings(file: &PathBuf) -> Settings { + let contents = std::fs::read_to_string(file) + .expect("Failed to read settings file"); + + let settings: Settings = serde_yaml::from_str(&contents) + .expect("Failed to parse YAML"); + + settings +} \ 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..01e0e1e --- /dev/null +++ b/generate/src/types/mod.rs @@ -0,0 +1,2 @@ +pub mod settings; +pub mod specification; \ 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..717a0d1 --- /dev/null +++ b/generate/src/types/settings.rs @@ -0,0 +1,77 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct KeySettings { + pub key_size: i32, + pub kty: String, + pub issuer_key: Key, + pub holder_key: Key, +} + +#[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, +} + +#[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..09afbc2 --- /dev/null +++ b/generate/src/types/specification.rs @@ -0,0 +1,119 @@ +use serde::{Deserialize, Deserializer, Serialize}; +use serde_yaml::value::TaggedValue; +use serde_yaml::Value; +use std::collections::HashMap; +use std::hash::{Hash, Hasher}; +use std::io; +use std::io::Read; +use serde::de::Error; + +const SD_TAG: &str = "!sd"; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct Claims(HashMap); + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum ClaimName { + Str(String), + Tag(TaggedValue), +} + +impl PartialEq for ClaimName { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (ClaimName::Str(a), ClaimName::Str(b)) => a == b, + (ClaimName::Tag(a), ClaimName::Tag(b)) => b.eq(a), + _ => false, + } + } +} + +impl Eq for ClaimName {} + +impl Hash for ClaimName { + fn hash(&self, state: &mut H) { + match self { + ClaimName::Str(string) => string.hash(state), + ClaimName::Tag(tag) => tag.hash(state), + } + } +} + +#[derive(Debug, Deserialize)] +pub struct Specification { + pub user_claims: UserClaims, + pub holder_disclosed_claims: Claims, +} + +impl From<&str> for Specification { + fn from(value: &str) -> Self { + serde_yaml::from_str(value).expect("Failed to parse YAML") + } +} + +#[derive(Serialize, PartialEq, Debug)] +pub struct UserClaims { + pub sub: String, + #[serde(flatten)] + pub claims: Claims, +} + +impl<'de> Deserialize<'de> for UserClaims { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let mut value: HashMap = Deserialize::deserialize(deserializer)?; + let sub = &Value::from("sub".to_string()); + + if let Some(Value::String(sub)) = value.remove(sub) { + let mut claims: Claims = Claims(HashMap::new()); + + for (k, v) in value { + match k { + Value::String(s) => { + claims.0.insert(ClaimName::Str(s), v); + } + Value::Tagged(tag) => { + if tag.tag.to_string() == SD_TAG { + claims.0.insert(ClaimName::Tag(*tag), v); + } else { + return Err(Error::custom("Unsupported tag in claim-name, only !sd tag is supported")); + } + } + _ => { + return Err(Error::custom("Unsupported type for claim-name, it can be only string or tagged")); + } + } + } + + return Ok(UserClaims { sub, claims }); + } + + Err(Error::custom("Unsupported structure for user-claims")) + } +} + +#[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 + name: Bois + !sd address: + street_address: Schulstr. 12 + !sd street_address1: Schulstr. 12 + + holder_disclosed_claims: {} + "#; + + let example = Specification::from(yaml_str); + println!("{:#?}", example); + } +} diff --git a/src/lib.rs b/src/lib.rs index 5312865..f9548bc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ use sha2::Digest; pub use {holder::SDJWTHolder, issuer::SDJWTIssuer, verifier::SDJWTVerifier}; -mod issuer; +pub mod issuer; mod verifier; mod holder; mod disclosure;