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::Evidence;
23use crate::credential::Issuer;
24use crate::credential::Policy;
25use crate::credential::RefreshService;
26use crate::credential::Schema;
27use crate::credential::Status;
28use crate::credential::Subject;
29use crate::error::Error;
30use crate::error::Result;
31
32use super::jwt_serialization::CredentialJwtClaims;
33use super::Proof;
34
35static BASE_CONTEXT: Lazy<Context> =
36 Lazy::new(|| Context::Url(Url::parse("https://www.w3.org/2018/credentials/v1").unwrap()));
37
38#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
40pub struct Credential<T = Object> {
41 #[serde(rename = "@context")]
43 pub context: OneOrMany<Context>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub id: Option<Url>,
47 #[serde(rename = "type")]
49 pub types: OneOrMany<String>,
50 #[serde(rename = "credentialSubject")]
52 pub credential_subject: OneOrMany<Subject>,
53 pub issuer: Issuer,
55 #[serde(rename = "issuanceDate")]
57 pub issuance_date: Timestamp,
58 #[serde(rename = "expirationDate", skip_serializing_if = "Option::is_none")]
60 pub expiration_date: Option<Timestamp>,
61 #[serde(default, rename = "credentialStatus", skip_serializing_if = "Option::is_none")]
63 pub credential_status: Option<Status>,
64 #[serde(default, rename = "credentialSchema", skip_serializing_if = "OneOrMany::is_empty")]
66 pub credential_schema: OneOrMany<Schema>,
67 #[serde(default, rename = "refreshService", skip_serializing_if = "OneOrMany::is_empty")]
69 pub refresh_service: OneOrMany<RefreshService>,
70 #[serde(default, rename = "termsOfUse", skip_serializing_if = "OneOrMany::is_empty")]
72 pub terms_of_use: OneOrMany<Policy>,
73 #[serde(default, skip_serializing_if = "OneOrMany::is_empty")]
75 pub evidence: OneOrMany<Evidence>,
76 #[serde(rename = "nonTransferable", skip_serializing_if = "Option::is_none")]
79 pub non_transferable: Option<bool>,
80 #[serde(flatten)]
82 pub properties: T,
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub proof: Option<Proof>,
86}
87
88impl<T> Credential<T> {
89 pub fn base_context() -> &'static Context {
91 &BASE_CONTEXT
92 }
93
94 pub const fn base_type() -> &'static str {
96 "VerifiableCredential"
97 }
98
99 pub fn builder(properties: T) -> CredentialBuilder<T> {
103 CredentialBuilder::new(properties)
104 }
105
106 pub fn from_builder(builder: CredentialBuilder<T>) -> Result<Self> {
108 let this: Self = Self {
109 context: OneOrMany::Many(builder.context),
110 id: builder.id,
111 types: builder.types.into(),
112 credential_subject: builder.subject.into(),
113 issuer: builder.issuer.ok_or(Error::MissingIssuer)?,
114 issuance_date: builder.issuance_date.unwrap_or_default(),
115 expiration_date: builder.expiration_date,
116 credential_status: builder.status,
117 credential_schema: builder.schema.into(),
118 refresh_service: builder.refresh_service.into(),
119 terms_of_use: builder.terms_of_use.into(),
120 evidence: builder.evidence.into(),
121 non_transferable: builder.non_transferable,
122 properties: builder.properties,
123 proof: builder.proof,
124 };
125
126 this.check_structure()?;
127
128 Ok(this)
129 }
130
131 pub fn check_structure(&self) -> Result<()> {
133 match self.context.get(0) {
135 Some(context) if context == Self::base_context() => {}
136 Some(_) | None => return Err(Error::MissingBaseContext),
137 }
138
139 if !self.types.iter().any(|type_| type_ == Self::base_type()) {
141 return Err(Error::MissingBaseType);
142 }
143
144 if self.credential_subject.is_empty() {
146 return Err(Error::MissingSubject);
147 }
148
149 for subject in self.credential_subject.iter() {
151 if subject.id.is_none() && subject.properties.is_empty() {
152 return Err(Error::InvalidSubject);
153 }
154 }
155
156 Ok(())
157 }
158
159 pub fn set_proof(&mut self, proof: Option<Proof>) {
163 self.proof = proof;
164 }
165
166 pub fn serialize_jwt(&self, custom_claims: Option<Object>) -> Result<String>
171 where
172 T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
173 {
174 let jwt_representation: CredentialJwtClaims<'_, T> = CredentialJwtClaims::new(self, custom_claims)?;
175 jwt_representation
176 .to_json()
177 .map_err(|err| Error::JwtClaimsSetSerializationError(err.into()))
178 }
179
180 #[cfg(feature = "jpt-bbs-plus")]
182 pub fn serialize_jpt(&self, custom_claims: Option<Object>) -> Result<JptClaims>
183 where
184 T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
185 {
186 let jwt_representation: CredentialJwtClaims<'_, T> = CredentialJwtClaims::new(self, custom_claims)?;
187 Ok(jwt_representation.into())
188 }
189}
190
191impl<T> Display for Credential<T>
192where
193 T: Serialize,
194{
195 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
196 self.fmt_json(f)
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use identity_core::common::OneOrMany;
203 use identity_core::common::Url;
204 use identity_core::convert::FromJson;
205
206 use crate::credential::credential::BASE_CONTEXT;
207 use crate::credential::Credential;
208 use crate::credential::Subject;
209
210 const JSON1: &str = include_str!("../../tests/fixtures/credential-1.json");
211 const JSON2: &str = include_str!("../../tests/fixtures/credential-2.json");
212 const JSON3: &str = include_str!("../../tests/fixtures/credential-3.json");
213 const JSON4: &str = include_str!("../../tests/fixtures/credential-4.json");
214 const JSON5: &str = include_str!("../../tests/fixtures/credential-5.json");
215 const JSON6: &str = include_str!("../../tests/fixtures/credential-6.json");
216 const JSON7: &str = include_str!("../../tests/fixtures/credential-7.json");
217 const JSON8: &str = include_str!("../../tests/fixtures/credential-8.json");
218 const JSON9: &str = include_str!("../../tests/fixtures/credential-9.json");
219 const JSON10: &str = include_str!("../../tests/fixtures/credential-10.json");
220 const JSON11: &str = include_str!("../../tests/fixtures/credential-11.json");
221 const JSON12: &str = include_str!("../../tests/fixtures/credential-12.json");
222
223 #[test]
224 fn test_from_json() {
225 let _credential: Credential = Credential::from_json(JSON1).unwrap();
226 let _credential: Credential = Credential::from_json(JSON2).unwrap();
227 let _credential: Credential = Credential::from_json(JSON3).unwrap();
228 let _credential: Credential = Credential::from_json(JSON4).unwrap();
229 let _credential: Credential = Credential::from_json(JSON5).unwrap();
230 let _credential: Credential = Credential::from_json(JSON6).unwrap();
231 let _credential: Credential = Credential::from_json(JSON7).unwrap();
232 let _credential: Credential = Credential::from_json(JSON8).unwrap();
233 let _credential: Credential = Credential::from_json(JSON9).unwrap();
234 let _credential: Credential = Credential::from_json(JSON10).unwrap();
235 let _credential: Credential = Credential::from_json(JSON11).unwrap();
236 let _credential: Credential = Credential::from_json(JSON12).unwrap();
237 }
238
239 #[test]
240 fn credential_with_single_context_is_list_of_contexts_with_single_item() {
241 let mut credential = Credential::builder(serde_json::Value::default())
242 .id(Url::parse("https://example.com/credentials/123").unwrap())
243 .issuer(Url::parse("https://example.com").unwrap())
244 .subject(Subject::with_id(Url::parse("https://example.com/users/123").unwrap()))
245 .build()
246 .unwrap();
247
248 assert!(matches!(credential.context, OneOrMany::Many(_)));
249 assert_eq!(credential.context.len(), 1);
250 assert!(credential.check_structure().is_ok());
251
252 credential.context = OneOrMany::One(BASE_CONTEXT.clone());
254 assert!(credential.check_structure().is_ok());
255 }
256}