@bsv/sdk 1.3.10 → 1.3.11

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 (41) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/auth/certificates/Certificate.js +1 -1
  3. package/dist/cjs/src/auth/certificates/Certificate.js.map +1 -1
  4. package/dist/cjs/src/auth/certificates/MasterCertificate.js +93 -63
  5. package/dist/cjs/src/auth/certificates/MasterCertificate.js.map +1 -1
  6. package/dist/cjs/src/auth/certificates/VerifiableCertificate.js +2 -2
  7. package/dist/cjs/src/auth/certificates/VerifiableCertificate.js.map +1 -1
  8. package/dist/cjs/src/auth/utils/getVerifiableCertificates.js +1 -1
  9. package/dist/cjs/src/auth/utils/getVerifiableCertificates.js.map +1 -1
  10. package/dist/cjs/src/auth/utils/validateCertificates.js +1 -1
  11. package/dist/cjs/src/auth/utils/validateCertificates.js.map +1 -1
  12. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  13. package/dist/esm/src/auth/certificates/Certificate.js +2 -2
  14. package/dist/esm/src/auth/certificates/Certificate.js.map +1 -1
  15. package/dist/esm/src/auth/certificates/MasterCertificate.js +93 -63
  16. package/dist/esm/src/auth/certificates/MasterCertificate.js.map +1 -1
  17. package/dist/esm/src/auth/certificates/VerifiableCertificate.js +2 -2
  18. package/dist/esm/src/auth/certificates/VerifiableCertificate.js.map +1 -1
  19. package/dist/esm/src/auth/utils/getVerifiableCertificates.js +1 -1
  20. package/dist/esm/src/auth/utils/getVerifiableCertificates.js.map +1 -1
  21. package/dist/esm/src/auth/utils/validateCertificates.js +1 -1
  22. package/dist/esm/src/auth/utils/validateCertificates.js.map +1 -1
  23. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  24. package/dist/types/src/auth/certificates/Certificate.d.ts +3 -3
  25. package/dist/types/src/auth/certificates/Certificate.d.ts.map +1 -1
  26. package/dist/types/src/auth/certificates/MasterCertificate.d.ts +41 -11
  27. package/dist/types/src/auth/certificates/MasterCertificate.d.ts.map +1 -1
  28. package/dist/types/src/auth/certificates/VerifiableCertificate.d.ts +1 -1
  29. package/dist/types/src/auth/certificates/VerifiableCertificate.d.ts.map +1 -1
  30. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  31. package/dist/umd/bundle.js +1 -1
  32. package/docs/auth.md +59 -21
  33. package/package.json +1 -1
  34. package/src/auth/__tests/Peer.test.ts +19 -47
  35. package/src/auth/certificates/Certificate.ts +3 -3
  36. package/src/auth/certificates/MasterCertificate.ts +131 -67
  37. package/src/auth/certificates/VerifiableCertificate.ts +3 -4
  38. package/src/auth/certificates/__tests/MasterCertificate.test.ts +142 -51
  39. package/src/auth/certificates/__tests/VerifiableCertificate.test.ts +25 -30
  40. package/src/auth/utils/getVerifiableCertificates.ts +2 -2
  41. package/src/auth/utils/validateCertificates.ts +2 -2
