Skip to main content

identity_kit/vc/
presentation.rs

1//! `Presentation`: a Verifiable Credentials Data Model 1.1 presentation of JWT credentials.
2
3use indexmap::IndexMap;
4use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
5use serde_json::Value;
6
7use crate::did::{Context, OneOrMany, Uri};
8
9use super::model::{CONTEXT_V1, append_context, check_context_and_type, push_type};
10use super::{CredentialError, Jwt, JwtClaims, Party, claims};
11
12/// A verifiable presentation of the Verifiable Credentials Data Model 1.1 whose credentials are
13/// JWTs, as data.
14///
15/// Deserialization checks that the first `@context` is the base context and `type` includes
16/// `VerifiablePresentation`. A presentation is secured by signing its JWT form, see
17/// [`to_jwt_claims`](Self::to_jwt_claims); the holder signs it, so the holder is the `iss`.
18///
19/// # Examples
20///
21/// ```
22/// use identity_kit::did::{Did, Uri};
23/// use identity_kit::vc::{Jwt, Presentation};
24///
25/// let credential: Jwt = "eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc.\
26///     hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg"
27///     .parse()?;
28/// let presentation = Presentation::new()
29///     .with_id(Uri::parse("urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5")?)
30///     .with_holder(Did::parse("did:example:ebfeb1f712ebc6f1c276e12ec21")?)
31///     .with_credential(credential);
32///
33/// let claims = presentation.to_jwt_claims()?.with_aud("did:example:4a57546973436f6f6c4a4a57573");
34/// assert_eq!(claims.iss.as_deref(), Some("did:example:ebfeb1f712ebc6f1c276e12ec21"));
35/// assert_eq!(claims.jti.as_deref(), Some("urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5"));
36/// assert_eq!(claims.additional["vp"]["verifiableCredential"].as_array().map(Vec::len), Some(1));
37/// assert_eq!(Presentation::from_jwt_claims(&claims)?, presentation);
38/// # Ok::<(), Box<dyn std::error::Error>>(())
39/// ```
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41#[non_exhaustive]
42#[serde(remote = "Self")]
43pub struct Presentation {
44    /// `@context`, starting with [`Presentation::CONTEXT`].
45    #[serde(rename = "@context")]
46    pub context: Context,
47    /// The identifier of the presentation.
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub id: Option<Uri>,
50    /// `type`, including [`Presentation::TYPE`].
51    #[serde(rename = "type")]
52    pub types: OneOrMany<String>,
53    /// The presented credentials, as JWTs.
54    #[serde(
55        rename = "verifiableCredential",
56        default,
57        skip_serializing_if = "Vec::is_empty"
58    )]
59    pub verifiable_credential: Vec<Jwt>,
60    /// The holder presenting the credentials.
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub holder: Option<Party>,
63    /// Every other property, in insertion order.
64    #[serde(flatten)]
65    pub properties: IndexMap<String, Value>,
66}
67
68impl Serialize for Presentation {
69    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
70        Self::serialize(self, serializer)
71    }
72}
73
74impl<'de> Deserialize<'de> for Presentation {
75    /// Reads the presentation and checks it with [`Presentation::check`].
76    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
77        let presentation = Self::deserialize(deserializer)?;
78        presentation.check().map_err(de::Error::custom)?;
79        Ok(presentation)
80    }
81}
82
83impl Default for Presentation {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89impl Presentation {
90    /// The base context, `https://www.w3.org/2018/credentials/v1`.
91    pub const CONTEXT: &'static str = CONTEXT_V1;
92
93    /// The type every presentation has, `VerifiablePresentation`.
94    pub const TYPE: &'static str = "VerifiablePresentation";
95
96    /// An empty presentation of type `VerifiablePresentation` in the base context.
97    pub fn new() -> Self {
98        Self {
99            context: Context::uri(Self::CONTEXT),
100            id: None,
101            types: OneOrMany::One(Self::TYPE.to_owned()),
102            verifiable_credential: Vec::new(),
103            holder: None,
104            properties: IndexMap::new(),
105        }
106    }
107
108    /// This presentation with `context` appended to its `@context`.
109    #[must_use]
110    pub fn with_context(mut self, context: impl Into<String>) -> Self {
111        self.context = append_context(self.context, context.into());
112        self
113    }
114
115    /// This presentation with its `id` set.
116    #[must_use]
117    pub fn with_id(mut self, id: impl Into<Uri>) -> Self {
118        self.id = Some(id.into());
119        self
120    }
121
122    /// This presentation with `presentation_type` appended to its `type`.
123    #[must_use]
124    pub fn with_type(mut self, presentation_type: impl Into<String>) -> Self {
125        push_type(&mut self.types, presentation_type.into());
126        self
127    }
128
129    /// This presentation with its `holder` set.
130    #[must_use]
131    pub fn with_holder(mut self, holder: impl Into<Party>) -> Self {
132        self.holder = Some(holder.into());
133        self
134    }
135
136    /// This presentation with `credential` added.
137    #[must_use]
138    pub fn with_credential(mut self, credential: Jwt) -> Self {
139        self.verifiable_credential.push(credential);
140        self
141    }
142
143    /// This presentation with the property `name` set to `value`.
144    #[must_use]
145    pub fn with_property(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
146        self.properties.insert(name.into(), value.into());
147        self
148    }
149
150    /// Whether `presentation_type` is among the types.
151    pub fn has_type(&self, presentation_type: &str) -> bool {
152        self.types.iter().any(|t| t == presentation_type)
153    }
154
155    /// Checks that the first `@context` is the base context and `type` includes
156    /// `VerifiablePresentation`.
157    ///
158    /// # Errors
159    ///
160    /// The first [`CredentialError`] found.
161    pub fn check(&self) -> Result<(), CredentialError> {
162        check_context_and_type(&self.context, Self::CONTEXT, &self.types, Self::TYPE)
163    }
164
165    /// The JWT claims set of the presentation (VCDM 1.1 §6.3.1 "JWT Encoding"): `iss` is the
166    /// holder, `jti` the id, and the presentation without those properties is the `vp` claim.
167    /// `aud`, `nbf`, `exp` and `nonce` are the caller's to add.
168    ///
169    /// # Errors
170    ///
171    /// What [`check`](Self::check) reports.
172    pub fn to_jwt_claims(&self) -> Result<JwtClaims, CredentialError> {
173        self.check()?;
174        let mut vp = claims::to_object(self);
175        let jti = claims::take_id(&mut vp, "id");
176        let iss = claims::take_id(&mut vp, "holder");
177        Ok(JwtClaims {
178            iss,
179            jti,
180            additional: IndexMap::from([("vp".to_owned(), Value::Object(vp))]),
181            ..JwtClaims::default()
182        })
183    }
184
185    /// The presentation of a JWT claims set (VCDM 1.1 §6.3.1 "JWT Decoding"): the `vp` claim
186    /// with `iss` and `jti` mapped back to `holder` and `id`.
187    ///
188    /// # Errors
189    ///
190    /// As [`Credential::from_jwt_claims`](super::Credential::from_jwt_claims), for `vp`.
191    pub fn from_jwt_claims(jwt_claims: &JwtClaims) -> Result<Self, CredentialError> {
192        let mut vp = claims::claim_object(jwt_claims, "vp")?;
193        claims::merge_id(&mut vp, "id", "jti", jwt_claims.jti.as_deref())?;
194        claims::merge_id(&mut vp, "holder", "iss", jwt_claims.iss.as_deref())?;
195        serde_json::from_value(Value::Object(vp)).map_err(|e| CredentialError::InvalidClaim {
196            claim: "vp",
197            reason: e.to_string(),
198        })
199    }
200}