@idos-network/credentials 1.5.0 → 2.2.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.
Files changed (82) hide show
  1. package/README.md +289 -85
  2. package/assets/idos-credential-subject-edd-v1.json +10 -0
  3. package/assets/idos-credential-subject-v1.json +1 -1
  4. package/assets/idos-credential-subject-v2.json +1 -1
  5. package/assets/idos-credential-subject-v3.json +83 -0
  6. package/assets/idos-credentials-v1.json +2 -2
  7. package/assets/idos-credentials-v2.json +10 -0
  8. package/dist/index-0NqXMjNk.d.mts +395 -0
  9. package/dist/index-0NqXMjNk.d.mts.map +1 -0
  10. package/dist/index-WW372Tf5.d.mts +57 -0
  11. package/dist/index-WW372Tf5.d.mts.map +1 -0
  12. package/dist/index.mjs +1 -1
  13. package/dist/loader-Cg6hs1-P.mjs +2 -0
  14. package/dist/loader-Cg6hs1-P.mjs.map +1 -0
  15. package/dist/parser/index.d.mts +19 -0
  16. package/dist/parser/index.d.mts.map +1 -0
  17. package/dist/parser/index.mjs +2 -0
  18. package/dist/parser/index.mjs.map +1 -0
  19. package/dist/schemas/index.d.mts +2 -0
  20. package/dist/schemas/index.mjs +1 -0
  21. package/dist/schemas-BVGuUlPe.mjs +2 -0
  22. package/dist/schemas-BVGuUlPe.mjs.map +1 -0
  23. package/dist/types/index.d.mts +2 -134
  24. package/dist/types/index.mjs +1 -63
  25. package/dist/utils/index.d.mts +37 -15
  26. package/dist/utils/index.d.mts.map +1 -1
  27. package/dist/utils/index.mjs +1 -116
  28. package/dist/utils/index.mjs.map +1 -1
  29. package/dist/verifier/index.d.mts +8 -0
  30. package/dist/verifier/index.d.mts.map +1 -0
  31. package/dist/verifier/index.mjs +2 -0
  32. package/dist/verifier/index.mjs.map +1 -0
  33. package/package.json +21 -9
  34. package/src/parser/index.test.ts +266 -0
  35. package/src/parser/index.ts +69 -0
  36. package/src/schemas/EddV1/index.test.ts +40 -0
  37. package/src/schemas/EddV1/index.ts +53 -0
  38. package/src/schemas/Envelope.ts +37 -0
  39. package/src/schemas/FaceIdV1/index.test.ts +26 -0
  40. package/src/schemas/FaceIdV1/index.ts +33 -0
  41. package/src/schemas/KycV1/IdDocument.ts +41 -0
  42. package/src/schemas/KycV1/ResidentialAddress.ts +56 -0
  43. package/src/schemas/KycV1/Subject.ts +86 -0
  44. package/src/schemas/KycV1/index.test.ts +156 -0
  45. package/src/schemas/KycV1/index.ts +40 -0
  46. package/src/schemas/KycV2/Subject.ts +67 -0
  47. package/src/schemas/KycV2/index.test.ts +162 -0
  48. package/src/schemas/KycV2/index.ts +45 -0
  49. package/src/schemas/KycV3/Biometric.ts +14 -0
  50. package/src/schemas/KycV3/Contact.ts +13 -0
  51. package/src/schemas/KycV3/EDD.ts +32 -0
  52. package/src/schemas/KycV3/IdDocument.ts +92 -0
  53. package/src/schemas/KycV3/Onboarding.ts +36 -0
  54. package/src/schemas/KycV3/Person.ts +105 -0
  55. package/src/schemas/KycV3/ResidentialAddress.ts +73 -0
  56. package/src/schemas/KycV3/Screening.ts +27 -0
  57. package/src/schemas/KycV3/SourceOfWealth.ts +44 -0
  58. package/src/schemas/KycV3/Subject.ts +54 -0
  59. package/src/schemas/KycV3/index.test.ts +682 -0
  60. package/src/schemas/KycV3/index.ts +108 -0
  61. package/src/schemas/VerifiableCredential.ts +232 -0
  62. package/src/schemas/enums.ts +126 -0
  63. package/src/schemas/index.ts +14 -0
  64. package/src/schemas/integration.test.ts +251 -0
  65. package/src/schemas/types.ts +11 -0
  66. package/src/schemas/utils.ts +274 -0
  67. package/src/types/credentials.ts +18 -249
  68. package/src/utils/{utils.test.ts → index.test.ts} +59 -67
  69. package/src/utils/index.ts +129 -134
  70. package/src/utils/issuer.ts +42 -0
  71. package/src/{builder → utils}/loader.ts +22 -17
  72. package/src/verifier/index.test.ts +149 -0
  73. package/src/{builder/verifier.ts → verifier/index.ts} +4 -8
  74. package/types/internals.d.ts +11 -6
  75. package/dist/builder/index.d.mts +0 -19
  76. package/dist/builder/index.d.mts.map +0 -1
  77. package/dist/builder/index.mjs +0 -619
  78. package/dist/builder/index.mjs.map +0 -1
  79. package/dist/types/index.d.mts.map +0 -1
  80. package/dist/types/index.mjs.map +0 -1
  81. package/src/builder/builder.test.ts +0 -407
  82. package/src/builder/index.ts +0 -122
