diff --git a/identity_credential/src/sd_jwt_vc/metadata/claim.rs b/identity_credential/src/sd_jwt_vc/metadata/claim.rs new file mode 100644 index 000000000..1c6c852ba --- /dev/null +++ b/identity_credential/src/sd_jwt_vc/metadata/claim.rs @@ -0,0 +1,104 @@ +use std::ops::Deref; + +use serde::Deserialize; +use serde::Serialize; +use serde::Serializer; +use serde_json::Value; + +/// Information about a particular claim for displaying and validation purposes. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ClaimMetadata { + /// [`ClaimPath`] of the claim or claims that are being addressed. + pub path: ClaimPath, + /// Object containing display information for the claim. + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub display: Vec, + /// A string indicating whether the claim is selectively disclosable. + pub sd: Option, + /// A string defining the ID of the claim for reference in the SVG template. + pub svg_id: Option, +} + +/// A non-empty list of string, `null` values, or non-negative integers. +/// It is used to selected a particular claim in the credential or a +/// set of claims. See [Claim Path](https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-05.html#name-claim-path) for more information. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(try_from = "Vec")] +pub struct ClaimPath(Vec); + +impl TryFrom> for ClaimPath { + type Error = anyhow::Error; + fn try_from(value: Vec) -> Result { + if value.is_empty() { + Err(anyhow::anyhow!("`ClaimPath` cannot be empty")) + } else { + Ok(Self(value)) + } + } +} + +impl Deref for ClaimPath { + type Target = [ClaimPathSegment]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// A single segment of a [`ClaimPath`]. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged, try_from = "Value")] +pub enum ClaimPathSegment { + /// JSON object property. + Name(String), + /// JSON array entry. + Position(usize), + /// All properties or entries. + #[serde(serialize_with = "serialize_all_variant")] + All, +} + +impl TryFrom for ClaimPathSegment { + type Error = anyhow::Error; + fn try_from(value: Value) -> Result { + match value { + Value::Null => Ok(ClaimPathSegment::All), + Value::String(s) => Ok(ClaimPathSegment::Name(s)), + Value::Number(n) => n + .as_u64() + .ok_or_else(|| anyhow::anyhow!("expected number greater or equal to 0")) + .map(|n| ClaimPathSegment::Position(n as usize)), + _ => Err(anyhow::anyhow!("expected either a string, number, or null")), + } + } +} + +fn serialize_all_variant(serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_none() +} + +/// Information about whether a given claim is selectively disclosable. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ClaimDisclosability { + /// The issuer **must** make the claim selectively disclosable. + Always, + /// The issuer **may** make the claim selectively disclosable. + #[default] + Allowed, + /// The issuer **must not** make the claim selectively disclosable. + Never, +} + +/// Display information for a given claim. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ClaimDisplay { + /// A language tag as defined in [RFC5646](https://www.rfc-editor.org/rfc/rfc5646.txt). + pub lang: String, + /// A human-readable label for the claim. + pub label: String, + /// A human-readable description for the claim. + pub description: Option, +} diff --git a/identity_credential/src/sd_jwt_vc/metadata/mod.rs b/identity_credential/src/sd_jwt_vc/metadata/mod.rs index 0413c4ce4..662c42032 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/mod.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/mod.rs @@ -1,11 +1,13 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +mod claim; mod display; mod integrity; mod issuer; mod vc_type; +pub use claim::*; pub use display::*; pub use integrity::*; pub use issuer::*; diff --git a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs index 255ffaf8a..dd4389aca 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs @@ -10,6 +10,7 @@ use crate::sd_jwt_vc::Error; use crate::sd_jwt_vc::Resolver; use crate::sd_jwt_vc::Result; +use super::ClaimMetadata; use super::DisplayMetadata; use super::IntegrityMetadata; @@ -31,8 +32,12 @@ pub struct TypeMetadata { /// Either an embedded schema or a reference to one. #[serde(flatten)] pub schema: Option, - /// An object containing display information for the type. - pub display: Option, + /// A list containing display information for the type. + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub display: Vec, + /// A list of [`ClaimMetadata`] containing information about particular claims. + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub claims: Vec, } impl TypeMetadata { @@ -186,7 +191,8 @@ mod tests { description: None, extends: None, extends_integrity: None, - display: None, + display: vec![], + claims: vec![], schema: Some(TypeSchema::Object { schema: json!({ "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -209,7 +215,8 @@ mod tests { description: None, extends: None, extends_integrity: None, - display: None, + display: vec![], + claims: vec![], schema: Some(TypeSchema::Uri { schema_uri: Url::parse("https://example.com/vc_types/1").unwrap(), schema_uri_integrity: None,