@bsv/sdk 1.3.10 → 1.3.12

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 (49) 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 +95 -65
  5. package/dist/cjs/src/auth/certificates/MasterCertificate.js.map +1 -1
  6. package/dist/cjs/src/auth/certificates/VerifiableCertificate.js +3 -3
  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/src/wallet/ProtoWallet.js +9 -9
  13. package/dist/cjs/src/wallet/ProtoWallet.js.map +1 -1
  14. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  15. package/dist/esm/src/auth/certificates/Certificate.js +2 -2
  16. package/dist/esm/src/auth/certificates/Certificate.js.map +1 -1
  17. package/dist/esm/src/auth/certificates/MasterCertificate.js +95 -65
  18. package/dist/esm/src/auth/certificates/MasterCertificate.js.map +1 -1
  19. package/dist/esm/src/auth/certificates/VerifiableCertificate.js +3 -3
  20. package/dist/esm/src/auth/certificates/VerifiableCertificate.js.map +1 -1
  21. package/dist/esm/src/auth/utils/getVerifiableCertificates.js +1 -1
  22. package/dist/esm/src/auth/utils/getVerifiableCertificates.js.map +1 -1
  23. package/dist/esm/src/auth/utils/validateCertificates.js +1 -1
  24. package/dist/esm/src/auth/utils/validateCertificates.js.map +1 -1
  25. package/dist/esm/src/wallet/ProtoWallet.js +9 -9
  26. package/dist/esm/src/wallet/ProtoWallet.js.map +1 -1
  27. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  28. package/dist/types/src/auth/certificates/Certificate.d.ts +5 -5
  29. package/dist/types/src/auth/certificates/Certificate.d.ts.map +1 -1
  30. package/dist/types/src/auth/certificates/MasterCertificate.d.ts +44 -14
  31. package/dist/types/src/auth/certificates/MasterCertificate.d.ts.map +1 -1
  32. package/dist/types/src/auth/certificates/VerifiableCertificate.d.ts +4 -4
  33. package/dist/types/src/auth/certificates/VerifiableCertificate.d.ts.map +1 -1
  34. package/dist/types/src/wallet/ProtoWallet.d.ts +12 -12
  35. package/dist/types/src/wallet/ProtoWallet.d.ts.map +1 -1
  36. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  37. package/dist/umd/bundle.js +1 -1
  38. package/docs/auth.md +75 -33
  39. package/docs/wallet.md +12 -12
  40. package/package.json +1 -1
  41. package/src/auth/__tests/Peer.test.ts +19 -47
  42. package/src/auth/certificates/Certificate.ts +4 -5
  43. package/src/auth/certificates/MasterCertificate.ts +138 -71
  44. package/src/auth/certificates/VerifiableCertificate.ts +5 -6
  45. package/src/auth/certificates/__tests/MasterCertificate.test.ts +142 -51
  46. package/src/auth/certificates/__tests/VerifiableCertificate.test.ts +54 -30
  47. package/src/auth/utils/getVerifiableCertificates.ts +2 -2
  48. package/src/auth/utils/validateCertificates.ts +2 -2
  49. package/src/wallet/ProtoWallet.ts +20 -11
@@ -8,10 +8,16 @@ import {
8
8
  PubKeyHex,
9
9
  Random,
10
10
  WalletCounterparty,
11
- WalletInterface
11
+ ProtoWallet,
12
+ OriginatorDomainNameStringUnder250Bytes
12
13
  } from '../../../mod.js'
13
14
  import Certificate from './Certificate.js'
14
15
 
