@interop/vc 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +29 -0
- package/README.md +542 -0
- package/dist/CredentialIssuancePurpose.d.ts +50 -0
- package/dist/CredentialIssuancePurpose.d.ts.map +1 -0
- package/dist/CredentialIssuancePurpose.js +67 -0
- package/dist/CredentialIssuancePurpose.js.map +1 -0
- package/dist/contexts/index.d.ts +2 -0
- package/dist/contexts/index.d.ts.map +1 -0
- package/dist/contexts/index.js +15 -0
- package/dist/contexts/index.js.map +1 -0
- package/dist/documentLoader.d.ts +6 -0
- package/dist/documentLoader.d.ts.map +1 -0
- package/dist/documentLoader.js +14 -0
- package/dist/documentLoader.js.map +1 -0
- package/dist/helpers.d.ts +42 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +79 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +279 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +736 -0
- package/dist/index.js.map +1 -0
- package/package.json +99 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A TypeScript implementation of Verifiable Credentials.
|
|
3
|
+
*
|
|
4
|
+
* @author Dave Longley
|
|
5
|
+
* @author David I. Lehn
|
|
6
|
+
* @author Dmitri Zagidulin
|
|
7
|
+
*
|
|
8
|
+
* @license BSD 3-Clause License
|
|
9
|
+
* Copyright (c) 2017-2025 Digital Bazaar, Inc.
|
|
10
|
+
* Copyright (c) 2022-2025 DCC (Digital Credentials Consortium).
|
|
11
|
+
* Copyright (c) 2026 TypeScript conversion by Interop Alliance.
|
|
12
|
+
* All rights reserved.
|
|
13
|
+
*
|
|
14
|
+
* Redistribution and use in source and binary forms, with or without
|
|
15
|
+
* modification, are permitted provided that the following conditions are met:
|
|
16
|
+
*
|
|
17
|
+
* Redistributions of source code must retain the above copyright notice,
|
|
18
|
+
* this list of conditions and the following disclaimer.
|
|
19
|
+
*
|
|
20
|
+
* Redistributions in binary form must reproduce the above copyright
|
|
21
|
+
* notice, this list of conditions and the following disclaimer in the
|
|
22
|
+
* documentation and/or other materials provided with the distribution.
|
|
23
|
+
*
|
|
24
|
+
* Neither the name of the Digital Bazaar, Inc. nor the names of its
|
|
25
|
+
* contributors may be used to endorse or promote products derived from
|
|
26
|
+
* this software without specific prior written permission.
|
|
27
|
+
*
|
|
28
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
|
29
|
+
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
|
30
|
+
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
|
31
|
+
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
32
|
+
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
33
|
+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
|
34
|
+
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
35
|
+
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
36
|
+
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
37
|
+
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
38
|
+
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
39
|
+
*/
|
|
40
|
+
import jsigs from '@interop/jsonld-signatures';
|
|
41
|
+
import jsonld from '@interop/jsonld';
|
|
42
|
+
import { assertCredentialContext, assertDateString, checkContextVersion, compareTime, CREDENTIALS_CONTEXT_V1_URL, CREDENTIALS_CONTEXT_V2_URL } from './helpers.js';
|
|
43
|
+
import { documentLoader as _documentLoader } from './documentLoader.js';
|
|
44
|
+
import { CredentialIssuancePurpose } from './CredentialIssuancePurpose.js';
|
|
45
|
+
const { AssertionProofPurpose, AuthenticationProofPurpose } = jsigs.purposes;
|
|
46
|
+
export { dateRegex } from './helpers.js';
|
|
47
|
+
export const defaultDocumentLoader = jsigs.extendContextLoader(_documentLoader);
|
|
48
|
+
export { CredentialIssuancePurpose };
|
|
49
|
+
/**
|
|
50
|
+
* Issues a verifiable credential (by taking a base credential document,
|
|
51
|
+
* and adding a digital signature to it).
|
|
52
|
+
*
|
|
53
|
+
* @param options - The options to use.
|
|
54
|
+
* @param options.credential - Base credential document.
|
|
55
|
+
* @param options.suite - Signature suite (with private key material or an API
|
|
56
|
+
* to use it), passed in to `sign()`.
|
|
57
|
+
* @param options.purpose - A ProofPurpose. If not specified, a default purpose
|
|
58
|
+
* will be created.
|
|
59
|
+
* @param options.documentLoader - A document loader.
|
|
60
|
+
* @param options.now - A string representing date time in ISO 8601 format or an
|
|
61
|
+
* instance of Date. Defaults to current date time.
|
|
62
|
+
* @param options.maxClockSkew - A maximum number of seconds that clocks may be
|
|
63
|
+
* skewed when checking date-times against `now`.
|
|
64
|
+
*
|
|
65
|
+
* @throws {Error} If missing required properties.
|
|
66
|
+
*
|
|
67
|
+
* @returns Resolves on completion.
|
|
68
|
+
*/
|
|
69
|
+
export async function issue({ credential, suite, purpose = new CredentialIssuancePurpose(), documentLoader = defaultDocumentLoader, now, maxClockSkew = 300 } = {}) {
|
|
70
|
+
// check to make sure the `suite` has required params
|
|
71
|
+
// Note: verificationMethod defaults to publicKey.id, in suite constructor
|
|
72
|
+
if (!suite) {
|
|
73
|
+
throw new TypeError('"suite" parameter is required for issuing.');
|
|
74
|
+
}
|
|
75
|
+
if (!suite.verificationMethod) {
|
|
76
|
+
throw new TypeError('"suite.verificationMethod" property is required.');
|
|
77
|
+
}
|
|
78
|
+
if (!credential) {
|
|
79
|
+
throw new TypeError('"credential" parameter is required for issuing.');
|
|
80
|
+
}
|
|
81
|
+
if (checkContextVersion({ credential, version: 1.0 }) &&
|
|
82
|
+
!credential.issuanceDate) {
|
|
83
|
+
const now = new Date().toJSON();
|
|
84
|
+
credential.issuanceDate = `${now.slice(0, now.length - 5)}Z`;
|
|
85
|
+
}
|
|
86
|
+
// run common credential checks
|
|
87
|
+
_checkCredential({ credential, now, mode: 'issue', maxClockSkew });
|
|
88
|
+
return (await jsigs.sign(credential, {
|
|
89
|
+
purpose,
|
|
90
|
+
documentLoader,
|
|
91
|
+
suite
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Derives a proof from the given verifiable credential, resulting in a new
|
|
96
|
+
* verifiable credential. This method is usually used to generate selective
|
|
97
|
+
* disclosure and / or unlinkable proofs.
|
|
98
|
+
*
|
|
99
|
+
* @param options - The options to use.
|
|
100
|
+
* @param options.verifiableCredential - The verifiable credential containing a
|
|
101
|
+
* base proof to derive another proof from.
|
|
102
|
+
* @param options.suite - Derived proof signature suite.
|
|
103
|
+
* @param options.documentLoader - A document loader.
|
|
104
|
+
*
|
|
105
|
+
* @throws {Error} If missing required properties.
|
|
106
|
+
*
|
|
107
|
+
* @returns Resolves on completion.
|
|
108
|
+
*/
|
|
109
|
+
export async function derive({ verifiableCredential, suite, documentLoader = defaultDocumentLoader } = {}) {
|
|
110
|
+
if (!verifiableCredential) {
|
|
111
|
+
throw new TypeError('"credential" parameter is required for deriving.');
|
|
112
|
+
}
|
|
113
|
+
if (!suite) {
|
|
114
|
+
throw new TypeError('"suite" parameter is required for deriving.');
|
|
115
|
+
}
|
|
116
|
+
// run common credential checks
|
|
117
|
+
_checkCredential({ credential: verifiableCredential, mode: 'issue' });
|
|
118
|
+
return (await jsigs.derive(verifiableCredential, {
|
|
119
|
+
purpose: new AssertionProofPurpose(),
|
|
120
|
+
documentLoader,
|
|
121
|
+
suite
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Verifies a verifiable presentation:
|
|
126
|
+
* - Checks that the presentation is well-formed
|
|
127
|
+
* - Checks the proofs (for example, checks digital signatures against the
|
|
128
|
+
* provided public keys).
|
|
129
|
+
*
|
|
130
|
+
* @param options - The options to use.
|
|
131
|
+
* @param options.presentation - Verifiable presentation, signed or unsigned,
|
|
132
|
+
* that may contain within it a verifiable credential.
|
|
133
|
+
* @param options.suite - One or more signature suites that are supported by the
|
|
134
|
+
* caller's use case. This is an explicit design decision -- the calling code
|
|
135
|
+
* must specify which signature types (ed25519, RSA, etc) are allowed.
|
|
136
|
+
* Although it is expected that the secure resolution/fetching of the public
|
|
137
|
+
* key material (to verify against) is to be handled by the documentLoader,
|
|
138
|
+
* the suite param can optionally include the key directly.
|
|
139
|
+
* @param options.unsignedPresentation - By default, this function assumes that
|
|
140
|
+
* a presentation is signed (and will return an error if a `proof` section is
|
|
141
|
+
* missing). Set this to `true` if you're using an unsigned presentation.
|
|
142
|
+
* @param options.presentationPurpose - Optional proof purpose (a default one
|
|
143
|
+
* will be created if not passed in).
|
|
144
|
+
* @param options.challenge - Required if purpose is not passed in.
|
|
145
|
+
* @param options.controller - A controller.
|
|
146
|
+
* @param options.domain - A domain.
|
|
147
|
+
* @param options.documentLoader - A document loader.
|
|
148
|
+
* @param options.checkStatus - Optional function for checking credential status
|
|
149
|
+
* if `credentialStatus` is present on the credential.
|
|
150
|
+
* @param options.now - A string representing date time in ISO 8601 format or an
|
|
151
|
+
* instance of Date. Defaults to current date time.
|
|
152
|
+
* @param options.maxClockSkew - A maximum number of seconds that clocks may be
|
|
153
|
+
* skewed when checking date-times against `now`.
|
|
154
|
+
* @param options.includeCredentials - Set to `true` to include each verified
|
|
155
|
+
* `credential` in its entry in `credentialResults`. Defaults to `true` to
|
|
156
|
+
* preserve backwards compatibility; set to `false` to omit them.
|
|
157
|
+
*
|
|
158
|
+
* @returns The verification result.
|
|
159
|
+
*/
|
|
160
|
+
export async function verify(options = {}) {
|
|
161
|
+
const { presentation } = options;
|
|
162
|
+
try {
|
|
163
|
+
if (!presentation) {
|
|
164
|
+
throw new TypeError('A "presentation" property is required for verifying.');
|
|
165
|
+
}
|
|
166
|
+
return await _verifyPresentation(options);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
return {
|
|
170
|
+
verified: false,
|
|
171
|
+
results: [{ presentation, verified: false, error }],
|
|
172
|
+
error: error
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Verifies a verifiable credential:
|
|
178
|
+
* - Checks that the credential is well-formed
|
|
179
|
+
* - Checks the proofs (for example, checks digital signatures against the
|
|
180
|
+
* provided public keys).
|
|
181
|
+
*
|
|
182
|
+
* @param options - The options.
|
|
183
|
+
* @param options.credential - Verifiable credential.
|
|
184
|
+
* @param options.suite - One or more signature suites that are supported by the
|
|
185
|
+
* caller's use case. This is an explicit design decision -- the calling code
|
|
186
|
+
* must specify which signature types (ed25519, RSA, etc) are allowed.
|
|
187
|
+
* Although it is expected that the secure resolution/fetching of the public
|
|
188
|
+
* key material (to verify against) is to be handled by the documentLoader,
|
|
189
|
+
* the suite param can optionally include the key directly.
|
|
190
|
+
* @param options.purpose - Optional proof purpose (a default one will be
|
|
191
|
+
* created if not passed in).
|
|
192
|
+
* @param options.documentLoader - A document loader.
|
|
193
|
+
* @param options.checkStatus - Optional function for checking credential status
|
|
194
|
+
* if `credentialStatus` is present on the credential.
|
|
195
|
+
* @param options.now - A string representing date time in ISO 8601 format or an
|
|
196
|
+
* instance of Date. Defaults to current date time.
|
|
197
|
+
* @param options.maxClockSkew - A maximum number of seconds that clocks may be
|
|
198
|
+
* skewed when checking date-times against `now`.
|
|
199
|
+
*
|
|
200
|
+
* @returns The verification result.
|
|
201
|
+
*/
|
|
202
|
+
export async function verifyCredential(options = {}) {
|
|
203
|
+
const { credential } = options;
|
|
204
|
+
try {
|
|
205
|
+
if (!credential) {
|
|
206
|
+
throw new TypeError('A "credential" property is required for verifying.');
|
|
207
|
+
}
|
|
208
|
+
return await _verifyCredential(options);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
return {
|
|
212
|
+
verified: false,
|
|
213
|
+
results: [{ credential, verified: false, error }],
|
|
214
|
+
error: error
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Verifies a verifiable credential.
|
|
220
|
+
*
|
|
221
|
+
* @param options - The options.
|
|
222
|
+
*
|
|
223
|
+
* @throws {Error} If required parameters are missing (in `_checkCredential`).
|
|
224
|
+
*
|
|
225
|
+
* @returns The verification result.
|
|
226
|
+
*/
|
|
227
|
+
async function _verifyCredential(options = {}) {
|
|
228
|
+
const credential = options.credential;
|
|
229
|
+
const { checkStatus, now, maxClockSkew = 300 } = options;
|
|
230
|
+
// Fine-grained result log containing checks performed. Example:
|
|
231
|
+
// [
|
|
232
|
+
// {id: 'valid_signature', valid: true},
|
|
233
|
+
// {id: 'issuer_did_resolves', valid: true},
|
|
234
|
+
// {id: 'expiration', valid: true},
|
|
235
|
+
// {id: 'revocation_status', valid: true},
|
|
236
|
+
// {id: 'suspension_status', valid: true}
|
|
237
|
+
// ]
|
|
238
|
+
const log = [];
|
|
239
|
+
const documentLoader = options.documentLoader || defaultDocumentLoader;
|
|
240
|
+
const { controller } = options;
|
|
241
|
+
const purpose = options.purpose || new CredentialIssuancePurpose({ controller });
|
|
242
|
+
let result;
|
|
243
|
+
try {
|
|
244
|
+
result = await jsigs.verify(credential, {
|
|
245
|
+
purpose,
|
|
246
|
+
documentLoader,
|
|
247
|
+
...options
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
log.push({ id: 'valid_signature', valid: false });
|
|
252
|
+
error.log = log;
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
// if verification has failed, skip status check
|
|
256
|
+
if (!result.verified) {
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
log.push({ id: 'valid_signature', valid: true });
|
|
260
|
+
log.push({ id: 'issuer_did_resolves', valid: true });
|
|
261
|
+
// if credential status is provided, a `checkStatus` function must be given
|
|
262
|
+
if (credential.credentialStatus && typeof checkStatus !== 'function') {
|
|
263
|
+
throw new TypeError('A "checkStatus" function must be given to verify credentials with ' +
|
|
264
|
+
'"credentialStatus".');
|
|
265
|
+
}
|
|
266
|
+
if (credential.credentialStatus) {
|
|
267
|
+
await addStatusInfoToLog({ options, result, log });
|
|
268
|
+
}
|
|
269
|
+
// run common credential checks (add check results to log)
|
|
270
|
+
_checkCredential({ credential, log, now, maxClockSkew });
|
|
271
|
+
result.log = log;
|
|
272
|
+
if (result.results) {
|
|
273
|
+
result.results[0].log = log;
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
async function addStatusInfoToLog({ options, result, log }) {
|
|
278
|
+
const { checkStatus } = options;
|
|
279
|
+
result.statusResult = await checkStatus(options);
|
|
280
|
+
if (!result.statusResult.verified) {
|
|
281
|
+
result.verified = false;
|
|
282
|
+
}
|
|
283
|
+
const statusResults = result.statusResult?.results ?? [];
|
|
284
|
+
for (const entry of statusResults) {
|
|
285
|
+
log.push({
|
|
286
|
+
id: `${entry.credentialStatus.statusPurpose}_status`,
|
|
287
|
+
valid: entry.verified
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Creates an unsigned presentation from a given verifiable credential.
|
|
293
|
+
*
|
|
294
|
+
* @param options - Options to use.
|
|
295
|
+
* @param options.verifiableCredential - One or more verifiable credential.
|
|
296
|
+
* @param options.id - Optional VP id.
|
|
297
|
+
* @param options.holder - Optional presentation holder url.
|
|
298
|
+
* @param options.now - A string representing date time in ISO 8601 format or an
|
|
299
|
+
* instance of Date. Defaults to current date time.
|
|
300
|
+
* @param options.version - The VC context version to use.
|
|
301
|
+
* @param options.verify - If set to true, throw verification errors for
|
|
302
|
+
* individual VCs (such as when the VC is expired, etc).
|
|
303
|
+
* @param options.maxClockSkew - A maximum number of seconds that clocks may be
|
|
304
|
+
* skewed when checking date-times against `now`.
|
|
305
|
+
*
|
|
306
|
+
* @throws {TypeError} If verifiableCredential param is missing.
|
|
307
|
+
* @throws {Error} If the credential (or the presentation params) are missing
|
|
308
|
+
* required properties.
|
|
309
|
+
*
|
|
310
|
+
* @returns The credential wrapped inside of a VerifiablePresentation.
|
|
311
|
+
*/
|
|
312
|
+
export function createPresentation({ verifiableCredential, id, holder, now, version = 2.0, verify = true, maxClockSkew = 300 } = {}) {
|
|
313
|
+
const initialContext = version === 2.0 ? CREDENTIALS_CONTEXT_V2_URL : CREDENTIALS_CONTEXT_V1_URL;
|
|
314
|
+
const presentation = {
|
|
315
|
+
'@context': [initialContext],
|
|
316
|
+
type: ['VerifiablePresentation']
|
|
317
|
+
};
|
|
318
|
+
if (verifiableCredential) {
|
|
319
|
+
const credentials = Array.isArray(verifiableCredential)
|
|
320
|
+
? verifiableCredential
|
|
321
|
+
: [verifiableCredential];
|
|
322
|
+
if (verify) {
|
|
323
|
+
// ensure all credentials are valid and verified
|
|
324
|
+
for (const credential of credentials) {
|
|
325
|
+
_checkCredential({
|
|
326
|
+
credential,
|
|
327
|
+
now,
|
|
328
|
+
maxClockSkew,
|
|
329
|
+
mode: verify ? 'verify' : 'do not force verify'
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
presentation.verifiableCredential = credentials;
|
|
334
|
+
}
|
|
335
|
+
if (id) {
|
|
336
|
+
presentation.id = id;
|
|
337
|
+
}
|
|
338
|
+
if (holder) {
|
|
339
|
+
presentation.holder = holder;
|
|
340
|
+
}
|
|
341
|
+
_checkPresentation(presentation);
|
|
342
|
+
return presentation;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Signs a given presentation.
|
|
346
|
+
*
|
|
347
|
+
* @param options - Options to use.
|
|
348
|
+
* @param options.presentation - A presentation.
|
|
349
|
+
* @param options.suite - Passed in to `sign()`.
|
|
350
|
+
* @param options.purpose - A ProofPurpose. If not specified, a default purpose
|
|
351
|
+
* will be created with the domain and challenge options.
|
|
352
|
+
* @param options.domain - A domain.
|
|
353
|
+
* @param options.challenge - A required challenge.
|
|
354
|
+
* @param options.documentLoader - A document loader.
|
|
355
|
+
*
|
|
356
|
+
* @returns A VerifiablePresentation with a proof.
|
|
357
|
+
*/
|
|
358
|
+
export async function signPresentation(options = {}) {
|
|
359
|
+
const { presentation, domain, challenge } = options;
|
|
360
|
+
const purpose = options.purpose ||
|
|
361
|
+
new AuthenticationProofPurpose({
|
|
362
|
+
domain,
|
|
363
|
+
challenge: challenge
|
|
364
|
+
});
|
|
365
|
+
const documentLoader = options.documentLoader || defaultDocumentLoader;
|
|
366
|
+
return (await jsigs.sign(presentation, {
|
|
367
|
+
...options,
|
|
368
|
+
purpose,
|
|
369
|
+
documentLoader
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Verifies that the VerifiablePresentation is well formed, and checks the
|
|
374
|
+
* proof signature if it's present. Also verifies all the VerifiableCredentials
|
|
375
|
+
* that are present in the presentation, if any.
|
|
376
|
+
*
|
|
377
|
+
* @param options - The options.
|
|
378
|
+
*
|
|
379
|
+
* @throws {Error} If presentation is missing required params.
|
|
380
|
+
*
|
|
381
|
+
* @returns The verification result.
|
|
382
|
+
*/
|
|
383
|
+
async function _verifyPresentation(options = {}) {
|
|
384
|
+
const { presentation, unsignedPresentation, includeCredentials = true } = options;
|
|
385
|
+
_checkPresentation(presentation);
|
|
386
|
+
const documentLoader = options.documentLoader || defaultDocumentLoader;
|
|
387
|
+
// FIXME: verify presentation first, then each individual credential
|
|
388
|
+
// only if that proof is verified
|
|
389
|
+
// if verifiableCredentials are present, verify them, individually
|
|
390
|
+
let credentialResults;
|
|
391
|
+
let verified = true;
|
|
392
|
+
const credentials = jsonld.getValues(presentation, 'verifiableCredential');
|
|
393
|
+
if (credentials.length > 0) {
|
|
394
|
+
// verify every credential in `verifiableCredential`
|
|
395
|
+
credentialResults = await Promise.all(credentials.map((credential) => {
|
|
396
|
+
return verifyCredential({ ...options, credential, documentLoader });
|
|
397
|
+
}));
|
|
398
|
+
for (const [i, credentialResult] of credentialResults.entries()) {
|
|
399
|
+
credentialResult.credentialId = credentials[i].id;
|
|
400
|
+
if (includeCredentials) {
|
|
401
|
+
credentialResult.credential = credentials[i];
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const allCredentialsVerified = credentialResults.every(result => result.verified);
|
|
405
|
+
if (!allCredentialsVerified) {
|
|
406
|
+
verified = false;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (unsignedPresentation) {
|
|
410
|
+
// No need to verify the proof section of this presentation
|
|
411
|
+
return { verified, results: [presentation], credentialResults };
|
|
412
|
+
}
|
|
413
|
+
const { controller, domain, challenge } = options;
|
|
414
|
+
if (!options.presentationPurpose && !challenge) {
|
|
415
|
+
throw new Error('A "challenge" param is required for AuthenticationProofPurpose.');
|
|
416
|
+
}
|
|
417
|
+
const purpose = options.presentationPurpose ||
|
|
418
|
+
new AuthenticationProofPurpose({
|
|
419
|
+
controller,
|
|
420
|
+
domain,
|
|
421
|
+
challenge: challenge
|
|
422
|
+
});
|
|
423
|
+
const presentationResult = await jsigs.verify(presentation, {
|
|
424
|
+
...options,
|
|
425
|
+
purpose,
|
|
426
|
+
documentLoader
|
|
427
|
+
});
|
|
428
|
+
return {
|
|
429
|
+
presentationResult,
|
|
430
|
+
verified: verified && presentationResult.verified,
|
|
431
|
+
credentialResults,
|
|
432
|
+
error: presentationResult.error
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* @param obj - Either an object with an id property or a string that is an id.
|
|
437
|
+
* @returns Either an id or undefined.
|
|
438
|
+
*/
|
|
439
|
+
function _getId(obj) {
|
|
440
|
+
if (typeof obj === 'string') {
|
|
441
|
+
return obj;
|
|
442
|
+
}
|
|
443
|
+
if (!('id' in obj)) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
return obj.id;
|
|
447
|
+
}
|
|
448
|
+
// export for testing
|
|
449
|
+
/**
|
|
450
|
+
* @param presentation - An object that could be a presentation.
|
|
451
|
+
*
|
|
452
|
+
* @throws {Error}
|
|
453
|
+
*/
|
|
454
|
+
export function _checkPresentation(presentation) {
|
|
455
|
+
// normalize to an array to allow the common case of context being a string
|
|
456
|
+
const context = Array.isArray(presentation['@context'])
|
|
457
|
+
? presentation['@context']
|
|
458
|
+
: [presentation['@context']];
|
|
459
|
+
assertCredentialContext({ context });
|
|
460
|
+
const types = jsonld.getValues(presentation, 'type');
|
|
461
|
+
// check type presence
|
|
462
|
+
if (!types.includes('VerifiablePresentation')) {
|
|
463
|
+
throw new Error('"type" must include "VerifiablePresentation".');
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
// these props of a VC must be an object with a type
|
|
467
|
+
// if present in a VC or VP
|
|
468
|
+
const mustHaveType = ['proof', 'credentialStatus', 'termsOfUse', 'evidence'];
|
|
469
|
+
// export for testing
|
|
470
|
+
/**
|
|
471
|
+
* @param options - The options.
|
|
472
|
+
* @param options.credential - An object that could be a VerifiableCredential.
|
|
473
|
+
* @param options.log - Optional events log, for fine-grained verification
|
|
474
|
+
* result reporting.
|
|
475
|
+
* @param options.now - A string representing date time in ISO 8601 format or an
|
|
476
|
+
* instance of Date. Defaults to current date time.
|
|
477
|
+
* @param options.mode - The mode of operation for this validation function,
|
|
478
|
+
* either `issue` or `verify`.
|
|
479
|
+
* @param options.maxClockSkew - A maximum number of seconds that clocks may be
|
|
480
|
+
* skewed when checking date-times against `now`.
|
|
481
|
+
*
|
|
482
|
+
* @throws {Error}
|
|
483
|
+
*/
|
|
484
|
+
export function _checkCredential({ credential, log = [], now = new Date(), mode = 'verify', maxClockSkew = 300 }) {
|
|
485
|
+
const nowDate = typeof now === 'string' ? new Date(now) : now;
|
|
486
|
+
assertCredentialContext({ context: credential['@context'] });
|
|
487
|
+
// check type presence and cardinality
|
|
488
|
+
if (!credential.type) {
|
|
489
|
+
throw new Error('"type" property is required.');
|
|
490
|
+
}
|
|
491
|
+
if (!jsonld.getValues(credential, 'type').includes('VerifiableCredential')) {
|
|
492
|
+
throw new Error('"type" must include `VerifiableCredential`.');
|
|
493
|
+
}
|
|
494
|
+
_checkCredentialSubjects({ credential });
|
|
495
|
+
if (!credential.issuer) {
|
|
496
|
+
throw new Error('"issuer" property is required.');
|
|
497
|
+
}
|
|
498
|
+
if (checkContextVersion({ credential, version: 1.0 })) {
|
|
499
|
+
// check issuanceDate exists
|
|
500
|
+
if (!credential.issuanceDate) {
|
|
501
|
+
throw new Error('"issuanceDate" property is required.');
|
|
502
|
+
}
|
|
503
|
+
// check issuanceDate format on issue
|
|
504
|
+
assertDateString({ credential, prop: 'issuanceDate' });
|
|
505
|
+
// check issuanceDate cardinality
|
|
506
|
+
if (jsonld.getValues(credential, 'issuanceDate').length > 1) {
|
|
507
|
+
throw new Error('"issuanceDate" property can only have one value.');
|
|
508
|
+
}
|
|
509
|
+
// optionally check expirationDate
|
|
510
|
+
if ('expirationDate' in credential) {
|
|
511
|
+
// check if `expirationDate` property is a date
|
|
512
|
+
try {
|
|
513
|
+
assertDateString({ credential, prop: 'expirationDate' });
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
log.push({ id: 'expiration', valid: false });
|
|
517
|
+
error.log = log;
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
if (mode === 'verify') {
|
|
521
|
+
// check if `now` is after `expirationDate`
|
|
522
|
+
const expirationDate = new Date(credential.expirationDate);
|
|
523
|
+
if (compareTime({ t1: nowDate, t2: expirationDate, maxClockSkew }) > 0) {
|
|
524
|
+
log.push({ id: 'expiration', valid: false });
|
|
525
|
+
const error = new Error('Credential has expired.');
|
|
526
|
+
error.log = log;
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
log.push({ id: 'expiration', valid: true });
|
|
532
|
+
// check if `now` is before `issuanceDate` on verification
|
|
533
|
+
if (mode === 'verify') {
|
|
534
|
+
const issuanceDate = new Date(credential.issuanceDate);
|
|
535
|
+
if (compareTime({ t1: issuanceDate, t2: nowDate, maxClockSkew }) > 0) {
|
|
536
|
+
throw new Error(`The current date time (${nowDate.toISOString()}) is before the ` +
|
|
537
|
+
`"issuanceDate" (${credential.issuanceDate}).`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (checkContextVersion({ credential, version: 2.0 })) {
|
|
542
|
+
// check if 'validUntil' and 'validFrom'
|
|
543
|
+
const { validUntil, validFrom } = credential;
|
|
544
|
+
if (validUntil) {
|
|
545
|
+
try {
|
|
546
|
+
assertDateString({ credential, prop: 'validUntil' });
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
log.push({ id: 'expiration', valid: false });
|
|
550
|
+
error.log = log;
|
|
551
|
+
throw error;
|
|
552
|
+
}
|
|
553
|
+
if (mode === 'verify') {
|
|
554
|
+
const validUntilDate = new Date(credential.validUntil);
|
|
555
|
+
if (compareTime({ t1: nowDate, t2: validUntilDate, maxClockSkew }) > 0) {
|
|
556
|
+
log.push({ id: 'expiration', valid: false });
|
|
557
|
+
const error = new Error(`The current date time (${nowDate.toISOString()}) is after ` +
|
|
558
|
+
`"validUntil" (${credential.validUntil}).`);
|
|
559
|
+
error.log = log;
|
|
560
|
+
throw error;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
log.push({ id: 'expiration', valid: true });
|
|
565
|
+
if (validFrom) {
|
|
566
|
+
assertDateString({ credential, prop: 'validFrom' });
|
|
567
|
+
if (mode === 'verify') {
|
|
568
|
+
// check if `now` is before `validFrom`
|
|
569
|
+
const validFromDate = new Date(credential.validFrom);
|
|
570
|
+
if (compareTime({ t1: validFromDate, t2: nowDate, maxClockSkew }) > 0) {
|
|
571
|
+
throw new Error(`The current date time (${nowDate.toISOString()}) is before ` +
|
|
572
|
+
`"validFrom" (${credential.validFrom}).`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
// check issuer cardinality
|
|
578
|
+
if (jsonld.getValues(credential, 'issuer').length > 1) {
|
|
579
|
+
throw new Error('"issuer" property can only have one value.');
|
|
580
|
+
}
|
|
581
|
+
// check issuer is a URL
|
|
582
|
+
if ('issuer' in credential) {
|
|
583
|
+
const issuer = _getId(credential.issuer);
|
|
584
|
+
if (!issuer) {
|
|
585
|
+
throw new Error(`"issuer" id is required.`);
|
|
586
|
+
}
|
|
587
|
+
_validateUriId({ id: issuer, propertyName: 'issuer' });
|
|
588
|
+
}
|
|
589
|
+
// check credentialStatus
|
|
590
|
+
jsonld.getValues(credential, 'credentialStatus').forEach((cs) => {
|
|
591
|
+
// check if optional "id" is a URL
|
|
592
|
+
if ('id' in cs) {
|
|
593
|
+
_validateUriId({ id: cs.id, propertyName: 'credentialStatus.id' });
|
|
594
|
+
}
|
|
595
|
+
// check "type" present
|
|
596
|
+
if (!cs.type) {
|
|
597
|
+
throw new Error('"credentialStatus" must include a type.');
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
// check evidences are URLs
|
|
601
|
+
jsonld.getValues(credential, 'evidence').forEach((evidence) => {
|
|
602
|
+
const evidenceId = _getId(evidence);
|
|
603
|
+
if (evidenceId) {
|
|
604
|
+
_validateUriId({ id: evidenceId, propertyName: 'evidence' });
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
// check if properties that require a type are
|
|
608
|
+
// defined, objects, and objects with types
|
|
609
|
+
for (const prop of mustHaveType) {
|
|
610
|
+
if (prop in credential) {
|
|
611
|
+
const value = credential[prop];
|
|
612
|
+
if (Array.isArray(value)) {
|
|
613
|
+
value.forEach(entry => _checkTypedObject(entry, prop));
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
_checkTypedObject(value, prop);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Checks that a property is a non-empty object with property type.
|
|
622
|
+
*
|
|
623
|
+
* @param obj - A potential object.
|
|
624
|
+
* @param name - The name of the property.
|
|
625
|
+
*
|
|
626
|
+
* @throws {Error} If the property is not an object with a type.
|
|
627
|
+
*/
|
|
628
|
+
function _checkTypedObject(obj, name) {
|
|
629
|
+
if (!isObject(obj)) {
|
|
630
|
+
throw new Error(`property "${name}" must be an object.`);
|
|
631
|
+
}
|
|
632
|
+
if (_emptyObject(obj)) {
|
|
633
|
+
throw new Error(`property "${name}" can not be an empty object.`);
|
|
634
|
+
}
|
|
635
|
+
if (!('type' in obj)) {
|
|
636
|
+
throw new Error(`property "${name}" must have property type.`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Takes in a credential and checks the credentialSubject(s).
|
|
641
|
+
*
|
|
642
|
+
* @param options - Options.
|
|
643
|
+
* @param options.credential - The credential to check.
|
|
644
|
+
*
|
|
645
|
+
* @throws {Error} Throws on errors in the credential subject.
|
|
646
|
+
*/
|
|
647
|
+
function _checkCredentialSubjects({ credential }) {
|
|
648
|
+
if (!credential?.credentialSubject) {
|
|
649
|
+
throw new Error('"credentialSubject" property is required.');
|
|
650
|
+
}
|
|
651
|
+
if (Array.isArray(credential.credentialSubject)) {
|
|
652
|
+
for (const subject of credential.credentialSubject) {
|
|
653
|
+
_checkCredentialSubject({ subject });
|
|
654
|
+
}
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
_checkCredentialSubject({ subject: credential.credentialSubject });
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Checks a credential subject is valid.
|
|
661
|
+
*
|
|
662
|
+
* @param options - Options.
|
|
663
|
+
* @param options.subject - A potential credential subject.
|
|
664
|
+
*
|
|
665
|
+
* @throws {Error} If the credentialSubject is not valid.
|
|
666
|
+
*/
|
|
667
|
+
function _checkCredentialSubject({ subject }) {
|
|
668
|
+
if (isObject(subject) === false) {
|
|
669
|
+
throw new Error('"credentialSubject" must be a non-null object.');
|
|
670
|
+
}
|
|
671
|
+
if (_emptyObject(subject)) {
|
|
672
|
+
throw new Error('"credentialSubject" must make a claim.');
|
|
673
|
+
}
|
|
674
|
+
// If credentialSubject.id is present and is not a URI, reject it
|
|
675
|
+
if (subject.id) {
|
|
676
|
+
_validateUriId({ id: subject.id, propertyName: 'credentialSubject.id' });
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Checks if parameter is an object.
|
|
681
|
+
*
|
|
682
|
+
* @param obj - A potential object.
|
|
683
|
+
*
|
|
684
|
+
* @returns False if not an object or null.
|
|
685
|
+
*/
|
|
686
|
+
function isObject(obj) {
|
|
687
|
+
// return false for null even though it has type object
|
|
688
|
+
if (obj === null) {
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
// if something has type object and is not null return true
|
|
692
|
+
if (typeof obj === 'object') {
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
// return false for strings, symbols, etc.
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Is it an empty object?
|
|
700
|
+
*
|
|
701
|
+
* @param obj - A potential object.
|
|
702
|
+
*
|
|
703
|
+
* @returns Is it empty?
|
|
704
|
+
*/
|
|
705
|
+
function _emptyObject(obj) {
|
|
706
|
+
// if the parameter is not an object return true
|
|
707
|
+
// as a non-object is an empty object
|
|
708
|
+
if (!isObject(obj)) {
|
|
709
|
+
return true;
|
|
710
|
+
}
|
|
711
|
+
return Object.keys(obj).length === 0;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Validates if an ID is a URL.
|
|
715
|
+
*
|
|
716
|
+
* @param options - Options.
|
|
717
|
+
* @param options.id - The id.
|
|
718
|
+
* @param options.propertyName - The property name.
|
|
719
|
+
*
|
|
720
|
+
* @throws {Error} Throws if an id is not a URL.
|
|
721
|
+
*/
|
|
722
|
+
function _validateUriId({ id, propertyName }) {
|
|
723
|
+
let parsed;
|
|
724
|
+
try {
|
|
725
|
+
parsed = new URL(id);
|
|
726
|
+
}
|
|
727
|
+
catch (cause) {
|
|
728
|
+
const error = new TypeError(`"${propertyName}" must be a URI: "${id}".`);
|
|
729
|
+
error.cause = cause;
|
|
730
|
+
throw error;
|
|
731
|
+
}
|
|
732
|
+
if (!parsed.protocol) {
|
|
733
|
+
throw new TypeError(`"${propertyName}" must be a URI: "${id}".`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
//# sourceMappingURL=index.js.map
|