@idos-network/credentials 1.1.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 +21 -0
- package/README.md +153 -0
- package/assets/country-codes.xml +271 -0
- package/assets/ed25519-signature-2020-v1.json +93 -0
- package/assets/idos-credential-subject-face-id-v1.json +8 -0
- package/assets/idos-credential-subject-v1.json +41 -0
- package/assets/idos-credential-subject-v2.json +37 -0
- package/assets/idos-credentials-v1.json +9 -0
- package/assets/v1.json +273 -0
- package/dist/builder/index.d.mts +20 -0
- package/dist/builder/index.d.mts.map +1 -0
- package/dist/builder/index.mjs +619 -0
- package/dist/builder/index.mjs.map +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1 -0
- package/dist/types/index.d.mts +135 -0
- package/dist/types/index.d.mts.map +1 -0
- package/dist/types/index.mjs +63 -0
- package/dist/types/index.mjs.map +1 -0
- package/dist/utils/index.d.mts +33 -0
- package/dist/utils/index.d.mts.map +1 -0
- package/dist/utils/index.mjs +124 -0
- package/dist/utils/index.mjs.map +1 -0
- package/package.json +73 -0
- package/src/builder/builder.test.ts +407 -0
- package/src/builder/index.ts +122 -0
- package/src/builder/loader.ts +76 -0
- package/src/builder/verifier.ts +55 -0
- package/src/index.ts +2 -0
- package/src/types/credentials.ts +282 -0
- package/src/types/index.ts +4 -0
- package/src/types/issuers.ts +21 -0
- package/src/utils/index.ts +277 -0
- package/src/utils/utils.test.ts +303 -0
- package/types/index.d.ts +4 -0
- package/types/internals.d.ts +124 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { Ed25519VerificationKey2020 } from "@digitalbazaar/ed25519-verification-key-2020";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { ZodError } from "zod";
|
|
5
|
+
|
|
6
|
+
import { buildCredential, buildFaceIdCredential } from ".";
|
|
7
|
+
import { fileToBase85 } from "../utils";
|
|
8
|
+
import { verifyCredential } from "./verifier";
|
|
9
|
+
|
|
10
|
+
describe("verifiableCredentials", () => {
|
|
11
|
+
describe("face-scan-id", () => {
|
|
12
|
+
it("should raise an error if the fields are invalid", async () => {
|
|
13
|
+
expect.assertions(4); // catch
|
|
14
|
+
|
|
15
|
+
const id = "z6MkszZtxCmA2Ce4vUV132PCuLQmwnaDD5mw2L23fGNnsiX3";
|
|
16
|
+
const issuer = "https://vc-issuers.cool.id/idos";
|
|
17
|
+
|
|
18
|
+
const validKey = await Ed25519VerificationKey2020.generate({
|
|
19
|
+
id: `${issuer}/keys/1`,
|
|
20
|
+
controller: `${issuer}/issuers/1`,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await buildFaceIdCredential(
|
|
25
|
+
{
|
|
26
|
+
id: `${issuer}/credentials/${id}`,
|
|
27
|
+
level: "human",
|
|
28
|
+
issued: new Date("2022-01-01"),
|
|
29
|
+
approvedAt: new Date("2022-01-01"),
|
|
30
|
+
expirationDate: new Date("2030-01-01"),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
// @ts-expect-error
|
|
34
|
+
id: `uuid:${id}`,
|
|
35
|
+
},
|
|
36
|
+
validKey,
|
|
37
|
+
);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
expect(error).toBeInstanceOf(ZodError);
|
|
40
|
+
const zodError = error as ZodError;
|
|
41
|
+
expect(zodError.issues).toHaveLength(1);
|
|
42
|
+
|
|
43
|
+
const faceSignUserId = zodError.issues.find((error) => error.path[0] === "faceSignUserId");
|
|
44
|
+
expect(faceSignUserId).toBeDefined();
|
|
45
|
+
expect(faceSignUserId?.message).toContain(
|
|
46
|
+
"Invalid input: expected string, received undefined",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should create and verify a verifiable vc", async () => {
|
|
52
|
+
const id = "z6MkszZtxCmA2Ce4vUV132PCuLQmwnaDD5mw2L23fGNnsiX3";
|
|
53
|
+
const issuer = "https://vc-issuers.cool.id/idos";
|
|
54
|
+
const issuer2 = "https://vc-issuers.cool.id/idos2";
|
|
55
|
+
|
|
56
|
+
const validKey = await Ed25519VerificationKey2020.generate({
|
|
57
|
+
id: `${issuer}/keys/1`,
|
|
58
|
+
controller: `${issuer}/issuers/1`,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const anotherKey = await Ed25519VerificationKey2020.generate({
|
|
62
|
+
id: `${issuer2}/keys/1`,
|
|
63
|
+
controller: `${issuer2}/issuers/1`,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const faceSignUserId = crypto.randomUUID();
|
|
67
|
+
|
|
68
|
+
const data = await buildFaceIdCredential(
|
|
69
|
+
{
|
|
70
|
+
id: `${issuer}/credentials/${id}`,
|
|
71
|
+
level: "human",
|
|
72
|
+
issued: new Date("2022-01-01"),
|
|
73
|
+
approvedAt: new Date("2022-01-01"),
|
|
74
|
+
expirationDate: new Date("2030-01-01"),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
faceSignUserId,
|
|
78
|
+
},
|
|
79
|
+
validKey,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Check if residential address is properly prefixed
|
|
83
|
+
expect(data.credentialSubject.faceSignUserId).toBe(faceSignUserId);
|
|
84
|
+
|
|
85
|
+
// Check if proof is properly constructed
|
|
86
|
+
expect(data.proof.proofPurpose).toBe("assertionMethod");
|
|
87
|
+
expect(data.proof.type).toBe("Ed25519Signature2020");
|
|
88
|
+
expect(data.proof.verificationMethod).toBe("https://vc-issuers.cool.id/idos/keys/1");
|
|
89
|
+
|
|
90
|
+
const allowedIssuers = [anotherKey, validKey];
|
|
91
|
+
|
|
92
|
+
const [verified, validResults] = await verifyCredential(data, allowedIssuers);
|
|
93
|
+
expect(verified).toBe(true);
|
|
94
|
+
|
|
95
|
+
const validResultsArray = validResults.values().toArray();
|
|
96
|
+
expect(validResultsArray).toHaveLength(2);
|
|
97
|
+
expect(validResultsArray[0].verified).toBe(false);
|
|
98
|
+
expect(validResultsArray[1].verified).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("tempered VCs will be rejected", async () => {
|
|
102
|
+
const id = "z6MkszZtxCmA2Ce4vUV132PCuLQmwnaDD5mw2L23fGNnsiX3";
|
|
103
|
+
const issuer = "https://vc-issuers.cool.id/idos";
|
|
104
|
+
|
|
105
|
+
const validKey = await Ed25519VerificationKey2020.generate({
|
|
106
|
+
id: `${issuer}/keys/1`,
|
|
107
|
+
controller: `${issuer}/issuers/1`,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const faceSignUserId = crypto.randomUUID();
|
|
111
|
+
const faceSignUserId2 = crypto.randomUUID();
|
|
112
|
+
|
|
113
|
+
const data = await buildFaceIdCredential(
|
|
114
|
+
{
|
|
115
|
+
id: `${issuer}/credentials/${id}`,
|
|
116
|
+
level: "human",
|
|
117
|
+
issued: new Date("2022-01-01"),
|
|
118
|
+
approvedAt: new Date("2022-01-01"),
|
|
119
|
+
expirationDate: new Date("2030-01-01"),
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
faceSignUserId,
|
|
123
|
+
},
|
|
124
|
+
validKey,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Check validity
|
|
128
|
+
const allowedIssuers = [validKey];
|
|
129
|
+
const [verified] = await verifyCredential(data, allowedIssuers);
|
|
130
|
+
expect(verified).toBe(true);
|
|
131
|
+
|
|
132
|
+
// Modify data
|
|
133
|
+
data.credentialSubject.faceSignUserId = faceSignUserId2;
|
|
134
|
+
const [verified2] = await verifyCredential(data, allowedIssuers);
|
|
135
|
+
expect(verified2).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("credentials", () => {
|
|
140
|
+
it("should raise an error if the fields are invalid", async () => {
|
|
141
|
+
expect.assertions(12); // catch
|
|
142
|
+
|
|
143
|
+
const id = "z6MkszZtxCmA2Ce4vUV132PCuLQmwnaDD5mw2L23fGNnsiX3";
|
|
144
|
+
const issuer = "https://vc-issuers.cool.id/idos";
|
|
145
|
+
|
|
146
|
+
const validKey = await Ed25519VerificationKey2020.generate({
|
|
147
|
+
id: `${issuer}/keys/1`,
|
|
148
|
+
controller: `${issuer}/issuers/1`,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
await buildCredential(
|
|
153
|
+
{
|
|
154
|
+
id: `${issuer}/credentials/${id}`,
|
|
155
|
+
level: "human",
|
|
156
|
+
issued: new Date("2022-01-01"),
|
|
157
|
+
approvedAt: new Date("2022-01-01"),
|
|
158
|
+
expirationDate: new Date("2030-01-01"),
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: `uuid:${id}`,
|
|
162
|
+
firstName: "John",
|
|
163
|
+
familyName: "Lennon",
|
|
164
|
+
// @ts-expect-error - This is a test, so the error is expected, since data is invalid
|
|
165
|
+
gender: "MALE",
|
|
166
|
+
email: "john.lennon@example.com",
|
|
167
|
+
phoneNumber: "+1234567890",
|
|
168
|
+
ssn: "203-456",
|
|
169
|
+
dateOfBirth: new Date("2025-02-30"),
|
|
170
|
+
placeOfBirth: "New York, NY",
|
|
171
|
+
idDocumentCountry: "DEU",
|
|
172
|
+
idDocumentNumber: "123456789",
|
|
173
|
+
idDocumentType: "PASSPORT",
|
|
174
|
+
idDocumentDateOfIssue: new Date("2022-01-01"),
|
|
175
|
+
idDocumentDateOfExpiry: new Date("2025-01-01"),
|
|
176
|
+
idDocumentFrontFile: Buffer.from("Front of ID document"),
|
|
177
|
+
residentialAddress: {
|
|
178
|
+
street: "Main St",
|
|
179
|
+
houseNumber: "123",
|
|
180
|
+
additionalAddressInfo: "Apt 1",
|
|
181
|
+
city: "New York",
|
|
182
|
+
postalCode: "10001",
|
|
183
|
+
country: "USA",
|
|
184
|
+
proofCategory: "Utility Bill",
|
|
185
|
+
proofDateOfIssue: new Date("2022-01-01"),
|
|
186
|
+
proofFile: Buffer.from("Proof of address"),
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
validKey,
|
|
190
|
+
);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
expect(error).toBeInstanceOf(ZodError);
|
|
193
|
+
const zodError = error as ZodError;
|
|
194
|
+
expect(zodError.issues).toHaveLength(5);
|
|
195
|
+
|
|
196
|
+
// idDocumentCountry
|
|
197
|
+
const idDocumentCountryError = zodError.issues.find(
|
|
198
|
+
(error) => error.path[0] === "idDocumentCountry",
|
|
199
|
+
);
|
|
200
|
+
expect(idDocumentCountryError).toBeDefined();
|
|
201
|
+
expect(idDocumentCountryError?.message).toContain(
|
|
202
|
+
"Too big: expected string to have <=2 characters",
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// selfieFile
|
|
206
|
+
const selfieFileError = zodError.issues.find((error) => error.path[0] === "selfieFile");
|
|
207
|
+
expect(selfieFileError).toBeDefined();
|
|
208
|
+
expect(selfieFileError?.message).toContain("expected Buffer, received undefined");
|
|
209
|
+
|
|
210
|
+
// residentialAddress country
|
|
211
|
+
const residentialAddressCountryError = zodError.issues.find(
|
|
212
|
+
(error) => error.path[0] === "residentialAddress",
|
|
213
|
+
);
|
|
214
|
+
expect(residentialAddressCountryError).toBeDefined();
|
|
215
|
+
expect(residentialAddressCountryError?.message).toContain(
|
|
216
|
+
"Too big: expected string to have <=2 characters",
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
// gender
|
|
220
|
+
const genderError = zodError.issues.find((error) => error.path[0] === "gender");
|
|
221
|
+
expect(genderError).toBeDefined();
|
|
222
|
+
expect(genderError?.message).toContain('Invalid option: expected one of "M"|"F"|"OTHER"');
|
|
223
|
+
|
|
224
|
+
// ssn
|
|
225
|
+
const ssnError = zodError.issues.find((error) => error.path[0] === "ssn");
|
|
226
|
+
expect(ssnError).toBeDefined();
|
|
227
|
+
expect(ssnError?.message).toContain("Too small: expected string to have >=9 characters");
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("test refinement (at least one of firstName or familyName must be provided)", async () => {
|
|
232
|
+
const id = "z6MkszZtxCmA2Ce4vUV132PCuLQmwnaDD5mw2L23fGNnsiX3";
|
|
233
|
+
const issuer = "https://vc-issuers.cool.id/idos";
|
|
234
|
+
|
|
235
|
+
const validKey = await Ed25519VerificationKey2020.generate({
|
|
236
|
+
id: `${issuer}/keys/1`,
|
|
237
|
+
controller: `${issuer}/issuers/1`,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect.assertions(3); // catch
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
await buildCredential(
|
|
244
|
+
{
|
|
245
|
+
id: `${issuer}/credentials/${id}`,
|
|
246
|
+
level: "human",
|
|
247
|
+
issued: new Date("2022-01-01"),
|
|
248
|
+
approvedAt: new Date("2022-01-01"),
|
|
249
|
+
expirationDate: new Date("2030-01-01"),
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: `uuid:${id}`,
|
|
253
|
+
ssn: "123456789",
|
|
254
|
+
firstName: "",
|
|
255
|
+
familyName: undefined,
|
|
256
|
+
gender: "M",
|
|
257
|
+
nationality: "US",
|
|
258
|
+
email: "john.lennon@example.com",
|
|
259
|
+
phoneNumber: "+1234567890",
|
|
260
|
+
dateOfBirth: new Date("1980-01-01"),
|
|
261
|
+
placeOfBirth: "New York, NY",
|
|
262
|
+
idDocumentCountry: "US",
|
|
263
|
+
idDocumentNumber: "123456789",
|
|
264
|
+
idDocumentType: "PASSPORT",
|
|
265
|
+
idDocumentDateOfIssue: new Date("2022-01-01"),
|
|
266
|
+
idDocumentDateOfExpiry: new Date("2025-01-01"),
|
|
267
|
+
idDocumentFrontFile: Buffer.from("Front of ID document"),
|
|
268
|
+
idDocumentBackFile: Buffer.from("Back of ID document"),
|
|
269
|
+
selfieFile: Buffer.from("Selfie"),
|
|
270
|
+
},
|
|
271
|
+
validKey,
|
|
272
|
+
);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
expect(error).toBeInstanceOf(ZodError);
|
|
275
|
+
const zodError = error as ZodError;
|
|
276
|
+
expect(zodError.issues).toHaveLength(1);
|
|
277
|
+
|
|
278
|
+
const refinementError = zodError.issues.find(
|
|
279
|
+
(error) => error.message === "At least one of firstName or familyName must be provided",
|
|
280
|
+
);
|
|
281
|
+
expect(refinementError).toBeDefined();
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("should create and verify a verifiable vc", async () => {
|
|
286
|
+
const id = "z6MkszZtxCmA2Ce4vUV132PCuLQmwnaDD5mw2L23fGNnsiX3";
|
|
287
|
+
const issuer = "https://vc-issuers.cool.id/idos";
|
|
288
|
+
const issuer2 = "https://vc-issuers.cool.id/idos2";
|
|
289
|
+
|
|
290
|
+
const validKey = await Ed25519VerificationKey2020.generate({
|
|
291
|
+
id: `${issuer}/keys/1`,
|
|
292
|
+
controller: `${issuer}/issuers/1`,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const anotherKey = await Ed25519VerificationKey2020.generate({
|
|
296
|
+
id: `${issuer2}/keys/1`,
|
|
297
|
+
controller: `${issuer2}/issuers/1`,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const data = await buildCredential(
|
|
301
|
+
{
|
|
302
|
+
id: `${issuer}/credentials/${id}`,
|
|
303
|
+
level: "human",
|
|
304
|
+
issued: new Date("2022-01-01"),
|
|
305
|
+
approvedAt: new Date("2022-01-01"),
|
|
306
|
+
expirationDate: new Date("2030-01-01"),
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
id: `uuid:${id}`,
|
|
310
|
+
firstName: "John",
|
|
311
|
+
middleName: "Paul",
|
|
312
|
+
familyName: "Lennon",
|
|
313
|
+
ssn: "123456789",
|
|
314
|
+
gender: "M",
|
|
315
|
+
nationality: "US",
|
|
316
|
+
email: "john.lennon@example.com",
|
|
317
|
+
phoneNumber: "+1234567890",
|
|
318
|
+
dateOfBirth: new Date("1980-01-01"),
|
|
319
|
+
placeOfBirth: "New York, NY",
|
|
320
|
+
idDocumentCountry: "US",
|
|
321
|
+
idDocumentNumber: "123456789",
|
|
322
|
+
idDocumentType: "PASSPORT",
|
|
323
|
+
idDocumentDateOfIssue: new Date("2022-01-01"),
|
|
324
|
+
idDocumentDateOfExpiry: new Date("2025-01-01"),
|
|
325
|
+
idDocumentFrontFile: Buffer.from("Front of ID document"),
|
|
326
|
+
idDocumentBackFile: Buffer.from("Back of ID document"),
|
|
327
|
+
selfieFile: Buffer.from("Selfie"),
|
|
328
|
+
residentialAddress: {
|
|
329
|
+
street: "Main St",
|
|
330
|
+
houseNumber: "123",
|
|
331
|
+
additionalAddressInfo: "Apt 1",
|
|
332
|
+
region: "NY",
|
|
333
|
+
city: "New York",
|
|
334
|
+
postalCode: "10001",
|
|
335
|
+
country: "US",
|
|
336
|
+
proofCategory: "Utility Bill",
|
|
337
|
+
proofDateOfIssue: new Date("2022-01-01"),
|
|
338
|
+
proofFile: Buffer.from("Proof of address"),
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
validKey,
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
// Check if residential address is properly prefixed
|
|
345
|
+
expect(data.credentialSubject.residentialAddressStreet).toBe("Main St");
|
|
346
|
+
expect(data.credentialSubject.residentialAddressHouseNumber).toBe("123");
|
|
347
|
+
expect(data.credentialSubject.residentialAddressAdditionalAddressInfo).toBe("Apt 1");
|
|
348
|
+
expect(data.credentialSubject.residentialAddressCity).toBe("New York");
|
|
349
|
+
expect(data.credentialSubject.residentialAddressPostalCode).toBe("10001");
|
|
350
|
+
expect(data.credentialSubject.residentialAddressCountry).toBe("US");
|
|
351
|
+
expect(data.credentialSubject.residentialAddressRegion).toBe("NY");
|
|
352
|
+
expect(data.credentialSubject.residentialAddressProofCategory).toBe("Utility Bill");
|
|
353
|
+
expect(data.credentialSubject.residentialAddressProofDateOfIssue).toEqual(
|
|
354
|
+
new Date("2022-01-01").toISOString(),
|
|
355
|
+
);
|
|
356
|
+
expect(data.credentialSubject.residentialAddressProofFile).toEqual(
|
|
357
|
+
fileToBase85(Buffer.from("Proof of address")),
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
expect(data.credentialSubject.email).toBe("john.lennon@example.com");
|
|
361
|
+
expect(data.credentialSubject.phoneNumber).toBe("+1234567890");
|
|
362
|
+
|
|
363
|
+
// Check if proof is properly constructed
|
|
364
|
+
expect(data.proof.proofPurpose).toBe("assertionMethod");
|
|
365
|
+
expect(data.proof.type).toBe("Ed25519Signature2020");
|
|
366
|
+
expect(data.proof.verificationMethod).toBe("https://vc-issuers.cool.id/idos/keys/1");
|
|
367
|
+
|
|
368
|
+
const allowedIssuers = [anotherKey, validKey];
|
|
369
|
+
|
|
370
|
+
const [verified, validResults] = await verifyCredential(data, allowedIssuers);
|
|
371
|
+
expect(verified).toBe(true);
|
|
372
|
+
|
|
373
|
+
const validResultsArray = validResults.values().toArray();
|
|
374
|
+
expect(validResultsArray).toHaveLength(2);
|
|
375
|
+
expect(validResultsArray[0].verified).toBe(false);
|
|
376
|
+
expect(validResultsArray[1].verified).toBe(true);
|
|
377
|
+
|
|
378
|
+
// Invalid issuer
|
|
379
|
+
const invalidIssuers = [
|
|
380
|
+
{
|
|
381
|
+
// Just the wrong issuer
|
|
382
|
+
issuer: issuer2,
|
|
383
|
+
publicKeyMultibase: anotherKey.publicKeyMultibase,
|
|
384
|
+
},
|
|
385
|
+
];
|
|
386
|
+
|
|
387
|
+
const [invalidVerified, invalidResults] = await verifyCredential(data, invalidIssuers);
|
|
388
|
+
expect(invalidVerified).toBe(false);
|
|
389
|
+
|
|
390
|
+
const invalidResultsArray = invalidResults.values().toArray();
|
|
391
|
+
expect(invalidResultsArray).toHaveLength(1);
|
|
392
|
+
expect(invalidResultsArray[0].verified).toBe(false);
|
|
393
|
+
expect(invalidResultsArray[0].error?.errors?.[0]?.message).toContain("not verify any proofs");
|
|
394
|
+
|
|
395
|
+
// Accepts issuer as issuer object
|
|
396
|
+
const validIssuer = [
|
|
397
|
+
{
|
|
398
|
+
issuer,
|
|
399
|
+
publicKeyMultibase: validKey.publicKeyMultibase,
|
|
400
|
+
},
|
|
401
|
+
];
|
|
402
|
+
|
|
403
|
+
const [validIssuerVerified] = await verifyCredential(data, validIssuer);
|
|
404
|
+
expect(validIssuerVerified).toBe(true);
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Ed25519Signature2020 } from "@digitalbazaar/ed25519-signature-2020";
|
|
2
|
+
import * as vc from "@digitalbazaar/vc";
|
|
3
|
+
|
|
4
|
+
import type { VerifyCredentialResult } from "./verifier";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type AvailableIssuerType,
|
|
8
|
+
type CredentialFields,
|
|
9
|
+
CredentialFieldsSchema,
|
|
10
|
+
type CredentialSubject,
|
|
11
|
+
type CredentialSubjectFaceId,
|
|
12
|
+
CredentialSubjectFaceIdSchema,
|
|
13
|
+
CredentialSubjectSchema,
|
|
14
|
+
type VerifiableCredential,
|
|
15
|
+
type VerifiableCredentialSubject,
|
|
16
|
+
} from "../types";
|
|
17
|
+
import { convertValues, issuerToKey } from "../utils";
|
|
18
|
+
import {
|
|
19
|
+
CONTEXT_IDOS_CREDENTIAL,
|
|
20
|
+
CONTEXT_IDOS_CREDENTIAL_FACE_ID,
|
|
21
|
+
CONTEXT_IDOS_CREDENTIAL_SUBJECT,
|
|
22
|
+
CONTEXT_IDOS_SIGNATURE,
|
|
23
|
+
CONTEXT_V1,
|
|
24
|
+
defaultDocumentLoader,
|
|
25
|
+
} from "./loader";
|
|
26
|
+
import { verifyCredential } from "./verifier";
|
|
27
|
+
|
|
28
|
+
export type Credential = VerifiableCredential<VerifiableCredentialSubject>;
|
|
29
|
+
export type FaceIdCredential = VerifiableCredential<CredentialSubjectFaceId>;
|
|
30
|
+
|
|
31
|
+
export type CredentialBuilder<TInput, TOutput = TInput> = (
|
|
32
|
+
fields: CredentialFields,
|
|
33
|
+
subject: TInput,
|
|
34
|
+
issuer: AvailableIssuerType,
|
|
35
|
+
validate?: boolean,
|
|
36
|
+
) => Promise<VerifiableCredential<TOutput>>;
|
|
37
|
+
|
|
38
|
+
export type CredentialConverter<TInput> = (
|
|
39
|
+
subject: TInput,
|
|
40
|
+
validate: boolean,
|
|
41
|
+
// oxlint-disable-next-line typescript/no-explicit-any -- any is needed here
|
|
42
|
+
) => Record<string, any>;
|
|
43
|
+
|
|
44
|
+
function genericCredentialBuilder<TInput, TOutput = TInput>(
|
|
45
|
+
credentialConverter: CredentialConverter<TInput>,
|
|
46
|
+
): CredentialBuilder<TInput, TOutput> {
|
|
47
|
+
async function builder(
|
|
48
|
+
fields: CredentialFields,
|
|
49
|
+
subject: TInput,
|
|
50
|
+
issuer: AvailableIssuerType,
|
|
51
|
+
validate = true,
|
|
52
|
+
) {
|
|
53
|
+
if (validate) {
|
|
54
|
+
// This raises an z.ZodError exception if the fields are invalid
|
|
55
|
+
CredentialFieldsSchema.parse(fields);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const credentialSubject = credentialConverter(subject, validate);
|
|
59
|
+
|
|
60
|
+
const key = await issuerToKey(issuer);
|
|
61
|
+
|
|
62
|
+
// Create credentials container
|
|
63
|
+
const credential = {
|
|
64
|
+
"@context": [CONTEXT_V1, CONTEXT_IDOS_CREDENTIAL, CONTEXT_IDOS_SIGNATURE],
|
|
65
|
+
type: ["VerifiableCredential"],
|
|
66
|
+
issuer: key.controller,
|
|
67
|
+
...convertValues(fields),
|
|
68
|
+
credentialSubject,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const suite = new Ed25519Signature2020({ key });
|
|
72
|
+
|
|
73
|
+
return vc.issue<TOutput>({
|
|
74
|
+
credential,
|
|
75
|
+
suite,
|
|
76
|
+
documentLoader: defaultDocumentLoader,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return builder;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const credentialSubjectConverter: CredentialConverter<CredentialSubject> = (
|
|
84
|
+
subject,
|
|
85
|
+
validate,
|
|
86
|
+
) => {
|
|
87
|
+
if (validate) {
|
|
88
|
+
CredentialSubjectSchema.parse(subject);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { residentialAddress, ...subjectData } = subject;
|
|
92
|
+
return {
|
|
93
|
+
"@context": CONTEXT_IDOS_CREDENTIAL_SUBJECT,
|
|
94
|
+
...convertValues(subjectData),
|
|
95
|
+
...(residentialAddress ? convertValues(residentialAddress, "residentialAddress") : {}),
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const credentialFaceIdSubjectConverter: CredentialConverter<CredentialSubjectFaceId> = (
|
|
100
|
+
subject,
|
|
101
|
+
validate,
|
|
102
|
+
) => {
|
|
103
|
+
if (validate) {
|
|
104
|
+
CredentialSubjectFaceIdSchema.parse(subject);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
"@context": CONTEXT_IDOS_CREDENTIAL_FACE_ID,
|
|
109
|
+
...convertValues(subject),
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const buildCredential: CredentialBuilder<CredentialSubject, VerifiableCredentialSubject> =
|
|
114
|
+
genericCredentialBuilder<CredentialSubject, VerifiableCredentialSubject>(
|
|
115
|
+
credentialSubjectConverter,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
export const buildFaceIdCredential: CredentialBuilder<CredentialSubjectFaceId> =
|
|
119
|
+
genericCredentialBuilder<CredentialSubjectFaceId>(credentialFaceIdSubjectConverter);
|
|
120
|
+
|
|
121
|
+
export type { VerifyCredentialResult };
|
|
122
|
+
export { verifyCredential };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type JsonLDDocumentLoaderInstance, JsonLdDocumentLoader } from "jsonld-document-loader";
|
|
2
|
+
|
|
3
|
+
import ed25519Signature2020V1 from "../../assets/ed25519-signature-2020-v1.json";
|
|
4
|
+
import idosCredentialSubjectFaceIdV1 from "../../assets/idos-credential-subject-face-id-v1.json";
|
|
5
|
+
import idosCredentialSubjectV1 from "../../assets/idos-credential-subject-v1.json";
|
|
6
|
+
import idosCredentialSubjectV2 from "../../assets/idos-credential-subject-v2.json";
|
|
7
|
+
import idosCredentialsV1 from "../../assets/idos-credentials-v1.json";
|
|
8
|
+
import v1 from "../../assets/v1.json";
|
|
9
|
+
|
|
10
|
+
export const CONTEXT_V1 = "https://www.w3.org/2018/credentials/v1";
|
|
11
|
+
export const CONTEXT_ED25519_SIGNATURE_2020_V1 = "https://w3id.org/security/suites/ed25519-2020/v1";
|
|
12
|
+
|
|
13
|
+
// Check .github/workflow/pages.yml how those are deployed
|
|
14
|
+
export const CONTEXT_IDOS_CREDENTIAL_V1 =
|
|
15
|
+
"https://idos-network.github.io/idos-sdk-js/credentials/idos-credentials-v1.json";
|
|
16
|
+
export const CONTEXT_IDOS_CREDENTIAL_V1_SUBJECT =
|
|
17
|
+
"https://idos-network.github.io/idos-sdk-js/credentials/idos-credential-subject-v1.json";
|
|
18
|
+
export const CONTEXT_IDOS_CREDENTIAL_V2_SUBJECT =
|
|
19
|
+
"https://idos-network.github.io/idos-sdk-js/credentials/idos-credential-subject-v2.json";
|
|
20
|
+
|
|
21
|
+
export const CONTEXT_IDOS_CREDENTIAL_V1_FACE_ID =
|
|
22
|
+
"https://idos-network.github.io/idos-sdk-js/credentials/idos-credential-subject-face-id-v1.json";
|
|
23
|
+
|
|
24
|
+
// Latest contexts for builder
|
|
25
|
+
export const CONTEXT_IDOS_CREDENTIAL: string = CONTEXT_IDOS_CREDENTIAL_V1;
|
|
26
|
+
export const CONTEXT_IDOS_CREDENTIAL_SUBJECT: string = CONTEXT_IDOS_CREDENTIAL_V2_SUBJECT;
|
|
27
|
+
export const CONTEXT_IDOS_SIGNATURE: string = CONTEXT_ED25519_SIGNATURE_2020_V1;
|
|
28
|
+
export const CONTEXT_IDOS_CREDENTIAL_FACE_ID: string = CONTEXT_IDOS_CREDENTIAL_V1_FACE_ID;
|
|
29
|
+
|
|
30
|
+
export function buildDocumentLoader(): JsonLDDocumentLoaderInstance {
|
|
31
|
+
const loader = new JsonLdDocumentLoader();
|
|
32
|
+
|
|
33
|
+
loader.addStatic(CONTEXT_V1, v1);
|
|
34
|
+
loader.addStatic(CONTEXT_IDOS_CREDENTIAL_V1, idosCredentialsV1);
|
|
35
|
+
loader.addStatic(CONTEXT_ED25519_SIGNATURE_2020_V1, ed25519Signature2020V1);
|
|
36
|
+
|
|
37
|
+
// plus / basic + liveness credentials
|
|
38
|
+
loader.addStatic(CONTEXT_IDOS_CREDENTIAL_V1_SUBJECT, idosCredentialSubjectV1);
|
|
39
|
+
loader.addStatic(CONTEXT_IDOS_CREDENTIAL_V2_SUBJECT, idosCredentialSubjectV2);
|
|
40
|
+
|
|
41
|
+
// PoP
|
|
42
|
+
loader.addStatic(CONTEXT_IDOS_CREDENTIAL_V1_FACE_ID, idosCredentialSubjectFaceIdV1);
|
|
43
|
+
|
|
44
|
+
// DEPRECATED: Those should be removed
|
|
45
|
+
[
|
|
46
|
+
"1356fea6672d22deb07fedc5fa427478b2e1654c",
|
|
47
|
+
"346f14468348e4f3dd00039c89ce9bb49d88777c",
|
|
48
|
+
"1e8421744e037dfd7b0289cb39261c73ab6d643c",
|
|
49
|
+
"0049d2e93f8913c42398372e2bb65d92bed38ac0",
|
|
50
|
+
].forEach((hash) => {
|
|
51
|
+
loader.addStatic(
|
|
52
|
+
`https://raw.githubusercontent.com/idos-network/idos-sdk-js/${hash}/packages/%40credentials/assets/idos-credentials-v1.json`,
|
|
53
|
+
idosCredentialsV1,
|
|
54
|
+
);
|
|
55
|
+
loader.addStatic(
|
|
56
|
+
`https://raw.githubusercontent.com/idos-network/idos-sdk-js/${hash}/packages/%40credentials/assets/idos-credential-subject-v1.json`,
|
|
57
|
+
idosCredentialSubjectV1,
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// DEPRECATED: Those should be removed
|
|
62
|
+
["1bc3503f5302a7e42777076445d5b05fec8db429"].forEach((hash) => {
|
|
63
|
+
loader.addStatic(
|
|
64
|
+
`https://raw.githubusercontent.com/idos-network/idos-sdk-js/${hash}/packages/issuer-sdk-js/assets/idos-credentials-v1.json`,
|
|
65
|
+
idosCredentialsV1,
|
|
66
|
+
);
|
|
67
|
+
loader.addStatic(
|
|
68
|
+
`https://raw.githubusercontent.com/idos-network/idos-sdk-js/${hash}/packages/issuer-sdk-js/assets/idos-credential-subject-v1.json`,
|
|
69
|
+
idosCredentialSubjectV1,
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return loader.build();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const defaultDocumentLoader: JsonLDDocumentLoaderInstance = buildDocumentLoader();
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { JsonLDDocumentLoaderInstance } from "jsonld-document-loader";
|
|
2
|
+
|
|
3
|
+
import { Ed25519Signature2020 } from "@digitalbazaar/ed25519-signature-2020";
|
|
4
|
+
import * as vc from "@digitalbazaar/vc";
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
AvailableIssuerType,
|
|
8
|
+
VerifiableCredential,
|
|
9
|
+
VerifiableCredentialSubject,
|
|
10
|
+
} from "../types";
|
|
11
|
+
|
|
12
|
+
import { issuerToKey } from "../utils";
|
|
13
|
+
import { defaultDocumentLoader } from "./loader";
|
|
14
|
+
|
|
15
|
+
export type VerifyCredentialResult = [boolean, Map<AvailableIssuerType, vc.VerifyCredentialResult>];
|
|
16
|
+
|
|
17
|
+
export async function verifyCredential<K = VerifiableCredentialSubject>(
|
|
18
|
+
credential: VerifiableCredential<K>,
|
|
19
|
+
issuers: AvailableIssuerType[],
|
|
20
|
+
customDocumentLoader?: JsonLDDocumentLoaderInstance,
|
|
21
|
+
): Promise<VerifyCredentialResult> {
|
|
22
|
+
const resultsByIssuer: Map<AvailableIssuerType, vc.VerifyCredentialResult> = new Map();
|
|
23
|
+
|
|
24
|
+
for (const issuer of issuers) {
|
|
25
|
+
const publicKey = await issuerToKey(issuer);
|
|
26
|
+
|
|
27
|
+
const vcVerifyingSuite = new Ed25519Signature2020({
|
|
28
|
+
key: publicKey,
|
|
29
|
+
verificationMethod: publicKey.id,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const controller = {
|
|
33
|
+
"@context": "https://w3id.org/security/v2",
|
|
34
|
+
id: publicKey.controller,
|
|
35
|
+
assertionMethod: [publicKey.id],
|
|
36
|
+
authentication: [publicKey.id],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Verify the signature
|
|
40
|
+
const verifyCredentialResult = await vc.verifyCredential<K>({
|
|
41
|
+
credential: credential,
|
|
42
|
+
suite: vcVerifyingSuite,
|
|
43
|
+
controller,
|
|
44
|
+
documentLoader: customDocumentLoader ?? defaultDocumentLoader,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
resultsByIssuer.set(issuer, verifyCredentialResult);
|
|
48
|
+
|
|
49
|
+
if (verifyCredentialResult.verified) {
|
|
50
|
+
return [true, resultsByIssuer];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return [false, resultsByIssuer];
|
|
55
|
+
}
|
package/src/index.ts
ADDED