package/docs/auth.md CHANGED
@@ -212,7 +212,7 @@ export default class Certificate {
212
212
  subject: PubKeyHex;
213
213
  certifier: PubKeyHex;
214
214
  revocationOutpoint: OutpointString;
215
- fields: Record<CertificateFieldNameUnder50Bytes, string>;
215
+ fields: Record<CertificateFieldNameUnder50Bytes, Base64String>;
216
216
  signature?: HexString;
217
217
  constructor(type: Base64String, serialNumber: Base64String, subject: PubKeyHex, certifier: PubKeyHex, revocationOutpoint: OutpointString, fields: Record<CertificateFieldNameUnder50Bytes, string>, signature?: HexString)
218
218
  toBinary(includeSignature: boolean = true): number[]
@@ -269,12 +269,12 @@ See also: [PubKeyHex](#type-pubkeyhex)
269
269
 
270
270
  #### Property fields
271
271
 
272
- All the fields present in the certificate, with field names as keys and field values as strings.
272
+ All the fields present in the certificate, with field names as keys and encrypted field values as Base64 strings.
273
273
 
274
274
  ```ts
275
- fields: Record<CertificateFieldNameUnder50Bytes, string>
275
+ fields: Record<CertificateFieldNameUnder50Bytes, Base64String>
276
276
  ```
277
- See also: [CertificateFieldNameUnder50Bytes](#type-certificatefieldnameunder50bytes)
277
+ See also: [Base64String](#type-base64string), [CertificateFieldNameUnder50Bytes](#type-certificatefieldnameunder50bytes)
278
278
 
279
279
  #### Property revocationOutpoint
280
280
 
@@ -466,9 +466,10 @@ export class MasterCertificate extends Certificate {
466
466
  declare signature?: HexString;
467
467
  masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>;
468
468
  constructor(type: Base64String, serialNumber: Base64String, subject: PubKeyHex, certifier: PubKeyHex, revocationOutpoint: OutpointString, fields: Record<CertificateFieldNameUnder50Bytes, Base64String>, masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>, signature?: HexString)
469
- async decryptFields(subjectWallet: WalletInterface): Promise<Record<CertificateFieldNameUnder50Bytes, string>>
470
- async createKeyringForVerifier(subjectWallet: WalletInterface, verifier: WalletCounterparty, fieldsToReveal: string[], originator?: string): Promise<Record<CertificateFieldNameUnder50Bytes, string>>
471
- static async issueCertificateForSubject(certifierWallet: WalletInterface, subject: WalletCounterparty, fields: Record<CertificateFieldNameUnder50Bytes, string>, certificateType: string, getRevocationOutpoint = async (serialNumber: string): Promise<string> => { return "Certificate revocation not tracked."; }): Promise<MasterCertificate>
469
+ static async createCertificateFields(creatorWallet: WalletInterface, certifierOrSubject: WalletCounterparty, fields: Record<CertificateFieldNameUnder50Bytes, string>): Promise<CreateCertificateFieldsResult>
470
+ static async createKeyringForVerifier(subjectWallet: WalletInterface, certifier: WalletCounterparty, verifier: WalletCounterparty, fields: Record<CertificateFieldNameUnder50Bytes, Base64String>, fieldsToReveal: string[], masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>, serialNumber: Base64String, originator?: string): Promise<Record<CertificateFieldNameUnder50Bytes, string>>
471
+ static async issueCertificateForSubject(certifierWallet: WalletInterface, subject: WalletCounterparty, fields: Record<CertificateFieldNameUnder50Bytes, string>, certificateType: string, getRevocationOutpoint = async (serialNumber: string): Promise<string> => { return "Certificate revocation not tracked."; }, serialNumber?: string): Promise<MasterCertificate>
472
+ static async decryptFields(subjectOrCertifierWallet: WalletInterface, masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>, fields: Record<CertificateFieldNameUnder50Bytes, Base64String>, counterparty: WalletCounterparty): Promise<Record<CertificateFieldNameUnder50Bytes, string>>
472
473
  }
473
474
  ```
474
475
 
@@ -478,6 +479,34 @@ See also: [Base64String](#type-base64string), [Certificate](#class-certificate),
478
479
 
479
480
  <summary>Class MasterCertificate Details</summary>
480
481
 
482
+ #### Method createCertificateFields
483
+
484
+ Encrypts certificate fields for a subject and generates a master keyring.
485
+ This method returns a master keyring tied to a specific certifier or subject who will validate
486
+ and sign off on the fields, along with the encrypted certificate fields.
487
+
488
+ ```ts
489
+ static async createCertificateFields(creatorWallet: WalletInterface, certifierOrSubject: WalletCounterparty, fields: Record<CertificateFieldNameUnder50Bytes, string>): Promise<CreateCertificateFieldsResult>
490
+ ```
491
+ See also: [CertificateFieldNameUnder50Bytes](#type-certificatefieldnameunder50bytes), [WalletCounterparty](#type-walletcounterparty), [WalletInterface](#interface-walletinterface)
492
+
493
+ Returns
494
+
495
+ A promise resolving to an object containing:
496
+ - `certificateFields` {Record<CertificateFieldNameUnder50Bytes, Base64String>}:
497
+ The encrypted certificate fields.
498
+ - `masterKeyring` {Record<CertificateFieldNameUnder50Bytes, Base64String>}:
499
+ The master keyring containing encrypted revelation keys for each field.
500
+
501
+ Argument Details
502
+
503
+ + **creatorWallet**
504
+ + The wallet of the creator responsible for encrypting the fields.
505
+ + **certifierOrSubject**
506
+ + The certifier or subject who will validate the certificate fields.
507
+ + **fields**
508
+ + A record of certificate field names (under 50 bytes) mapped to their values.
509
+
481
510
  #### Method createKeyringForVerifier
482
511
 
483
512
  Creates a keyring for a verifier, enabling them to decrypt specific certificate fields.
@@ -486,9 +515,9 @@ for the verifier's identity key. The result is a keyring containing the keys nec
486
515
  for the verifier to access the designated fields.
487
516
 
488
517
  ```ts
489
- async createKeyringForVerifier(subjectWallet: WalletInterface, verifier: WalletCounterparty, fieldsToReveal: string[], originator?: string): Promise<Record<CertificateFieldNameUnder50Bytes, string>>
518
+ static async createKeyringForVerifier(subjectWallet: WalletInterface, certifier: WalletCounterparty, verifier: WalletCounterparty, fields: Record<CertificateFieldNameUnder50Bytes, Base64String>, fieldsToReveal: string[], masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>, serialNumber: Base64String, originator?: string): Promise<Record<CertificateFieldNameUnder50Bytes, string>>
490
519
  ```
491
- See also: [CertificateFieldNameUnder50Bytes](#type-certificatefieldnameunder50bytes), [WalletCounterparty](#type-walletcounterparty), [WalletInterface](#interface-walletinterface)
520
+ See also: [Base64String](#type-base64string), [CertificateFieldNameUnder50Bytes](#type-certificatefieldnameunder50bytes), [WalletCounterparty](#type-walletcounterparty), [WalletInterface](#interface-walletinterface)
492
521
 
493
522
  Returns
494
523
 
@@ -514,24 +543,33 @@ Throws an error if:
514
543
 
515
544
  #### Method decryptFields
516
545
 
517
- Decrypts all fields in the MasterCertificate using the subject's wallet.
546
+ Decrypts all fields in the MasterCertificate using the subject's or certifier's wallet.
518
547
 
519
- This method uses the `masterKeyring` to decrypt each field's encryption key and then
520
- decrypts the field values. The result is a record of plaintext field names and values.
548
+ This method allows the subject or certifier to decrypt the `masterKeyring` and retrieve
549
+ the encryption keys for each field, which are then used to decrypt the corresponding field values.
550
+ The counterparty used for decryption depends on how the certificate fields were created:
551
+ - If the certificate is self-signed, the counterparty should be set to 'self'.
552
+ - Otherwise, the counterparty should always be the other party involved in the certificate issuance process (the subject or certifier).
521
553
 
522
554
  ```ts
523
- async decryptFields(subjectWallet: WalletInterface): Promise<Record<CertificateFieldNameUnder50Bytes, string>>
555
+ static async decryptFields(subjectOrCertifierWallet: WalletInterface, masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>, fields: Record<CertificateFieldNameUnder50Bytes, Base64String>, counterparty: WalletCounterparty): Promise<Record<CertificateFieldNameUnder50Bytes, string>>
524
556
  ```
525
- See also: [CertificateFieldNameUnder50Bytes](#type-certificatefieldnameunder50bytes), [WalletInterface](#interface-walletinterface)
557
+ See also: [Base64String](#type-base64string), [CertificateFieldNameUnder50Bytes](#type-certificatefieldnameunder50bytes), [WalletCounterparty](#type-walletcounterparty), [WalletInterface](#interface-walletinterface)
526
558
 
527
559
  Returns
528
560
 
529
- - A record of field names and their decrypted values in plaintext.
561
+ A promise resolving to a record of field names and their decrypted values in plaintext.
530
562
 
531
563
  Argument Details
532
564
 
533
- + **subjectWallet**
534
- + The wallet of the subject, used to decrypt the master keyring and field values.
565
+ + **subjectOrCertifierWallet**
566
+ + The wallet of the subject or certifier, used to decrypt the master keyring and field values.
567
+ + **masterKeyring**
568
+ + A record containing encrypted keys for each field.
569
+ + **fields**
570
+ + A record of encrypted field names and their values.
571
+ + **counterparty**
572
+ + The counterparty responsible for creating or signing the certificate. For self-signed certificates, use 'self'.
535
573
 
536
574
  Throws
537
575
 
@@ -547,7 +585,7 @@ generated symmetric key, which is then encrypted for the subject. The certificat
547
585
  can also includes a revocation outpoint to manage potential revocation.
548
586
 
549
587
  ```ts
550
- static async issueCertificateForSubject(certifierWallet: WalletInterface, subject: WalletCounterparty, fields: Record<CertificateFieldNameUnder50Bytes, string>, certificateType: string, getRevocationOutpoint = async (serialNumber: string): Promise<string> => { return "Certificate revocation not tracked."; }): Promise<MasterCertificate>
588
+ static async issueCertificateForSubject(certifierWallet: WalletInterface, subject: WalletCounterparty, fields: Record<CertificateFieldNameUnder50Bytes, string>, certificateType: string, getRevocationOutpoint = async (serialNumber: string): Promise<string> => { return "Certificate revocation not tracked."; }, serialNumber?: string): Promise<MasterCertificate>
551
589
  ```
552
590
  See also: [CertificateFieldNameUnder50Bytes](#type-certificatefieldnameunder50bytes), [MasterCertificate](#class-mastercertificate), [WalletCounterparty](#type-walletcounterparty), [WalletInterface](#interface-walletinterface)
553
591
 
@@ -1078,7 +1116,7 @@ export class VerifiableCertificate extends Certificate {
1078
1116
  declare signature?: HexString;
1079
1117
  keyring: Record<CertificateFieldNameUnder50Bytes, string>;
1080
1118
  decryptedFields?: Record<CertificateFieldNameUnder50Bytes, Base64String>;
1081
- constructor(type: Base64String, serialNumber: Base64String, subject: PubKeyHex, certifier: PubKeyHex, revocationOutpoint: OutpointString, fields: Record<CertificateFieldNameUnder50Bytes, string>, signature?: HexString, keyring?: Record<CertificateFieldNameUnder50Bytes, string>, decryptedFields?: Record<CertificateFieldNameUnder50Bytes, Base64String>)
1119
+ constructor(type: Base64String, serialNumber: Base64String, subject: PubKeyHex, certifier: PubKeyHex, revocationOutpoint: OutpointString, fields: Record<CertificateFieldNameUnder50Bytes, string>, keyring: Record<CertificateFieldNameUnder50Bytes, string>, signature?: HexString, decryptedFields?: Record<CertificateFieldNameUnder50Bytes, Base64String>)
1082
1120
  async decryptFields(verifierWallet: WalletInterface): Promise<Record<CertificateFieldNameUnder50Bytes, string>>
1083
1121
  }
1084
1122
  ```
@@ -1214,7 +1252,7 @@ getVerifiableCertificates = async (wallet: WalletInterface, requestedCertificate
1214
1252
  fieldsToReveal: requestedCertificates.types[certificate.type],
1215
1253
  verifier: verifierIdentityKey
1216
1254
  });
1217
- return new VerifiableCertificate(certificate.type, certificate.serialNumber, certificate.subject, certificate.certifier, certificate.revocationOutpoint, certificate.fields, certificate.signature, keyringForVerifier);
1255
+ return new VerifiableCertificate(certificate.type, certificate.serialNumber, certificate.subject, certificate.certifier, certificate.revocationOutpoint, certificate.fields, keyringForVerifier, certificate.signature);
1218
1256
  }));
