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