16
+ interface CreateCertificateFieldsResult {
17
+ certificateFields: Record<CertificateFieldNameUnder50Bytes, Base64String>
18
+ masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>
19
+ }
20
+
15
21
  /**
16
22
  * MasterCertificate extends the base Certificate class to manage a master keyring, enabling the creation of verifiable certificates.
17
23
  *
@@ -56,38 +62,43 @@ export class MasterCertificate extends Certificate {
56
62
  }
57
63
 
58
64
  /**
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.
65
+ * Encrypts certificate fields for a subject and generates a master keyring.
66
+ * This method returns a master keyring tied to a specific certifier or subject who will validate
67
+ * and sign off on the fields, along with the encrypted certificate fields.
68
+ *
69
+ * @param {ProtoWallet} creatorWallet - The wallet of the creator responsible for encrypting the fields.
70
+ * @param {WalletCounterparty} certifierOrSubject - The certifier or subject who will validate the certificate fields.
71
+ * @param {Record<CertificateFieldNameUnder50Bytes, string>} fields - A record of certificate field names (under 50 bytes) mapped to their values.
72
+ * @returns {Promise<CreateCertificateFieldsResult>} A promise resolving to an object containing:
73
+ * - `certificateFields` {Record<CertificateFieldNameUnder50Bytes, Base64String>}:
74
+ * The encrypted certificate fields.
75
+ * - `masterKeyring` {Record<CertificateFieldNameUnder50Bytes, Base64String>}:
76
+ * The master keyring containing encrypted revelation keys for each field.
68
77
  */
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!')
78
+ static async createCertificateFields(
79
+ creatorWallet: ProtoWallet,
80
+ certifierOrSubject: WalletCounterparty,
81
+ fields: Record<CertificateFieldNameUnder50Bytes, string>,
82
+ originator?: OriginatorDomainNameStringUnder250Bytes
83
+ ): Promise<CreateCertificateFieldsResult> {
84
+ const certificateFields: Record<CertificateFieldNameUnder50Bytes, Base64String> = {}
85
+ const masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String> = {}
86
+ for (const [fieldName, fieldValue] of Object.entries(fields)) {
87
+ const fieldSymmetricKey = SymmetricKey.fromRandom()
88
+ const encryptedFieldValue = fieldSymmetricKey.encrypt(Utils.toArray(fieldValue, 'utf8'))
89
+ certificateFields[fieldName] = Utils.toBase64(encryptedFieldValue as number[])
90
+
91
+ const { ciphertext: encryptedFieldRevelationKey } = await creatorWallet.encrypt({
92
+ plaintext: fieldSymmetricKey.toArray(),
93
+ ...Certificate.getCertificateFieldEncryptionDetails(fieldName), // Only fieldName used on MasterCertificate
94
+ counterparty: certifierOrSubject
95
+ }, originator)
96
+ masterKeyring[fieldName] = Utils.toBase64(encryptedFieldRevelationKey)
74
97
  }
75
98
 
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.')
99
+ return {
100
+ certificateFields,
101
+ masterKeyring
91
102
  }
92
103
  }
93
104
 