1219
1257
  }
1220
1258
  ```
@@ -1232,7 +1270,7 @@ validateCertificates = async (verifierWallet: WalletInterface, message: AuthMess
1232
1270
  if (incomingCert.subject !== message.identityKey) {
1233
1271
  throw new Error(`The subject of one of your certificates ("${incomingCert.subject}") is not the same as the request sender ("${message.identityKey}").`);
1234
1272
  }
1235
- const certToVerify = new VerifiableCertificate(incomingCert.type, incomingCert.serialNumber, incomingCert.subject, incomingCert.certifier, incomingCert.revocationOutpoint, incomingCert.fields, incomingCert.signature, incomingCert.keyring);
1273
+ const certToVerify = new VerifiableCertificate(incomingCert.type, incomingCert.serialNumber, incomingCert.subject, incomingCert.certifier, incomingCert.revocationOutpoint, incomingCert.fields, incomingCert.keyring, incomingCert.signature);
1236
1274
  const isValidCert = await certToVerify.verify();
1237
1275
  if (!isValidCert) {
1238
1276
  throw new Error(`The signature for the certificate with serial number ${certToVerify.serialNumber} is invalid!`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/sdk",
3
- "version": "1.3.10",
3
+ "version": "1.3.11",
4
4
  "type": "module",
5
5
  "description": "BSV Blockchain Software Development Kit",
6
6
  "main": "dist/cjs/mod.js",
@@ -7,44 +7,8 @@ import { Utils, PrivateKey, SymmetricKey } from '../../../dist/cjs/src/primitive
7
7
  import { VerifiableCertificate, } from "../../../dist/cjs/src/auth/certificates/VerifiableCertificate.js"
8
8
  import { MasterCertificate } from '../../../dist/cjs/src/auth/certificates/MasterCertificate.js'
9
9
  import { getVerifiableCertificates } from '../../../dist/cjs/src/auth/utils/getVerifiableCertificates.js'
10
- import { Certificate } from "../../../dist/cjs/src/auth/certificates/index.js"
11
10
  jest.mock('../../../dist/cjs/src/auth/utils/getVerifiableCertificates.js')
12
11
 
13
- /**
14
- * A helper function to decrypt a VerifiableCertificate's fields using the provided wallets.
15
- */
16
- async function decryptCertificateFields(
17
- cert: VerifiableCertificate,
18
- localWallet: Wallet,
19
- counterpartyWallet: Wallet
20
- ): Promise<Record<string, string>> {
21
- const entries = await Promise.all(
22
- Object.entries(cert.keyring).map(async ([fieldName, encryptedKey]) => {
23
- // Decrypt the per-field symmetric key
24
- const { plaintext: masterFieldKey } = await localWallet.decrypt({
25
- ciphertext: Utils.toArray(encryptedKey, 'base64'),
26
- ...Certificate.getCertificateFieldEncryptionDetails(cert.serialNumber, fieldName),
27
- counterparty: (await counterpartyWallet.getPublicKey({ identityKey: true })).publicKey,
28
- })
29
-
30
- // Decrypt the actual field contents using the decrypted symmetric key
31
- try {
32
- const decryptedData = new SymmetricKey(masterFieldKey).decrypt(
33
- Utils.toArray(cert.fields[fieldName], 'base64')
34
- )
35
- return { key: fieldName, value: Utils.toUTF8(decryptedData as number[]) }
36
- } catch (_) {
37
- throw new Error(`Decryption of the "${fieldName}" field with its revelation key failed.`)
38
- }
39
- })
40
- )
41
-
42
- return entries.reduce((acc, { key, value }) => {
43
- acc[key] = value
44
- return acc
45
- }, {} as Record<string, string>)
46
- }
47
-
48
12
  class LocalTransport implements Transport {
49
13
  private peerTransport?: LocalTransport
50
14
  private onDataCallback?: (message: AuthMessage) => void
@@ -117,7 +81,15 @@ describe('Peer class mutual authentication and certificate exchange', () => {
117
81
  ): Promise<VerifiableCertificate> {
118
82
  const certifierWallet = new ProtoWallet(certifierPrivateKey)
119
83
 
120
- const keyringForVerifier = await masterCertificate.createKeyringForVerifier(wallet, verifierIdentityKey, fieldsToReveal)
84
+ const keyringForVerifier = await MasterCertificate.createKeyringForVerifier(
85
+ wallet,
86
+ certifierWallet.keyDeriver.identityKey,
87
+ verifierIdentityKey,
88
+ masterCertificate.fields,
89
+ fieldsToReveal,
90
+ masterCertificate.masterKeyring,
91
+ masterCertificate.serialNumber
92
+ )
121
93
  return new VerifiableCertificate(
122
94
  masterCertificate.type,
123
95
  masterCertificate.serialNumber,
@@ -125,8 +97,8 @@ describe('Peer class mutual authentication and certificate exchange', () => {
125
97
  masterCertificate.certifier,
126
98
  masterCertificate.revocationOutpoint,
127
99
  masterCertificate.fields,
128
- masterCertificate.signature,
129
- keyringForVerifier
100
+ keyringForVerifier,
101
+ masterCertificate.signature
130
102
  )
131
103
  }
132
104
 
@@ -234,7 +206,7 @@ describe('Peer class mutual authentication and certificate exchange', () => {
234
206
  if (certificatesReceivedByBob?.length !== 0) {
235
207
  certificatesReceivedByBob?.forEach(async cert => {
236
208
  // Decrypt to ensure it has the correct fields
237
- const decryptedFields = await decryptCertificateFields(cert, walletB, walletA)
209
+ const decryptedFields = await cert.decryptFields(walletB)
238
210
  if (cert.certifier !== 'bob') {
239
211
  console.log('Bob accepted the message:', Utils.toUTF8(payload))
240
212
  console.log('Decrypted fields:', decryptedFields)
@@ -279,7 +251,7 @@ describe('Peer class mutual authentication and certificate exchange', () => {
279
251
  alice.listenForCertificatesReceived(async (senderPublicKey, certificates) => {
280
252
  for (const cert of certificates) {
281
253
  // Decrypt Bob's certificate fields
282
- const decryptedFields = await decryptCertificateFields(cert, walletA, walletB)
254
+ const decryptedFields = await cert.decryptFields(walletA)
283
255
 
284
256
  // Check and use the decrypted fields
285
257
  if (Object.keys(decryptedFields).length !== 0 && decryptedFields.libraryCardNumber) {
@@ -342,7 +314,7 @@ describe('Peer class mutual authentication and certificate exchange', () => {
342
314
  if (certificates.length > 0) {
343
315
  // Decrypt to confirm
344
316
  for (const cert of certificates) {
345
- const decrypted = await decryptCertificateFields(cert, walletB, walletA)
317
+ const decrypted = await cert.decryptFields(walletB)
346
318
  console.log('Bob received additional certificates from Alice:', cert)
347
319
  console.log('Decrypted fields:', decrypted)
348
320
  }
@@ -384,7 +356,7 @@ describe('Peer class mutual authentication and certificate exchange', () => {
384
356
  bob.listenForCertificatesReceived(async (senderPublicKey, certificates) => {
385
357
  for (const cert of certificates) {
386
358
  // Decrypt Alice's certificate fields
387
- const decryptedFields = await decryptCertificateFields(cert, walletB, walletA)
359
+ const decryptedFields = await cert.decryptFields(walletB)
388
360
  if (decryptedFields.membershipStatus) {
389
361
  console.log(`Bob received Alice's membership status: ${decryptedFields.membershipStatus}`)
