@bsv/sdk 1.3.9 → 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 (72) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/auth/Peer.js.map +1 -1
  3. package/dist/cjs/src/auth/certificates/Certificate.js +1 -1
  4. package/dist/cjs/src/auth/certificates/Certificate.js.map +1 -1
  5. package/dist/cjs/src/auth/certificates/MasterCertificate.js +93 -63
  6. package/dist/cjs/src/auth/certificates/MasterCertificate.js.map +1 -1
  7. package/dist/cjs/src/auth/certificates/VerifiableCertificate.js +2 -2
  8. package/dist/cjs/src/auth/certificates/VerifiableCertificate.js.map +1 -1
  9. package/dist/cjs/src/auth/utils/createNonce.js +9 -3
  10. package/dist/cjs/src/auth/utils/createNonce.js.map +1 -1
  11. package/dist/cjs/src/auth/utils/getVerifiableCertificates.js +1 -1
  12. package/dist/cjs/src/auth/utils/getVerifiableCertificates.js.map +1 -1
  13. package/dist/cjs/src/auth/utils/validateCertificates.js +1 -1
  14. package/dist/cjs/src/auth/utils/validateCertificates.js.map +1 -1
  15. package/dist/cjs/src/auth/utils/verifyNonce.js +3 -2
  16. package/dist/cjs/src/auth/utils/verifyNonce.js.map +1 -1
  17. package/dist/cjs/src/overlay-tools/OverlayAdminTokenTemplate.js.map +1 -1
  18. package/dist/cjs/src/wallet/ProtoWallet.js.map +1 -1
  19. package/dist/cjs/src/wallet/WalletClient.js.map +1 -1
  20. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  21. package/dist/esm/src/auth/Peer.js.map +1 -1
  22. package/dist/esm/src/auth/certificates/Certificate.js +2 -2
  23. package/dist/esm/src/auth/certificates/Certificate.js.map +1 -1
  24. package/dist/esm/src/auth/certificates/MasterCertificate.js +93 -63
  25. package/dist/esm/src/auth/certificates/MasterCertificate.js.map +1 -1
  26. package/dist/esm/src/auth/certificates/VerifiableCertificate.js +2 -2
  27. package/dist/esm/src/auth/certificates/VerifiableCertificate.js.map +1 -1
  28. package/dist/esm/src/auth/utils/createNonce.js +9 -3
  29. package/dist/esm/src/auth/utils/createNonce.js.map +1 -1
  30. package/dist/esm/src/auth/utils/getVerifiableCertificates.js +1 -1
  31. package/dist/esm/src/auth/utils/getVerifiableCertificates.js.map +1 -1
  32. package/dist/esm/src/auth/utils/validateCertificates.js +1 -1
  33. package/dist/esm/src/auth/utils/validateCertificates.js.map +1 -1
  34. package/dist/esm/src/auth/utils/verifyNonce.js +3 -2
  35. package/dist/esm/src/auth/utils/verifyNonce.js.map +1 -1
  36. package/dist/esm/src/overlay-tools/OverlayAdminTokenTemplate.js.map +1 -1
  37. package/dist/esm/src/wallet/ProtoWallet.js.map +1 -1
  38. package/dist/esm/src/wallet/WalletClient.js.map +1 -1
  39. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  40. package/dist/types/src/auth/Peer.d.ts.map +1 -1
  41. package/dist/types/src/auth/certificates/Certificate.d.ts +3 -3
  42. package/dist/types/src/auth/certificates/Certificate.d.ts.map +1 -1
  43. package/dist/types/src/auth/certificates/MasterCertificate.d.ts +41 -11
  44. package/dist/types/src/auth/certificates/MasterCertificate.d.ts.map +1 -1
  45. package/dist/types/src/auth/certificates/VerifiableCertificate.d.ts +1 -1
  46. package/dist/types/src/auth/certificates/VerifiableCertificate.d.ts.map +1 -1
  47. package/dist/types/src/auth/utils/createNonce.d.ts +4 -3
  48. package/dist/types/src/auth/utils/createNonce.d.ts.map +1 -1
  49. package/dist/types/src/auth/utils/verifyNonce.d.ts +3 -2
  50. package/dist/types/src/auth/utils/verifyNonce.d.ts.map +1 -1
  51. package/dist/types/src/overlay-tools/OverlayAdminTokenTemplate.d.ts.map +1 -1
  52. package/dist/types/src/wallet/ProtoWallet.d.ts.map +1 -1
  53. package/dist/types/src/wallet/WalletClient.d.ts.map +1 -1
  54. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  55. package/dist/umd/bundle.js +1 -1
  56. package/docs/auth.md +71 -26
  57. package/package.json +1 -1
  58. package/src/auth/Peer.ts +21 -21
  59. package/src/auth/__tests/Peer.test.ts +19 -47
  60. package/src/auth/certificates/Certificate.ts +3 -3
  61. package/src/auth/certificates/MasterCertificate.ts +131 -67
  62. package/src/auth/certificates/VerifiableCertificate.ts +3 -4
  63. package/src/auth/certificates/__tests/MasterCertificate.test.ts +142 -51
  64. package/src/auth/certificates/__tests/VerifiableCertificate.test.ts +25 -30
  65. package/src/auth/utils/__tests/cryptononce.test.ts +42 -7
  66. package/src/auth/utils/createNonce.ts +10 -4
  67. package/src/auth/utils/getVerifiableCertificates.ts +2 -2
  68. package/src/auth/utils/validateCertificates.ts +2 -2
  69. package/src/auth/utils/verifyNonce.ts +4 -3
  70. package/src/overlay-tools/OverlayAdminTokenTemplate.ts +4 -4
  71. package/src/wallet/ProtoWallet.ts +10 -10
  72. package/src/wallet/WalletClient.ts +30 -30
