generated from TBD54566975/tbd-project-template
-
Notifications
You must be signed in to change notification settings - Fork 57
/
verifiable-credential.ts
352 lines (307 loc) · 13.8 KB
/
verifiable-credential.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
import type { BearerDid } from '@web5/dids';
import type { ICredential, ICredentialSubject} from '@sphereon/ssi-types';
import { utils as cryptoUtils } from '@web5/crypto';
import { Jwt } from './jwt.js';
import { SsiValidator } from './validators.js';
import { getCurrentXmlSchema112Timestamp, getXmlSchema112Timestamp } from './utils.js';
/** The default Verifiable Credential context. */
export const DEFAULT_VC_CONTEXT = 'https://www.w3.org/2018/credentials/v1';
/** The default Verifiable Credential type. */
export const DEFAULT_VC_TYPE = 'VerifiableCredential';
/**
* A Verifiable Credential is a set of one or more claims made by the same entity.
*
* @see {@link https://www.w3.org/TR/vc-data-model/#credentials | VC Data Model}
*/
export type VcDataModel = ICredential;
/**
* A credential schema defines the structure and content of the data, enabling verifiers to assess if the data adheres to the established schema.
*/
export type CredentialSchema = {
id: string;
type: string;
};
/**
* Options for creating a verifiable credential.
* @param type Optional. The type of the credential, can be a string or an array of strings.
* @param issuer The issuer URI of the credential, as a string.
* @param subject The subject URI of the credential, as a string.
* @param data The credential data, as a generic type any.
* @param issuanceDate Optional. The issuance date of the credential, as a string.
* Defaults to the current date if not specified.
* @param expirationDate Optional. The expiration date of the credential, as a string.
* @param evidence Optional. Evidence can be included by an issuer to provide the verifier with additional supporting information in a verifiable credential.
*/
export type VerifiableCredentialCreateOptions = {
/** The type of the credential, can be a string or an array of strings. */
type?: string | string[];
/** The issuer URI of the credential, as a string. */
issuer: string;
/** The subject URI of the credential, as a string. */
subject: string;
/** The credential data, as a generic type any. */
data: any;
/** The issuance date of the credential, as a string. */
issuanceDate?: string;
/** The expiration date of the credential, as a string. */
expirationDate?: string;
/** The credential schema of the credential */
credentialSchema?: CredentialSchema;
/** The evidence of the credential, as an array of any. */
evidence?: any[];
};
/**
* Options for signing a verifiable credential.
* @param did - The issuer DID of the credential, represented as a PortableDid.
*/
export type VerifiableCredentialSignOptions = {
/** The issuer DID of the credential, represented as a PortableDid. */
did: BearerDid;
};
/** The credential subject of a verifiable credential. */
type CredentialSubject = ICredentialSubject;
/**
* `VerifiableCredential` represents a digitally verifiable credential according to the
* [W3C Verifiable Credentials Data Model](https://www.w3.org/TR/vc-data-model/).
*
* It provides functionalities to sign, verify, and create credentials, offering a concise API to
* work with JWT representations of verifiable credentials and ensuring that the signatures
* and claims within those JWTs can be validated.
*
* @property vcDataModel The [VcDataModel] instance representing the core data model of a verifiable credential.
*/
export class VerifiableCredential {
constructor(public vcDataModel: VcDataModel) {}
/** The type of the credential. */
get type(): string {
return this.vcDataModel.type[this.vcDataModel.type.length - 1];
}
/** The issuer of the credential. */
get issuer(): string {
return this.vcDataModel.issuer.toString();
}
/** The subject of the credential. */
get subject(): string {
if (Array.isArray(this.vcDataModel.credentialSubject)) {
return this.vcDataModel.credentialSubject[0].id!;
} else {
return this.vcDataModel.credentialSubject.id!;
}
}
/**
* Signs the verifiable credential and returns it as a signed JWT.
*
* @example
* ```ts
* const vcJwt = verifiableCredential.sign({ did: myDid });
* ```
*
* @param options - The sign options used to sign the credential.
* @returns The JWT representing the signed verifiable credential.
*/
public async sign(options: VerifiableCredentialSignOptions): Promise<string> {
const vcJwt: string = await Jwt.sign({
signerDid : options.did,
payload : {
vc : this.vcDataModel,
nbf : Math.floor(new Date(this.vcDataModel.issuanceDate).getTime() / 1000),
jti : this.vcDataModel.id,
iss : options.did.uri,
sub : this.subject,
iat : Math.floor(Date.now() / 1000),
...(this.vcDataModel.expirationDate && {
exp: Math.floor(new Date(this.vcDataModel.expirationDate).getTime() / 1000),
}),
}
});
return vcJwt;
}
/**
* Converts the current object to its JSON representation.
*
* @returns The JSON representation of the object.
*/
public toString(): string {
return JSON.stringify(this.vcDataModel);
}
/**
* Create a [VerifiableCredential] based on the provided parameters.
*
* @example
* ```ts
* const vc = await VerifiableCredential.create({
* type: 'StreetCredibility',
* issuer: 'did:ex:issuer',
* subject: 'did:ex:subject',
* data: { 'arbitrary': 'data' }
* })
* ```
*
* @param options - The options to use when creating the Verifiable Credential.
* @returns A [VerifiableCredential] instance.
*/
public static async create(options: VerifiableCredentialCreateOptions): Promise<VerifiableCredential> {
const { type, issuer, subject, data, issuanceDate, expirationDate, credentialSchema, evidence } = options;
const jsonData = JSON.parse(JSON.stringify(data));
if (typeof jsonData !== 'object') {
throw new Error('Expected data to be parseable into a JSON object');
}
if(!issuer || !subject) {
throw new Error('Issuer and subject must be defined');
}
if(typeof issuer !== 'string' || typeof subject !== 'string') {
throw new Error('Issuer and subject must be of type string');
}
const credentialSubject: CredentialSubject = {
id: subject,
...jsonData
};
const vcDataModel: VcDataModel = {
'@context' : [DEFAULT_VC_CONTEXT],
type : Array.isArray(type)
? [DEFAULT_VC_TYPE, ...type]
: (type ? [DEFAULT_VC_TYPE, type] : [DEFAULT_VC_TYPE]),
id : `urn:uuid:${cryptoUtils.randomUuid()}`,
issuer : issuer,
issuanceDate : issuanceDate || getCurrentXmlSchema112Timestamp(),
credentialSubject : credentialSubject,
// Include optional properties only if they have values
...(expirationDate && { expirationDate }),
...(credentialSchema && { credentialSchema }),
...(evidence && { evidence }),
};
validatePayload(vcDataModel);
return new VerifiableCredential(vcDataModel);
}
/**
* Verifies the integrity and authenticity of a Verifiable Credential (VC) encoded as a JSON Web Token (JWT).
*
* This function performs several crucial validation steps to ensure the trustworthiness of the provided VC:
* - Parses and validates the structure of the JWT.
* - Ensures the presence of critical header elements `alg` and `kid` in the JWT header.
* - Resolves the Decentralized Identifier (DID) and retrieves the associated DID Document.
* - Validates the DID and establishes a set of valid verification method IDs.
* - Identifies the correct Verification Method from the DID Document based on the `kid` parameter.
* - Verifies the JWT's signature using the public key associated with the Verification Method.
*
* If any of these steps fail, the function will throw a [Error] with a message indicating the nature of the failure:
* - exp MUST represent the expirationDate property, encoded as a UNIX timestamp (NumericDate).
* - iss MUST represent the issuer property of a verifiable credential or the holder property of a verifiable presentation.
* - nbf MUST represent issuanceDate, encoded as a UNIX timestamp (NumericDate).
* - jti MUST represent the id property of the verifiable credential or verifiable presentation.
* - sub MUST represent the id property contained in the credentialSubject.
*
* Once the verifications are successful, when recreating the VC data model object, this function will:
* - If exp is present, the UNIX timestamp MUST be converted to an [XMLSCHEMA11-2] date-time, and MUST be used to set the value of the expirationDate property of credentialSubject of the new JSON object.
* - If iss is present, the value MUST be used to set the issuer property of the new credential JSON object or the holder property of the new presentation JSON object.
* - If nbf is present, the UNIX timestamp MUST be converted to an [XMLSCHEMA11-2] date-time, and MUST be used to set the value of the issuanceDate property of the new JSON object.
* - If sub is present, the value MUST be used to set the value of the id property of credentialSubject of the new credential JSON object.
* - If jti is present, the value MUST be used to set the value of the id property of the new JSON object.
*
* @example
* ```ts
* try {
* VerifiableCredential.verify({ vcJwt: signedVcJwt })
* console.log("VC Verification successful!")
* } catch (e: Error) {
* console.log("VC Verification failed: ${e.message}")
* }
* ```
* @param param - The parameters for the verification process.
* @param param.vcJwt The Verifiable Credential in JWT format as a [string].
* @throws Error if the verification fails at any step, providing a message with failure details.
* @throws Error if critical JWT header elements are absent.
*/
public static async verify({ vcJwt }: {
vcJwt: string
}) {
const { payload } = await Jwt.verify({ jwt: vcJwt });
const { exp, iss, nbf, jti, sub, vc } = payload;
if (!vc) {
throw new Error('vc property missing.');
}
const vcTyped: VcDataModel = payload['vc'] as VcDataModel;
// exp MUST represent the expirationDate property, encoded as a UNIX timestamp (NumericDate).
if(exp && vcTyped.expirationDate && exp !== Math.floor(new Date(vcTyped.expirationDate).getTime() / 1000)) {
throw new Error('Verification failed: exp claim does not match expirationDate');
}
// If exp is present, the UNIX timestamp MUST be converted to an [XMLSCHEMA11-2] date-time, and MUST be used to set the value of the expirationDate property of credentialSubject of the new JSON object.
if(exp) {
vcTyped.expirationDate = getXmlSchema112Timestamp(exp);
}
if (!iss) throw new Error('Verification failed: iss claim is required');
// iss MUST represent the issuer property of a verifiable credential or the holder property of a verifiable presentation.
if (iss !== vcTyped.issuer) {
throw new Error('Verification failed: iss claim does not match expected issuer');
}
// nbf cannot represent time in the future
if(nbf && nbf > Math.floor(Date.now() / 1000)) {
throw new Error('Verification failed: nbf claim is in the future');
}
// nbf MUST represent issuanceDate, encoded as a UNIX timestamp (NumericDate).
if(nbf && vcTyped.issuanceDate && nbf !== Math.floor(new Date(vcTyped.issuanceDate).getTime() / 1000)) {
throw new Error('Verification failed: nbf claim does not match issuanceDate');
}
// If nbf is present, the UNIX timestamp MUST be converted to an [XMLSCHEMA11-2] date-time, and MUST be used to set the value of the issuanceDate property of the new JSON object.
if(nbf) {
vcTyped.issuanceDate = getXmlSchema112Timestamp(nbf);
}
// sub MUST represent the id property contained in the credentialSubject.
if(sub && !Array.isArray(vcTyped.credentialSubject) && sub !== vcTyped.credentialSubject.id) {
throw new Error('Verification failed: sub claim does not match credentialSubject.id');
}
// If sub is present, the value MUST be used to set the value of the id property of credentialSubject of the new credential JSON object.
if(sub && !Array.isArray(vcTyped.credentialSubject)) {
vcTyped.credentialSubject.id = sub;
}
// jti MUST represent the id property of the verifiable credential or verifiable presentation.
if(jti && jti !== vcTyped.id) {
throw new Error('Verification failed: jti claim does not match id');
}
if(jti) {
vcTyped.id = jti;
}
validatePayload(vcTyped);
return {
/** The issuer of the VC. */
issuer : payload.iss!,
/** The subject of the VC. */
subject : payload.sub!,
/** The VC data model object. */
vc : vcTyped
};
}
/**
* Parses a JWT into a [VerifiableCredential] instance.
*
* @example
* ```ts
* const vc = VerifiableCredential.parseJwt({ vcJwt: signedVcJwt })
* ```
*
* @param vcJwt The verifiable credential JWT as a [String].
* @returns A [VerifiableCredential] instance derived from the JWT.
*/
public static parseJwt({ vcJwt }: { vcJwt: string }): VerifiableCredential {
const parsedJwt = Jwt.parse({ jwt: vcJwt });
const vcDataModel: VcDataModel = parsedJwt.decoded.payload['vc'] as VcDataModel;
if(!vcDataModel) {
throw Error('Jwt payload missing vc property');
}
validatePayload(vcDataModel);
return new VerifiableCredential(vcDataModel);
}
}
/**
* Validates the structure and integrity of a Verifiable Credential payload.
*
* @param vc - The Verifiable Credential object to validate.
* @throws Error if any validation check fails.
*/
function validatePayload(vc: VcDataModel): void {
SsiValidator.validateContext(vc['@context']);
SsiValidator.validateVcType(vc.type);
SsiValidator.validateCredentialSubject(vc.credentialSubject);
if (vc.issuanceDate) SsiValidator.validateTimestamp(vc.issuanceDate);
if (vc.expirationDate) SsiValidator.validateTimestamp(vc.expirationDate);
}