@idos-network/credentials 1.2.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.
- package/README.md +289 -85
- package/assets/idos-credential-subject-edd-v1.json +10 -0
- package/assets/idos-credential-subject-v1.json +1 -1
- package/assets/idos-credential-subject-v2.json +1 -1
- package/assets/idos-credential-subject-v3.json +83 -0
- package/assets/idos-credentials-v1.json +2 -2
- package/assets/idos-credentials-v2.json +10 -0
- package/dist/index-0NqXMjNk.d.mts +395 -0
- package/dist/index-0NqXMjNk.d.mts.map +1 -0
- package/dist/index-WW372Tf5.d.mts +57 -0
- package/dist/index-WW372Tf5.d.mts.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/loader-Cg6hs1-P.mjs +2 -0
- package/dist/loader-Cg6hs1-P.mjs.map +1 -0
- package/dist/parser/index.d.mts +19 -0
- package/dist/parser/index.d.mts.map +1 -0
- package/dist/parser/index.mjs +2 -0
- package/dist/parser/index.mjs.map +1 -0
- package/dist/schemas/index.d.mts +2 -0
- package/dist/schemas/index.mjs +1 -0
- package/dist/schemas-BVGuUlPe.mjs +2 -0
- package/dist/schemas-BVGuUlPe.mjs.map +1 -0
- package/dist/types/index.d.mts +2 -135
- package/dist/types/index.mjs +1 -63
- package/dist/utils/index.d.mts +38 -22
- package/dist/utils/index.d.mts.map +1 -1
- package/dist/utils/index.mjs +1 -123
- package/dist/utils/index.mjs.map +1 -1
- package/dist/verifier/index.d.mts +8 -0
- package/dist/verifier/index.d.mts.map +1 -0
- package/dist/verifier/index.mjs +2 -0
- package/dist/verifier/index.mjs.map +1 -0
- package/package.json +31 -15
- package/src/parser/index.test.ts +266 -0
- package/src/parser/index.ts +69 -0
- package/src/schemas/EddV1/index.test.ts +40 -0
- package/src/schemas/EddV1/index.ts +53 -0
- package/src/schemas/Envelope.ts +37 -0
- package/src/schemas/FaceIdV1/index.test.ts +26 -0
- package/src/schemas/FaceIdV1/index.ts +33 -0
- package/src/schemas/KycV1/IdDocument.ts +41 -0
- package/src/schemas/KycV1/ResidentialAddress.ts +56 -0
- package/src/schemas/KycV1/Subject.ts +86 -0
- package/src/schemas/KycV1/index.test.ts +156 -0
- package/src/schemas/KycV1/index.ts +40 -0
- package/src/schemas/KycV2/Subject.ts +67 -0
- package/src/schemas/KycV2/index.test.ts +162 -0
- package/src/schemas/KycV2/index.ts +45 -0
- package/src/schemas/KycV3/Biometric.ts +14 -0
- package/src/schemas/KycV3/Contact.ts +13 -0
- package/src/schemas/KycV3/EDD.ts +32 -0
- package/src/schemas/KycV3/IdDocument.ts +92 -0
- package/src/schemas/KycV3/Onboarding.ts +36 -0
- package/src/schemas/KycV3/Person.ts +105 -0
- package/src/schemas/KycV3/ResidentialAddress.ts +73 -0
- package/src/schemas/KycV3/Screening.ts +27 -0
- package/src/schemas/KycV3/SourceOfWealth.ts +44 -0
- package/src/schemas/KycV3/Subject.ts +54 -0
- package/src/schemas/KycV3/index.test.ts +682 -0
- package/src/schemas/KycV3/index.ts +108 -0
- package/src/schemas/VerifiableCredential.ts +232 -0
- package/src/schemas/enums.ts +126 -0
- package/src/schemas/index.ts +14 -0
- package/src/schemas/integration.test.ts +251 -0
- package/src/schemas/types.ts +11 -0
- package/src/schemas/utils.ts +274 -0
- package/src/types/credentials.ts +18 -249
- package/src/utils/{utils.test.ts → index.test.ts} +59 -67
- package/src/utils/index.ts +129 -137
- package/src/utils/issuer.ts +42 -0
- package/src/{builder → utils}/loader.ts +22 -17
- package/src/verifier/index.test.ts +149 -0
- package/src/{builder/verifier.ts → verifier/index.ts} +4 -8
- package/types/internals.d.ts +11 -6
- package/dist/builder/index.d.mts +0 -20
- package/dist/builder/index.d.mts.map +0 -1
- package/dist/builder/index.mjs +0 -619
- package/dist/builder/index.mjs.map +0 -1
- package/dist/types/index.d.mts.map +0 -1
- package/dist/types/index.mjs.map +0 -1
- package/src/builder/builder.test.ts +0 -407
- package/src/builder/index.ts +0 -122
package/src/utils/index.ts
CHANGED
|
@@ -1,125 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
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";
|
|
4
4
|
import { every, get } from "es-toolkit/compat";
|
|
5
|
-
import invariant from "tiny-invariant";
|
|
6
5
|
import nacl from "tweetnacl";
|
|
7
6
|
|
|
8
|
-
import type {
|
|
9
|
-
AvailableIssuerType,
|
|
10
|
-
CredentialFields,
|
|
11
|
-
CredentialResidentialAddress,
|
|
12
|
-
CredentialSubject,
|
|
13
|
-
CredentialSubjectFaceId,
|
|
14
|
-
CustomIssuerType,
|
|
15
|
-
InsertableIDOSCredential,
|
|
16
|
-
} from "../types";
|
|
17
|
-
|
|
18
|
-
export function fileToBase85(file: Buffer): string {
|
|
19
|
-
return base85.encode(file, "ascii85");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function base85ToFile(data: string): Buffer | false {
|
|
23
|
-
return base85.decode(data);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function capitalizeFirstLetter(str: string): string {
|
|
27
|
-
return str[0].toUpperCase() + str.slice(1);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function isIssuerKey(issuer: AvailableIssuerType): issuer is Ed25519VerificationKey2020 {
|
|
31
|
-
return (
|
|
32
|
-
typeof issuer === "object" &&
|
|
33
|
-
issuer !== null &&
|
|
34
|
-
"type" in issuer &&
|
|
35
|
-
"id" in issuer &&
|
|
36
|
-
"controller" in issuer
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function isCustomIssuerType(issuer: AvailableIssuerType): issuer is CustomIssuerType {
|
|
41
|
-
return (
|
|
42
|
-
typeof issuer === "object" &&
|
|
43
|
-
issuer !== null &&
|
|
44
|
-
"issuer" in issuer &&
|
|
45
|
-
"publicKeyMultibase" in issuer
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export async function issuerToKey(
|
|
50
|
-
issuer: AvailableIssuerType,
|
|
51
|
-
): Promise<Ed25519VerificationKey2020> {
|
|
52
|
-
if (isIssuerKey(issuer)) {
|
|
53
|
-
return issuer;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (isCustomIssuerType(issuer)) {
|
|
57
|
-
return await Ed25519VerificationKey2020.from({
|
|
58
|
-
id: `${issuer.issuer}/keys/1`,
|
|
59
|
-
controller: `${issuer.issuer}/issuers/1`,
|
|
60
|
-
publicKeyMultibase: issuer.publicKeyMultibase,
|
|
61
|
-
privateKeyMultibase: issuer.privateKeyMultibase,
|
|
62
|
-
type: "Ed25519VerificationKey2020",
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return await Ed25519VerificationKey2020.from({ ...issuer, type: "Ed25519VerificationKey2020" });
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function convertValues<
|
|
70
|
-
K extends
|
|
71
|
-
| CredentialFields
|
|
72
|
-
| CredentialSubject
|
|
73
|
-
| CredentialResidentialAddress
|
|
74
|
-
| CredentialSubjectFaceId,
|
|
75
|
-
>(fields: K, prefix?: string): Record<string, unknown> {
|
|
76
|
-
const acc: Record<string, unknown> = {};
|
|
77
|
-
|
|
78
|
-
for (const key in fields) {
|
|
79
|
-
if (Object.hasOwn(fields, key)) {
|
|
80
|
-
const value = fields[key];
|
|
81
|
-
const name = prefix ? `${prefix}${capitalizeFirstLetter(key)}` : key;
|
|
82
|
-
if (value instanceof Date) {
|
|
83
|
-
acc[name] = value.toISOString();
|
|
84
|
-
} else if (value instanceof Buffer) {
|
|
85
|
-
// Convert file to base85
|
|
86
|
-
acc[name] = fileToBase85(value);
|
|
87
|
-
} else {
|
|
88
|
-
acc[name] = value;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return acc;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
type BaseLevel = "basic" | "plus";
|
|
97
|
-
type Addon = "liveness" | "email" | "phoneNumber";
|
|
98
|
-
|
|
99
|
-
export function deriveLevel(credential: CredentialSubject): string {
|
|
100
|
-
let level: BaseLevel = "basic";
|
|
101
|
-
|
|
102
|
-
// Address is a sign for plus+
|
|
103
|
-
const address = credential.residentialAddress;
|
|
104
|
-
if (address?.proofFile && address?.city && address?.proofCategory) {
|
|
105
|
-
level = "plus";
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const addons: Addon[] = [];
|
|
109
|
-
if (credential.selfieFile) {
|
|
110
|
-
addons.push("liveness");
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (credential.email) {
|
|
114
|
-
addons.push("email");
|
|
115
|
-
}
|
|
7
|
+
import type { idOSCredential, SignedCredentialContentReference } from "../types";
|
|
116
8
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
9
|
+
// Proxying functions
|
|
10
|
+
export * from "./issuer";
|
|
120
11
|
|
|
121
|
-
|
|
122
|
-
|
|
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";
|
|
123
21
|
|
|
124
22
|
export function parseLevel(level: string): {
|
|
125
23
|
base: BaseLevel;
|
|
@@ -129,6 +27,18 @@ export function parseLevel(level: string): {
|
|
|
129
27
|
return { base, addons };
|
|
130
28
|
}
|
|
131
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
|
+
|
|
132
42
|
export function matchLevelOrHigher(
|
|
133
43
|
level: BaseLevel,
|
|
134
44
|
requiredAddons: Addon[],
|
|
@@ -136,7 +46,12 @@ export function matchLevelOrHigher(
|
|
|
136
46
|
): boolean {
|
|
137
47
|
const { base: currentBaseLevel, addons: currentAddons } = parseLevel(currentLevel);
|
|
138
48
|
|
|
139
|
-
//
|
|
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
|
|
140
55
|
if (level === "plus" && currentBaseLevel !== "plus") {
|
|
141
56
|
return false;
|
|
142
57
|
}
|
|
@@ -148,6 +63,10 @@ export function levelScore(level: string): number {
|
|
|
148
63
|
const { base, addons } = parseLevel(level);
|
|
149
64
|
let score = 0;
|
|
150
65
|
|
|
66
|
+
if (base === "basic") {
|
|
67
|
+
score += 50;
|
|
68
|
+
}
|
|
69
|
+
|
|
151
70
|
if (base === "plus") {
|
|
152
71
|
score += 100;
|
|
153
72
|
}
|
|
@@ -242,36 +161,109 @@ export function recordFilter(
|
|
|
242
161
|
return true;
|
|
243
162
|
}
|
|
244
163
|
|
|
245
|
-
export function
|
|
246
|
-
userId: string,
|
|
164
|
+
export function buildSignedCredentialContentReference(
|
|
247
165
|
publicNotes: string,
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
):
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const ephemeralAuthenticationKeyPair = nacl.sign.keyPair();
|
|
166
|
+
contentUri: string,
|
|
167
|
+
issuerSigningSecretKey: Uint8Array,
|
|
168
|
+
): SignedCredentialContentReference {
|
|
169
|
+
const { publicKey, secretKey } = nacl.sign.keyPair.fromSecretKey(issuerSigningSecretKey);
|
|
254
170
|
|
|
255
|
-
const publicNotesSignature = nacl.sign.detached(
|
|
256
|
-
utf8Encode(publicNotes),
|
|
257
|
-
ephemeralAuthenticationKeyPair.secretKey,
|
|
258
|
-
);
|
|
171
|
+
const publicNotesSignature = nacl.sign.detached(utf8Encode(publicNotes), secretKey);
|
|
259
172
|
|
|
260
173
|
return {
|
|
261
|
-
user_id: userId,
|
|
262
|
-
content,
|
|
263
|
-
|
|
264
174
|
public_notes: publicNotes,
|
|
265
175
|
public_notes_signature: base64Encode(publicNotesSignature),
|
|
266
176
|
|
|
267
177
|
broader_signature: base64Encode(
|
|
268
178
|
nacl.sign.detached(
|
|
269
|
-
Uint8Array.from([...publicNotesSignature, ...
|
|
270
|
-
|
|
179
|
+
Uint8Array.from([...publicNotesSignature, ...utf8Encode(contentUri)]),
|
|
180
|
+
secretKey,
|
|
271
181
|
),
|
|
272
182
|
),
|
|
273
183
|
|
|
274
|
-
issuer_auth_public_key: hexEncode(
|
|
275
|
-
|
|
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),
|
|
276
268
|
};
|
|
277
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
|
-
|
|
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
|
-
//
|
|
25
|
-
export const
|
|
26
|
-
export const
|
|
27
|
-
export const
|
|
28
|
-
export const
|
|
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
|
-
//
|
|
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 "
|
|
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
|
|
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
|
|
37
|
+
credential,
|
|
42
38
|
suite: vcVerifyingSuite,
|
|
43
39
|
controller,
|
|
44
40
|
documentLoader: customDocumentLoader ?? defaultDocumentLoader,
|
package/types/internals.d.ts
CHANGED
|
@@ -46,7 +46,7 @@ declare module "@digitalbazaar/vc" {
|
|
|
46
46
|
documentLoader: JsonLDDocumentLoaderInstance;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export interface
|
|
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
|
|
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
|
|
66
|
+
expirationDate?: string;
|
|
66
67
|
credentialSubject: K;
|
|
67
68
|
issuanceDate: string;
|
|
68
|
-
proof:
|
|
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<
|
|
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:
|
|
80
|
+
credential: VerifiableCredential<K>;
|
|
76
81
|
suite: Ed25519Signature2020;
|
|
77
82
|
controller: object;
|
|
78
83
|
documentLoader: JsonLDDocumentLoaderInstance;
|
package/dist/builder/index.d.mts
DELETED
|
@@ -1,20 +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
|
-
|
|
5
|
-
//#region src/builder/verifier.d.ts
|
|
6
|
-
type VerifyCredentialResult = [boolean, Map<AvailableIssuerType, vc.VerifyCredentialResult>];
|
|
7
|
-
declare function verifyCredential<K = VerifiableCredentialSubject>(credential: VerifiableCredential<K>, issuers: AvailableIssuerType[], customDocumentLoader?: JsonLDDocumentLoaderInstance): Promise<VerifyCredentialResult>;
|
|
8
|
-
//#endregion
|
|
9
|
-
//#region src/builder/index.d.ts
|
|
10
|
-
type Credential = VerifiableCredential<VerifiableCredentialSubject>;
|
|
11
|
-
type FaceIdCredential = VerifiableCredential<CredentialSubjectFaceId>;
|
|
12
|
-
type CredentialBuilder<TInput, TOutput = TInput> = (fields: CredentialFields, subject: TInput, issuer: AvailableIssuerType, validate?: boolean) => Promise<VerifiableCredential<TOutput>>;
|
|
13
|
-
type CredentialConverter<TInput> = (subject: TInput, validate: boolean) => Record<string, any>;
|
|
14
|
-
declare const credentialSubjectConverter: CredentialConverter<CredentialSubject>;
|
|
15
|
-
declare const credentialFaceIdSubjectConverter: CredentialConverter<CredentialSubjectFaceId>;
|
|
16
|
-
declare const buildCredential: CredentialBuilder<CredentialSubject, VerifiableCredentialSubject>;
|
|
17
|
-
declare const buildFaceIdCredential: CredentialBuilder<CredentialSubjectFaceId>;
|
|
18
|
-
//#endregion
|
|
19
|
-
export { Credential, CredentialBuilder, CredentialConverter, FaceIdCredential, type VerifyCredentialResult, buildCredential, buildFaceIdCredential, credentialFaceIdSubjectConverter, credentialSubjectConverter, verifyCredential };
|
|
20
|
-
//# 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,sBAAA,aAAmC,GAAA,CAAI,mBAAA,EAAqB,EAAA,CAAG,sBAAA;AAAA,iBAErD,gBAAA,KAAqB,2BAAA,CAAA,CACzC,UAAA,EAAY,oBAAA,CAAqB,CAAA,GACjC,OAAA,EAAS,mBAAA,IACT,oBAAA,GAAuB,4BAAA,GACtB,OAAA,CAAQ,sBAAA;;;KCOC,UAAA,GAAa,oBAAA,CAAqB,2BAAA;AAAA,KAClC,gBAAA,GAAmB,oBAAA,CAAqB,uBAAA;AAAA,KAExC,iBAAA,mBAAoC,MAAA,KAC9C,MAAA,EAAQ,gBAAA,EACR,OAAA,EAAS,MAAA,EACT,MAAA,EAAQ,mBAAA,EACR,QAAA,eACG,OAAA,CAAQ,oBAAA,CAAqB,OAAA;AAAA,KAEtB,mBAAA,YACV,OAAA,EAAS,MAAA,EACT,QAAA,cAEG,MAAA;AAAA,cAyCQ,0BAAA,EAA4B,mBAAA,CAAoB,iBAAA;AAAA,cAgBhD,gCAAA,EAAkC,mBAAA,CAAoB,uBAAA;AAAA,cActD,eAAA,EAAiB,iBAAA,CAAkB,iBAAA,EAAmB,2BAAA;AAAA,cAKtD,qBAAA,EAAuB,iBAAA,CAAkB,uBAAA"}
|