@@ -1,122 +1,23 @@
1
- import { Ed25519VerificationKey2020 } from "@digitalbazaar/ed25519-verification-key-2020";
2
- import {
3
- base64Decode,
4
- base64Encode,
5
- hexEncode,
6
- utf8Encode,
7
- fileToBase85,
8
- } from "@idos-network/utils/codecs";
1
+ import { createBlobContentReference } from "@idos-network/utils/blob-gateway";
2
+ import { base64Encode, hexEncode, utf8Encode } from "@idos-network/utils/codecs";
3
+ import { encryptContent } from "@idos-network/utils/cryptography";
9
4
  import { every, get } from "es-toolkit/compat";
10
- import invariant from "tiny-invariant";
11
5
  import nacl from "tweetnacl";
12
6
 
13
- import type {
14
- AvailableIssuerType,
15
- CredentialFields,
16
- CredentialResidentialAddress,
17
- CredentialSubject,
18
- CredentialSubjectFaceId,
19
- CustomIssuerType,
20
- InsertableIDOSCredential,
21
- } from "../types";
22
-
23
- export function capitalizeFirstLetter(str: string): string {
24
- return str[0].toUpperCase() + str.slice(1);
25
- }
26
-
27
- function isIssuerKey(issuer: AvailableIssuerType): issuer is Ed25519VerificationKey2020 {
28
- return (
29
- typeof issuer === "object" &&
30
- issuer !== null &&
31
- "type" in issuer &&
32
- "id" in issuer &&
33
- "controller" in issuer
34
- );
35
- }
36
-
37
- function isCustomIssuerType(issuer: AvailableIssuerType): issuer is CustomIssuerType {
38
- return (
39
- typeof issuer === "object" &&
40
- issuer !== null &&
41
- "issuer" in issuer &&
42
- "publicKeyMultibase" in issuer
43
- );
44
- }
45
-
46
- export async function issuerToKey(
47
- issuer: AvailableIssuerType,
48
- ): Promise<Ed25519VerificationKey2020> {
49
- if (isIssuerKey(issuer)) {
50
- return issuer;
51
- }
52
-
53
- if (isCustomIssuerType(issuer)) {
54
- return await Ed25519VerificationKey2020.from({
55
- id: `${issuer.issuer}/keys/1`,
56
- controller: `${issuer.issuer}/issuers/1`,
57
- publicKeyMultibase: issuer.publicKeyMultibase,
58
- privateKeyMultibase: issuer.privateKeyMultibase,
59
- type: "Ed25519VerificationKey2020",
60
- });
61
- }
62
-
63
- return await Ed25519VerificationKey2020.from({ ...issuer, type: "Ed25519VerificationKey2020" });
64
- }
7
+ import type { idOSCredential, SignedCredentialContentReference } from "../types";
65
8
 
66
- export function convertValues<
67
- K extends
68
- | CredentialFields
69
- | CredentialSubject
70
- | CredentialResidentialAddress
71
- | CredentialSubjectFaceId,
72
- >(fields: K, prefix?: string): Record<string, unknown> {
73
- const acc: Record<string, unknown> = {};
74
-
75
- for (const key in fields) {
76
- if (Object.hasOwn(fields, key)) {
77
- const value = fields[key];
78
- const name = prefix ? `${prefix}${capitalizeFirstLetter(key)}` : key;
79
- if (value instanceof Date) {
80
- acc[name] = value.toISOString();
81
- } else if (value instanceof Buffer) {
82
- // Convert file to base85
83
- acc[name] = fileToBase85(value);
84
- } else {
85
- acc[name] = value;
86
- }
87
- }
88
- }
89
-
90
- return acc;
91
- }
92
-
93
- type BaseLevel = "basic" | "plus";
94
- type Addon = "liveness" | "email" | "phoneNumber";
95
-
96
- export function deriveLevel(credential: CredentialSubject): string {
97
- let level: BaseLevel = "basic";
98
-
99
- // Address is a sign for plus+
100
- const address = credential.residentialAddress;
101
- if (address?.proofFile && address?.city && address?.proofCategory) {
102
- level = "plus";
103
- }
9
+ // Proxying functions
10
+ export * from "./issuer";
104
11
 
105
- const addons: Addon[] = [];
106
- if (credential.selfieFile) {
107
- addons.push("liveness");
108
- }
109
-
110
- if (credential.email) {
111
- addons.push("email");
112
- }
113
-
114
- if (credential.phoneNumber) {
115
- addons.push("phoneNumber");
116
- }
117
-
118
- return [level, ...addons].join("+");
119
- }
12
+ export type BaseLevel = "unverified" | "basic" | "plus";
13
+ export type Addon =
14
+ | "liveness"
15
+ | "email"
16
+ | "phoneNumber"
17
+ | "edd"
18
+ | "sow"
19
+ | "screening"
20
+ | "onboarding";
120
21
 
