@verii/metadata-registration 1.0.0-pre.1752076816

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.
@@ -0,0 +1,495 @@
1
+ /**
2
+ * Copyright 2023 Velocity Team
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ const { mapWithIndex } = require('@verii/common-functions');
18
+ const {
19
+ initContractClient,
20
+ initContractWithTransactingClient,
21
+ } = require('@verii/base-contract-io');
22
+
23
+ const {
24
+ encrypt,
25
+ decrypt,
26
+ get2BytesHash,
27
+ deriveEncryptionSecretFromPassword,
28
+ } = require('@verii/crypto');
29
+ const { jwkFromSecp256k1Key, hexFromJwk } = require('@verii/jwt');
30
+ const { find, flow, isEmpty, last, map, partition } = require('lodash/fp');
31
+
32
+ const contractAbi = require('./contracts/metadata-registry.json');
33
+ const { RESOLUTION_METADATA_ERROR } = require('./constants');
34
+
35
+ const VERSION = '1';
36
+ const ALG_TYPE = 'aes-256-gcm';
37
+
38
+ const initMetadataRegistry = async (
39
+ { privateKey, contractAddress, rpcProvider },
40
+ context
41
+ ) => {
42
+ const { log } = context;
43
+ log.info({ contractAddress }, 'initMetadataRegistry');
44
+
45
+ const { contractClient, pullEvents } = await initContractClient(
46
+ {
47
+ privateKey,
48
+ contractAddress,
49
+ rpcProvider,
50
+ contractAbi,
51
+ },
52
+ context
53
+ );
54
+
55
+ // TODO this check should NOT require a contractClient call. All free types should be cached on startup, and reloaded every X (configurable) hours
56
+ const isExistMetadataList = (id, accountId) => {
57
+ log.info({ id, accountId }, 'isExistMetadataList');
58
+ return contractClient.isExistMetadataList(accountId, id);
59
+ };
60
+
61
+ // TODO this check should NOT require a contractClient call. All free types should be cached on startup, and reloaded every X (configurable) hours
62
+ const isFreeCredentialType = (credentialType) => {
63
+ log.info({ credentialType }, 'isFreeCredentialType');
64
+ return contractClient.isFreeCredentialType(get2BytesHash(credentialType));
65
+ };
66
+
67
+ const isFreeCredentialTypeList = async (freeCredentialTypesList) => {
68
+ log.info({ freeCredentialTypesList }, 'isFreeCredentialTypeList');
69
+ const checkList = await Promise.all(
70
+ freeCredentialTypesList.map(isFreeCredentialType)
71
+ );
72
+ return checkList.every((check) => check);
73
+ };
74
+
75
+ const setEntrySigned = async (
76
+ credentialType,
77
+ encryptedPK,
78
+ listId,
79
+ index,
80
+ caoDid
81
+ ) => {
82
+ log.info(
83
+ { credentialType, encryptedPK, listId, index, caoDid },
84
+ 'setEntrySigned'
85
+ );
86
+ const { transactingClient, signature } =
87
+ await initContractWithTransactingClient(
88
+ {
89
+ privateKey,
90
+ contractAddress,
91
+ rpcProvider,
92
+ contractAbi,
93
+ },
94
+ context
95
+ );
96
+ const tx = await transactingClient.contractClient.setEntrySigned(
97
+ credentialType,
98
+ encryptedPK,
99
+ listId,
100
+ index,
101
+ context.traceId,
102
+ caoDid,
103
+ signature
104
+ );
105
+ const txResult = await tx.wait();
106
+ log.info({ events: txResult.logs }, 'setEntrySigned Complete');
107
+ return last(txResult.logs).args;
108
+ };
109
+
110
+ const getFreeEntries = async (indexEntries) => {
111
+ log.info({ indexEntries }, 'getFreeEntries');
112
+ return contractClient.getFreeEntries(indexEntries);
113
+ };
114
+
115
+ const getPaidEntriesSigned = async (
116
+ indexEntries,
117
+ traceId,
118
+ caoDid,
119
+ burnerDid
120
+ ) => {
121
+ log.info(
122
+ { indexEntries, traceId, caoDid, burnerDid },
123
+ 'getPaidEntriesSigned'
124
+ );
125
+
126
+ const { transactingClient, signature } =
127
+ await initContractWithTransactingClient(
128
+ {
129
+ privateKey,
130
+ contractAddress,
131
+ rpcProvider,
132
+ contractAbi,
133
+ },
134
+ context
135
+ );
136
+
137
+ await transactingClient.contractClient.getPaidEntriesSigned.staticCall(
138
+ indexEntries,
139
+ traceId,
140
+ caoDid,
141
+ burnerDid,
142
+ signature
143
+ );
144
+
145
+ const tx = await transactingClient.contractClient.getPaidEntriesSigned(
146
+ indexEntries,
147
+ traceId,
148
+ caoDid,
149
+ burnerDid,
150
+ signature
151
+ );
152
+ const txResult = await tx.wait();
153
+ return last(txResult.logs)?.args?.credentialMetadataList;
154
+ };
155
+
156
+ const createCredentialMetadataList = async (
157
+ accountId,
158
+ listId,
159
+ issuerVC,
160
+ caoDid,
161
+ algType = ALG_TYPE,
162
+ version = VERSION
163
+ ) => {
164
+ log.info(
165
+ { listId, issuerVC, caoDid, algType, version },
166
+ 'createCredentialMetadataList'
167
+ );
168
+ const { transactingClient, signature } =
169
+ await initContractWithTransactingClient(
170
+ {
171
+ privateKey,
172
+ contractAddress,
173
+ rpcProvider,
174
+ contractAbi,
175
+ },
176
+ context
177
+ );
178
+
179
+ try {
180
+ const tx = await transactingClient.contractClient.newMetadataListSigned(
181
+ listId,
182
+ get2BytesHash(algType),
183
+ get2BytesHash(version),
184
+ `0x${Buffer.from(issuerVC).toString('hex')}`,
185
+ context.traceId,
186
+ caoDid,
187
+ signature
188
+ );
189
+ await tx.wait();
190
+ return true;
191
+ } catch (creationError) {
192
+ if (!(await isExistMetadataList(listId, accountId))) {
193
+ throw creationError;
194
+ }
195
+ return false;
196
+ }
197
+ };
198
+
199
+ const addCredentialMetadataEntry = async (
200
+ { listId, index, credentialTypeEncoded, publicKey },
201
+ password,
202
+ caoDid
203
+ ) => {
204
+ log.info(
205
+ { listId, index, credentialTypeEncoded, caoDid, publicKey },
206
+ 'addCredentialMetadataEntry'
207
+ );
208
+ const secret = await deriveEncryptionSecretFromPassword(password);
209
+ const encryptedPK = `0x${Buffer.from(
210
+ encrypt(hexFromJwk(publicKey, false), secret),
211
+ 'base64'
212
+ ).toString('hex')}`;
213
+
214
+ try {
215
+ await setEntrySigned(
216
+ credentialTypeEncoded,
217
+ encryptedPK,
218
+ listId,
219
+ index,
220
+ caoDid
221
+ );
222
+ return true;
223
+ } catch (e) {
224
+ throw modifySetEntrySignedError(e);
225
+ }
226
+ };
227
+
228
+ const modifySetEntrySignedError = (e) => {
229
+ let errorCode;
230
+ switch (e.reason) {
231
+ case 'Permissions: primary of operator lacks credential:issue permission':
232
+ errorCode = 'career_issuing_not_permitted';
233
+ break;
234
+ case 'Permissions: primary of operator lacks credential:identityissue permission':
235
+ errorCode = 'identity_issuing_not_permitted';
236
+ break;
237
+ case 'Permissions: primary of operator lacks credential:contactissue permission':
238
+ errorCode = 'contact_issuing_not_permitted';
239
+ break;
240
+ default:
241
+ break;
242
+ }
243
+ // eslint-disable-next-line better-mutation/no-mutation
244
+ e.errorCode = errorCode;
245
+ return e;
246
+ };
247
+ const parseVelocityV2Did = (did) => {
248
+ log.info({ did }, 'parseVelocityV2Did');
249
+ if (!did.startsWith('did:velocity:v2:')) {
250
+ throw new Error(`Wrong did ${did}`);
251
+ }
252
+
253
+ const multiToken = ':multi:';
254
+ if (did.indexOf(multiToken) === -1) {
255
+ const [, , , accountId, listId, index, contentHash] = did.split(':');
256
+ return [{ accountId, listId, index, contentHash }];
257
+ }
258
+
259
+ const [, entriesPart] = did.split(multiToken);
260
+ return map((entryString) => {
261
+ const [accountId, listId, index, contentHash] = entryString.split(':');
262
+ return { accountId, listId, index, contentHash };
263
+ }, entriesPart.split(';'));
264
+ };
265
+
266
+ const resolvePublicKey = ({ id, entry, secret }) => {
267
+ log.info({ id, entry, secret }, 'resolvePublicKey');
268
+ const { algType, version } = entry;
269
+ if (
270
+ version !== get2BytesHash(VERSION) ||
271
+ algType !== get2BytesHash(ALG_TYPE)
272
+ ) {
273
+ throw new Error(
274
+ `Unsupported encryption algorithm "${ALG_TYPE}" or version "${VERSION}"`
275
+ );
276
+ }
277
+
278
+ const encryptedPublicKey = Buffer.from(
279
+ entry.encryptedPublicKey.slice(2),
280
+ 'hex'
281
+ ).toString('base64');
282
+
283
+ try {
284
+ const publicKeyJwk = jwkFromSecp256k1Key(
285
+ decrypt(encryptedPublicKey, secret),
286
+ false
287
+ );
288
+ return {
289
+ id: `${id}#key-1`,
290
+ publicKeyJwk,
291
+ };
292
+ } catch (e) {
293
+ log.error({ err: e }, 'resolvePublicKey: DECRYPTION FAIL');
294
+ return {
295
+ id,
296
+ };
297
+ }
298
+ };
299
+
300
+ const resolveService = ({ id, entry, credentialType }) => {
301
+ log.info({ id, entry, credentialType }, 'resolveService');
302
+ if (
303
+ !credentialType ||
304
+ entry.credentialType !== get2BytesHash(credentialType) // TODO - looks like credential types are stored incorrectly
305
+ ) {
306
+ throw new Error(`Invalid hash credentialType "${credentialType}"`);
307
+ }
308
+ return {
309
+ id: `${id}#service`,
310
+ credentialType,
311
+ };
312
+ };
313
+
314
+ const resolveIssuerVc = ({ id, entry }) => {
315
+ log.info({ id, entry }, 'resolveIssuerVc');
316
+ return {
317
+ id,
318
+ format: 'jwt_vc',
319
+ vc: Buffer.from(entry.issuerVc.slice(2), 'hex').toString(),
320
+ };
321
+ };
322
+
323
+ const resolveContractEntries = async ({
324
+ credentials,
325
+ indexEntries,
326
+ traceId,
327
+ caoDid,
328
+ burnerDid,
329
+ }) => {
330
+ log.info(
331
+ { credentials, indexEntries, traceId, caoDid, burnerDid },
332
+ 'resolveContractEntries'
333
+ );
334
+ const isFree = await flow(
335
+ map(({ id, credentialType }) => {
336
+ if (!credentialType) {
337
+ throw new Error(
338
+ `Could not resolve credential type from VC with ${id}`
339
+ );
340
+ }
341
+ return credentialType;
342
+ }),
343
+ isFreeCredentialTypeList
344
+ )(credentials);
345
+ if (isEmpty(indexEntries)) {
346
+ return [];
347
+ }
348
+ if (isFree) {
349
+ return getFreeEntries(indexEntries);
350
+ }
351
+ return getPaidEntriesSigned(indexEntries, traceId, caoDid, burnerDid);
352
+ };
353
+
354
+ const resolveDidDocument = async ({
355
+ did,
356
+ credentials,
357
+ burnerDid,
358
+ caoDid,
359
+ }) => {
360
+ log.info({ did, credentials, burnerDid, caoDid }, 'resolveDidDocument');
361
+ const { traceId } = context;
362
+ const indexEntries = parseVelocityV2Did(did);
363
+ const entries = await resolveContractEntries({
364
+ credentials,
365
+ indexEntries,
366
+ traceId,
367
+ caoDid,
368
+ burnerDid,
369
+ });
370
+ if (isEmpty(entries)) {
371
+ throw new Error(`Entries were not retrieved for ${indexEntries}.`);
372
+ }
373
+
374
+ const credentialEntries = await Promise.all(
375
+ mapWithIndex(async (entry, i) => {
376
+ const id = toDID(indexEntries[i]).toLowerCase();
377
+ const credential = find((c) => c.id.toLowerCase() === id, credentials);
378
+ const secret =
379
+ indexEntries[i].contentHash != null
380
+ ? await deriveEncryptionSecretFromPassword(
381
+ indexEntries[i].contentHash
382
+ )
383
+ : await deriveEncryptionSecret(credential);
384
+ return {
385
+ entry,
386
+ id,
387
+ credentialType: credential.credentialType,
388
+ secret,
389
+ };
390
+ }, entries)
391
+ );
392
+ log.info({ credentialEntries }, 'resolveDidDocument 1');
393
+
394
+ const [resolvedPublicKeys, unresolvedPublicKeys] = flow(
395
+ map(resolvePublicKey),
396
+ partition(({ publicKeyJwk }) => !!publicKeyJwk)
397
+ )(credentialEntries);
398
+
399
+ const service = map(resolveService, credentialEntries);
400
+
401
+ const didDocument = {
402
+ id: did,
403
+ publicKey: resolvedPublicKeys,
404
+ service,
405
+ };
406
+
407
+ const didDocumentMetadata = {
408
+ boundIssuerVcs: map(resolveIssuerVc, credentialEntries),
409
+ };
410
+
411
+ log.info(
412
+ { didDocument, didDocumentMetadata, unresolvedPublicKeys },
413
+ 'resolveDidDocument 3'
414
+ );
415
+
416
+ if (isEmpty(unresolvedPublicKeys)) {
417
+ return {
418
+ didDocument,
419
+ didDocumentMetadata,
420
+ didResolutionMetadata: {},
421
+ };
422
+ }
423
+
424
+ const didResolutionMetadata = {
425
+ error: RESOLUTION_METADATA_ERROR.UNRESOLVED_MULTI_DID_ENTRIES,
426
+ unresolvedMultiDidEntries: map(
427
+ ({ id }) => ({
428
+ id,
429
+ error: RESOLUTION_METADATA_ERROR.DATA_INTEGRITY_ERROR,
430
+ }),
431
+ unresolvedPublicKeys
432
+ ),
433
+ };
434
+
435
+ log.error({ didResolutionMetadata }, 'Unable to resolve did entries');
436
+
437
+ return {
438
+ didDocument,
439
+ didDocumentMetadata,
440
+ didResolutionMetadata,
441
+ };
442
+ };
443
+
444
+ const setPermissionsAddress = async (permissionsContractAddress) => {
445
+ const tx = await contractClient.setPermissionsAddress(
446
+ permissionsContractAddress
447
+ );
448
+
449
+ return tx.wait();
450
+ };
451
+
452
+ const toDID = ({ accountId, listId, index, contentHash }) => {
453
+ if (contentHash != null) {
454
+ return `did:velocity:v2:${accountId}:${listId}:${index}:${contentHash}`;
455
+ }
456
+
457
+ return `did:velocity:v2:${accountId}:${listId}:${index}`;
458
+ };
459
+
460
+ const pullCreatedMetadataListEvents = pullEvents('CreatedMetadataList');
461
+
462
+ const pullAddedCredentialMetadataEvents = pullEvents(
463
+ 'AddedCredentialMetadata'
464
+ );
465
+
466
+ return {
467
+ createCredentialMetadataList,
468
+ addCredentialMetadataEntry,
469
+ contractClient,
470
+ isFreeCredentialTypeList,
471
+ isFreeCredentialType,
472
+ isExistMetadataList,
473
+ getPaidEntriesSigned,
474
+ getFreeEntries,
475
+ setEntrySigned,
476
+ resolveContractEntries,
477
+ resolveDidDocument,
478
+ setPermissionsAddress,
479
+ pullCreatedMetadataListEvents,
480
+ pullAddedCredentialMetadataEvents,
481
+ parseVelocityV2Did,
482
+ };
483
+ };
484
+
485
+ const deriveEncryptionSecret = async (credential) => {
486
+ const contentHash = credential?.contentHash;
487
+ if (!contentHash) {
488
+ throw new Error(
489
+ `Could not resolve content hash from VC with ${credential.id}`
490
+ );
491
+ }
492
+ return deriveEncryptionSecretFromPassword(contentHash);
493
+ };
494
+
495
+ module.exports = initMetadataRegistry;
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Copyright 2023 Velocity Team
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ /* eslint-disable camelcase */
18
+ const ethUrlParser = require('eth-url-parser');
19
+ const {
20
+ initContractClient,
21
+ initContractWithTransactingClient,
22
+ } = require('@verii/base-contract-io');
23
+
24
+ const contractAbi = require('./contracts/revocation-registry.json');
25
+
26
+ const initRevocationRegistry = async (
27
+ { privateKey, contractAddress, rpcProvider },
28
+ context
29
+ ) => {
30
+ const { log } = context;
31
+ log.info({ privateKey, contractAddress }, 'initRevocationRegistry');
32
+
33
+ const { contractClient, pullEvents } = await initContractClient(
34
+ {
35
+ privateKey,
36
+ contractAddress,
37
+ rpcProvider,
38
+ contractAbi,
39
+ },
40
+ context
41
+ );
42
+
43
+ const addWalletToRegistrySigned = async ({ caoDid }) => {
44
+ log.info({ caoDid }, 'addWalletToRegistrySigned');
45
+ const { traceId } = context;
46
+ const { transactingClient, signature } =
47
+ await initContractWithTransactingClient(
48
+ {
49
+ privateKey,
50
+ contractAddress,
51
+ rpcProvider,
52
+ contractAbi,
53
+ },
54
+ context
55
+ );
56
+ const tx = await transactingClient.contractClient.addWalletSigned(
57
+ traceId,
58
+ caoDid,
59
+ signature
60
+ );
61
+ const txResult = await tx.wait();
62
+ return { txResult };
63
+ };
64
+
65
+ const addRevocationListSigned = async (listId, caoDid) => {
66
+ log.info({ listId, caoDid }, 'addRevocationListSigned');
67
+ const { traceId } = context;
68
+ const { transactingClient, signature } =
69
+ await initContractWithTransactingClient(
70
+ {
71
+ privateKey,
72
+ contractAddress,
73
+ rpcProvider,
74
+ contractAbi,
75
+ },
76
+ context
77
+ );
78
+ const tx = await transactingClient.contractClient.addRevocationListSigned(
79
+ listId,
80
+ traceId,
81
+ caoDid,
82
+ signature
83
+ );
84
+ const txResult = await tx.wait();
85
+ return { txResult };
86
+ };
87
+
88
+ const getRevokeUrl = (accountId, listId, index) => {
89
+ return ethUrlParser.build({
90
+ scheme: 'ethereum',
91
+ target_address: contractAddress,
92
+ function_name: 'getRevokedStatus',
93
+ parameters: {
94
+ address: accountId,
95
+ listId,
96
+ index,
97
+ },
98
+ });
99
+ };
100
+
101
+ const setRevokedStatusSigned = async ({
102
+ accountId,
103
+ listId,
104
+ index,
105
+ caoDid,
106
+ }) => {
107
+ log.info({ listId, index }, 'setRevokedStatusSigned');
108
+
109
+ const { traceId } = context;
110
+ const { transactingClient, signature } =
111
+ await initContractWithTransactingClient(
112
+ {
113
+ privateKey,
114
+ contractAddress,
115
+ rpcProvider,
116
+ contractAbi,
117
+ },
118
+ context
119
+ );
120
+ const tx = await transactingClient.contractClient.setRevokedStatusSigned(
121
+ listId,
122
+ index,
123
+ traceId,
124
+ caoDid,
125
+ signature
126
+ );
127
+ const txResult = await tx.wait();
128
+ return {
129
+ url: getRevokeUrl(accountId, listId, index),
130
+ txResult,
131
+ };
132
+ };
133
+
134
+ const getRevokedStatus = (url) => {
135
+ log.info({ url }, 'getRevokedStatus');
136
+
137
+ const {
138
+ scheme,
139
+ target_address,
140
+ function_name,
141
+ parameters: { address, listId, index },
142
+ } = ethUrlParser.parse(url);
143
+
144
+ if (
145
+ target_address !== contractAddress ||
146
+ function_name !== 'getRevokedStatus' ||
147
+ scheme !== 'ethereum'
148
+ ) {
149
+ throw new Error(
150
+ 'Wrong url, please check the params: scheme, target_address, function_name'
151
+ );
152
+ }
153
+
154
+ return contractClient.getRevokedStatus(address, listId, index);
155
+ };
156
+
157
+ const setPermissionsAddress = async (permissionsContractAddress) => {
158
+ const tx = await contractClient.setPermissionsAddress(
159
+ permissionsContractAddress
160
+ );
161
+
162
+ return tx.wait();
163
+ };
164
+
165
+ const pullWalletAddedEvents = pullEvents('WalletAdded');
166
+
167
+ const pullRevocationListCreateEvents = pullEvents('RevocationListCreate');
168
+
169
+ const pullRevokedStatusUpdateEvents = pullEvents('RevokedStatusUpdate');
170
+
171
+ return {
172
+ contractClient,
173
+ addWalletToRegistrySigned,
174
+ addRevocationListSigned,
175
+ setRevokedStatusSigned,
176
+ getRevokedStatus,
177
+ getRevokeUrl,
178
+ setPermissionsAddress,
179
+ pullWalletAddedEvents,
180
+ pullRevocationListCreateEvents,
181
+ pullRevokedStatusUpdateEvents,
182
+ };
183
+ };
184
+
185
+ module.exports = initRevocationRegistry;