390
362
  bobAcceptedMembershipStatus()
@@ -451,7 +423,7 @@ describe('Peer class mutual authentication and certificate exchange', () => {
451
423
  const waitForAliceToAcceptBobDL = new Promise<void>((resolve) => {
452
424
  alice.listenForCertificatesReceived(async (senderPublicKey, certificates) => {
453
425
  for (const cert of certificates) {
454
- const decryptedFields = await decryptCertificateFields(cert, walletA, walletB)
426
+ const decryptedFields = await cert.decryptFields(walletA)
455
427
  if (decryptedFields.driversLicenseNumber) {
456
428
  console.log(`Alice received Bob's driver's license number: ${decryptedFields.driversLicenseNumber}`)
457
429
  aliceAcceptedBobDL()
@@ -464,7 +436,7 @@ describe('Peer class mutual authentication and certificate exchange', () => {
464
436
  const waitForBobToAcceptAliceDL = new Promise<void>((resolve) => {
465
437
  bob.listenForCertificatesReceived(async (senderPublicKey, certificates) => {
466
438
  for (const cert of certificates) {
467
- const decryptedFields = await decryptCertificateFields(cert, walletB, walletA)
439
+ const decryptedFields = await cert.decryptFields(walletB)
468
440
  if (decryptedFields.driversLicenseNumber) {
469
441
  console.log(`Bob received Alice's driver's license number: ${decryptedFields.driversLicenseNumber}`)
470
442
  bobAcceptedAliceDL()
@@ -544,7 +516,7 @@ describe('Peer class mutual authentication and certificate exchange', () => {
544
516
  const waitForAlicePartialCert = new Promise<void>((resolve) => {
545
517
  alice.listenForCertificatesReceived(async (senderPublicKey, certificates) => {
546
518
  for (const cert of certificates) {
547
- const decryptedFields = await decryptCertificateFields(cert, walletA, walletB)
519
+ const decryptedFields = await cert.decryptFields(walletA)
548
520
  if (decryptedFields.email || decryptedFields.name) {
549
521
  console.log(`Alice received Bob's certificate with fields: ${Object.keys(decryptedFields).join(', ')}`)
550
522
  aliceAcceptedPartialCert()
@@ -557,7 +529,7 @@ describe('Peer class mutual authentication and certificate exchange', () => {
557
529
  const waitForBobPartialCert = new Promise<void>((resolve) => {
558
530
  bob.listenForCertificatesReceived(async (senderPublicKey, certificates) => {
559
531
  for (const cert of certificates) {
560
- const decryptedFields = await decryptCertificateFields(cert, walletB, walletA)
532
+ const decryptedFields = await cert.decryptFields(walletB)
561
533
  if (decryptedFields.email || decryptedFields.name) {
562
534
  console.log(`Bob received Alice's certificate with fields: ${Object.keys(decryptedFields).join(', ')}`)
563
535
  bobAcceptedPartialCert()
@@ -43,9 +43,9 @@ export default class Certificate {
43
43
  revocationOutpoint: OutpointString
44
44
 
45
45
  /**
46
- * All the fields present in the certificate, with field names as keys and field values as strings.
46
+ * All the fields present in the certificate, with field names as keys and encrypted field values as Base64 strings.
47
47
  */
48
- fields: Record<CertificateFieldNameUnder50Bytes, string>
48
+ fields: Record<CertificateFieldNameUnder50Bytes, Base64String>
49
49
 
50
50
  /**
51
51
  * Certificate signature by the certifier's private key, DER encoded hex string.
@@ -258,7 +258,7 @@ export default class Certificate {
258
258
  * - `protocolID` (WalletProtocol): The protocol ID for certificate field encryption.
259
259
  * - `keyID` (string): A unique key identifier derived from the serial number and field name.
260
260
  */
261
- static getCertificateFieldEncryptionDetails(serialNumber: string, fieldName: string): { protocolID: WalletProtocol, keyID: string } {
261
+ static getCertificateFieldEncryptionDetails(fieldName: string, serialNumber?: string): { protocolID: WalletProtocol, keyID: string } {
262
262
  return { protocolID: [2, 'certificate field encryption'], keyID: `${serialNumber} ${fieldName}` }
263
263
  }
264
264
  }
@@ -12,6 +12,11 @@ import {
12
12
  } from '../../../mod.js'
13
13
  import Certificate from './Certificate.js'
14
14
 
15
+ interface CreateCertificateFieldsResult {
16
+ certificateFields: Record<CertificateFieldNameUnder50Bytes, Base64String>
17
+ masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>
18
+ }
19
+
15
20
  /**
16
21
  * MasterCertificate extends the base Certificate class to manage a master keyring, enabling the creation of verifiable certificates.
17
22
  *
@@ -56,38 +61,42 @@ export class MasterCertificate extends Certificate {
56
61
  }
57
62
 
58
63
  /**
59
- * Decrypts all fields in the MasterCertificate using the subject's wallet.
60
- *
61
- * This method uses the `masterKeyring` to decrypt each field's encryption key and then
62
- * decrypts the field values. The result is a record of plaintext field names and values.
63
- *
64
- * @param {WalletInterface} subjectWallet - The wallet of the subject, used to decrypt the master keyring and field values.
65
- * @returns {Promise<Record<CertificateFieldNameUnder50Bytes, string>>} - A record of field names and their decrypted values in plaintext.
66
- *
67
- * @throws {Error} Throws an error if the `masterKeyring` is invalid or if decryption fails for any field.
64
+ * Encrypts certificate fields for a subject and generates a master keyring.
65
+ * This method returns a master keyring tied to a specific certifier or subject who will validate
66
+ * and sign off on the fields, along with the encrypted certificate fields.
67
+ *
68
+ * @param {WalletInterface} creatorWallet - The wallet of the creator responsible for encrypting the fields.
69
+ * @param {WalletCounterparty} certifierOrSubject - The certifier or subject who will validate the certificate fields.
70
+ * @param {Record<CertificateFieldNameUnder50Bytes, string>} fields - A record of certificate field names (under 50 bytes) mapped to their values.
71
+ * @returns {Promise<CreateCertificateFieldsResult>} A promise resolving to an object containing:
72
+ * - `certificateFields` {Record<CertificateFieldNameUnder50Bytes, Base64String>}:
73
+ * The encrypted certificate fields.
74
+ * - `masterKeyring` {Record<CertificateFieldNameUnder50Bytes, Base64String>}:
75
+ * The master keyring containing encrypted revelation keys for each field.
68
76
  */
69
- async decryptFields(subjectWallet: WalletInterface): Promise<Record<CertificateFieldNameUnder50Bytes, string>> {
70
- // const fields: Record<CertificateFieldNameUnder50Bytes, Base64String> = this.fields
71
- const decryptedFields: Record<CertificateFieldNameUnder50Bytes, string> = {}
72
- if (!this.masterKeyring || Object.keys(this.masterKeyring).length === 0) {
73
- throw new Error('A MasterCertificate must have a valid masterKeyring!')
77
+ static async createCertificateFields(
78
+ creatorWallet: WalletInterface,
79
+ certifierOrSubject: WalletCounterparty,
80
+ fields: Record<CertificateFieldNameUnder50Bytes, string>
81
+ ): Promise<CreateCertificateFieldsResult> {
82
+ const certificateFields: Record<CertificateFieldNameUnder50Bytes, Base64String> = {}
83
+ const masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String> = {}
84
+ for (const [fieldName, fieldValue] of Object.entries(fields)) {
85
+ const fieldSymmetricKey = SymmetricKey.fromRandom()
86
+ const encryptedFieldValue = fieldSymmetricKey.encrypt(Utils.toArray(fieldValue, 'utf8'))
87
+ certificateFields[fieldName] = Utils.toBase64(encryptedFieldValue as number[])
88
+
89
+ const { ciphertext: encryptedFieldRevelationKey } = await creatorWallet.encrypt({
90
+ plaintext: fieldSymmetricKey.toArray(),
91
+ ...Certificate.getCertificateFieldEncryptionDetails(fieldName), // Only fieldName used on MasterCertificate
92
+ counterparty: certifierOrSubject
93
+ })
94
+ masterKeyring[fieldName] = Utils.toBase64(encryptedFieldRevelationKey)
74
95
  }
75
96
 
76
- try {
77
- // Note: we want to iterate through all fields, not just masterKeyring keys/value pairs.
78
- for (const fieldName of Object.keys(this.fields)) {
79
- const { plaintext: fieldRevelationKey } = await subjectWallet.decrypt({
80
- ciphertext: Utils.toArray(this.masterKeyring[fieldName], 'base64'),
81
- counterparty: this.certifier,
82
- ...Certificate.getCertificateFieldEncryptionDetails(this.serialNumber, fieldName)
83
- })
84
-
85
- const fieldValue = new SymmetricKey(fieldRevelationKey).decrypt(Utils.toArray(this.fields[fieldName], 'base64'))
86
- decryptedFields[fieldName] = Utils.toUTF8(fieldValue as number[])
87
- }
88
- return decryptedFields
89
- } catch (e) {
90
- throw new Error('Failed to decrypt all master certificate fields.')
97
+ return {
98
+ certificateFields,
99
+ masterKeyring
91
100
  }
92
101
  }
93
102
 
@@ -107,37 +116,32 @@ export class MasterCertificate extends Certificate {
107
116
  * - A field in `fieldsToReveal` does not exist in the certificate.
108
117
  * - The decrypted master field key fails to decrypt the corresponding field (indicating an invalid key).
109
118
  */
110
- async createKeyringForVerifier(subjectWallet: WalletInterface, verifier: WalletCounterparty, fieldsToReveal: string[], originator?: string): Promise<Record<CertificateFieldNameUnder50Bytes, string>> {
119
+ static async createKeyringForVerifier(
120
+ subjectWallet: WalletInterface,
121
+ certifier: WalletCounterparty,
122
+ verifier: WalletCounterparty,
123
+ fields: Record<CertificateFieldNameUnder50Bytes, Base64String>,
124
+ fieldsToReveal: string[],
125
+ masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>,
126
+ serialNumber: Base64String,
127
+ originator?: string): Promise<Record<CertificateFieldNameUnder50Bytes, string>> {
111
128
  if (!Array.isArray(fieldsToReveal)) {
112
129
  throw new Error('fieldsToReveal must be an array of strings')
113
130
  }
114
131
  const fieldRevelationKeyring = {}
115
132
  for (const fieldName of fieldsToReveal) {
116
133
  // Make sure that fields to reveal is a subset of the certificate fields
117
- if (!this.fields[fieldName]) {
134
+ if (!fields[fieldName]) {
118
135
  throw new Error(`Fields to reveal must be a subset of the certificate fields. Missing the "${fieldName}" field.`)
119
136
  }
120
137
 
121
- const encryptedMasterFieldKey = this.masterKeyring[fieldName]
122
-
123
- // Decrypt the master field key
124
- const { plaintext: masterFieldKey } = await subjectWallet.decrypt({
125
- ciphertext: Utils.toArray(encryptedMasterFieldKey, 'base64'),
126
- ...Certificate.getCertificateFieldEncryptionDetails(this.serialNumber, fieldName),
127
- counterparty: this.certifier
128
- }, originator)
129
-
130
- // Verify that derived key actually decrypts requested field
131
- try {
132
- new SymmetricKey(masterFieldKey).decrypt(Utils.toArray(this.fields[fieldName], 'base64'))
133
- } catch (_) {
134
- throw new Error(`Decryption of the "${fieldName}" field with its revelation key failed.`)
135
- }
138
+ // Decrypt the master field key and verify that derived key actually decrypts requested field
139
+ const masterFieldKey = (await this.decryptField(subjectWallet, masterKeyring, fieldName, fields[fieldName], certifier)).fieldRevelationKey
136
140
 
137
141
  // Encrypt derived fieldRevelationKey for verifier
138
142
  const { ciphertext: encryptedFieldRevelationKey } = await subjectWallet.encrypt({
139
143
  plaintext: masterFieldKey,
140
- ...Certificate.getCertificateFieldEncryptionDetails(this.serialNumber, fieldName),
144
+ ...Certificate.getCertificateFieldEncryptionDetails(fieldName, serialNumber),
141
145
  counterparty: verifier
142
146
  }, originator)
143
147
 
@@ -175,27 +179,21 @@ export class MasterCertificate extends Certificate {
175
179
  certificateType: string,
176
180
  getRevocationOutpoint = async (
177
181
  serialNumber: string
178
- ): Promise<string> => { return 'Certificate revocation not tracked.' }
182
+ ): Promise<string> => { return 'Certificate revocation not tracked.' },
183
+ serialNumber?: string
179
184
  ): Promise<MasterCertificate> {
180
- // 1. Generate serialNumber
181
- const serialNumber = Utils.toBase64(Random(32))
182
-
183
- const encryptedCertificateFields: Record<CertificateFieldNameUnder50Bytes, Base64String> = {}
184
- const masterKeyringForSubject: Record<CertificateFieldNameUnder50Bytes, Base64String> = {}
185
-
186
- // 2. For each field, generate a random key -> encrypt field -> encrypt key
187
- for (const [fieldName, fieldValue] of Object.entries(fields)) {
188
- const fieldSymmetricKey = SymmetricKey.fromRandom()
189
- const encryptedFieldValue = fieldSymmetricKey.encrypt(Utils.toArray(fieldValue, 'utf8'))
190
- encryptedCertificateFields[fieldName] = Utils.toBase64(encryptedFieldValue as number[])
191
- const { ciphertext: encryptedFieldRevelationKey } = await certifierWallet.encrypt({
192
- plaintext: fieldSymmetricKey.toArray(),
193
- ...Certificate.getCertificateFieldEncryptionDetails(serialNumber, fieldName),
194
- counterparty: subject
195
- })
196
- masterKeyringForSubject[fieldName] = Utils.toBase64(encryptedFieldRevelationKey)
185
+ // 1. Generate a random serialNumber if not provided
186
+ if (!serialNumber) {
187
+ serialNumber = Utils.toBase64(Random(32))
197
188
  }
198
189
 
190
+ // 2. Create encrypted certificate fields and associated master keyring
191
+ const { certificateFields, masterKeyring } = await this.createCertificateFields(
192
+ certifierWallet,
193
+ subject,
194
+ fields,
195
+ )
196
+
199
197
  // 3. Obtain a revocation outpoint (ex. certifier can call wallet.createAction())
200
198
  const revocationOutpoint = await getRevocationOutpoint(serialNumber)
201
199
  // TODO: Validate revocation outpoint format
@@ -207,12 +205,78 @@ export class MasterCertificate extends Certificate {
207
205
  subject,
208
206
  (await certifierWallet.getPublicKey({ identityKey: true })).publicKey,
209
207
  revocationOutpoint,
210
- encryptedCertificateFields,
211
- masterKeyringForSubject
208
+ certificateFields,
209
+ masterKeyring
212
210
  )
213
211
 
214
212
  // 5. Sign and return the new MasterCertificate certifying the subject.
215
213
  await certificate.sign(certifierWallet)
216
214
  return certificate
217
215
  }
216
+
217
+
218
+ /**
219
+ * Decrypts all fields in the MasterCertificate using the subject's or certifier's wallet.
220
+ *
221
+ * This method allows the subject or certifier to decrypt the `masterKeyring` and retrieve
222
+ * the encryption keys for each field, which are then used to decrypt the corresponding field values.
223
+ * The counterparty used for decryption depends on how the certificate fields were created:
224
+ * - If the certificate is self-signed, the counterparty should be set to 'self'.
225
+ * - Otherwise, the counterparty should always be the other party involved in the certificate issuance process (the subject or certifier).
226
+ *
227
+ * @param {WalletInterface} subjectOrCertifierWallet - The wallet of the subject or certifier, used to decrypt the master keyring and field values.
228
+ * @param {Record<CertificateFieldNameUnder50Bytes, Base64String>} masterKeyring - A record containing encrypted keys for each field.
229
+ * @param {Record<CertificateFieldNameUnder50Bytes, Base64String>} fields - A record of encrypted field names and their values.
230
+ * @param {WalletCounterparty} counterparty - The counterparty responsible for creating or signing the certificate. For self-signed certificates, use 'self'.
231
+ * @returns {Promise<Record<CertificateFieldNameUnder50Bytes, string>>} A promise resolving to a record of field names and their decrypted values in plaintext.
232
+ *
233
+ * @throws {Error} Throws an error if the `masterKeyring` is invalid or if decryption fails for any field.
234
+ */
235
+ static async decryptFields(
236
+ subjectOrCertifierWallet: WalletInterface,
237
+ masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>,
238
+ fields: Record<CertificateFieldNameUnder50Bytes, Base64String>,
239
+ counterparty: WalletCounterparty
240
+ ): Promise<Record<CertificateFieldNameUnder50Bytes, string>> {
241
+ if (!masterKeyring || Object.keys(masterKeyring).length === 0) {
242
+ throw new Error('A MasterCertificate must have a valid masterKeyring!')
243
+ }
244
+ try {
245
+ const decryptedFields: Record<CertificateFieldNameUnder50Bytes, string> = {}
246
+ // Note: we want to iterate through all fields, not just masterKeyring keys/value pairs.
247
+ for (const fieldName of Object.keys(fields)) {
248
+ decryptedFields[fieldName] = (await this.decryptField(subjectOrCertifierWallet, masterKeyring, fieldName, fields[fieldName], counterparty)).decryptedFieldValue
249
+ }
250
+ return decryptedFields
251
+ } catch (e) {
252
+ throw new Error('Failed to decrypt all master certificate fields.')
253
+ }
254
+ }
255
+
256
+ static async decryptField(
257
+ subjectOrCertifierWallet: WalletInterface,
258
+ masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>,
259
+ fieldName: Base64String,
260
+ fieldValue: Base64String,
261
+ counterparty: WalletCounterparty
262
+ ): Promise<{ fieldRevelationKey: number[], decryptedFieldValue: string }> {
263
+ if (!masterKeyring || Object.keys(masterKeyring).length === 0) {
264
+ throw new Error('A MasterCertificate must have a valid masterKeyring!')
265
+ }
266
+ try {
267
+ const { plaintext: fieldRevelationKey } = await subjectOrCertifierWallet.decrypt({
268
+ ciphertext: Utils.toArray(masterKeyring[fieldName], 'base64'),
269
+ ...Certificate.getCertificateFieldEncryptionDetails(fieldName), // Only fieldName used on MasterCertificate
270
+ counterparty
271
+ })
272
+
273
+ const decryptedFieldValue = new SymmetricKey(fieldRevelationKey).decrypt(Utils.toArray(fieldValue, 'base64'))
274
+ return {
275
+ fieldRevelationKey,
276
+ decryptedFieldValue: Utils.toUTF8(decryptedFieldValue as number[])
277
+ }
278
+ } catch (e) {
279
+ throw new Error('Failed to decrypt certificate field!')
280
+ }
281
+ }
218
282
  }
@@ -6,8 +6,7 @@ import {
6
6
  HexString,
7
7
  OutpointString,
8
8
  PubKeyHex,
9
- WalletInterface,
10
- WalletError
9
+ WalletInterface
11
10
  } from '../../../mod.js'
12
11
  import Certificate from './Certificate.js'
13
12
 
@@ -34,8 +33,8 @@ export class VerifiableCertificate extends Certificate {
34
33
  certifier: PubKeyHex,
35
34
  revocationOutpoint: OutpointString,
36
35
  fields: Record<CertificateFieldNameUnder50Bytes, string>,
36
+ keyring: Record<CertificateFieldNameUnder50Bytes, string>,
37
37
  signature?: HexString,
38
- keyring?: Record<CertificateFieldNameUnder50Bytes, string>,
39
38
  decryptedFields?: Record<CertificateFieldNameUnder50Bytes, Base64String>
40
39
  ) {
41
40
  super(type, serialNumber, subject, certifier, revocationOutpoint, fields, signature)
@@ -58,7 +57,7 @@ export class VerifiableCertificate extends Certificate {
58
57
  for (const fieldName in this.keyring) {
59
58
  const { plaintext: fieldRevelationKey } = await verifierWallet.decrypt({
60
59
  ciphertext: Utils.toArray(this.keyring[fieldName], 'base64'),
61
- ...Certificate.getCertificateFieldEncryptionDetails(this.serialNumber, fieldName),
60
+ ...Certificate.getCertificateFieldEncryptionDetails(fieldName, this.serialNumber),
62
61
  counterparty: this.subject
63
62
  })
64
63