121
22
  export function parseLevel(level: string): {
122
23
  base: BaseLevel;
@@ -126,6 +27,18 @@ export function parseLevel(level: string): {
126
27
  return { base, addons };
127
28
  }
128
29
 
30
+ export function assertNoExtraFields(
31
+ section: string,
32
+ expectedFields: ReadonlySet<string>,
33
+ value: object | undefined,
34
+ ): void {
35
+ const extraFields = Object.keys(value ?? {}).filter((field) => !expectedFields.has(field));
36
+
37
+ if (extraFields.length > 0) {
38
+ throw new Error(`Unexpected ${section} fields: ${extraFields.join(", ")}`);
39
+ }
40
+ }
41
+
129
42
  export function matchLevelOrHigher(
130
43
  level: BaseLevel,
131
44
  requiredAddons: Addon[],
@@ -133,7 +46,12 @@ export function matchLevelOrHigher(
133
46
  ): boolean {
134
47
  const { base: currentBaseLevel, addons: currentAddons } = parseLevel(currentLevel);
135
48
 
136
- // TODO: Consider pop+ or uniqueness+ scenarios
49
+ // Basic can only match basic or plus not unverified
50
+ if (level === "basic" && currentBaseLevel === "unverified") {
51
+ return false;
52
+ }
53
+
54
+ // Plus can only match plus
137
55
  if (level === "plus" && currentBaseLevel !== "plus") {
138
56
  return false;
139
57
  }
@@ -145,6 +63,10 @@ export function levelScore(level: string): number {
145
63
  const { base, addons } = parseLevel(level);
146
64
  let score = 0;
147
65
 
66
+ if (base === "basic") {
67
+ score += 50;
68
+ }
69
+
148
70
  if (base === "plus") {
149
71
  score += 100;
150
72
  }
@@ -239,36 +161,109 @@ export function recordFilter(
239
161
  return true;
240
162
  }
241
163
 
242
- export function buildInsertableIDOSCredential(
243
- userId: string,
164
+ export function buildSignedCredentialContentReference(
244
165
  publicNotes: string,
245
- content: string,
246
- encryptorPublicKey: string,
247
- ): InsertableIDOSCredential {
248
- invariant(encryptorPublicKey, "Missing `encryptorPublicKey`");
249
-
250
- const ephemeralAuthenticationKeyPair = nacl.sign.keyPair();
166
+ contentUri: string,
167
+ issuerSigningSecretKey: Uint8Array,
168
+ ): SignedCredentialContentReference {
169
+ const { publicKey, secretKey } = nacl.sign.keyPair.fromSecretKey(issuerSigningSecretKey);
251
170
 
252
- const publicNotesSignature = nacl.sign.detached(
253
- utf8Encode(publicNotes),
254
- ephemeralAuthenticationKeyPair.secretKey,
255
- );
171
+ const publicNotesSignature = nacl.sign.detached(utf8Encode(publicNotes), secretKey);
256
172
 
257
173
  return {
258
- user_id: userId,
259
- content,
260
-
261
174
  public_notes: publicNotes,
262
175
  public_notes_signature: base64Encode(publicNotesSignature),
263
176
 
264
177
  broader_signature: base64Encode(
265
178
  nacl.sign.detached(
266
- Uint8Array.from([...publicNotesSignature, ...base64Decode(content)]),
267
- ephemeralAuthenticationKeyPair.secretKey,
179
+ Uint8Array.from([...publicNotesSignature, ...utf8Encode(contentUri)]),
180
+ secretKey,
268
181
  ),
269
182
  ),
270
183
 
271
- issuer_auth_public_key: hexEncode(ephemeralAuthenticationKeyPair.publicKey, true),
272
- encryptor_public_key: encryptorPublicKey,
184
+ issuer_auth_public_key: hexEncode(publicKey, true),
185
+ };
186
+ }
187
+
188
+ /** User-issued share copy: sign with a fresh ephemeral key (need not match original issuer). */
189
+ export function buildEphemeralSignedCredentialContentReference(
190
+ publicNotes: string,
191
+ contentUri: string,
192
+ ): SignedCredentialContentReference {
193
+ return buildSignedCredentialContentReference(
194
+ publicNotes,
195
+ contentUri,
196
+ nacl.sign.keyPair().secretKey,
197
+ );
198
+ }
199
+
200
+ export type PreliminaryIDOSCredential = {
201
+ id: string;
202
+ contentUri: string;
203
+ contentSize: number;
204
+ encryptedContent: Uint8Array;
205
+ publicNotes: string;
206
+ publicNotesSignature: string;
207
+ broaderSignature: string;
208
+ issuerAuthPublicKey: string;
209
+ encryptorPublicKey: string;
210
+ };
211
+
212
+ export function mapPreliminaryToIDOSCredential(
213
+ preliminaryCredential: PreliminaryIDOSCredential,
214
+ ): Omit<idOSCredential, "user_id"> {
215
+ return {
216
+ id: preliminaryCredential.id,
217
+ content_uri: preliminaryCredential.contentUri,
218
+ content_size: preliminaryCredential.contentSize,
219
+ public_notes: preliminaryCredential.publicNotes,
220
+ issuer_auth_public_key: preliminaryCredential.issuerAuthPublicKey,
221
+ encryptor_public_key: preliminaryCredential.encryptorPublicKey,
222
+ };
223
+ }
224
+
225
+ export interface BuildPreliminaryIDOSCredentialArgs {
226
+ publicNotes: string;
227
+ plaintextContent: Uint8Array;
228
+ recipientEncryptionPublicKey: Uint8Array;
229
+ issuerSigningSecretKey?: Uint8Array;
230
+ /** When set (MM / `ukyc://`), skip IPFS CID hashing. */
231
+ contentUri?: string;
232
+ }
233
+
234
+ export async function buildPreliminaryIDOSCredential({
235
+ publicNotes,
236
+ plaintextContent,
237
+ recipientEncryptionPublicKey,
238
+ // For user-issued credentials, use a fresh ephemeral key for the signed reference
239
+ // For issuer-side, use the signing key pair
240
+ issuerSigningSecretKey = nacl.sign.keyPair().secretKey,
241
+ contentUri,
242
+ }: BuildPreliminaryIDOSCredentialArgs): Promise<Omit<PreliminaryIDOSCredential, "id">> {
243
+ const ephemeralKeyPair = nacl.box.keyPair();
244
+ const encryptedContent = encryptContent(
245
+ plaintextContent,
246
+ recipientEncryptionPublicKey, // user or dwg recipient
247
+ ephemeralKeyPair.secretKey,
248
+ );
249
+
250
+ const contentReference = contentUri
251
+ ? { uri: contentUri, size: encryptedContent.byteLength }
252
+ : await createBlobContentReference(encryptedContent);
253
+ const signedReference = buildSignedCredentialContentReference(
254
+ publicNotes,
255
+ contentReference.uri,
256
+ issuerSigningSecretKey,
257
+ );
258
+
259
+ return {
260
+ contentUri: contentReference.uri,
261
+ contentSize: contentReference.size,
262
+ encryptedContent,
263
+ publicNotes,
264
+ publicNotesSignature: signedReference.public_notes_signature,
265
+ broaderSignature: signedReference.broader_signature,
266
+ issuerAuthPublicKey: signedReference.issuer_auth_public_key,
267
+ encryptorPublicKey: base64Encode(ephemeralKeyPair.publicKey),
273
268
  };
274
269
  }
@@ -0,0 +1,42 @@
1
+ import { Ed25519VerificationKey2020 } from "@digitalbazaar/ed25519-verification-key-2020";
2
+
3
+ import type { AvailableIssuerType, CustomIssuerType } from "../types";
4
+
5
+ function isIssuerKey(issuer: AvailableIssuerType): issuer is Ed25519VerificationKey2020 {
6
+ return (
7
+ typeof issuer === "object" &&
8
+ issuer !== null &&
9
+ "type" in issuer &&
10
+ "id" in issuer &&
11
+ "controller" in issuer
12
+ );
13
+ }
14
+
15
+ function isCustomIssuerType(issuer: AvailableIssuerType): issuer is CustomIssuerType {
16
+ return (
17
+ typeof issuer === "object" &&
18
+ issuer !== null &&
19
+ "issuer" in issuer &&
20
+ "publicKeyMultibase" in issuer
21
+ );
22
+ }
23
+
24
+ export async function issuerToKey(
25
+ issuer: AvailableIssuerType,
26
+ ): Promise<Ed25519VerificationKey2020> {
27
+ if (isIssuerKey(issuer)) {
28
+ return issuer;
29
+ }
30
+
31
+ if (isCustomIssuerType(issuer)) {
32
+ return await Ed25519VerificationKey2020.from({
33
+ id: `${issuer.issuer}/keys/1`,
34
+ controller: `${issuer.issuer}/issuers/1`,
35
+ publicKeyMultibase: issuer.publicKeyMultibase,
36
+ privateKeyMultibase: issuer.privateKeyMultibase,
37
+ type: "Ed25519VerificationKey2020",
38
+ });
39
+ }
40
+
41
+ return await Ed25519VerificationKey2020.from({ ...issuer, type: "Ed25519VerificationKey2020" });
42
+ }
@@ -1,46 +1,51 @@
1
1
  import { type JsonLDDocumentLoaderInstance, JsonLdDocumentLoader } from "jsonld-document-loader";
2
2
 
3
3
  import ed25519Signature2020V1 from "../../assets/ed25519-signature-2020-v1.json";
4
+ import idosCredentialSubjectEddV1 from "../../assets/idos-credential-subject-edd-v1.json";
5
+ // Maybe we can generate those imports alongside the JSON-LD
4
6
  import idosCredentialSubjectFaceIdV1 from "../../assets/idos-credential-subject-face-id-v1.json";
5
7
  import idosCredentialSubjectV1 from "../../assets/idos-credential-subject-v1.json";
6
8
  import idosCredentialSubjectV2 from "../../assets/idos-credential-subject-v2.json";
9
+ import idosCredentialSubjectV3 from "../../assets/idos-credential-subject-v3.json";
7
10
  import idosCredentialsV1 from "../../assets/idos-credentials-v1.json";
11
+ import idosCredentialsV2 from "../../assets/idos-credentials-v2.json";
8
12
  import v1 from "../../assets/v1.json";
9
13
 
10
14
  export const CONTEXT_V1 = "https://www.w3.org/2018/credentials/v1";
11
15
  export const CONTEXT_ED25519_SIGNATURE_2020_V1 = "https://w3id.org/security/suites/ed25519-2020/v1";
12
16
 
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";
17
+ export const BASE_PUBLIC_URL = "https://idos-network.github.io/idos-sdk-js/credentials/";
23
18
 
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;
19
+ // Check .github/workflow/pages.yml how those are deployed
20
+ export const CONTEXT_IDOS_CREDENTIAL_V1: string = `${BASE_PUBLIC_URL}idos-credentials-v1.json`;
21
+ export const CONTEXT_IDOS_CREDENTIAL_V2: string = `${BASE_PUBLIC_URL}idos-credentials-v2.json`;
22
+ export const CONTEXT_IDOS_CREDENTIAL_V1_SUBJECT: string = `${BASE_PUBLIC_URL}idos-credential-subject-v1.json`;
23
+ export const CONTEXT_IDOS_CREDENTIAL_V2_SUBJECT: string = `${BASE_PUBLIC_URL}idos-credential-subject-v2.json`;
24
+ export const CONTEXT_IDOS_CREDENTIAL_V3_SUBJECT: string = `${BASE_PUBLIC_URL}idos-credential-subject-v3.json`;
25
+ export const CONTEXT_IDOS_CREDENTIAL_V1_FACE_ID: string = `${BASE_PUBLIC_URL}idos-credential-subject-face-id-v1.json`;
26
+ export const CONTEXT_IDOS_CREDENTIAL_V1_EDD: string = `${BASE_PUBLIC_URL}idos-credential-subject-edd-v1.json`;
29
27
 
30
28
  export function buildDocumentLoader(): JsonLDDocumentLoaderInstance {
31
29
  const loader = new JsonLdDocumentLoader();
32
30
 
33
31
  loader.addStatic(CONTEXT_V1, v1);
34
- loader.addStatic(CONTEXT_IDOS_CREDENTIAL_V1, idosCredentialsV1);
35
32
  loader.addStatic(CONTEXT_ED25519_SIGNATURE_2020_V1, ed25519Signature2020V1);
36
33
 
37
- // plus / basic + liveness credentials
34
+ // Envelopes
35
+ loader.addStatic(CONTEXT_IDOS_CREDENTIAL_V1, idosCredentialsV1);
36
+ loader.addStatic(CONTEXT_IDOS_CREDENTIAL_V2, idosCredentialsV2);
37
+
38
+ // KYC
38
39
  loader.addStatic(CONTEXT_IDOS_CREDENTIAL_V1_SUBJECT, idosCredentialSubjectV1);
39
40
  loader.addStatic(CONTEXT_IDOS_CREDENTIAL_V2_SUBJECT, idosCredentialSubjectV2);
41
+ loader.addStatic(CONTEXT_IDOS_CREDENTIAL_V3_SUBJECT, idosCredentialSubjectV3);
40
42
 
41
43
  // PoP
42
44
  loader.addStatic(CONTEXT_IDOS_CREDENTIAL_V1_FACE_ID, idosCredentialSubjectFaceIdV1);
43
45
 
46
+ // EDD
47
+ loader.addStatic(CONTEXT_IDOS_CREDENTIAL_V1_EDD, idosCredentialSubjectEddV1);
48
+
44
49
  // DEPRECATED: Those should be removed
45
50
  [
46
51
  "1356fea6672d22deb07fedc5fa427478b2e1654c",
@@ -0,0 +1,149 @@
1
+ import { Ed25519VerificationKey2020 } from "@digitalbazaar/ed25519-verification-key-2020";
2
+ import crypto from "node:crypto";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ import { verifyCredential } from ".";
6
+ import { VerifiableCredentialFaceIdV1, VerifiableCredentialKycV3 } from "../schemas";
7
+
8
+ const ISSUER = "https://vc-issuers.cool.id/idos";
9
+ const ISSUER2 = "https://vc-issuers.cool.id/idos2";
10
+ const CRED_ID = `${ISSUER}/credentials/z6MkszZtxCmA2Ce4vUV132PCuLQmwnaDD5mw2L23fGNnsiX3`;
11
+ const ISSUED = new Date("2022-01-01");
12
+ const APPROVED_AT = new Date("2022-01-02");
13
+ const EXPIRATION_DATE = new Date("2030-01-01");
14
+
15
+ /** The flat credential subject of a signed credential, which `issue()` types as unknown. */
16
+ function subjectOf(credential: { credentialSubject: unknown }): Record<string, unknown> {
17
+ return credential.credentialSubject as Record<string, unknown>;
18
+ }
19
+
20
+ function generateKey(issuer: string) {
21
+ return Ed25519VerificationKey2020.generate({
22
+ id: `${issuer}/keys/1`,
23
+ controller: `${issuer}/issuers/1`,
24
+ });
25
+ }
26
+
27
+ async function issueFaceId(key: Ed25519VerificationKey2020) {
28
+ const credential = new VerifiableCredentialFaceIdV1();
29
+ credential.setEnvelope({ id: CRED_ID });
30
+ credential.setSubject({ faceSignUserId: crypto.randomUUID() });
31
+ return credential.issue(key);
32
+ }
33
+
34
+ async function issueKycV3(key: Ed25519VerificationKey2020) {
35
+ const credential = new VerifiableCredentialKycV3();
36
+ credential.setEnvelope({
37
+ id: CRED_ID,
38
+ issued: ISSUED,
39
+ approvedAt: APPROVED_AT,
40
+ expirationDate: EXPIRATION_DATE,
41
+ });
42
+ credential.setSubject({ id: "uuid:abc" });
43
+ credential.addSection("person", {
44
+ firstName: "John",
45
+ familyName: "Lennon",
46
+ nationality: "US",
47
+ dateOfBirth: new Date("1980-01-01"),
48
+ });
49
+ credential.addSection("idDocument", {
50
+ type: "PASSPORT",
51
+ number: "123456789",
52
+ country: "US",
53
+ frontFile: Buffer.from("Front"),
54
+ frontFileType: "image/jpeg",
55
+ frontFileName: "front.jpg",
56
+ });
57
+ credential.addSection("biometric", { selfieFile: Buffer.from("Selfie"), selfieMatch: 99 });
58
+ return credential.issue(key);
59
+ }
60
+
61
+ describe("issue + verifyCredential round-trip", () => {
62
+ it("issues a signed FaceId v1 credential that verifies against its issuer", async () => {
63
+ const validKey = await generateKey(ISSUER);
64
+ const otherKey = await generateKey(ISSUER2);
65
+
66
+ const data = await issueFaceId(validKey);
67
+
68
+ expect(data.proof.proofPurpose).toBe("assertionMethod");
69
+ expect(data.proof.type).toBe("Ed25519Signature2020");
70
+ expect(data.proof.verificationMethod).toBe(`${ISSUER}/keys/1`);
71
+
72
+ // Verifies against the correct issuer even when a wrong one is also allowed.
73
+ const [verified, results] = await verifyCredential(data, [otherKey, validKey]);
74
+ expect(verified).toBe(true);
75
+
76
+ const resultsArray = [...results.values()];
77
+ expect(resultsArray).toHaveLength(2);
78
+ expect(resultsArray[0].verified).toBe(false); // otherKey
79
+ expect(resultsArray[1].verified).toBe(true); // validKey
80
+ });
81
+
82
+ it("issues a signed KYC v3 credential that verifies against its issuer", async () => {
83
+ const validKey = await generateKey(ISSUER);
84
+
85
+ const data = await issueKycV3(validKey);
86
+
87
+ // Envelope fields are carried onto the signed credential.
88
+ expect(data.id).toBe(CRED_ID);
89
+ expect(data.issued).toBe(ISSUED.toISOString());
90
+ expect(data.approvedAt).toBe(APPROVED_AT.toISOString());
91
+ expect(data.expirationDate).toBe(EXPIRATION_DATE.toISOString());
92
+ expect(data.level).toBe("basic+liveness");
93
+ expect(data.kycLevel).toBe(1);
94
+
95
+ // Structured subject is flattened + prefixed on the signed credential.
96
+ expect(subjectOf(data).personFirstName).toBe("John");
97
+ expect(subjectOf(data).idDocumentType).toBe("PASSPORT");
98
+
99
+ const [verified] = await verifyCredential(data, [validKey]);
100
+ expect(verified).toBe(true);
101
+ });
102
+
103
+ it("rejects a tampered FaceId credential", async () => {
104
+ const validKey = await generateKey(ISSUER);
105
+ const data = await issueFaceId(validKey);
106
+
107
+ const [verified] = await verifyCredential(data, [validKey]);
108
+ expect(verified).toBe(true);
109
+
110
+ // Flip a subject field after signing — signature must no longer verify.
111
+ subjectOf(data).faceSignUserId = crypto.randomUUID();
112
+ const [verifiedAfterTamper] = await verifyCredential(data, [validKey]);
113
+ expect(verifiedAfterTamper).toBe(false);
114
+ });
115
+
116
+ it("rejects a tampered KYC v3 credential", async () => {
117
+ const validKey = await generateKey(ISSUER);
118
+ const data = await issueKycV3(validKey);
119
+
120
+ const [verified] = await verifyCredential(data, [validKey]);
121
+ expect(verified).toBe(true);
122
+
123
+ // Tamper with a subject field.
124
+ subjectOf(data).personFirstName = "Jane";
125
+ const [verifiedAfterSubjectTamper] = await verifyCredential(data, [validKey]);
126
+ expect(verifiedAfterSubjectTamper).toBe(false);
127
+ });
128
+
129
+ it("rejects a tampered KYC v3 envelope field", async () => {
130
+ const validKey = await generateKey(ISSUER);
131
+ const data = await issueKycV3(validKey);
132
+
133
+ // Tamper with a signed envelope field (e.g. extend expiry).
134
+ data.expirationDate = new Date("2999-01-01").toISOString();
135
+ const [verified] = await verifyCredential(data, [validKey]);
136
+ expect(verified).toBe(false);
137
+ });
138
+
139
+ it("rejects verification against the wrong issuer key", async () => {
140
+ const validKey = await generateKey(ISSUER);
141
+ const otherKey = await generateKey(ISSUER2);
142
+
143
+ const data = await issueFaceId(validKey);
144
+
145
+ const [verified, results] = await verifyCredential(data, [otherKey]);
146
+ expect(verified).toBe(false);
147
+ expect([...results.values()][0].verified).toBe(false);
148
+ });
149
+ });
@@ -3,18 +3,14 @@ import type { JsonLDDocumentLoaderInstance } from "jsonld-document-loader";
3
3
  import { Ed25519Signature2020 } from "@digitalbazaar/ed25519-signature-2020";
4
4
  import * as vc from "@digitalbazaar/vc";
5
5
 
6
- import type {
7
- AvailableIssuerType,
8
- VerifiableCredential,
9
- VerifiableCredentialSubject,
10
- } from "../types";
6
+ import type { AvailableIssuerType, VerifiableCredential } from "../types";
11
7
 
12
8
  import { issuerToKey } from "../utils";
13
- import { defaultDocumentLoader } from "./loader";
9
+ import { defaultDocumentLoader } from "../utils/loader";
14
10
 
15
11
  export type VerifyCredentialResult = [boolean, Map<AvailableIssuerType, vc.VerifyCredentialResult>];
16
12
 
17
- export async function verifyCredential<K = VerifiableCredentialSubject>(
13
+ export async function verifyCredential<K>(
18
14
  credential: VerifiableCredential<K>,
19
15
  issuers: AvailableIssuerType[],
20
16
  customDocumentLoader?: JsonLDDocumentLoaderInstance,
@@ -38,7 +34,7 @@ export async function verifyCredential<K = VerifiableCredentialSubject>(
38
34
 
39
35
  // Verify the signature
40
36
  const verifyCredentialResult = await vc.verifyCredential<K>({
41
- credential: credential,
37
+ credential,
42
38
  suite: vcVerifyingSuite,
43
39
  controller,
44
40
  documentLoader: customDocumentLoader ?? defaultDocumentLoader,
@@ -46,7 +46,7 @@ declare module "@digitalbazaar/vc" {
46
46
  documentLoader: JsonLDDocumentLoaderInstance;
47
47
  }
48
48
 
49
- export interface VerifiedCredentialsProof {
49
+ export interface VerifiedCredentialProof {
50
50
  type: string;
51
51
  created: string;
52
52
  verificationMethod: string;
@@ -54,25 +54,30 @@ declare module "@digitalbazaar/vc" {
54
54
  proofPurpose: string;
55
55
  }
56
56
 
57
- export interface VerifiedCredentials<K> {
57
+ export interface VerifiedCredential<K> {
58
58
  "@context": string[];
59
59
  type: string[];
60
60
  issuer: string;
61
61
  id: string;
62
62
  level: string;
63
+ kycLevel?: number;
63
64
  issued: string;
64
65
  approvedAt: string;
65
- expirationDate: string;
66
+ expirationDate?: string;
66
67
  credentialSubject: K;
67
68
  issuanceDate: string;
68
- proof: VerifiedCredentialsProof;
69
+ proof: VerifiableCredentialProof;
69
70
  }
70
71
 
72
+ export type VerifiableCredential<K> = VerifiedCredential<K>;
73
+
71
74
  // oxlint-disable-next-line typescript/no-explicit-any -- I don't know the right type
72
- export declare function issue<K = any>(options: IssueOptions): Promise<VerifiedCredentials<K>>;
75
+ export declare function issue<CredentialSubject = any, TExtension = any>(
76
+ options: IssueOptions,
77
+ ): Promise<VerifiedCredential<CredentialSubject> & TExtension>;
73
78
 
74
79
  export interface VerifyCredentialOptions<K> {
75
- credential: VerifiedCredentials<K>;
80
+ credential: VerifiableCredential<K>;
76
81
  suite: Ed25519Signature2020;
77
82
  controller: object;
78
83
  documentLoader: JsonLDDocumentLoaderInstance;
@@ -1,19 +0,0 @@
1
- import { AvailableIssuerType, CredentialFields, CredentialSubject, CredentialSubjectFaceId, VerifiableCredential, VerifiableCredentialSubject } from "../types/index.mjs";
2
- import * as vc from "@digitalbazaar/vc";
3
- import { JsonLDDocumentLoaderInstance } from "jsonld-document-loader";
4
- //#region src/builder/verifier.d.ts
5
- type VerifyCredentialResult = [boolean, Map<AvailableIssuerType, vc.VerifyCredentialResult>];
6
- declare function verifyCredential<K = VerifiableCredentialSubject>(credential: VerifiableCredential<K>, issuers: AvailableIssuerType[], customDocumentLoader?: JsonLDDocumentLoaderInstance): Promise<VerifyCredentialResult>;
7
- //#endregion
8
- //#region src/builder/index.d.ts
9
- type Credential = VerifiableCredential<VerifiableCredentialSubject>;
10
- type FaceIdCredential = VerifiableCredential<CredentialSubjectFaceId>;
11
- type CredentialBuilder<TInput, TOutput = TInput> = (fields: CredentialFields, subject: TInput, issuer: AvailableIssuerType, validate?: boolean) => Promise<VerifiableCredential<TOutput>>;
12
- type CredentialConverter<TInput> = (subject: TInput, validate: boolean) => Record<string, any>;
13
- declare const credentialSubjectConverter: CredentialConverter<CredentialSubject>;
14
- declare const credentialFaceIdSubjectConverter: CredentialConverter<CredentialSubjectFaceId>;
15
- declare const buildCredential: CredentialBuilder<CredentialSubject, VerifiableCredentialSubject>;
16
- declare const buildFaceIdCredential: CredentialBuilder<CredentialSubjectFaceId>;
17
- //#endregion
18
- export { Credential, CredentialBuilder, CredentialConverter, FaceIdCredential, type VerifyCredentialResult, buildCredential, buildFaceIdCredential, credentialFaceIdSubjectConverter, credentialSubjectConverter, verifyCredential };
19
- //# sourceMappingURL=index.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/builder/verifier.ts","../../src/builder/index.ts"],"mappings":";;;;KAcY,mCAAmC,IAAI,qBAAqB,GAAG;iBAErD,iBAAiB,IAAI,6BACzC,YAAY,qBAAqB,IACjC,SAAS,uBACT,uBAAuB,+BACtB,QAAQ;;;KCOC,aAAa,qBAAqB;KAClC,mBAAmB,qBAAqB;KAExC,kBAAkB,QAAQ,UAAU,WAC9C,QAAQ,kBACR,SAAS,QACT,QAAQ,qBACR,uBACG,QAAQ,qBAAqB;KAEtB,oBAAoB,WAC9B,SAAS,QACT,sBAEG;cAyCQ,4BAA4B,oBAAoB;cAgBhD,kCAAkC,oBAAoB;cActD,iBAAiB,kBAAkB,mBAAmB;cAKtD,uBAAuB,kBAAkB"}