@@ -97,7 +108,7 @@ export class MasterCertificate extends Certificate {
97
108
  * for the verifier's identity key. The result is a keyring containing the keys necessary
98
109
  * for the verifier to access the designated fields.
99
110
  *
100
- * @param {WalletInterface} subjectWallet - The wallet instance of the subject, used to decrypt and re-encrypt field keys.
111
+ * @param {ProtoWallet} subjectWallet - The wallet instance of the subject, used to decrypt and re-encrypt field keys.
101
112
  * @param {WalletCounterparty} verifier - The verifier who will receive access to the selectively revealed fields. Can be an identity key as hex, 'anyone', or 'self'.
102
113
  * @param {string[]} fieldsToReveal - An array of field names to be revealed to the verifier. Must be a subset of the certificate's fields.
103
114
  * @param {string} [originator] - Optional originator identifier, used if additional context is needed for decryption and encryption operations.
@@ -107,37 +118,32 @@ export class MasterCertificate extends Certificate {
107
118
  * - A field in `fieldsToReveal` does not exist in the certificate.
108
119
  * - The decrypted master field key fails to decrypt the corresponding field (indicating an invalid key).
109
120
  */
110
- async createKeyringForVerifier(subjectWallet: WalletInterface, verifier: WalletCounterparty, fieldsToReveal: string[], originator?: string): Promise<Record<CertificateFieldNameUnder50Bytes, string>> {
121
+ static async createKeyringForVerifier(
122
+ subjectWallet: ProtoWallet,
123
+ certifier: WalletCounterparty,
124
+ verifier: WalletCounterparty,
125
+ fields: Record<CertificateFieldNameUnder50Bytes, Base64String>,
126
+ fieldsToReveal: string[],
127
+ masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>,
128
+ serialNumber: Base64String,
129
+ originator?: OriginatorDomainNameStringUnder250Bytes): Promise<Record<CertificateFieldNameUnder50Bytes, string>> {
111
130
  if (!Array.isArray(fieldsToReveal)) {
112
131
  throw new Error('fieldsToReveal must be an array of strings')
113
132
  }
114
133
  const fieldRevelationKeyring = {}
115
134
  for (const fieldName of fieldsToReveal) {
116
135
  // Make sure that fields to reveal is a subset of the certificate fields
117
- if (!this.fields[fieldName]) {
136
+ if (!fields[fieldName]) {
118
137
  throw new Error(`Fields to reveal must be a subset of the certificate fields. Missing the "${fieldName}" field.`)
119
138
  }
120
139
 
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
- }
140
+ // Decrypt the master field key and verify that derived key actually decrypts requested field
141
+ const masterFieldKey = (await this.decryptField(subjectWallet, masterKeyring, fieldName, fields[fieldName], certifier)).fieldRevelationKey
136
142
 
137
143
  // Encrypt derived fieldRevelationKey for verifier
138
144
  const { ciphertext: encryptedFieldRevelationKey } = await subjectWallet.encrypt({
139
145
  plaintext: masterFieldKey,
140
- ...Certificate.getCertificateFieldEncryptionDetails(this.serialNumber, fieldName),
146
+ ...Certificate.getCertificateFieldEncryptionDetails(fieldName, serialNumber),
141
147
  counterparty: verifier
142
148
  }, originator)
143
149
 
@@ -157,7 +163,7 @@ export class MasterCertificate extends Certificate {
157
163
  * generated symmetric key, which is then encrypted for the subject. The certificate
158
164
  * can also includes a revocation outpoint to manage potential revocation.
159
165
  *
160
- * @param {WalletInterface} certifierWallet - The wallet of the certifier, used to sign the certificate and encrypt field keys.
166
+ * @param {ProtoWallet} certifierWallet - The wallet of the certifier, used to sign the certificate and encrypt field keys.
161
167
  * @param {WalletCounterparty} subject - The subject for whom the certificate is issued.
162
168
  * @param {Record<CertificateFieldNameUnder50Bytes, string>} fields - Unencrypted certificate fields to include, with their names and values.
163
169
  * @param {string} certificateType - The type of certificate being issued.
@@ -169,33 +175,27 @@ export class MasterCertificate extends Certificate {
169
175
  * @throws {Error} Throws an error if any operation (e.g., encryption, signing) fails during certificate issuance.
170
176
  */
171
177
  static async issueCertificateForSubject(
172
- certifierWallet: WalletInterface,
178
+ certifierWallet: ProtoWallet,
173
179
  subject: WalletCounterparty,
174
180
  fields: Record<CertificateFieldNameUnder50Bytes, string>,
175
181
  certificateType: string,
176
182
  getRevocationOutpoint = async (
177
183
  serialNumber: string
178
- ): Promise<string> => { return 'Certificate revocation not tracked.' }
184
+ ): Promise<string> => { return 'Certificate revocation not tracked.' },
185
+ serialNumber?: string
179
186
  ): 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)
187
+ // 1. Generate a random serialNumber if not provided
188
+ if (!serialNumber) {
189
+ serialNumber = Utils.toBase64(Random(32))
197
190
  }
198
191
 
192
+ // 2. Create encrypted certificate fields and associated master keyring
193
+ const { certificateFields, masterKeyring } = await this.createCertificateFields(
194
+ certifierWallet,
195
+ subject,
196
+ fields,
197
+ )
198
+
199
199
  // 3. Obtain a revocation outpoint (ex. certifier can call wallet.createAction())
200
200
  const revocationOutpoint = await getRevocationOutpoint(serialNumber)
201
201
  // TODO: Validate revocation outpoint format
@@ -207,12 +207,79 @@ export class MasterCertificate extends Certificate {
207
207
  subject,
208
208
  (await certifierWallet.getPublicKey({ identityKey: true })).publicKey,
209
209
  revocationOutpoint,
210
- encryptedCertificateFields,
211
- masterKeyringForSubject
210
+ certificateFields,
211
+ masterKeyring
212
212
  )
213
213
 
214
214
  // 5. Sign and return the new MasterCertificate certifying the subject.
215
215
  await certificate.sign(certifierWallet)
216
216
  return certificate
217
217
  }
218
+
219
+
220
+ /**
221
+ * Decrypts all fields in the MasterCertificate using the subject's or certifier's wallet.
222
+ *
223
+ * This method allows the subject or certifier to decrypt the `masterKeyring` and retrieve
224
+ * the encryption keys for each field, which are then used to decrypt the corresponding field values.
225
+ * The counterparty used for decryption depends on how the certificate fields were created:
226
+ * - If the certificate is self-signed, the counterparty should be set to 'self'.
227
+ * - Otherwise, the counterparty should always be the other party involved in the certificate issuance process (the subject or certifier).
228
+ *
229
+ * @param {ProtoWallet} subjectOrCertifierWallet - The wallet of the subject or certifier, used to decrypt the master keyring and field values.
230
+ * @param {Record<CertificateFieldNameUnder50Bytes, Base64String>} masterKeyring - A record containing encrypted keys for each field.
231
+ * @param {Record<CertificateFieldNameUnder50Bytes, Base64String>} fields - A record of encrypted field names and their values.
232
+ * @param {WalletCounterparty} counterparty - The counterparty responsible for creating or signing the certificate. For self-signed certificates, use 'self'.
233
+ * @returns {Promise<Record<CertificateFieldNameUnder50Bytes, string>>} A promise resolving to a record of field names and their decrypted values in plaintext.
234
+ *
235
+ * @throws {Error} Throws an error if the `masterKeyring` is invalid or if decryption fails for any field.
236
+ */
237
+ static async decryptFields(
238
+ subjectOrCertifierWallet: ProtoWallet,
239
+ masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>,
240
+ fields: Record<CertificateFieldNameUnder50Bytes, Base64String>,
241
+ counterparty: WalletCounterparty
242
+ ): Promise<Record<CertificateFieldNameUnder50Bytes, string>> {
243
+ if (!masterKeyring || Object.keys(masterKeyring).length === 0) {
244
+ throw new Error('A MasterCertificate must have a valid masterKeyring!')
245
+ }
246
+ try {
247
+ const decryptedFields: Record<CertificateFieldNameUnder50Bytes, string> = {}
248
+ // Note: we want to iterate through all fields, not just masterKeyring keys/value pairs.
249
+ for (const fieldName of Object.keys(fields)) {
250
+ decryptedFields[fieldName] = (await this.decryptField(subjectOrCertifierWallet, masterKeyring, fieldName, fields[fieldName], counterparty)).decryptedFieldValue
251
+ }
252
+ return decryptedFields
253
+ } catch (e) {
254
+ throw new Error('Failed to decrypt all master certificate fields.')
255
+ }
256
+ }
257
+
258
+ static async decryptField(
259
+ subjectOrCertifierWallet: ProtoWallet,
260
+ masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>,
261
+ fieldName: Base64String,
262
+ fieldValue: Base64String,
263
+ counterparty: WalletCounterparty,
264
+ originator?: OriginatorDomainNameStringUnder250Bytes
265
+ ): Promise<{ fieldRevelationKey: number[], decryptedFieldValue: string }> {
266
+ if (!masterKeyring || Object.keys(masterKeyring).length === 0) {
267
+ throw new Error('A MasterCertificate must have a valid masterKeyring!')
268
+ }
269
+ try {
270
+ const { plaintext: fieldRevelationKey } = await subjectOrCertifierWallet.decrypt({
271
+ ciphertext: Utils.toArray(masterKeyring[fieldName], 'base64'),
272
+ ...Certificate.getCertificateFieldEncryptionDetails(fieldName), // Only fieldName used on MasterCertificate
273
+ counterparty
274
+ }, originator)
275
+
276
+ const decryptedFieldValue = new SymmetricKey(fieldRevelationKey).decrypt(Utils.toArray(fieldValue, 'base64'))
277
+ return {
278
+ fieldRevelationKey,
279
+ decryptedFieldValue: Utils.toUTF8(decryptedFieldValue as number[])
280
+ }
281
+ } catch (e) {
282
+ throw new Error('Failed to decrypt certificate field!')
283
+ }
284
+ }
218
285
  }
@@ -6,8 +6,7 @@ import {
6
6
  HexString,
7
7
  OutpointString,
8
8
  PubKeyHex,
9
- WalletInterface,
10
- WalletError
9
+ ProtoWallet
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)
@@ -45,11 +44,11 @@ export class VerifiableCertificate extends Certificate {
45
44
 
46
45
  /**
47
46
  * Decrypts selectively revealed certificate fields using the provided keyring and verifier wallet
48
- * @param {WalletInterface} verifierWallet - The wallet instance of the certificate's verifier, used to decrypt field keys.
47
+ * @param {ProtoWallet} verifierWallet - The wallet instance of the certificate's verifier, used to decrypt field keys.
49
48
  * @returns {Promise<Record<CertificateFieldNameUnder50Bytes, string>>} - A promise that resolves to an object where each key is a field name and each value is the decrypted field value as a string.
50
49
  * @throws {Error} Throws an error if any of the decryption operations fail, with a message indicating the failure context.
51
50
  */
52
- async decryptFields(verifierWallet: WalletInterface): Promise<Record<CertificateFieldNameUnder50Bytes, string>> {
51
+ async decryptFields(verifierWallet: ProtoWallet): Promise<Record<CertificateFieldNameUnder50Bytes, string>> {
53
52
  if (!this.keyring || Object.keys(this.keyring).length === 0) {
54
53
  throw new Error('A keyring is required to decrypt certificate fields for the verifier.')
55
54
  }
@@ -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