identity_credential/sd_jwt_vc/metadata/
claim.rs

1// Copyright 2020-2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::fmt::Display;
5use std::ops::Deref;
6
7use anyhow::anyhow;
8use anyhow::Context;
9use itertools::Itertools;
10use serde::Deserialize;
11use serde::Serialize;
12use serde::Serializer;
13use serde_json::Value;
14
15use crate::sd_jwt_vc::Error;
16
17/// Information about a particular claim for displaying and validation purposes.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct ClaimMetadata {
20  /// [`ClaimPath`] of the claim or claims that are being addressed.
21  pub path: ClaimPath,
22  /// Object containing display information for the claim.
23  #[serde(skip_serializing_if = "Vec::is_empty", default)]
24  pub display: Vec<ClaimDisplay>,
25  /// A string indicating whether the claim is selectively disclosable.
26  pub sd: Option<ClaimDisclosability>,
27  /// A string defining the ID of the claim for reference in the SVG template.
28  pub svg_id: Option<String>,
29}
30
31impl ClaimMetadata {
32  /// Checks whether `value` is compliant with the disclosability policy imposed by this [`ClaimMetadata`].
33  pub fn check_value_disclosability(&self, value: &Value) -> Result<(), Error> {
34    if self.sd.unwrap_or_default() == ClaimDisclosability::Allowed {
35      return Ok(());
36    }
37
38    let interested_claims = self.path.reverse_index(value);
39    if self.sd.unwrap_or_default() == ClaimDisclosability::Always && interested_claims.is_ok() {
40      return Err(Error::Validation(anyhow!(
41        "claim or claims with path {} must always be disclosable",
42        &self.path
43      )));
44    }
45
46    if self.sd.unwrap_or_default() == ClaimDisclosability::Never && interested_claims.is_err() {
47      return Err(Error::Validation(anyhow!(
48        "claim or claims with path {} must never be disclosable",
49        &self.path
50      )));
51    }
52
53    Ok(())
54  }
55}
56
57/// A non-empty list of string, `null` values, or non-negative integers.
58/// It is used to select a particular claim in the credential or a
59/// 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.
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(try_from = "Vec<ClaimPathSegment>")]
62pub struct ClaimPath(Vec<ClaimPathSegment>);
63
64impl ClaimPath {
65  fn reverse_index<'v>(&self, value: &'v Value) -> anyhow::Result<OneOrManyValue<'v>> {
66    let mut segments = self.iter();
67    let first_segment = segments.next().context("empty claim path")?;
68    segments.try_fold(index_value(value, first_segment)?, |values, segment| {
69      values.get(segment)
70    })
71  }
72}
73
74impl TryFrom<Vec<ClaimPathSegment>> for ClaimPath {
75  type Error = anyhow::Error;
76  fn try_from(value: Vec<ClaimPathSegment>) -> Result<Self, Self::Error> {
77    if value.is_empty() {
78      Err(anyhow::anyhow!("`ClaimPath` cannot be empty"))
79    } else {
80      Ok(Self(value))
81    }
82  }
83}
84
85impl Display for ClaimPath {
86  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87    let segments = self.iter().join(", ");
88    write!(f, "[{segments}]")
89  }
90}
91
92impl Deref for ClaimPath {
93  type Target = [ClaimPathSegment];
94  fn deref(&self) -> &Self::Target {
95    &self.0
96  }
97}
98
99/// A single segment of a [`ClaimPath`].
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(untagged, try_from = "Value")]
102pub enum ClaimPathSegment {
103  /// JSON object property.
104  Name(String),
105  /// JSON array entry.
106  Position(usize),
107  /// All properties or entries.
108  #[serde(serialize_with = "serialize_all_variant")]
109  All,
110}
111
112impl Display for ClaimPathSegment {
113  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114    match self {
115      Self::All => write!(f, "null"),
116      Self::Name(name) => write!(f, "\"{name}\""),
117      Self::Position(i) => write!(f, "{i}"),
118    }
119  }
120}
121
122impl TryFrom<Value> for ClaimPathSegment {
123  type Error = anyhow::Error;
124  fn try_from(value: Value) -> Result<Self, Self::Error> {
125    match value {
126      Value::Null => Ok(ClaimPathSegment::All),
127      Value::String(s) => Ok(ClaimPathSegment::Name(s)),
128      Value::Number(n) => n
129        .as_u64()
130        .ok_or_else(|| anyhow::anyhow!("expected number greater or equal to 0"))
131        .map(|n| ClaimPathSegment::Position(n as usize)),
132      _ => Err(anyhow::anyhow!("expected either a string, number, or null")),
133    }
134  }
135}
136
137fn serialize_all_variant<S>(serializer: S) -> Result<S::Ok, S::Error>
138where
139  S: Serializer,
140{
141  serializer.serialize_none()
142}
143
144/// Information about whether a given claim is selectively disclosable.
145#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
146#[serde(rename_all = "lowercase")]
147pub enum ClaimDisclosability {
148  /// The issuer **must** make the claim selectively disclosable.
149  Always,
150  /// The issuer **may** make the claim selectively disclosable.
151  #[default]
152  Allowed,
153  /// The issuer **must not** make the claim selectively disclosable.
154  Never,
155}
156
157/// Display information for a given claim.
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
159pub struct ClaimDisplay {
160  /// A language tag as defined in [RFC5646](https://www.rfc-editor.org/rfc/rfc5646.txt).
161  pub lang: String,
162  /// A human-readable label for the claim.
163  pub label: String,
164  /// A human-readable description for the claim.
165  pub description: Option<String>,
166}
167
168enum OneOrManyValue<'v> {
169  One(&'v Value),
170  Many(Box<dyn Iterator<Item = &'v Value> + 'v>),
171}
172
173impl<'v> OneOrManyValue<'v> {
174  fn get(self, segment: &ClaimPathSegment) -> anyhow::Result<OneOrManyValue<'v>> {
175    match self {
176      Self::One(value) => index_value(value, segment),
177      Self::Many(values) => {
178        let new_values = values
179          .map(|value| index_value(value, segment))
180          .collect::<anyhow::Result<Vec<_>>>()?
181          .into_iter()
182          .flatten();
183
184        Ok(OneOrManyValue::Many(Box::new(new_values)))
185      }
186    }
187  }
188}
189
190struct OneOrManyValueIter<'v>(Option<OneOrManyValue<'v>>);
191
192impl<'v> OneOrManyValueIter<'v> {
193  fn new(value: OneOrManyValue<'v>) -> Self {
194    Self(Some(value))
195  }
196}
197
198impl<'v> IntoIterator for OneOrManyValue<'v> {
199  type IntoIter = OneOrManyValueIter<'v>;
200  type Item = &'v Value;
201  fn into_iter(self) -> Self::IntoIter {
202    OneOrManyValueIter::new(self)
203  }
204}
205
206impl<'v> Iterator for OneOrManyValueIter<'v> {
207  type Item = &'v Value;
208  fn next(&mut self) -> Option<Self::Item> {
209    match self.0.take()? {
210      OneOrManyValue::One(v) => Some(v),
211      OneOrManyValue::Many(mut values) => {
212        let value = values.next();
213        self.0 = Some(OneOrManyValue::Many(values));
214
215        value
216      }
217    }
218  }
219}
220
221fn index_value<'v>(value: &'v Value, segment: &ClaimPathSegment) -> anyhow::Result<OneOrManyValue<'v>> {
222  match segment {
223    ClaimPathSegment::Name(name) => value.get(name).map(OneOrManyValue::One),
224    ClaimPathSegment::Position(i) => value.get(i).map(OneOrManyValue::One),
225    ClaimPathSegment::All => value
226      .as_array()
227      .map(|values| OneOrManyValue::Many(Box::new(values.iter()))),
228  }
229  .ok_or_else(|| anyhow::anyhow!("value {value:#} has no element {segment}"))
230}
231
232#[cfg(test)]
233mod tests {
234  use serde_json::json;
235
236  use super::*;
237
238  fn sample_obj() -> Value {
239    json!({
240      "vct": "https://betelgeuse.example.com/education_credential",
241      "name": "Arthur Dent",
242      "address": {
243        "street_address": "42 Market Street",
244        "city": "Milliways",
245        "postal_code": "12345"
246      },
247      "degrees": [
248        {
249          "type": "Bachelor of Science",
250          "university": "University of Betelgeuse"
251        },
252        {
253          "type": "Master of Science",
254          "university": "University of Betelgeuse"
255        }
256      ],
257      "nationalities": ["British", "Betelgeusian"]
258    })
259  }
260
261  #[test]
262  fn claim_path_works() {
263    let name_path = serde_json::from_value::<ClaimPath>(json!(["name"])).unwrap();
264    let city_path = serde_json::from_value::<ClaimPath>(json!(["address", "city"])).unwrap();
265    let first_degree_path = serde_json::from_value::<ClaimPath>(json!(["degrees", 0])).unwrap();
266    let degrees_types_path = serde_json::from_value::<ClaimPath>(json!(["degrees", null, "type"])).unwrap();
267
268    assert!(matches!(
269      name_path.reverse_index(&sample_obj()).unwrap(),
270      OneOrManyValue::One(&Value::String(_))
271    ));
272    assert!(matches!(
273      city_path.reverse_index(&sample_obj()).unwrap(),
274      OneOrManyValue::One(&Value::String(_))
275    ));
276    assert!(matches!(
277      first_degree_path.reverse_index(&sample_obj()).unwrap(),
278      OneOrManyValue::One(&Value::Object(_))
279    ));
280    let obj = &sample_obj();
281    let mut degree_types = degrees_types_path.reverse_index(obj).unwrap().into_iter();
282    assert_eq!(degree_types.next().unwrap().as_str(), Some("Bachelor of Science"));
283    assert_eq!(degree_types.next().unwrap().as_str(), Some("Master of Science"));
284    assert_eq!(degree_types.next(), None);
285  }
286}