identity_credential/credential/
credential.rs1use core::fmt::Display;
5use core::fmt::Formatter;
6
7use identity_core::convert::ToJson;
8#[cfg(feature = "jpt-bbs-plus")]
9use jsonprooftoken::jpt::claims::JptClaims;
10use once_cell::sync::Lazy;
11use serde::Deserialize;
12use serde::Serialize;
13
14use identity_core::common::Context;
15use identity_core::common::Object;
16use identity_core::common::OneOrMany;
17use identity_core::common::Timestamp;
18use identity_core::common::Url;
19use identity_core::convert::FmtJson;
20
21use crate::credential::CredentialBuilder;
22use crate::credential::CredentialSealed;
23use crate::credential::CredentialT;
24use crate::credential::Evidence;
25use crate::credential::Issuer;
26use crate::credential::Policy;
27use crate::credential::RefreshService;
28use crate::credential::Schema;
29use crate::credential::Status;
30use crate::credential::Subject;
31use crate::error::Error;
32use crate::error::Result;
33
34use super::jwt_serialization::CredentialJwtClaims;
35use super::Proof;
36
37static BASE_CONTEXT: Lazy<Context> =
38 Lazy::new(|| Context::Url(Url::parse("https://www.w3.org/2018/credentials/v1").unwrap()));
39
40#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
42pub struct Credential<T = Object> {
43 #[serde(rename = "@context")]
45 pub context: OneOrMany<Context>,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub id: Option<Url>,
49 #[serde(rename = "type")]
51 pub types: OneOrMany<String>,
52 #[serde(rename = "credentialSubject")]
54 pub credential_subject: OneOrMany<Subject>,
55 pub issuer: Issuer,
57 #[serde(rename = "issuanceDate")]
59 pub issuance_date: Timestamp,
60 #[serde(rename = "expirationDate", skip_serializing_if = "Option::is_none")]
62 pub expiration_date: Option<Timestamp>,
63 #[serde(default, rename = "credentialStatus", skip_serializing_if = "Option::is_none")]
65 pub credential_status: Option<Status>,
66 #[serde(default, rename = "credentialSchema", skip_serializing_if = "OneOrMany::is_empty")]
68 pub credential_schema: OneOrMany<Schema>,
69 #[serde(default, rename = "refreshService", skip_serializing_if = "OneOrMany::is_empty")]
71 pub refresh_service: OneOrMany<RefreshService>,
72 #[serde(default, rename = "termsOfUse", skip_serializing_if = "OneOrMany::is_empty")]
74 pub terms_of_use: OneOrMany<Policy>,
75 #[serde(default, skip_serializing_if = "OneOrMany::is_empty")]
77 pub evidence: OneOrMany<Evidence>,
78 #[serde(rename = "nonTransferable", skip_serializing_if = "Option::is_none")]
81 pub non_transferable: Option<bool>,
82 #[serde(flatten)]
84 pub properties: T,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub proof: Option<Proof>,
88}
89
90impl<T> Credential<T> {
91 pub fn base_context() -> &'static Context {
93 &BASE_CONTEXT
94 }
95
96 pub const fn base_type() -> &'static str {
98 "VerifiableCredential"
99 }
100
101 pub fn builder(properties: T) -> CredentialBuilder<T> {
105 CredentialBuilder::new(properties)
106 }
107
108 pub fn from_builder(mut builder: CredentialBuilder<T>) -> Result<Self> {
110 if builder.context.first() != Some(Self::base_context()) {
111 builder.context.insert(0, Self::base_context().clone());
112 }
113
114 if builder.types.first().map(String::as_str) != Some(Self::base_type()) {
115 builder.types.insert(0, Self::base_type().to_owned());
116 }
117
118 let this: Self = Self {
119 context: OneOrMany::Many(builder.context),
120 id: builder.id,
121 types: builder.types.into(),
122 credential_subject: builder.subject.into(),
123 issuer: builder.issuer.ok_or(Error::MissingIssuer)?,
124 issuance_date: builder.issuance_date.unwrap_or_default(),
125 expiration_date: builder.expiration_date,
126 credential_status: builder.status,
127 credential_schema: builder.schema.into(),
128 refresh_service: builder.refresh_service.into(),
129 terms_of_use: builder.terms_of_use.into(),
130 evidence: builder.evidence.into(),
131 non_transferable: builder.non_transferable,
132 properties: builder.properties,
133 proof: builder.proof,
134 };
135
136 this.check_structure()?;
137
138 Ok(this)
139 }
140
141 pub fn check_structure(&self) -> Result<()> {
143 match self.context.get(0) {
145 Some(context) if context == Self::base_context() => {}
146 Some(_) | None => return Err(Error::MissingBaseContext),
147 }
148
149 if !self.types.iter().any(|type_| type_ == Self::base_type()) {
151 return Err(Error::MissingBaseType);
152 }
153
154 if self.credential_subject.is_empty() {
156 return Err(Error::MissingSubject);
157 }
158
159 for subject in self.credential_subject.iter() {
161 if subject.id.is_none() && subject.properties.is_empty() {
162 return Err(Error::InvalidSubject);
163 }
164 }
165
166 Ok(())
167 }
168
169 pub fn set_proof(&mut self, proof: Option<Proof>) {
173 self.proof = proof;
174 }
175
176 pub fn serialize_jwt(&self, custom_claims: Option<Object>) -> Result<String>
181 where
182 T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
183 {
184 let jwt_representation: CredentialJwtClaims<'_, T> = CredentialJwtClaims::new(self, custom_claims)?;
185 jwt_representation
186 .to_json()
187 .map_err(|err| Error::JwtClaimsSetSerializationError(err.into()))
188 }
189
190 pub fn to_jwt_claims(&self, custom_claims: Option<Object>) -> Result<serde_json::Map<String, serde_json::Value>>
192 where
193 T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
194 {
195 let jwt_representation: CredentialJwtClaims<'_, T> = CredentialJwtClaims::new(self, custom_claims)?;
196 let serde_json::Value::Object(jwt_claims) = jwt_representation
197 .to_json_value()
198 .map_err(|err| Error::JwtClaimsSetSerializationError(err.into()))?
199 else {
200 unreachable!("CredentialJwtClaims always serializes to a JSON object");
201 };
202
203 Ok(jwt_claims)
204 }
205
206 #[cfg(feature = "jpt-bbs-plus")]
208 pub fn serialize_jpt(&self, custom_claims: Option<Object>) -> Result<JptClaims>
209 where
210 T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
211 {
212 let jwt_representation: CredentialJwtClaims<'_, T> = CredentialJwtClaims::new(self, custom_claims)?;
213 Ok(jwt_representation.into())
214 }
215}
216
217impl<T> Display for Credential<T>
218where
219 T: Serialize,
220{
221 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
222 self.fmt_json(f)
223 }
224}
225
226impl<T> CredentialSealed for Credential<T> {}
227
228impl<T> CredentialT for Credential<T>
229where
230 T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
231{
232 type Properties = T;
233
234 fn base_context(&self) -> &'static Context {
235 Self::base_context()
236 }
237
238 fn type_(&self) -> &OneOrMany<String> {
239 &self.types
240 }
241
242 fn context(&self) -> &OneOrMany<Context> {
243 &self.context
244 }
245
246 fn subject(&self) -> &OneOrMany<Subject> {
247 &self.credential_subject
248 }
249
250 fn issuer(&self) -> &Issuer {
251 &self.issuer
252 }
253
254 fn valid_from(&self) -> Timestamp {
255 self.issuance_date
256 }
257
258 fn valid_until(&self) -> Option<Timestamp> {
259 self.expiration_date
260 }
261
262 fn properties(&self) -> &Self::Properties {
263 &self.properties
264 }
265
266 fn status(&self) -> Option<&Status> {
267 self.credential_status.as_ref()
268 }
269
270 fn non_transferable(&self) -> bool {
271 self.non_transferable.unwrap_or_default()
272 }
273
274 fn serialize_jwt(&self, custom_claims: Option<Object>) -> Result<String> {
275 self.serialize_jwt(custom_claims)
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use identity_core::common::OneOrMany;
282 use identity_core::common::Url;
283 use identity_core::convert::FromJson;
284
285 use crate::credential::credential::BASE_CONTEXT;
286 use crate::credential::Credential;
287 use crate::credential::Subject;
288
289 const JSON1: &str = include_str!("../../tests/fixtures/credential-1.json");
290 const JSON2: &str = include_str!("../../tests/fixtures/credential-2.json");
291 const JSON3: &str = include_str!("../../tests/fixtures/credential-3.json");
292 const JSON4: &str = include_str!("../../tests/fixtures/credential-4.json");
293 const JSON5: &str = include_str!("../../tests/fixtures/credential-5.json");
294 const JSON6: &str = include_str!("../../tests/fixtures/credential-6.json");
295 const JSON7: &str = include_str!("../../tests/fixtures/credential-7.json");
296 const JSON8: &str = include_str!("../../tests/fixtures/credential-8.json");
297 const JSON9: &str = include_str!("../../tests/fixtures/credential-9.json");
298 const JSON10: &str = include_str!("../../tests/fixtures/credential-10.json");
299 const JSON11: &str = include_str!("../../tests/fixtures/credential-11.json");
300 const JSON12: &str = include_str!("../../tests/fixtures/credential-12.json");
301
302 #[test]
303 fn test_from_json() {
304 let _credential: Credential = Credential::from_json(JSON1).unwrap();
305 let _credential: Credential = Credential::from_json(JSON2).unwrap();
306 let _credential: Credential = Credential::from_json(JSON3).unwrap();
307 let _credential: Credential = Credential::from_json(JSON4).unwrap();
308 let _credential: Credential = Credential::from_json(JSON5).unwrap();
309 let _credential: Credential = Credential::from_json(JSON6).unwrap();
310 let _credential: Credential = Credential::from_json(JSON7).unwrap();
311 let _credential: Credential = Credential::from_json(JSON8).unwrap();
312 let _credential: Credential = Credential::from_json(JSON9).unwrap();
313 let _credential: Credential = Credential::from_json(JSON10).unwrap();
314 let _credential: Credential = Credential::from_json(JSON11).unwrap();
315 let _credential: Credential = Credential::from_json(JSON12).unwrap();
316 }
317
318 #[test]
319 fn credential_with_single_context_is_list_of_contexts_with_single_item() {
320 let mut credential = Credential::builder(serde_json::Value::default())
321 .id(Url::parse("https://example.com/credentials/123").unwrap())
322 .issuer(Url::parse("https://example.com").unwrap())
323 .subject(Subject::with_id(Url::parse("https://example.com/users/123").unwrap()))
324 .build()
325 .unwrap();
326
327 assert!(matches!(credential.context, OneOrMany::Many(_)));
328 assert_eq!(credential.context.len(), 1);
329 assert!(credential.check_structure().is_ok());
330
331 credential.context = OneOrMany::One(BASE_CONTEXT.clone());
333 assert!(credential.check_structure().is_ok());
334 }
335}