Skip to content

Commit

Permalink
Merge pull request #5 from robjsliwa/parse_yaml
Browse files Browse the repository at this point in the history
Support issuing claims with yaml.
  • Loading branch information
robjsliwa authored Jan 26, 2024
2 parents 834812b + 20bc9cd commit 1fe6494
Show file tree
Hide file tree
Showing 7 changed files with 734 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.9.30"
thiserror = "1.0.51"
rand = "0.8.5"
base64 = "0.21.5"
Expand Down
4 changes: 4 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,8 @@ pub enum Error {
RsaPkcs8Error(#[from] rsa::pkcs8::Error),
#[error("UTF8 conversion error")]
Utf8Error(#[from] std::str::Utf8Error),
#[error("YAML parsing error")]
YamlError(#[from] serde_yaml::Error),
#[error("YAML invalid sd tag: {0}")]
YamlInvalidSDTag(String),
}
48 changes: 48 additions & 0 deletions src/issuer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::Header;
use crate::Jwk;
use crate::{encode, KeyForEncoding};
use chrono::{Duration, Utc};
use core::slice::Iter;
use serde::Serialize;
use serde_json::Value;
use std::ops::Deref;
Expand Down Expand Up @@ -261,6 +262,53 @@ impl Issuer {
self
}

/// Marks claims as disclosable.
/// This method is useful when you want to mark multiple claims as disclosable.
/// It accepts an iterator of claim paths.
///
/// # Arguments
/// * `path_iter` - An iterator of claim paths.
///
/// # Returns
/// A mutable reference to the issuer for method chaining.
///
/// # Examples
/// ```
/// use sdjwt::Issuer;
///
/// let claims = serde_json::json!({
/// "sub": "user_42",
/// "given_name": "John",
/// "family_name": "Doe",
/// "email": "johndoe@example",
/// "address": {
/// "street_address": "123 Main St",
/// "locality": "Anytown",
/// "region": "Anystate",
/// "country": "US"
/// },
/// "nationalities": [
/// "US",
/// "DE"
/// ]
/// });
///
/// let mut issuer = Issuer::new(claims).unwrap();
/// issuer.iter_disclosable(vec![
/// "/given_name".to_string(),
/// "/family_name".to_string(),
/// "/address/street_address".to_string(),
/// "/address/locality".to_string(),
/// "/nationalities/0".to_string(),
/// "/nationalities/1".to_string()].iter());
/// ```
pub fn iter_disclosable(&mut self, path_iter: Iter<String>) -> &mut Self {
path_iter.for_each(|path| {
self.disclosable(path);
});
self
}

/// Encodes the issuer into a SD-JWT.
///
/// # Arguments
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod header;
pub mod holder;
pub mod issuer;
pub mod jwk;
mod parser;
#[cfg(feature = "noring")]
pub(crate) mod registries;
mod utils;
Expand All @@ -27,5 +28,6 @@ pub use header::*;
pub use holder::*;
pub use issuer::*;
pub use jwk::*;
pub use parser::parse_yaml;
pub use validation::*;
pub use verifier::*;
252 changes: 252 additions & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
use crate::Error;
use serde::{Deserialize, Deserializer};
use serde_json::Value as JsonValue;
use serde_yaml::Value as YamlValue;
use std::collections::VecDeque;

#[derive(Debug)]
struct CustomYamlValue {
value: YamlValue,
tagged_paths: Vec<String>,
}

impl<'de> Deserialize<'de> for CustomYamlValue {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserialize_custom_yaml(deserializer)
}
}

fn deserialize_custom_yaml<'de, D>(deserializer: D) -> Result<CustomYamlValue, D::Error>
where
D: Deserializer<'de>,
{
let mut value = YamlValue::deserialize(deserializer)?;
let mut path = VecDeque::new();
let mut tagged_paths = Vec::new();
collect_tagged_keys(&mut value, &mut path, &mut tagged_paths).map_err(|e| {
serde::de::Error::custom(format!("Error parsing YAML at path {:?}: {:?}", path, e))
})?;
Ok(CustomYamlValue {
value,
tagged_paths,
})
}

fn collect_tagged_keys(
node: &mut YamlValue,
path: &mut VecDeque<String>,
paths: &mut Vec<String>,
) -> Result<(), Error> {
match node {
YamlValue::Mapping(map) => {
for (key, value) in map {
if let YamlValue::Tagged(tag) = &key {
let key_str = tag.as_ref().tag.to_string();
if key_str == "!sd" {
let new_val = tag
.as_ref()
.value
.as_str()
.ok_or(Error::YamlInvalidSDTag(key_str))?;
let full_path = build_full_path(path, new_val);
paths.push(full_path);
}
} else if let YamlValue::String(key) = &key {
path.push_back(key.to_string());
collect_tagged_keys(value, path, paths)?;
path.pop_back();
}
}
}
YamlValue::Sequence(seq) => {
for (index, value) in seq.iter_mut().enumerate() {
path.push_back(index.to_string());
collect_tagged_keys(value, path, paths)?;
// Ugly hack to remove tag from sequence
if let YamlValue::Tagged(tag) = &value {
let key_str = tag.as_ref().tag.to_string();
if key_str == "!sd" {
let new_val = tag
.as_ref()
.value
.as_str()
.ok_or(Error::YamlInvalidSDTag(key_str))?;
*value = YamlValue::String(new_val.to_string());
}
}
path.pop_back();
}
}
YamlValue::Tagged(tag) => {
let key_str = tag.as_ref().tag.to_string();
if key_str == "!sd" {
let mut full_path = String::new();
for (index, path_fragment) in path.iter().enumerate() {
if index == 0 {
full_path = format!("/{}", path_fragment);
} else {
full_path = format!("{}/{}", full_path, path_fragment)
}
}
paths.push(full_path);
}
}
_ => {}
}

Ok(())
}

fn build_full_path(path: &VecDeque<String>, additional_segment: &str) -> String {
let full_path = path
.iter()
.fold(String::new(), |acc, frag| format!("{}/{}", acc, frag));
format!("{}/{}", full_path, additional_segment)
}

/// Parses claims as a YAML string, converts it to JSON, and collects paths of elements tagged with `!sd`.
///
/// # Arguments
/// * `yaml_str` - A string slice that holds the YAML data.
///
/// # Returns
/// A `Result` containing a tuple of JSON value and a vector of tagged paths, or an error.
pub fn parse_yaml(yaml_str: &str) -> Result<(JsonValue, Vec<String>), Error> {
let yaml_value: CustomYamlValue = serde_yaml::from_str(yaml_str)?;
let json_str = serde_yaml::to_string(&yaml_value.value)?;
let json: JsonValue = serde_yaml::from_str(&json_str)?;
let tagged_paths = yaml_value.tagged_paths;
Ok((json, tagged_paths))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_yaml1() {
let yaml_str = r#"
sub: user_42
!sd given_name: John
!sd family_name: Doe
email: "johndoe@example.com"
phone_number: "+1-202-555-0101"
phone_number_verified: true
address:
!sd street_address: "123 Main St"
!sd locality: Anytown
region: Anystate
country: US
birthdate: "1940-01-01"
updated_at: 1570000000
!sd nationalities:
- US
- DE
"#;

let (json, tagged_paths) = parse_yaml(yaml_str).unwrap();
println!("{:?}", json);
println!("{:?}", tagged_paths);

assert_eq!(
json,
serde_json::json!({
"sub": "user_42",
"given_name": "John",
"family_name": "Doe",
"email": "johndoe@example.com",
"phone_number": "+1-202-555-0101",
"phone_number_verified": true,
"address": {
"street_address": "123 Main St",
"locality": "Anytown",
"region": "Anystate",
"country": "US"
},
"birthdate": "1940-01-01",
"updated_at": 1570000000,
"nationalities": [
"US",
"DE"
]
})
);

assert_eq!(
tagged_paths,
vec![
"/given_name",
"/family_name",
"/address/street_address",
"/address/locality",
"/nationalities",
]
);
}

#[test]
fn test_parse_yaml2() {
let yaml_str = r#"
sub: user_42
!sd given_name: John
!sd family_name: Doe
email: "johndoe@example.com"
phone_number: "+1-202-555-0101"
phone_number_verified: true
!sd address:
street_address: "123 Main St"
locality: Anytown
region: Anystate
country: US
birthdate: "1940-01-01"
updated_at: 1570000000
nationalities:
- !sd US
- !sd DE
- PL
"#;

let (json, tagged_paths) = parse_yaml(yaml_str).unwrap();
println!("{:?}", json);
println!("{:?}", tagged_paths);

assert_eq!(
json,
serde_json::json!({
"sub": "user_42",
"given_name": "John",
"family_name": "Doe",
"email": "johndoe@example.com",
"phone_number": "+1-202-555-0101",
"phone_number_verified": true,
"address": {
"street_address": "123 Main St",
"locality": "Anytown",
"region": "Anystate",
"country": "US"
},
"birthdate": "1940-01-01",
"updated_at": 1570000000,
"nationalities": [
"US",
"DE",
"PL"
]
})
);

assert_eq!(
tagged_paths,
vec![
"/given_name",
"/family_name",
"/address",
"/nationalities/0",
"/nationalities/1",
]
);
}
}
Loading

0 comments on commit 1fe6494

Please sign in to comment.