identity_credential/credential/
enveloped_credential.rs

1// Copyright 2020-2025 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::fmt::Display;
5use std::ops::Deref;
6
7use identity_core::common::Context;
8use identity_core::common::DataUrl;
9use identity_core::common::InvalidDataUrl;
10use identity_core::common::Object;
11use identity_core::common::OneOrMany;
12use serde::Deserialize;
13use serde::Deserializer;
14use serde::Serialize;
15
16use crate::credential::credential_v2::deserialize_vc2_0_context;
17use crate::credential::CredentialV2;
18
19const ENVELOPED_VC_TYPE: &str = "EnvelopedVerifiableCredential";
20
21fn deserialize_enveloped_vc_type<'de, D>(deserializer: D) -> Result<Box<str>, D::Error>
22where
23  D: Deserializer<'de>,
24{
25  use serde::de::Error;
26  use serde::de::Unexpected;
27
28  let str = <&'de str>::deserialize(deserializer)?;
29  if str == ENVELOPED_VC_TYPE {
30    Ok(ENVELOPED_VC_TYPE.to_owned().into_boxed_str())
31  } else {
32    Err(Error::invalid_value(
33      Unexpected::Str(str),
34      &format!("\"{}\"", ENVELOPED_VC_TYPE).as_str(),
35    ))
36  }
37}
38
39/// An Enveloped Verifiable Credential as defined in
40/// [VC Data Model 2.0](https://www.w3.org/TR/vc-data-model-2.0/#enveloped-verifiable-credentials).
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[non_exhaustive]
43pub struct EnvelopedVc {
44  /// The set of JSON-LD contexts that apply to this object.
45  #[serde(rename = "@context", deserialize_with = "deserialize_vc2_0_context")]
46  context: OneOrMany<Context>,
47  /// [VcDataUrl] containing the actual Verifiable Credential.
48  pub id: VcDataUrl,
49  /// The type of this object, which is always "EnvelopedVerifiableCredential".
50  #[serde(rename = "type", deserialize_with = "deserialize_enveloped_vc_type")]
51  type_: Box<str>,
52  /// Additional properties.
53  #[serde(flatten)]
54  pub properties: Object,
55}
56
57impl EnvelopedVc {
58  /// Constructs a new [EnvelopedVc] with the given `id`.
59  pub fn new(id: VcDataUrl) -> Self {
60    Self {
61      context: OneOrMany::One(CredentialV2::<()>::base_context().clone()),
62      id,
63      type_: ENVELOPED_VC_TYPE.to_owned().into_boxed_str(),
64      properties: Object::default(),
65    }
66  }
67
68  /// The value of this object's "type" property, which is always "EnvelopedVerifiableCredential".
69  pub fn type_(&self) -> &str {
70    &self.type_
71  }
72
73  /// The value of this object's "@context" property.
74  pub fn context(&self) -> &[Context] {
75    self.context.as_slice()
76  }
77
78  /// Sets the value of this object's "@context" property.
79  /// # Notes
80  /// This method will always ensure the very first context is "https://www.w3.org/ns/credentials/v2"
81  /// and that no duplicated contexts are present.
82  /// # Example
83  /// ```
84  /// # use identity_credential::credential::{EnvelopedVc, VcDataUrl};
85  /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
86  /// let mut enveloped_vc = EnvelopedVc::new(VcDataUrl::parse("data:application/vc,QzVjV...RMjU")?);
87  /// enveloped_vc.set_context(vec![]);
88  /// assert_eq!(
89  ///   enveloped_vc.context(),
90  ///   &["https://www.w3.org/ns/credentials/v2"]
91  /// );
92  /// # Ok(())
93  /// # }
94  /// ```
95  pub fn set_context(&mut self, contexts: impl IntoIterator<Item = Context>) {
96    use itertools::Itertools;
97
98    let contexts = std::iter::once(CredentialV2::<()>::base_context().clone())
99      .chain(contexts)
100      .unique()
101      .collect_vec();
102
103    self.context = contexts.into();
104  }
105}
106
107/// A [DataUrl] encoding a VC within it (recognized through the use of the "application/vc" media type)
108/// for use as the `id` of an [EnvelopedVc].
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
110#[serde(transparent)]
111pub struct VcDataUrl(DataUrl);
112
113impl VcDataUrl {
114  /// Parses the given input string as a [VcDataUrl].
115  /// # Example
116  /// ```
117  /// # use identity_credential::credential::{VcDataUrl, VcDataUrlParsingError};
118  /// # fn main() -> Result<(), VcDataUrlParsingError> {
119  /// let plaintext_vc_data_url = VcDataUrl::parse("data:application/vc;base64,eyVjV...RMjU")?;
120  /// let jwt_vc_data_url = VcDataUrl::parse("data:application/vc+jwt,eyJraWQiO...zhwGfQ")?;
121  /// let sd_jwt_vc_data_url = VcDataUrl::parse("data:application/vc+sd-jwt,QzVjV...RMjU")?;
122  /// #   Ok(())
123  /// # }
124  /// ```
125  pub fn parse(input: &str) -> Result<Self, VcDataUrlParsingError> {
126    let data_url = DataUrl::parse(input)?;
127
128    if data_url.media_type().starts_with("application/vc") {
129      Ok(Self(data_url))
130    } else {
131      Err(VcDataUrlParsingError::InvalidMediaType(InvalidMediaType {
132        got: data_url.media_type().to_string(),
133      }))
134    }
135  }
136}
137
138impl Deref for VcDataUrl {
139  type Target = DataUrl;
140
141  fn deref(&self) -> &Self::Target {
142    &self.0
143  }
144}
145
146impl TryFrom<DataUrl> for VcDataUrl {
147  type Error = InvalidMediaType;
148
149  fn try_from(value: DataUrl) -> Result<Self, Self::Error> {
150    if value.media_type().starts_with("application/vc") {
151      Ok(Self(value))
152    } else {
153      Err(InvalidMediaType {
154        got: value.media_type().to_string(),
155      })
156    }
157  }
158}
159
160impl From<VcDataUrl> for DataUrl {
161  fn from(value: VcDataUrl) -> Self {
162    value.0
163  }
164}
165
166impl Display for VcDataUrl {
167  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168    write!(f, "{}", self.0)
169  }
170}
171
172/// Errors that can occur when parsing a [VcDataUrl].
173#[derive(Debug, thiserror::Error)]
174#[non_exhaustive]
175pub enum VcDataUrlParsingError {
176  /// The input string did not conform to the [DataUrl] format.
177  #[error(transparent)]
178  NotADataUrl(#[from] InvalidDataUrl),
179  /// The [DataUrl] does not have a valid media type for a VC.
180  #[error(transparent)]
181  InvalidMediaType(#[from] InvalidMediaType),
182}
183
184/// Error indicating that a [DataUrl] does not have a valid media type for a VC.
185#[derive(Debug, thiserror::Error)]
186#[error("invalid media type `{got}`: expected `application/vc` or related media type")]
187#[non_exhaustive]
188pub struct InvalidMediaType {
189  /// The invalid media type that was found.
190  pub got: String,
191}
192
193#[cfg(test)]
194mod tests {
195  use super::*;
196
197  #[test]
198  fn serde_roundtrip() {
199    let vc_data_url = VcDataUrl::parse("data:application/vc,QzVjV...RMjU").unwrap();
200    let enveloped_vc = EnvelopedVc::new(vc_data_url.clone());
201
202    let serialized = serde_json::to_string(&enveloped_vc).unwrap();
203    let deserialized: EnvelopedVc = serde_json::from_str(&serialized).unwrap();
204
205    assert_eq!(deserialized.type_(), ENVELOPED_VC_TYPE);
206    assert_eq!(deserialized.id, vc_data_url);
207    assert_eq!(deserialized.context(), &[CredentialV2::<()>::base_context().clone()]);
208  }
209
210  #[test]
211  fn deserialization_of_spec_example() {
212    let json = r#"
213{
214  "@context": "https://www.w3.org/ns/credentials/v2",
215  "id": "data:application/vc+sd-jwt,QzVjV...RMjU",
216  "type": "EnvelopedVerifiableCredential"
217}
218    "#;
219
220    let _enveloped_vc: EnvelopedVc = serde_json::from_str(json).unwrap();
221  }
222
223  #[test]
224  fn deserialization_of_invalid_type_fails() {
225    let err = deserialize_enveloped_vc_type(&mut serde_json::Deserializer::from_str("\"InvalidType\"")).unwrap_err();
226    assert_eq!(
227      err.to_string(),
228      "invalid value: string \"InvalidType\", expected \"EnvelopedVerifiableCredential\""
229    );
230  }
231}