bsv-bap 0.0.1
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/.babelrc +20 -0
- package/.eslintrc +46 -0
- package/LICENSE +25 -0
- package/README.md +819 -0
- package/babel.config.js +6 -0
- package/bun.lockb +0 -0
- package/coverage/clover.xml +6 -0
- package/coverage/coverage-final.json +1 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +101 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +196 -0
- package/coverage/lcov-report/src/constants.ts.html +113 -0
- package/coverage/lcov-report/src/id.ts.html +2207 -0
- package/coverage/lcov-report/src/index.html +156 -0
- package/coverage/lcov-report/src/index.ts.html +1877 -0
- package/coverage/lcov-report/src/utils.ts.html +404 -0
- package/coverage/lcov-report/tests/data/index.html +111 -0
- package/coverage/lcov-report/tests/data/keys.js.html +86 -0
- package/coverage/lcov.info +0 -0
- package/dist/jest.config.d.ts +8 -0
- package/dist/src/constants.d.ts +8 -0
- package/dist/src/id.d.ts +295 -0
- package/dist/src/index.d.ts +238 -0
- package/dist/src/interface.d.ts +23 -0
- package/dist/src/poa.d.ts +6 -0
- package/dist/src/utils.d.ts +54 -0
- package/dist/typescript-npm-package.cjs.d.ts +554 -0
- package/dist/typescript-npm-package.cjs.js +1320 -0
- package/dist/typescript-npm-package.cjs.js.map +1 -0
- package/dist/typescript-npm-package.esm.d.ts +554 -0
- package/dist/typescript-npm-package.esm.js +1312 -0
- package/dist/typescript-npm-package.esm.js.map +1 -0
- package/dist/typescript-npm-package.umd.d.ts +554 -0
- package/dist/typescript-npm-package.umd.js +110193 -0
- package/dist/typescript-npm-package.umd.js.map +1 -0
- package/jest.config.ts +196 -0
- package/jsdoc.json +16 -0
- package/package.json +80 -0
- package/rollup.config.js +64 -0
- package/setup-jest.js +1 -0
- package/src/README.md +80 -0
- package/src/attributes.json +119 -0
- package/src/constants.ts +11 -0
- package/src/id.ts +783 -0
- package/src/index.ts +631 -0
- package/src/interface.ts +26 -0
- package/src/poa.ts +9 -0
- package/src/utils.ts +111 -0
- package/tests/data/ids.json +30 -0
- package/tests/data/keys.js +2 -0
- package/tests/data/old-ids.json +25 -0
- package/tests/data/test-vectors.json +122 -0
- package/tests/id.test.js +286 -0
- package/tests/index.test.js +335 -0
- package/tests/regression.test.js +28 -0
- package/tests/utils.test.js +27 -0
- package/tsconfig.json +17 -0
package/src/index.ts
ADDED
@@ -0,0 +1,631 @@
|
|
1
|
+
import { BigNumber, BSM, HD, type PublicKey, Signature, ECIES } from "@bsv/sdk";
|
2
|
+
import "node-fetch";
|
3
|
+
|
4
|
+
import { Utils } from "./utils";
|
5
|
+
import { BAP_ID } from "./id";
|
6
|
+
import {
|
7
|
+
ENCRYPTION_PATH,
|
8
|
+
BAP_SERVER,
|
9
|
+
BAP_BITCOM_ADDRESS,
|
10
|
+
BAP_BITCOM_ADDRESS_HEX,
|
11
|
+
AIP_BITCOM_ADDRESS,
|
12
|
+
} from "./constants";
|
13
|
+
import type { Attestation, Identity, PathPrefix } from "./interface";
|
14
|
+
import { Utils as BSVUtils } from "@bsv/sdk";
|
15
|
+
const { toArray, toUTF8, toBase64 } = BSVUtils;
|
16
|
+
const { bitcoreDecrypt, bitcoreEncrypt } = ECIES;
|
17
|
+
|
18
|
+
/**
|
19
|
+
* BAP class
|
20
|
+
*
|
21
|
+
* Creates an instance of the BAP class and uses the given HDPrivateKey for all BAP operations.
|
22
|
+
*
|
23
|
+
* @param HDPrivateKey
|
24
|
+
*/
|
25
|
+
export const BAP = class {
|
26
|
+
#HDPrivateKey;
|
27
|
+
#ids: { [key: string]: BAP_ID } = {};
|
28
|
+
#BAP_SERVER = BAP_SERVER;
|
29
|
+
#BAP_TOKEN = "";
|
30
|
+
#lastIdPath = "";
|
31
|
+
|
32
|
+
constructor(HDPrivateKey: string, token = "") {
|
33
|
+
if (!HDPrivateKey) {
|
34
|
+
throw new Error("No HDPrivateKey given");
|
35
|
+
}
|
36
|
+
this.#HDPrivateKey = HD.fromString(HDPrivateKey);
|
37
|
+
|
38
|
+
if (token) {
|
39
|
+
this.#BAP_TOKEN = token;
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
get lastIdPath(): string {
|
44
|
+
return this.#lastIdPath;
|
45
|
+
}
|
46
|
+
|
47
|
+
/**
|
48
|
+
* Get the public key of the given childPath, or of the current HDPrivateKey of childPath is empty
|
49
|
+
*
|
50
|
+
* @param childPath Full derivation path for this child
|
51
|
+
* @returns {*}
|
52
|
+
*/
|
53
|
+
getPublicKey(childPath = ""): string {
|
54
|
+
if (childPath) {
|
55
|
+
return this.#HDPrivateKey.derive(childPath).pubKey.toString();
|
56
|
+
}
|
57
|
+
|
58
|
+
return this.#HDPrivateKey.pubKey.toString();
|
59
|
+
}
|
60
|
+
|
61
|
+
/**
|
62
|
+
* Get the public key of the given childPath, or of the current HDPrivateKey of childPath is empty
|
63
|
+
*
|
64
|
+
* @param childPath Full derivation path for this child
|
65
|
+
* @returns {*}
|
66
|
+
*/
|
67
|
+
getHdPublicKey(childPath = ""): string {
|
68
|
+
if (childPath) {
|
69
|
+
return this.#HDPrivateKey.derive(childPath).toPublic().toString();
|
70
|
+
}
|
71
|
+
|
72
|
+
return this.#HDPrivateKey.toPublic().toString();
|
73
|
+
}
|
74
|
+
|
75
|
+
set BAP_SERVER(bapServer) {
|
76
|
+
this.#BAP_SERVER = bapServer;
|
77
|
+
Object.keys(this.#ids).forEach((key) => {
|
78
|
+
// @ts-ignore - does not recognize private fields that can be set
|
79
|
+
this.#ids[key].BAP_SERVER = bapServer;
|
80
|
+
});
|
81
|
+
}
|
82
|
+
|
83
|
+
get BAP_SERVER(): string {
|
84
|
+
return this.#BAP_SERVER;
|
85
|
+
}
|
86
|
+
|
87
|
+
set BAP_TOKEN(token) {
|
88
|
+
this.#BAP_TOKEN = token;
|
89
|
+
Object.keys(this.#ids).forEach((key) => {
|
90
|
+
// @ts-ignore - does not recognize private fields that can be set
|
91
|
+
this.#ids[key].BAP_TOKEN = token;
|
92
|
+
});
|
93
|
+
}
|
94
|
+
|
95
|
+
get BAP_TOKEN(): string {
|
96
|
+
return this.#BAP_TOKEN;
|
97
|
+
}
|
98
|
+
|
99
|
+
/**
|
100
|
+
* This function verifies that the given bapId matches the given root address
|
101
|
+
* This is used as a data integrity check
|
102
|
+
*
|
103
|
+
* @param bapId BAP_ID instance
|
104
|
+
*/
|
105
|
+
checkIdBelongs(bapId: BAP_ID): boolean {
|
106
|
+
const derivedChild = this.#HDPrivateKey.derive(bapId.rootPath);
|
107
|
+
const checkRootAddress = derivedChild.pubKey.toAddress();
|
108
|
+
if (checkRootAddress !== bapId.rootAddress) {
|
109
|
+
throw new Error("ID does not belong to this private key");
|
110
|
+
}
|
111
|
+
|
112
|
+
return true;
|
113
|
+
}
|
114
|
+
|
115
|
+
/**
|
116
|
+
* Returns a list of all the identity keys that are stored in this instance
|
117
|
+
*
|
118
|
+
* @returns {string[]}
|
119
|
+
*/
|
120
|
+
listIds(): string[] {
|
121
|
+
return Object.keys(this.#ids);
|
122
|
+
}
|
123
|
+
|
124
|
+
/**
|
125
|
+
* Create a new Id and link it to this BAP instance
|
126
|
+
*
|
127
|
+
* This function uses the length of the #ids of this class to determine the next valid path.
|
128
|
+
* If not all ids related to this HDPrivateKey have been loaded, determine the path externally
|
129
|
+
* and pass it to newId when creating a new ID.
|
130
|
+
*
|
131
|
+
* @param path
|
132
|
+
* @param identityAttributes
|
133
|
+
* @param idSeed
|
134
|
+
* @returns {*}
|
135
|
+
*/
|
136
|
+
newId(path = "", identityAttributes: any = {}, idSeed = ""): BAP_ID {
|
137
|
+
if (!path) {
|
138
|
+
// get next usable path for this key
|
139
|
+
path = this.getNextValidPath();
|
140
|
+
}
|
141
|
+
|
142
|
+
const newIdentity = new BAP_ID(
|
143
|
+
this.#HDPrivateKey,
|
144
|
+
identityAttributes,
|
145
|
+
idSeed,
|
146
|
+
);
|
147
|
+
newIdentity.BAP_SERVER = this.#BAP_SERVER;
|
148
|
+
newIdentity.BAP_TOKEN = this.#BAP_TOKEN;
|
149
|
+
|
150
|
+
newIdentity.rootPath = path;
|
151
|
+
newIdentity.currentPath = Utils.getNextPath(path);
|
152
|
+
|
153
|
+
const idKey = newIdentity.getIdentityKey();
|
154
|
+
this.#ids[idKey] = newIdentity;
|
155
|
+
this.#lastIdPath = path;
|
156
|
+
|
157
|
+
return this.#ids[idKey];
|
158
|
+
}
|
159
|
+
|
160
|
+
/**
|
161
|
+
* Remove identity
|
162
|
+
*
|
163
|
+
* @param idKey
|
164
|
+
* @returns {*}
|
165
|
+
*/
|
166
|
+
removeId(idKey: string): void {
|
167
|
+
delete this.#ids[idKey];
|
168
|
+
}
|
169
|
+
|
170
|
+
/**
|
171
|
+
* Get the next valid path for the used HDPrivateKey and loaded #ids
|
172
|
+
*
|
173
|
+
* @returns {string}
|
174
|
+
*/
|
175
|
+
getNextValidPath(): PathPrefix {
|
176
|
+
// prefer hardened paths
|
177
|
+
if (this.#lastIdPath) {
|
178
|
+
return Utils.getNextIdentityPath(this.#lastIdPath);
|
179
|
+
}
|
180
|
+
|
181
|
+
return `/0'/${Object.keys(this.#ids).length}'/0'`;
|
182
|
+
}
|
183
|
+
|
184
|
+
/**
|
185
|
+
* Get a certain Id
|
186
|
+
*
|
187
|
+
* @param identityKey
|
188
|
+
* @returns {null}
|
189
|
+
*/
|
190
|
+
getId(identityKey: string): BAP_ID | null {
|
191
|
+
return this.#ids[identityKey] || null;
|
192
|
+
}
|
193
|
+
|
194
|
+
/**
|
195
|
+
* This function is used when manipulating ID's, adding or removing attributes etc
|
196
|
+
* First create an id through this class and then use getId to get it. Then you can add/edit or
|
197
|
+
* increment the signing path and then re-set it with this function.
|
198
|
+
*
|
199
|
+
* Note: when you getId() from this class, you will be working on the same object as this class
|
200
|
+
* has and any changes made will be propagated to the id in this class. When you call exportIds
|
201
|
+
* your new changes will also be included, without having to setId().
|
202
|
+
*
|
203
|
+
* @param bapId
|
204
|
+
*/
|
205
|
+
setId(bapId: BAP_ID): void {
|
206
|
+
this.checkIdBelongs(bapId);
|
207
|
+
this.#ids[bapId.getIdentityKey()] = bapId;
|
208
|
+
}
|
209
|
+
|
210
|
+
/**
|
211
|
+
* This function is used to import IDs and attributes from some external storage
|
212
|
+
*
|
213
|
+
* The ID information should NOT be stored together with the HD private key !
|
214
|
+
*
|
215
|
+
* @param idData Array of ids that have been exported
|
216
|
+
* @param encrypted Whether the data should be treated as being encrypted (default true)
|
217
|
+
*/
|
218
|
+
importIds(idData: any, encrypted = true): void {
|
219
|
+
if (encrypted) {
|
220
|
+
// we first need to decrypt the ids array using ECIES
|
221
|
+
|
222
|
+
const derivedChild = this.#HDPrivateKey.derive(ENCRYPTION_PATH);
|
223
|
+
const decrypted = toUTF8(bitcoreDecrypt(
|
224
|
+
toArray(
|
225
|
+
Buffer.from(idData, Utils.isHex(idData) ? "hex" : "base64").toString(
|
226
|
+
"hex",
|
227
|
+
),
|
228
|
+
"hex",
|
229
|
+
),
|
230
|
+
derivedChild.privKey,
|
231
|
+
));
|
232
|
+
idData = JSON.parse(decrypted) as Identity[];
|
233
|
+
}
|
234
|
+
|
235
|
+
let oldFormatImport = false;
|
236
|
+
if (!idData.ids) {
|
237
|
+
// old format id container
|
238
|
+
oldFormatImport = true;
|
239
|
+
idData = {
|
240
|
+
lastIdPath: "",
|
241
|
+
ids: idData,
|
242
|
+
};
|
243
|
+
}
|
244
|
+
|
245
|
+
for (const id of idData.ids as Identity[]) {
|
246
|
+
if (!id.identityKey || !id.identityAttributes || !id.rootAddress) {
|
247
|
+
throw new Error("ID cannot be imported as it is not complete");
|
248
|
+
}
|
249
|
+
const importId = new BAP_ID(this.#HDPrivateKey, {}, id.idSeed);
|
250
|
+
importId.BAP_SERVER = this.#BAP_SERVER;
|
251
|
+
importId.BAP_TOKEN = this.#BAP_TOKEN;
|
252
|
+
importId.import(id);
|
253
|
+
|
254
|
+
this.checkIdBelongs(importId);
|
255
|
+
|
256
|
+
this.#ids[importId.getIdentityKey()] = importId;
|
257
|
+
|
258
|
+
if (oldFormatImport) {
|
259
|
+
// overwrite with the last value on this array
|
260
|
+
idData.lastIdPath = importId.currentPath;
|
261
|
+
}
|
262
|
+
}
|
263
|
+
|
264
|
+
this.#lastIdPath = idData.lastIdPath;
|
265
|
+
}
|
266
|
+
|
267
|
+
/**
|
268
|
+
* Export all the IDs of this instance for external storage
|
269
|
+
*
|
270
|
+
* By default this function will encrypt the data, using a derivative child of the main HD key
|
271
|
+
*
|
272
|
+
* @param encrypted Whether the data should be encrypted (default true)
|
273
|
+
* @returns {[]|*}
|
274
|
+
*/
|
275
|
+
exportIds(encrypted = true): any {
|
276
|
+
const idData = {
|
277
|
+
lastIdPath: this.#lastIdPath,
|
278
|
+
ids: [] as Identity[],
|
279
|
+
};
|
280
|
+
|
281
|
+
for (const key of Object.keys(this.#ids)) {
|
282
|
+
idData.ids.push(this.#ids[key].export());
|
283
|
+
}
|
284
|
+
|
285
|
+
if (encrypted) {
|
286
|
+
const derivedChild = this.#HDPrivateKey.derive(ENCRYPTION_PATH);
|
287
|
+
return toBase64(bitcoreEncrypt(toArray(JSON.stringify(idData), 'utf8'), derivedChild.pubKey))
|
288
|
+
}
|
289
|
+
|
290
|
+
return idData;
|
291
|
+
}
|
292
|
+
|
293
|
+
/**
|
294
|
+
* Encrypt a string of data
|
295
|
+
*
|
296
|
+
* @param string
|
297
|
+
* @returns {string}
|
298
|
+
*/
|
299
|
+
encrypt(string: string): string {
|
300
|
+
|
301
|
+
const derivedChild = this.#HDPrivateKey.derive(ENCRYPTION_PATH);
|
302
|
+
return toBase64(bitcoreEncrypt(toArray(string, 'utf8'), derivedChild.pubKey))
|
303
|
+
}
|
304
|
+
|
305
|
+
/**
|
306
|
+
* Decrypt a string of data
|
307
|
+
*
|
308
|
+
* @param string
|
309
|
+
* @returns {string}
|
310
|
+
*/
|
311
|
+
decrypt(string: string): string {
|
312
|
+
const derivedChild = this.#HDPrivateKey.derive(ENCRYPTION_PATH);
|
313
|
+
return toUTF8(bitcoreDecrypt(toArray(string, 'base64'), derivedChild.privKey));
|
314
|
+
}
|
315
|
+
|
316
|
+
/**
|
317
|
+
* Sign an attestation for a user
|
318
|
+
*
|
319
|
+
* @param attestationHash The computed attestation hash for the user - this should be calculated with the BAP_ID class for an identity for the user
|
320
|
+
* @param identityKey The identity key we are using for the signing
|
321
|
+
* @param counter
|
322
|
+
* @param dataString Optional data string that will be appended to the BAP attestation
|
323
|
+
* @returns {string[]}
|
324
|
+
*/
|
325
|
+
signAttestationWithAIP(
|
326
|
+
attestationHash: string,
|
327
|
+
identityKey: string,
|
328
|
+
counter = 0,
|
329
|
+
dataString = "",
|
330
|
+
) {
|
331
|
+
const id = this.getId(identityKey);
|
332
|
+
if (!id) {
|
333
|
+
throw new Error("Could not find identity to attest with");
|
334
|
+
}
|
335
|
+
|
336
|
+
const attestationBuffer = this.getAttestationBuffer(
|
337
|
+
attestationHash,
|
338
|
+
counter,
|
339
|
+
dataString,
|
340
|
+
);
|
341
|
+
const { address, signature } = id.signMessage(attestationBuffer);
|
342
|
+
|
343
|
+
return this.createAttestationTransaction(
|
344
|
+
attestationHash,
|
345
|
+
counter,
|
346
|
+
address,
|
347
|
+
signature,
|
348
|
+
dataString,
|
349
|
+
);
|
350
|
+
}
|
351
|
+
|
352
|
+
/**
|
353
|
+
* Verify an AIP signed attestation for a user
|
354
|
+
*
|
355
|
+
* [
|
356
|
+
* '0x6a',
|
357
|
+
* '0x31424150537561506e66476e53424d33474c56397968785564596534764762644d54',
|
358
|
+
* '0x415454455354',
|
359
|
+
* '0x33656166366361396334313936356538353831366439336439643034333136393032376633396661623034386333633031333663343364663635376462383761',
|
360
|
+
* '0x30',
|
361
|
+
* '0x7c',
|
362
|
+
* '0x313550636948473232534e4c514a584d6f5355615756693757537163376843667661',
|
363
|
+
* '0x424954434f494e5f4543445341',
|
364
|
+
* '0x31477531796d52567a595557634638776f6f506a7a4a4c764d383550795a64655876',
|
365
|
+
* '0x20ef60c5555001ddb1039bb0f215e46571fcb39ee46f48b089d1c08b0304dbcb3366d8fdf8bafd82be24b5ac42dcd6a5e96c90705dd42e3ad918b1b47ac3ce6ac2'
|
366
|
+
* ]
|
367
|
+
*
|
368
|
+
* @param tx Array of hex values for the OP_RETURN values
|
369
|
+
* @returns {{}}
|
370
|
+
*/
|
371
|
+
verifyAttestationWithAIP(tx: string[]): Attestation {
|
372
|
+
if (
|
373
|
+
!Array.isArray(tx) ||
|
374
|
+
tx[0] !== "0x6a" ||
|
375
|
+
tx[1] !== BAP_BITCOM_ADDRESS_HEX
|
376
|
+
) {
|
377
|
+
throw new Error("Not a valid BAP transaction");
|
378
|
+
}
|
379
|
+
|
380
|
+
const dataOffset = tx[7] === "0x44415441" ? 5 : 0; // DATA
|
381
|
+
const attestation: Attestation = {
|
382
|
+
type: Utils.hexDecode(tx[2]),
|
383
|
+
hash: Utils.hexDecode(tx[3]),
|
384
|
+
sequence: Utils.hexDecode(tx[4]),
|
385
|
+
signingProtocol: Utils.hexDecode(tx[7 + dataOffset]),
|
386
|
+
signingAddress: Utils.hexDecode(tx[8 + dataOffset]),
|
387
|
+
signature: Utils.hexDecode(tx[9 + dataOffset], "base64"),
|
388
|
+
};
|
389
|
+
|
390
|
+
if (dataOffset && tx[3] === tx[8]) {
|
391
|
+
// valid data addition
|
392
|
+
attestation.data = Utils.hexDecode(tx[9]);
|
393
|
+
}
|
394
|
+
|
395
|
+
try {
|
396
|
+
const signatureBufferStatements = [];
|
397
|
+
for (let i = 0; i < 6 + dataOffset; i++) {
|
398
|
+
signatureBufferStatements.push(
|
399
|
+
Buffer.from(tx[i].replace("0x", ""), "hex"),
|
400
|
+
);
|
401
|
+
}
|
402
|
+
const attestationBuffer = Buffer.concat([...signatureBufferStatements]);
|
403
|
+
attestation.verified = this.verifySignature(
|
404
|
+
attestationBuffer,
|
405
|
+
attestation.signingAddress,
|
406
|
+
attestation.signature,
|
407
|
+
);
|
408
|
+
} catch (e) {
|
409
|
+
attestation.verified = false;
|
410
|
+
}
|
411
|
+
|
412
|
+
return attestation;
|
413
|
+
}
|
414
|
+
|
415
|
+
/**
|
416
|
+
* For BAP attestations we use all fields for the attestation
|
417
|
+
*
|
418
|
+
* @param attestationHash
|
419
|
+
* @param counter
|
420
|
+
* @param address
|
421
|
+
* @param signature
|
422
|
+
* @param dataString Optional data string that will be appended to the BAP attestation
|
423
|
+
* @returns {[string]}
|
424
|
+
*/
|
425
|
+
createAttestationTransaction(
|
426
|
+
attestationHash: string,
|
427
|
+
counter: number,
|
428
|
+
address: string,
|
429
|
+
signature: string,
|
430
|
+
dataString = "",
|
431
|
+
): string[] {
|
432
|
+
const transaction = ["0x6a", Utils.hexEncode(BAP_BITCOM_ADDRESS)];
|
433
|
+
transaction.push(Utils.hexEncode("ATTEST"));
|
434
|
+
transaction.push(Utils.hexEncode(attestationHash));
|
435
|
+
transaction.push(Utils.hexEncode(`${counter}`));
|
436
|
+
transaction.push("0x7c"); // |
|
437
|
+
if (dataString) {
|
438
|
+
// data should be a string, either encrypted or stringified JSON if applicable
|
439
|
+
transaction.push(Utils.hexEncode(BAP_BITCOM_ADDRESS));
|
440
|
+
transaction.push(Utils.hexEncode("DATA"));
|
441
|
+
transaction.push(Utils.hexEncode(attestationHash));
|
442
|
+
transaction.push(Utils.hexEncode(dataString));
|
443
|
+
transaction.push("0x7c"); // |
|
444
|
+
}
|
445
|
+
transaction.push(Utils.hexEncode(AIP_BITCOM_ADDRESS));
|
446
|
+
transaction.push(Utils.hexEncode("BITCOIN_ECDSA"));
|
447
|
+
transaction.push(Utils.hexEncode(address));
|
448
|
+
transaction.push(`0x${Buffer.from(signature, "base64").toString("hex")}`);
|
449
|
+
|
450
|
+
return transaction;
|
451
|
+
}
|
452
|
+
|
453
|
+
/**
|
454
|
+
* This is a re-creation of how the bitcoinfiles-sdk creates a hash to sign for AIP
|
455
|
+
*
|
456
|
+
* @param attestationHash
|
457
|
+
* @param counter
|
458
|
+
* @param dataString Optional data string
|
459
|
+
* @returns {Buffer}
|
460
|
+
*/
|
461
|
+
getAttestationBuffer(
|
462
|
+
attestationHash: string,
|
463
|
+
counter = 0,
|
464
|
+
dataString = "",
|
465
|
+
): Buffer {
|
466
|
+
// re-create how AIP creates the buffer to sign
|
467
|
+
let dataStringBuffer = Buffer.from("");
|
468
|
+
if (dataString) {
|
469
|
+
dataStringBuffer = Buffer.concat([
|
470
|
+
Buffer.from(BAP_BITCOM_ADDRESS),
|
471
|
+
Buffer.from("DATA"),
|
472
|
+
Buffer.from(attestationHash),
|
473
|
+
Buffer.from(dataString),
|
474
|
+
Buffer.from("7c", "hex"),
|
475
|
+
]);
|
476
|
+
}
|
477
|
+
return Buffer.concat([
|
478
|
+
Buffer.from("6a", "hex"), // OP_RETURN
|
479
|
+
Buffer.from(BAP_BITCOM_ADDRESS),
|
480
|
+
Buffer.from("ATTEST"),
|
481
|
+
Buffer.from(attestationHash),
|
482
|
+
Buffer.from(`${counter}`),
|
483
|
+
Buffer.from("7c", "hex"),
|
484
|
+
dataStringBuffer,
|
485
|
+
]);
|
486
|
+
}
|
487
|
+
|
488
|
+
/**
|
489
|
+
* Verify that the identity challenge is signed by the address
|
490
|
+
*
|
491
|
+
* @param message Buffer or utf-8 string
|
492
|
+
* @param address Bitcoin address of signee
|
493
|
+
* @param signature Signature base64 string
|
494
|
+
*
|
495
|
+
* @return boolean
|
496
|
+
*/
|
497
|
+
verifySignature(
|
498
|
+
message: string | Buffer,
|
499
|
+
address: string,
|
500
|
+
signature: string,
|
501
|
+
): boolean {
|
502
|
+
// check the signature against the challenge
|
503
|
+
const messageBuffer = Buffer.isBuffer(message)
|
504
|
+
? message
|
505
|
+
: Buffer.from(message);
|
506
|
+
const sig = Signature.fromCompact(signature, "base64");
|
507
|
+
let publicKey: PublicKey | undefined;
|
508
|
+
const msg = toArray(messageBuffer.toString("hex"), "hex");
|
509
|
+
for (let recovery = 0; recovery < 4; recovery++) {
|
510
|
+
try {
|
511
|
+
publicKey = sig.RecoverPublicKey(
|
512
|
+
recovery,
|
513
|
+
new BigNumber(BSM.magicHash(msg)),
|
514
|
+
);
|
515
|
+
const sigFitsPubkey = BSM.verify(msg, sig, publicKey);
|
516
|
+
if (sigFitsPubkey && publicKey.toAddress() === address) {
|
517
|
+
return true;
|
518
|
+
}
|
519
|
+
} catch (e) {
|
520
|
+
// try next recovery
|
521
|
+
}
|
522
|
+
}
|
523
|
+
return false;
|
524
|
+
}
|
525
|
+
|
526
|
+
/**
|
527
|
+
* Check whether the given transaction (BAP OP_RETURN) is valid, is signed and that the
|
528
|
+
* identity signing is also valid at the time of signing
|
529
|
+
*
|
530
|
+
* @param idKey
|
531
|
+
* @param address
|
532
|
+
* @param challenge
|
533
|
+
* @param signature
|
534
|
+
*
|
535
|
+
* @returns {Promise<boolean|*>}
|
536
|
+
*/
|
537
|
+
async verifyChallengeSignature(
|
538
|
+
idKey: string,
|
539
|
+
address: string,
|
540
|
+
challenge: string,
|
541
|
+
signature: string,
|
542
|
+
): Promise<boolean> {
|
543
|
+
// first we test locally before sending to server
|
544
|
+
if (this.verifySignature(challenge, address, signature)) {
|
545
|
+
const result = await this.getApiData("/attestation/valid", {
|
546
|
+
idKey,
|
547
|
+
challenge,
|
548
|
+
signature,
|
549
|
+
});
|
550
|
+
return result.data;
|
551
|
+
}
|
552
|
+
|
553
|
+
return false;
|
554
|
+
}
|
555
|
+
|
556
|
+
/**
|
557
|
+
* Check whether the given transaction (BAP OP_RETURN) is valid, is signed and that the
|
558
|
+
* identity signing is also valid at the time of signing
|
559
|
+
*
|
560
|
+
* @param tx
|
561
|
+
* @returns {Promise<boolean|*>}
|
562
|
+
*/
|
563
|
+
async isValidAttestationTransaction(tx: string[]): Promise<any> {
|
564
|
+
// first we test locally before sending to server
|
565
|
+
if (this.verifyAttestationWithAIP(tx)) {
|
566
|
+
return this.getApiData("/attestation/valid", {
|
567
|
+
tx,
|
568
|
+
});
|
569
|
+
}
|
570
|
+
|
571
|
+
return false;
|
572
|
+
}
|
573
|
+
|
574
|
+
/**
|
575
|
+
* Get all signing keys for the given idKey
|
576
|
+
*
|
577
|
+
* @param address
|
578
|
+
* @returns {Promise<*>}
|
579
|
+
*/
|
580
|
+
async getIdentityFromAddress(address: string): Promise<any> {
|
581
|
+
return this.getApiData("/identity/from-address", {
|
582
|
+
address,
|
583
|
+
});
|
584
|
+
}
|
585
|
+
|
586
|
+
/**
|
587
|
+
* Get all signing keys for the given idKey
|
588
|
+
*
|
589
|
+
* @param idKey
|
590
|
+
* @returns {Promise<*>}
|
591
|
+
*/
|
592
|
+
async getIdentity(idKey: string): Promise<any> {
|
593
|
+
return this.getApiData("/identity", {
|
594
|
+
idKey,
|
595
|
+
});
|
596
|
+
}
|
597
|
+
|
598
|
+
/**
|
599
|
+
* Get all attestations for the given attestation hash
|
600
|
+
*
|
601
|
+
* @param attestationHash
|
602
|
+
*/
|
603
|
+
async getAttestationsForHash(attestationHash: string): Promise<any> {
|
604
|
+
// get all BAP ATTEST records for the given attestationHash
|
605
|
+
return this.getApiData("/attestations", {
|
606
|
+
hash: attestationHash,
|
607
|
+
});
|
608
|
+
}
|
609
|
+
|
610
|
+
/**
|
611
|
+
* Helper function to get attestation from a BAP API server
|
612
|
+
*
|
613
|
+
* @param apiUrl
|
614
|
+
* @param apiData
|
615
|
+
* @returns {Promise<any>}
|
616
|
+
*/
|
617
|
+
async getApiData(apiUrl: string, apiData: any): Promise<any> {
|
618
|
+
const url = `${this.#BAP_SERVER}${apiUrl}`;
|
619
|
+
const response = await fetch(url, {
|
620
|
+
method: "post",
|
621
|
+
headers: {
|
622
|
+
"Content-type": "application/json; charset=utf-8",
|
623
|
+
token: this.#BAP_TOKEN,
|
624
|
+
format: "json",
|
625
|
+
},
|
626
|
+
body: JSON.stringify(apiData),
|
627
|
+
});
|
628
|
+
|
629
|
+
return response.json();
|
630
|
+
}
|
631
|
+
};
|
package/src/interface.ts
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
|
2
|
+
export interface Identity {
|
3
|
+
name: string;
|
4
|
+
description: string;
|
5
|
+
identityKey: string;
|
6
|
+
rootPath: string;
|
7
|
+
rootAddress: string;
|
8
|
+
previousPath: string;
|
9
|
+
currentPath: string;
|
10
|
+
lastIdPath: string;
|
11
|
+
idSeed: string;
|
12
|
+
identityAttributes: any;
|
13
|
+
}
|
14
|
+
|
15
|
+
export type PathPrefix = `/${number}/${number}/${number}` | `/${number}'/${number}'/${number}'`
|
16
|
+
|
17
|
+
export interface Attestation {
|
18
|
+
type: string;
|
19
|
+
hash: string;
|
20
|
+
sequence: string;
|
21
|
+
signingProtocol: string;
|
22
|
+
signingAddress: string;
|
23
|
+
signature: string;
|
24
|
+
data?: string;
|
25
|
+
verified?: boolean;
|
26
|
+
}
|