@@ -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
 
@@ -19,11 +19,12 @@ describe('MasterCertificate', () => {
19
19
 
20
20
  const subjectWallet = new CompletedProtoWallet(subjectPrivateKey)
21
21
  const certifierWallet = new CompletedProtoWallet(certifierPrivateKey)
22
- let subjectPubKey, certifierPubKey
22
+ let subjectIdentityKey: string
23
+ let certifierIdentityKey: string
23
24
 
24
25
  beforeAll(async () => {
25
- subjectPubKey = (await subjectWallet.getPublicKey({ identityKey: true })).publicKey
26
- certifierPubKey = (await certifierWallet.getPublicKey({ identityKey: true })).publicKey
26
+ subjectIdentityKey = (await subjectWallet.getPublicKey({ identityKey: true })).publicKey
27
+ certifierIdentityKey = (await certifierWallet.getPublicKey({ identityKey: true })).publicKey
27
28
  })
28
29
 
29
30
  describe('constructor', () => {
@@ -42,8 +43,8 @@ describe('MasterCertificate', () => {
42
43
  const certificate = new MasterCertificate(
43
44
  Utils.toBase64(Random(16)), // type
44
45
  Utils.toBase64(Random(16)), // serialNumber
45
- subjectPubKey,
46
- certifierPubKey,
46
+ subjectIdentityKey,
47
+ certifierIdentityKey,
47
48
  mockRevocationOutpoint,
48
49
  fields,
49
50
  masterKeyring
@@ -52,8 +53,8 @@ describe('MasterCertificate', () => {
52
53
  expect(certificate).toBeInstanceOf(MasterCertificate)
53
54
  expect(certificate.fields).toEqual(fields)
54
55
  expect(certificate.masterKeyring).toEqual(masterKeyring)
55
- expect(certificate.subject).toEqual(subjectPubKey)
56
- expect(certificate.certifier).toEqual(certifierPubKey)
56
+ expect(certificate.subject).toEqual(subjectIdentityKey)
57
+ expect(certificate.certifier).toEqual(certifierIdentityKey)
57
58
  })
58
59
 
59
60
  it('should throw if masterKeyring is missing a key for any field', () => {
@@ -64,8 +65,8 @@ describe('MasterCertificate', () => {
64
65
  new MasterCertificate(
65
66
  Utils.toBase64(Random(16)), // type
66
67
  Utils.toBase64(Random(16)), // serialNumber
67
- subjectPubKey,
68
- certifierPubKey,
68
+ subjectIdentityKey,
69
+ certifierIdentityKey,
69
70
  mockRevocationOutpoint,
70
71
  fields,
71
72
  masterKeyring
@@ -74,18 +75,23 @@ describe('MasterCertificate', () => {
74
75
  })
75
76
  })
76
77
 
77
- describe('decryptFields', () => {
78
+ describe('decryptFields (static)', () => {
78
79
  it('should decrypt all fields correctly using subject wallet', async () => {
79
80
  // Issue a certificate for the subject, which includes a valid masterKeyring
80
81
  const certificate = await MasterCertificate.issueCertificateForSubject(
81
82
  certifierWallet,
82
- subjectPubKey,
83
+ subjectIdentityKey,
83
84
  plaintextFields,
84
85
  'TEST_CERT'
85
86
  )
86
87
 
87
- // Now subject should be able to decrypt all fields
88
- const decrypted = await certificate.decryptFields(subjectWallet)
88
+ // Now subject should be able to decrypt all fields via static method
89
+ const decrypted = await MasterCertificate.decryptFields(
90
+ subjectWallet,
91
+ certificate.masterKeyring,
92
+ certificate.fields,
93
+ certificate.certifier // because certifier was the encryption counterparty
94
+ )
89
95
  expect(decrypted).toEqual(plaintextFields)
90
96
  })
91
97
 
@@ -94,8 +100,8 @@ describe('MasterCertificate', () => {
94
100
  expect(() => new MasterCertificate(
95
101
  Utils.toBase64(Random(16)),
96
102
  Utils.toBase64(Random(16)),
97
- subjectPubKey,
98
- certifierPubKey,
103
+ subjectIdentityKey,
104
+ certifierIdentityKey,
99
105
  mockRevocationOutpoint,
100
106
  { name: Utils.toBase64([1, 2, 3]) },
101
107
  {}
@@ -108,8 +114,8 @@ describe('MasterCertificate', () => {
108
114
  const badKeyCertificate = new MasterCertificate(
109
115
  Utils.toBase64(Random(16)),
110
116
  Utils.toBase64(Random(16)),
111
- subjectPubKey,
112
- certifierPubKey,
117
+ subjectIdentityKey,
118
+ certifierIdentityKey,
113
119
  mockRevocationOutpoint,
114
120
  {
115
121
  name: Utils.toBase64(SymmetricKey.fromRandom().encrypt(Utils.toArray('Alice', 'utf8')) as number[])
@@ -117,24 +123,30 @@ describe('MasterCertificate', () => {
117
123
  { name: badKeyMasterKeyring }
118
124
  )
119
125
 
120
- await expect(badKeyCertificate.decryptFields(subjectWallet))
121
- .rejects
122
- .toThrow('Failed to decrypt all master certificate fields.')
126
+ await expect(
127
+ MasterCertificate.decryptFields(
128
+ subjectWallet,
129
+ badKeyCertificate.masterKeyring,
130
+ badKeyCertificate.fields,
131
+ badKeyCertificate.certifier
132
+ )
133
+ ).rejects.toThrow('Failed to decrypt all master certificate fields.')
123
134
  })
124
135
  })
125
136
 
126
- describe('createKeyringForVerifier', () => {
137
+ describe('createKeyringForVerifier (static)', () => {
127
138
  const verifierPrivateKey = PrivateKey.fromRandom()
128
139
  const verifierWallet = new CompletedProtoWallet(verifierPrivateKey)
129
- let verifierPubKey
140
+ let verifierIdentityKey: string
130
141
 
131
142
  let issuedCert: MasterCertificate
132
143
 
133
144
  beforeAll(async () => {
134
- verifierPubKey = (await verifierWallet.getPublicKey({ identityKey: true })).publicKey
145
+ verifierIdentityKey = (await verifierWallet.getPublicKey({ identityKey: true })).publicKey
146
+ // Issue a certificate to reuse in tests
135
147
  issuedCert = await MasterCertificate.issueCertificateForSubject(
136
148
  certifierWallet,
137
- subjectPubKey,
149
+ subjectIdentityKey,
138
150
  plaintextFields,
139
151
  'TEST_CERT'
140
152
  )
@@ -143,10 +155,15 @@ describe('MasterCertificate', () => {
143
155
  it('should create a verifier keyring for specified fields', async () => {
144
156
  // We only want to share "name" with the verifier
145
157
  const fieldsToReveal = ['name']
146
- const keyringForVerifier = await issuedCert.createKeyringForVerifier(
158
+
159
+ const keyringForVerifier = await MasterCertificate.createKeyringForVerifier(
147
160
  subjectWallet,
148
- verifierPubKey,
149
- fieldsToReveal
161
+ issuedCert.certifier, // the original certifier
162
+ verifierIdentityKey, // the new verifier
163
+ issuedCert.fields, // encrypted fields
164
+ fieldsToReveal,
165
+ issuedCert.masterKeyring,
166
+ issuedCert.serialNumber
150
167
  )
151
168
 
152
169
  // The new keyring should only contain "name"
@@ -161,8 +178,8 @@ describe('MasterCertificate', () => {
161
178
  issuedCert.certifier,
162
179
  issuedCert.revocationOutpoint,
163
180
  issuedCert.fields,
164
- issuedCert.signature,
165
- keyringForVerifier
181
+ keyringForVerifier,
182
+ issuedCert.signature
166
183
  )
167
184
 
168
185
  // The verifier should successfully decrypt the "name" field
@@ -170,16 +187,23 @@ describe('MasterCertificate', () => {
170
187
  expect(decrypted).toEqual({ name: plaintextFields.name })
171
188
  })
172
189
 
173
- it('should throw if fields to reveal are not subset of the certificate fields', async () => {
190
+ it('should throw if fields to reveal are not a subset of the certificate fields', async () => {
174
191
  await expect(
175
- issuedCert.createKeyringForVerifier(subjectWallet, verifierPubKey, ['nonexistent_field'])
192
+ MasterCertificate.createKeyringForVerifier(
193
+ subjectWallet,
194
+ issuedCert.certifier,
195
+ verifierIdentityKey,
196
+ issuedCert.fields,
197
+ ['nonexistent_field'],
198
+ issuedCert.masterKeyring
199
+ )
176
200
  ).rejects.toThrow(
177
201
  /Fields to reveal must be a subset of the certificate fields\. Missing the "nonexistent_field" field\./
178
202
  )
179
203
  })
180
204
 
181
205
  it('should throw if the master key fails to decrypt the corresponding field', async () => {
182
- // We'll tamper the certificate's masterKeyring so that a field key is invalid
206
+ // We'll tamper with the certificate's masterKeyring so that a field key is invalid
183
207
  const tamperedCert = new MasterCertificate(
184
208
  issuedCert.type,
185
209
  issuedCert.serialNumber,
@@ -197,46 +221,65 @@ describe('MasterCertificate', () => {
197
221
  )
198
222
 
199
223
  await expect(
200
- tamperedCert.createKeyringForVerifier(subjectWallet, verifierPubKey, ['name'])
201
- ).rejects.toThrow('Decryption failed!')
224
+ MasterCertificate.createKeyringForVerifier(
225
+ subjectWallet,
226
+ tamperedCert.certifier,
227
+ verifierIdentityKey,
228
+ tamperedCert.fields,
229
+ ['name'],
230
+ tamperedCert.masterKeyring,
231
+ tamperedCert.serialNumber
232
+ )
233
+ ).rejects.toThrow('Failed to decrypt certificate field!')
202
234
  })
203
235
 
204
236
  it('should support optional originator parameter', async () => {
205
- // Just to ensure coverage for the originator-based flows
206
237
  const fieldsToReveal = ['name']
207
- const keyringForVerifier = await issuedCert.createKeyringForVerifier(
238
+ const keyringForVerifier = await MasterCertificate.createKeyringForVerifier(
208
239
  subjectWallet,
209
- verifierPubKey,
240
+ issuedCert.certifier,
241
+ verifierIdentityKey,
242
+ issuedCert.fields,
210
243
  fieldsToReveal,
244
+ issuedCert.masterKeyring,
245
+ issuedCert.serialNumber,
211
246
  'my-originator'
212
247
  )
213
248
  expect(keyringForVerifier).toHaveProperty('name')
214
249
  })
215
250
 
216
- it('should support counterparty of "anyone"', async () => {
217
- // Create a keyring for public disclosure of selected fields.
251
+ it('should support counterparty of "anyone" or "self"', async () => {
218
252
  const fieldsToReveal = ['name']
219
- const keyringForVerifier = await issuedCert.createKeyringForVerifier(
253
+
254
+ // "anyone"
255
+ const anyoneKeyring = await MasterCertificate.createKeyringForVerifier(
220
256
  subjectWallet,
257
+ issuedCert.certifier,
221
258
  'anyone',
259
+ issuedCert.fields,
222
260
  fieldsToReveal,
261
+ issuedCert.masterKeyring,
262
+ issuedCert.serialNumber,
223
263
  'my-originator'
224
264
  )
225
- expect(keyringForVerifier).toHaveProperty('name')
226
- })
227
- it('should support counterparty of "self"', async () => {
228
- const fieldsToReveal = ['name']
229
- const keyringForVerifier = await issuedCert.createKeyringForVerifier(
265
+ expect(anyoneKeyring).toHaveProperty('name')
266
+
267
+ // "self"
268
+ const selfKeyring = await MasterCertificate.createKeyringForVerifier(
230
269
  subjectWallet,
270
+ issuedCert.certifier,
231
271
  'self',
272
+ issuedCert.fields,
232
273
  fieldsToReveal,
274
+ issuedCert.masterKeyring,
275
+ issuedCert.serialNumber,
233
276
  'my-originator'
234
277
  )
235
- expect(keyringForVerifier).toHaveProperty('name')
278
+ expect(selfKeyring).toHaveProperty('name')
236
279
  })
237
280
  })
238
281
 
239
- describe('issueCertificateForSubject', () => {
282
+ describe('issueCertificateForSubject (static)', () => {
240
283
  it('should issue a valid MasterCertificate for the given subject', async () => {
241
284
  const newPlaintextFields = {
242
285
  project: 'Top Secret',
@@ -247,16 +290,16 @@ describe('MasterCertificate', () => {
247
290
 
248
291
  const newCert = await MasterCertificate.issueCertificateForSubject(
249
292
  certifierWallet,
250
- subjectPubKey,
293
+ subjectIdentityKey,
251
294
  newPlaintextFields,
252
295
  'TEST_CERT',
253
- revocationFn,
296
+ revocationFn
254
297
  )
255
298
 
256
299
  expect(newCert).toBeInstanceOf(MasterCertificate)
257
300
  // The certificate's fields should be encrypted base64
258
301
  for (const fieldName in newPlaintextFields) {
259
- expect(newCert.fields[fieldName]).toMatch(/^[A-Za-z0-9+/]+=*$/) // base64 check
302
+ expect(newCert.fields[fieldName]).toMatch(/^[A-Za-z0-9+/]+=*$/) // quick base64 check
260
303
  }
261
304
  // The masterKeyring should also contain base64 strings
262
305
  for (const fieldName in newPlaintextFields) {
@@ -266,8 +309,56 @@ describe('MasterCertificate', () => {
266
309
  expect(newCert.revocationOutpoint).toEqual(mockRevocationOutpoint)
267
310
  // Check we have a signature
268
311
  expect(newCert.signature).toBeDefined()
269
- // Check that the revocationFn were called
312
+ // Check that the revocationFn was called
270
313
  expect(revocationFn).toHaveBeenCalledWith(newCert.serialNumber)
271
314
  })
315
+
316
+ it('should allow passing a custom serial number when issuing the certificate', async () => {
317
+ const customSerialNumber = Utils.toBase64(Random(32))
318
+ const newPlaintextFields = { status: 'Approved' }
319
+ const newCert = await MasterCertificate.issueCertificateForSubject(
320
+ certifierWallet,
321
+ subjectIdentityKey,
322
+ newPlaintextFields,
323
+ 'TEST_CERT',
324
+ undefined, // No custom revocation function
325
+ customSerialNumber // Pass our custom serial number
326
+ )
327
+
328
+ expect(newCert).toBeInstanceOf(MasterCertificate)
329
+ expect(newCert.serialNumber).toEqual(customSerialNumber) // Must match exactly
330
+ // Check encryption
331
+ for (const fieldName in newPlaintextFields) {
332
+ expect(newCert.fields[fieldName]).toMatch(/^[A-Za-z0-9+/]+=*$/)
333
+ }
334
+ })
335
+ it('should allow issuing a self-signed certificate and decrypt it with the same wallet', async () => {
336
+ // In a self-signed scenario, the subject and certifier are the same
337
+ const subjectWallet = new CompletedProtoWallet(PrivateKey.fromRandom())
338
+
339
+ // Some sample fields
340
+ const selfSignedFields = {
341
+ owner: 'Bob',
342
+ organization: 'SelfCo'
343
+ }
344
+
345
+ // Issue the certificate for "self"
346
+ const selfSignedCert = await MasterCertificate.issueCertificateForSubject(
347
+ subjectWallet, // act as certifier
348
+ 'self',
349
+ selfSignedFields,
350
+ 'SELF_SIGNED_TEST'
351
+ )
352
+
353
+ // Now we attempt to decrypt the fields with the same wallet
354
+ const decrypted = await MasterCertificate.decryptFields(
355
+ subjectWallet,
356
+ selfSignedCert.masterKeyring,
357
+ selfSignedCert.fields,
358
+ 'self'
359
+ )
360
+
361
+ expect(decrypted).toEqual(selfSignedFields)
362
+ })
272
363
  })
273
364
  })