@bsv/sdk 1.3.5 → 1.3.7

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 (108) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/auth/Peer.js +1 -1
  3. package/dist/cjs/src/auth/Peer.js.map +1 -1
  4. package/dist/cjs/src/auth/certificates/Certificate.js +25 -6
  5. package/dist/cjs/src/auth/certificates/Certificate.js.map +1 -1
  6. package/dist/cjs/src/auth/certificates/MasterCertificate.js +99 -17
  7. package/dist/cjs/src/auth/certificates/MasterCertificate.js.map +1 -1
  8. package/dist/cjs/src/auth/certificates/VerifiableCertificate.js +3 -4
  9. package/dist/cjs/src/auth/certificates/VerifiableCertificate.js.map +1 -1
  10. package/dist/cjs/src/auth/certificates/__tests/CompletedProtoWallet.js +80 -0
  11. package/dist/cjs/src/auth/certificates/__tests/CompletedProtoWallet.js.map +1 -0
  12. package/dist/cjs/src/auth/certificates/index.js +1 -0
  13. package/dist/cjs/src/auth/certificates/index.js.map +1 -1
  14. package/dist/cjs/src/auth/utils/index.js +0 -1
  15. package/dist/cjs/src/auth/utils/index.js.map +1 -1
  16. package/dist/cjs/src/transaction/Beef.js +0 -1
  17. package/dist/cjs/src/transaction/Beef.js.map +1 -1
  18. package/dist/cjs/src/transaction/index.js +16 -3
  19. package/dist/cjs/src/transaction/index.js.map +1 -1
  20. package/dist/cjs/src/wallet/CachedKeyDeriver.js.map +1 -1
  21. package/dist/cjs/src/wallet/KeyDeriver.js +3 -2
  22. package/dist/cjs/src/wallet/KeyDeriver.js.map +1 -1
  23. package/dist/cjs/src/wallet/ProtoWallet.js +1 -1
  24. package/dist/cjs/src/wallet/ProtoWallet.js.map +1 -1
  25. package/dist/cjs/src/wallet/WalletClient.js.map +1 -1
  26. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  27. package/dist/esm/src/auth/Peer.js +1 -1
  28. package/dist/esm/src/auth/Peer.js.map +1 -1
  29. package/dist/esm/src/auth/certificates/Certificate.js +25 -6
  30. package/dist/esm/src/auth/certificates/Certificate.js.map +1 -1
  31. package/dist/esm/src/auth/certificates/MasterCertificate.js +100 -18
  32. package/dist/esm/src/auth/certificates/MasterCertificate.js.map +1 -1
  33. package/dist/esm/src/auth/certificates/VerifiableCertificate.js +3 -4
  34. package/dist/esm/src/auth/certificates/VerifiableCertificate.js.map +1 -1
  35. package/dist/esm/src/auth/certificates/__tests/CompletedProtoWallet.js +76 -0
  36. package/dist/esm/src/auth/certificates/__tests/CompletedProtoWallet.js.map +1 -0
  37. package/dist/esm/src/auth/certificates/index.js +1 -0
  38. package/dist/esm/src/auth/certificates/index.js.map +1 -1
  39. package/dist/esm/src/auth/utils/index.js +0 -1
  40. package/dist/esm/src/auth/utils/index.js.map +1 -1
  41. package/dist/esm/src/transaction/Beef.js +0 -1
  42. package/dist/esm/src/transaction/Beef.js.map +1 -1
  43. package/dist/esm/src/transaction/index.js +1 -1
  44. package/dist/esm/src/transaction/index.js.map +1 -1
  45. package/dist/esm/src/wallet/CachedKeyDeriver.js.map +1 -1
  46. package/dist/esm/src/wallet/KeyDeriver.js +3 -2
  47. package/dist/esm/src/wallet/KeyDeriver.js.map +1 -1
  48. package/dist/esm/src/wallet/ProtoWallet.js +1 -1
  49. package/dist/esm/src/wallet/ProtoWallet.js.map +1 -1
  50. package/dist/esm/src/wallet/WalletClient.js.map +1 -1
  51. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  52. package/dist/types/src/auth/Peer.d.ts +1 -1
  53. package/dist/types/src/auth/Peer.d.ts.map +1 -1
  54. package/dist/types/src/auth/certificates/Certificate.d.ts +16 -3
  55. package/dist/types/src/auth/certificates/Certificate.d.ts.map +1 -1
  56. package/dist/types/src/auth/certificates/MasterCertificate.d.ts +46 -13
  57. package/dist/types/src/auth/certificates/MasterCertificate.d.ts.map +1 -1
  58. package/dist/types/src/auth/certificates/VerifiableCertificate.d.ts +1 -1
  59. package/dist/types/src/auth/certificates/VerifiableCertificate.d.ts.map +1 -1
  60. package/dist/types/src/auth/certificates/__tests/CompletedProtoWallet.d.ts +24 -0
  61. package/dist/types/src/auth/certificates/__tests/CompletedProtoWallet.d.ts.map +1 -0
  62. package/dist/types/src/auth/certificates/index.d.ts +1 -0
  63. package/dist/types/src/auth/certificates/index.d.ts.map +1 -1
  64. package/dist/types/src/auth/utils/index.d.ts +0 -1
  65. package/dist/types/src/auth/utils/index.d.ts.map +1 -1
  66. package/dist/types/src/transaction/Beef.d.ts +0 -1
  67. package/dist/types/src/transaction/Beef.d.ts.map +1 -1
  68. package/dist/types/src/transaction/index.d.ts +1 -1
  69. package/dist/types/src/transaction/index.d.ts.map +1 -1
  70. package/dist/types/src/wallet/CachedKeyDeriver.d.ts.map +1 -1
  71. package/dist/types/src/wallet/KeyDeriver.d.ts +5 -7
  72. package/dist/types/src/wallet/KeyDeriver.d.ts.map +1 -1
  73. package/dist/types/src/wallet/ProtoWallet.d.ts.map +1 -1
  74. package/dist/types/src/wallet/Wallet.interfaces.d.ts.map +1 -1
  75. package/dist/types/src/wallet/WalletClient.d.ts +1 -2
  76. package/dist/types/src/wallet/WalletClient.d.ts.map +1 -1
  77. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  78. package/dist/umd/bundle.js +1 -1
  79. package/docs/auth.md +111 -87
  80. package/docs/wallet.md +26 -97
  81. package/package.json +1 -1
  82. package/src/auth/Peer.ts +23 -23
  83. package/src/auth/__tests/Peer.test.ts +19 -27
  84. package/src/auth/certificates/Certificate.ts +31 -8
  85. package/src/auth/certificates/MasterCertificate.ts +134 -23
  86. package/src/auth/certificates/VerifiableCertificate.ts +6 -6
  87. package/src/auth/certificates/__tests/Certificate.test.ts +45 -7
  88. package/src/auth/certificates/__tests/CompletedProtoWallet.ts +101 -0
  89. package/src/auth/certificates/__tests/MasterCertificate.test.ts +273 -0
  90. package/src/auth/certificates/__tests/VerifiableCertificate.test.ts +117 -0
  91. package/src/auth/certificates/index.ts +2 -1
  92. package/src/auth/utils/index.ts +0 -1
  93. package/src/transaction/Beef.ts +0 -2
  94. package/src/transaction/__tests/Beef.test.ts +1 -1
  95. package/src/transaction/index.ts +1 -1
  96. package/src/wallet/CachedKeyDeriver.ts +1 -2
  97. package/src/wallet/KeyDeriver.ts +18 -20
  98. package/src/wallet/ProtoWallet.ts +21 -21
  99. package/src/wallet/Wallet.interfaces.ts +8 -9
  100. package/src/wallet/WalletClient.ts +1 -2
  101. package/src/wallet/__tests/KeyDeriver.test.ts +2 -2
  102. package/dist/cjs/src/auth/utils/certificateHelpers.js +0 -51
  103. package/dist/cjs/src/auth/utils/certificateHelpers.js.map +0 -1
  104. package/dist/esm/src/auth/utils/certificateHelpers.js +0 -47
  105. package/dist/esm/src/auth/utils/certificateHelpers.js.map +0 -1
  106. package/dist/types/src/auth/utils/certificateHelpers.d.ts +0 -26
  107. package/dist/types/src/auth/utils/certificateHelpers.d.ts.map +0 -1
  108. package/src/auth/utils/certificateHelpers.ts +0 -86
package/src/auth/Peer.ts CHANGED
@@ -25,7 +25,7 @@ export class Peer {
25
25
  private callbackIdCounter: number = 0
26
26
 
27
27
  // Whether to auto-persist the session with the last-interacted-with peer
28
- private autoPersistLastSession: boolean = true
28
+ private readonly autoPersistLastSession: boolean = true
29
29
 
30
30
  // Last-interacted-with peer identity key
31
31
  private lastInteractedWithPeer: string | undefined
@@ -39,7 +39,7 @@ export class Peer {
39
39
  * @param {SessionManager} [sessionManager] - Optional SessionManager to be used for managing peer sessions.
40
40
  * @param {boolean} [autoPersistLastSession] - Whether to auto-persist the session with the last-interacted-with peer. Defaults to true.
41
41
  */
42
- constructor(
42
+ constructor (
43
43
  wallet: Wallet,
44
44
  transport: Transport,
45
45
  certificatesToRequest?: RequestedCertificateSet,
@@ -51,7 +51,7 @@ export class Peer {
51
51
  this.certificatesToRequest = certificatesToRequest ?? { certifiers: [], types: {} }
52
52
  this.transport.onData(this.handleIncomingMessage.bind(this))
53
53
  this.sessionManager = sessionManager || new SessionManager()
54
- if (autoPersistLastSession === false) {
54
+ if (!autoPersistLastSession) {
55
55
  this.autoPersistLastSession = false
56
56
  } else {
57
57
  this.autoPersistLastSession = true
@@ -66,7 +66,7 @@ export class Peer {
66
66
  * @returns {Promise<void>}
67
67
  * @throws Will throw an error if the message fails to send.
68
68
  */
69
- async toPeer(message: number[], identityKey?: string, maxWaitTime?: number): Promise<void> {
69
+ async toPeer (message: number[], identityKey?: string, maxWaitTime?: number): Promise<void> {
70
70
  if (this.autoPersistLastSession && this.lastInteractedWithPeer && typeof identityKey !== 'string') {
71
71
  identityKey = this.lastInteractedWithPeer
72
72
  }
@@ -111,7 +111,7 @@ export class Peer {
111
111
  * @returns {Promise<void>} Resolves if the certificate request message is successfully sent.
112
112
  * @throws Will throw an error if the peer session is not authenticated or if sending the request fails.
113
113
  */
114
- async requestCertificates(certificatesToRequest: RequestedCertificateSet, identityKey?: string, maxWaitTime = 10000): Promise<void> {
114
+ async requestCertificates (certificatesToRequest: RequestedCertificateSet, identityKey?: string, maxWaitTime = 10000): Promise<void> {
115
115
  const peerSession = await this.getAuthenticatedSession(identityKey, maxWaitTime)
116
116
 
117
117
  // Prepare the general message
@@ -152,7 +152,7 @@ export class Peer {
152
152
  * @returns {Promise<PeerSession>} - A promise that resolves with an authenticated `PeerSession`.
153
153
  * @throws {Error} - Throws an error if the transport is not connected or if the handshake fails.
154
154
  */
155
- async getAuthenticatedSession(identityKey?: string, maxWaitTime?: number): Promise<PeerSession> {
155
+ async getAuthenticatedSession (identityKey?: string, maxWaitTime?: number): Promise<PeerSession> {
156
156
  if (!this.transport) {
157
157
  throw new Error('Peer transport is not connected!')
158
158
  }
@@ -175,7 +175,7 @@ export class Peer {
175
175
  * @param {(senderPublicKey: string, payload: number[]) => void} callback - The function to call when a general message is received.
176
176
  * @returns {number} The ID of the callback listener.
177
177
  */
178
- listenForGeneralMessages(callback: (senderPublicKey: string, payload: number[]) => void): number {
178
+ listenForGeneralMessages (callback: (senderPublicKey: string, payload: number[]) => void): number {
179
179
  const callbackID = this.callbackIdCounter++
180
180
  this.onGeneralMessageReceivedCallbacks.set(callbackID, callback)
181
181
  return callbackID
@@ -186,7 +186,7 @@ export class Peer {
186
186
  *
187
187
  * @param {number} callbackID - The ID of the callback to remove.
188
188
  */
189
- stopListeningForGeneralMessages(callbackID: number): void {
189
+ stopListeningForGeneralMessages (callbackID: number): void {
190
190
  this.onGeneralMessageReceivedCallbacks.delete(callbackID)
191
191
  }
192
192
 
@@ -196,7 +196,7 @@ export class Peer {
196
196
  * @param {(certs: VerifiableCertificate[]) => void} callback - The function to call when certificates are received.
197
197
  * @returns {number} The ID of the callback listener.
198
198
  */
199
- listenForCertificatesReceived(callback: (senderPublicKey: string, certs: VerifiableCertificate[]) => void): number {
199
+ listenForCertificatesReceived (callback: (senderPublicKey: string, certs: VerifiableCertificate[]) => void): number {
200
200
  const callbackID = this.callbackIdCounter++
201
201
  this.onCertificatesReceivedCallbacks.set(callbackID, callback)
202
202
  return callbackID
@@ -207,7 +207,7 @@ export class Peer {
207
207
  *
208
208
  * @param {number} callbackID - The ID of the certificates received callback to cancel.
209
209
  */
210
- stopListeningForCertificatesReceived(callbackID: number): void {
210
+ stopListeningForCertificatesReceived (callbackID: number): void {
211
211
  this.onCertificatesReceivedCallbacks.delete(callbackID)
212
212
  }
213
213
 
@@ -217,7 +217,7 @@ export class Peer {
217
217
  * @param {(requestedCertificates: RequestedCertificateSet) => void} callback - The function to call when a certificate request is received
218
218
  * @returns {number} The ID of the callback listener.
219
219
  */
220
- listenForCertificatesRequested(callback: (senderPublicKey: string, requestedCertificates: RequestedCertificateSet) => void): number {
220
+ listenForCertificatesRequested (callback: (senderPublicKey: string, requestedCertificates: RequestedCertificateSet) => void): number {
221
221
  const callbackID = this.callbackIdCounter++
222
222
  this.onCertificateRequestReceivedCallbacks.set(callbackID, callback)
223
223
  return callbackID
@@ -228,7 +228,7 @@ export class Peer {
228
228
  *
229
229
  * @param {number} callbackID - The ID of the requested certificates callback to cancel.
230
230
  */
231
- stopListeningForCertificatesRequested(callbackID: number): void {
231
+ stopListeningForCertificatesRequested (callbackID: number): void {
232
232
  this.onCertificateRequestReceivedCallbacks.delete(callbackID)
233
233
  }
234
234
 
@@ -239,7 +239,7 @@ export class Peer {
239
239
  * @param {string} [identityKey] - The identity public key of the peer.
240
240
  * @returns {Promise<string>} A promise that resolves to the session nonce.
241
241
  */
242
- private async initiateHandshake(identityKey?: string, maxWaitTime = 10000): Promise<string> {
242
+ private async initiateHandshake (identityKey?: string, maxWaitTime = 10000): Promise<string> {
243
243
  const sessionNonce = await createNonce(this.wallet) // Initial request nonce
244
244
  this.sessionManager.addSession({
245
245
  isAuthenticated: false,
@@ -265,7 +265,7 @@ export class Peer {
265
265
  * @param {string} sessionNonce - The session nonce created in the initial request.
266
266
  * @returns {Promise<string>} A promise that resolves with the session nonce when the initial response is received.
267
267
  */
268
- private async waitForInitialResponse(sessionNonce: string, maxWaitTime = 10000): Promise<string> {
268
+ private async waitForInitialResponse (sessionNonce: string, maxWaitTime = 10000): Promise<string> {
269
269
  return await new Promise((resolve, reject) => {
270
270
  const callbackID = this.listenForInitialResponse(sessionNonce, (sessionNonce) => {
271
271
  clearTimeout(timeoutHandle)
@@ -288,7 +288,7 @@ export class Peer {
288
288
  * @param {(sessionNonce: string) => void} callback - The callback to invoke when the initial response is received.
289
289
  * @returns {number} The ID of the callback listener.
290
290
  */
291
- private listenForInitialResponse(sessionNonce: string, callback: (sessionNonce: string) => void) {
291
+ private listenForInitialResponse (sessionNonce: string, callback: (sessionNonce: string) => void) {
292
292
  const callbackID = this.callbackIdCounter++
293
293
  this.onInitialResponseReceivedCallbacks.set(callbackID, { callback, sessionNonce })
294
294
  return callbackID
@@ -300,7 +300,7 @@ export class Peer {
300
300
  * @private
301
301
  * @param {number} callbackID - The ID of the callback to remove.
302
302
  */
303
- private stopListeningForInitialResponses(callbackID: number) {
303
+ private stopListeningForInitialResponses (callbackID: number) {
304
304
  this.onInitialResponseReceivedCallbacks.delete(callbackID)
305
305
  }
306
306
 
@@ -310,7 +310,7 @@ export class Peer {
310
310
  * @param {AuthMessage} message - The incoming message to process.
311
311
  * @returns {Promise<void>}
312
312
  */
313
- private async handleIncomingMessage(message: AuthMessage): Promise<void> {
313
+ private async handleIncomingMessage (message: AuthMessage): Promise<void> {
314
314
  if (!message.version || message.version !== AUTH_VERSION) {
315
315
  console.error(`Invalid message auth version! Received: ${message.version}, expected: ${AUTH_VERSION}`)
316
316
  return
@@ -343,7 +343,7 @@ export class Peer {
343
343
  * @param {AuthMessage} message - The incoming initial request message.
344
344
  * @returns {Promise<void>}
345
345
  */
346
- async processInitialRequest(message: AuthMessage) {
346
+ async processInitialRequest (message: AuthMessage) {
347
347
  if (!message.identityKey || !message.initialNonce) {
348
348
  throw new Error('Missing required fields in initialResponse message.')
349
349
  }
@@ -406,7 +406,7 @@ export class Peer {
406
406
  * @returns {Promise<void>}
407
407
  * @throws Will throw an error if nonce verification or signature verification fails.
408
408
  */
409
- private async processInitialResponse(message: AuthMessage) {
409
+ private async processInitialResponse (message: AuthMessage) {
410
410
  const validNonce = await verifyNonce(message.yourNonce, this.wallet)
411
411
  if (!validNonce) {
412
412
  throw new Error(`Initial response nonce verification failed from peer: ${message.identityKey}`)
@@ -478,7 +478,7 @@ export class Peer {
478
478
  * @param {AuthMessage} message - The certificate request message received from the peer.
479
479
  * @throws {Error} Throws an error if nonce verification fails, or the message signature is invalid.
480
480
  */
481
- private async processCertificateRequest(message: AuthMessage) {
481
+ private async processCertificateRequest (message: AuthMessage) {
482
482
  const validNonce = await verifyNonce(message.yourNonce, this.wallet)
483
483
  if (!validNonce) {
484
484
  throw new Error(`Unable to verify nonce for certificate request message from: ${message.identityKey}`)
@@ -520,7 +520,7 @@ export class Peer {
520
520
  *
521
521
  * @throws {Error} Throws an error if the peer session could not be authenticated or if message signing fails.
522
522
  */
523
- async sendCertificateResponse(
523
+ async sendCertificateResponse (
524
524
  verifierIdentityKey: string,
525
525
  certificates: VerifiableCertificate[]
526
526
  ) {
@@ -559,7 +559,7 @@ export class Peer {
559
559
  * @returns {Promise<void>}
560
560
  * @throws Will throw an error if nonce verification or signature verification fails.
561
561
  */
562
- private async processCertificateResponse(
562
+ private async processCertificateResponse (
563
563
  message: AuthMessage
564
564
  ) {
565
565
  const validNonce = await verifyNonce(message.yourNonce, this.wallet)
@@ -597,7 +597,7 @@ export class Peer {
597
597
  * @returns {Promise<void>}
598
598
  * @throws Will throw an error if nonce verification or signature verification fails.
599
599
  */
600
- private async processGeneralMessage(message: AuthMessage) {
600
+ private async processGeneralMessage (message: AuthMessage) {
601
601
  const validNonce = await verifyNonce(message.yourNonce, this.wallet)
602
602
  if (!validNonce) {
603
603
  throw new Error(`Unable to verify nonce for general message from: ${message.identityKey}`)
@@ -7,6 +7,7 @@ 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"
10
11
  jest.mock('../../../dist/cjs/src/auth/utils/getVerifiableCertificates.js')
11
12
 
12
13
  /**
@@ -22,8 +23,7 @@ async function decryptCertificateFields(
22
23
  // Decrypt the per-field symmetric key
23
24
  const { plaintext: masterFieldKey } = await localWallet.decrypt({
24
25
  ciphertext: Utils.toArray(encryptedKey, 'base64'),
25
- protocolID: [2, 'certificate field encryption'],
26
- keyID: `${cert.serialNumber} ${fieldName}`,
26
+ ...Certificate.getCertificateFieldEncryptionDetails(cert.serialNumber, fieldName),
27
27
  counterparty: (await counterpartyWallet.getPublicKey({ identityKey: true })).publicKey,
28
28
  })
29
29
 
@@ -87,33 +87,26 @@ describe('Peer class mutual authentication and certificate exchange', () => {
87
87
  const aliceFields = { name: 'Alice', email: 'alice@example.com', libraryCardNumber: 'A123456' }
88
88
  const bobFields = { name: 'Bob', email: 'bob@example.com', libraryCardNumber: 'B654321' }
89
89
 
90
- async function createMasterCertificate(wallet: Wallet, fields: Record<string, string>) {
91
- const certificateFields: Record<string, string> = {}
92
- const masterKeyring: Record<string, string> = {}
93
-
94
- for (const fieldName in fields) {
95
- const fieldSymmetricKey = SymmetricKey.fromRandom()
96
- const encryptedFieldValue = fieldSymmetricKey.encrypt(Utils.toArray(fields[fieldName], 'utf8'))
97
- certificateFields[fieldName] = Utils.toBase64(encryptedFieldValue as number[])
98
-
99
- const encryptedFieldKey = await wallet.encrypt({
100
- plaintext: fieldSymmetricKey.toArray(),
101
- protocolID: [2, 'certificate field encryption'],
102
- keyID: `${certificateSerialNumber} ${fieldName}`,
103
- counterparty: 'self'
104
- })
105
- masterKeyring[fieldName] = Utils.toBase64(encryptedFieldKey.ciphertext)
106
- }
90
+ async function createMasterCertificate(subjectWallet: Wallet, fields: Record<string, string>) {
91
+ const subjectPubKey = (await subjectWallet.getPublicKey({ identityKey: true })).publicKey
92
+ const certifierWallet = new ProtoWallet(certifierPrivateKey)
107
93
 
108
- return new MasterCertificate(
94
+ // Issue a new MasterCertificate for the subject (e.g. Alice/Bob)
95
+ const masterCertificate = await MasterCertificate.issueCertificateForSubject(
96
+ certifierWallet,
97
+ subjectPubKey,
98
+ fields,
109
99
  certificateType,
110
- certificateSerialNumber,
111
- (await wallet.getPublicKey({ identityKey: true })).publicKey,
112
- certifierPublicKey,
113
- 'revocationOutpoint',
114
- certificateFields,
115
- masterKeyring
100
+ async () => 'revocationOutpoint' // or any revocation outpoint logic you want
116
101
  )
102
+
103
+ // For test consistency, override the automatically generated serialNumber
104
+ // with the globally used 'certificateSerialNumber' and re-sign:
105
+ // masterCertificate.signature = undefined
106
+ // masterCertificate.serialNumber = certificateSerialNumber
107
+ // await masterCertificate.sign(certifierWallet)
108
+
109
+ return masterCertificate
117
110
  }
118
111
 
119
112
  async function createVerifiableCertificate(
@@ -123,7 +116,6 @@ describe('Peer class mutual authentication and certificate exchange', () => {
123
116
  fieldsToReveal: string[]
124
117
  ): Promise<VerifiableCertificate> {
125
118
  const certifierWallet = new ProtoWallet(certifierPrivateKey)
126
- await masterCertificate.sign(certifierWallet)
127
119
 
128
120
  const keyringForVerifier = await masterCertificate.createKeyringForVerifier(wallet, verifierIdentityKey, fieldsToReveal)
129
121
  return new VerifiableCertificate(
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  Utils,
3
- Wallet,
4
3
  Base64String,
5
4
  PubKeyHex,
6
5
  HexString,
7
6
  OutpointString,
8
7
  CertificateFieldNameUnder50Bytes,
9
8
  ProtoWallet,
10
- Signature
9
+ Signature,
10
+ WalletProtocol
11
11
  } from '../../../mod.js'
12
12
 
13
13
  /**
@@ -112,9 +112,12 @@ export default class Certificate {
112
112
  writer.writeVarIntNum(Number(outputIndex))
113
113
 
114
114
  // Write fields
115
- const fieldEntries = Object.entries(this.fields)
116
- writer.writeVarIntNum(fieldEntries.length)
117
- for (const [fieldName, fieldValue] of fieldEntries) {
115
+ // Sort field names lexicographically
116
+ const fieldNames = Object.keys(this.fields).sort()
117
+ writer.writeVarIntNum(fieldNames.length)
118
+ for (const fieldName of fieldNames) {
119
+ const fieldValue = this.fields[fieldName]
120
+
118
121
  // Field name
119
122
  const fieldNameBytes = Utils.toArray(fieldName, 'utf8')
120
123
  writer.writeVarIntNum(fieldNameBytes.length)
@@ -225,16 +228,36 @@ export default class Certificate {
225
228
  /**
226
229
  * Signs the certificate using the provided certifier wallet.
227
230
  *
228
- * @param {Wallet} certifier - The wallet representing the certifier.
231
+ * @param {Wallet} certifierWallet - The wallet representing the certifier.
229
232
  * @returns {Promise<void>}
230
233
  */
231
- async sign(certifier: ProtoWallet): Promise<void> {
234
+ async sign(certifierWallet: ProtoWallet): Promise<void> {
235
+ if (this.signature) {
236
+ throw new Error(`Certificate has already been signed! Signature present: ${this.signature}`)
237
+ }
238
+
239
+ // Ensure the certifier declared is the one actually signing
240
+ this.certifier = (await certifierWallet.getPublicKey({ identityKey: true })).publicKey
241
+
232
242
  const preimage = this.toBinary(false) // Exclude the signature when signing
233
- const { signature } = await certifier.createSignature({
243
+ const { signature } = await certifierWallet.createSignature({
234
244
  data: preimage,
235
245
  protocolID: [2, 'certificate signature'],
236
246
  keyID: `${this.type} ${this.serialNumber}`
237
247
  })
238
248
  this.signature = Utils.toHex(signature)
239
249
  }
250
+
251
+ /**
252
+ * Helper function which retrieves the protocol ID and key ID for certificate field encryption.
253
+ *
254
+ * @param serialNumber - The serial number of the certificate.
255
+ * @param fieldName - The name of the field within the certificate to be encrypted.
256
+ * @returns An object containing the protocol ID and key ID:
257
+ * - `protocolID` (WalletProtocol): The protocol ID for certificate field encryption.
258
+ * - `keyID` (string): A unique key identifier derived from the serial number and field name.
259
+ */
260
+ static getCertificateFieldEncryptionDetails(serialNumber: string, fieldName: string): { protocolID: WalletProtocol, keyID: string } {
261
+ return { protocolID: [2, 'certificate field encryption'], keyID: `${serialNumber} ${fieldName}` }
262
+ }
240
263
  }
@@ -6,8 +6,9 @@ import {
6
6
  HexString,
7
7
  OutpointString,
8
8
  PubKeyHex,
9
- Wallet,
10
- ProtoWallet
9
+ ProtoWallet,
10
+ Random,
11
+ WalletCounterparty
11
12
  } from '../../../mod.js'
12
13
  import Certificate from './Certificate.js'
13
14
 
@@ -25,10 +26,10 @@ export class MasterCertificate extends Certificate {
25
26
  declare subject: PubKeyHex
26
27
  declare certifier: PubKeyHex
27
28
  declare revocationOutpoint: OutpointString
28
- declare fields: Record<CertificateFieldNameUnder50Bytes, string>
29
+ declare fields: Record<CertificateFieldNameUnder50Bytes, Base64String>
29
30
  declare signature?: HexString
30
31
 
31
- masterKeyring: Record<CertificateFieldNameUnder50Bytes, string>
32
+ masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>
32
33
 
33
34
  constructor(
34
35
  type: Base64String,
@@ -36,30 +37,77 @@ export class MasterCertificate extends Certificate {
36
37
  subject: PubKeyHex,
37
38
  certifier: PubKeyHex,
38
39
  revocationOutpoint: OutpointString,
39
- fields: Record<CertificateFieldNameUnder50Bytes, string>,
40
- masterKeyring: Record<CertificateFieldNameUnder50Bytes, string>,
40
+ fields: Record<CertificateFieldNameUnder50Bytes, Base64String>,
41
+ masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>,
41
42
  signature?: HexString
42
43
  ) {
43
44
  super(type, serialNumber, subject, certifier, revocationOutpoint, fields, signature)
45
+
46
+ // Ensure every field in `fields` is a string and has a corresponding key in `masterKeyring`
47
+ for (const fieldName of Object.keys(fields)) {
48
+ if (!masterKeyring[fieldName]) {
49
+ throw new Error(
50
+ `Master keyring must contain a value for every field. Missing key for field: "${fieldName}".`
51
+ )
52
+ }
53
+ }
54
+
44
55
  this.masterKeyring = masterKeyring
45
56
  }
46
57
 
47
58
  /**
48
- * Creates a verifiable certificate structure for a specific verifier, allowing them access to specified fields.
49
- * This method decrypts the master field keys for each field specified in `fieldsToReveal` and re-encrypts them
50
- * for the verifier's identity key. The resulting certificate structure includes only the fields intended to be
51
- * revealed and a verifier-specific keyring for field decryption.
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 {ProtoWallet} 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.
68
+ */
69
+ async decryptFields(subjectWallet: ProtoWallet): 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!')
74
+ }
75
+
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.')
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Creates a keyring for a verifier, enabling them to decrypt specific certificate fields.
96
+ * This method decrypts the master field keys for the specified fields and re-encrypts them
97
+ * for the verifier's identity key. The result is a keyring containing the keys necessary
98
+ * for the verifier to access the designated fields.
52
99
  *
53
- * @param {Wallet} subjectWallet - The wallet instance of the subject, used to decrypt and re-encrypt field keys.
54
- * @param {string} verifierIdentityKey - The public identity key of the verifier who will receive access to the specified fields.
100
+ * @param {ProtoWallet} subjectWallet - The wallet instance of the subject, used to decrypt and re-encrypt field keys.
101
+ * @param {WalletCounterparty} verifier - The verifier who will receive access to the selectively revealed fields. Can be an identity key as hex, 'anyone', or 'self'.
55
102
  * @param {string[]} fieldsToReveal - An array of field names to be revealed to the verifier. Must be a subset of the certificate's fields.
56
103
  * @param {string} [originator] - Optional originator identifier, used if additional context is needed for decryption and encryption operations.
57
- * @returns {Promise<Object>} - A new certificate structure containing the original encrypted fields, the verifier-specific field decryption keyring, and essential certificate metadata.
104
+ * @returns {Promise<Record<CertificateFieldNameUnder50Bytes, string>>} - A keyring mapping field names to encrypted field revelation keys, allowing the verifier to decrypt specified fields.
58
105
  * @throws {Error} Throws an error if:
59
- * - fieldsToReveal is empty or a field in `fieldsToReveal` does not exist in the certificate.
106
+ * - fieldsToReveal is not an array of strings.
107
+ * - A field in `fieldsToReveal` does not exist in the certificate.
60
108
  * - The decrypted master field key fails to decrypt the corresponding field (indicating an invalid key).
61
109
  */
62
- async createKeyringForVerifier(subjectWallet: ProtoWallet, verifierIdentityKey: string, fieldsToReveal: string[], originator?: string): Promise<Record<CertificateFieldNameUnder50Bytes, string>> {
110
+ async createKeyringForVerifier(subjectWallet: ProtoWallet, verifier: WalletCounterparty, fieldsToReveal: string[], originator?: string): Promise<Record<CertificateFieldNameUnder50Bytes, string>> {
63
111
  if (!Array.isArray(fieldsToReveal)) {
64
112
  throw new Error('fieldsToReveal must be an array of strings')
65
113
  }
@@ -70,16 +118,13 @@ export class MasterCertificate extends Certificate {
70
118
  throw new Error(`Fields to reveal must be a subset of the certificate fields. Missing the "${fieldName}" field.`)
71
119
  }
72
120
 
73
- // Create a keyID
74
- const keyID = `${this.serialNumber} ${fieldName}`
75
121
  const encryptedMasterFieldKey = this.masterKeyring[fieldName]
76
122
 
77
123
  // Decrypt the master field key
78
124
  const { plaintext: masterFieldKey } = await subjectWallet.decrypt({
79
125
  ciphertext: Utils.toArray(encryptedMasterFieldKey, 'base64'),
80
- protocolID: [2, 'certificate field encryption'],
81
- keyID,
82
- counterparty: 'self'
126
+ ...Certificate.getCertificateFieldEncryptionDetails(this.serialNumber, fieldName),
127
+ counterparty: this.certifier
83
128
  }, originator)
84
129
 
85
130
  // Verify that derived key actually decrypts requested field
@@ -92,9 +137,8 @@ export class MasterCertificate extends Certificate {
92
137
  // Encrypt derived fieldRevelationKey for verifier
93
138
  const { ciphertext: encryptedFieldRevelationKey } = await subjectWallet.encrypt({
94
139
  plaintext: masterFieldKey,
95
- protocolID: [2, 'certificate field encryption'],
96
- keyID: `${this.serialNumber} ${fieldName}`,
97
- counterparty: verifierIdentityKey
140
+ ...Certificate.getCertificateFieldEncryptionDetails(this.serialNumber, fieldName),
141
+ counterparty: verifier
98
142
  }, originator)
99
143
 
100
144
  // Add encryptedFieldRevelationKey to fieldRevelationKeyring
@@ -104,4 +148,71 @@ export class MasterCertificate extends Certificate {
104
148
  // Return the field revelation keyring which can be used to create a verifiable certificate for a verifier.
105
149
  return fieldRevelationKeyring
106
150
  }
151
+
152
+ /**
153
+ * Issues a new MasterCertificate for a specified subject.
154
+ *
155
+ * This method generates a certificate containing encrypted fields and a keyring
156
+ * for the subject to decrypt all fields. Each field is encrypted with a randomly
157
+ * generated symmetric key, which is then encrypted for the subject. The certificate
158
+ * can also includes a revocation outpoint to manage potential revocation.
159
+ *
160
+ * @param {ProtoWallet} certifierWallet - The wallet of the certifier, used to sign the certificate and encrypt field keys.
161
+ * @param {WalletCounterparty} subject - The subject for whom the certificate is issued.
162
+ * @param {Record<CertificateFieldNameUnder50Bytes, string>} fields - Unencrypted certificate fields to include, with their names and values.
163
+ * @param {string} certificateType - The type of certificate being issued.
164
+ * @param {function(string, Record<CertificateFieldNameUnder50Bytes, string>?): Promise<string>} getRevocationOutpoint -
165
+ * Optional function to obtain a revocation outpoint for the certificate. Defaults to a placeholder.
166
+ * @param {function(string): Promise<void>} updateProgress - Optional callback for reporting progress updates during the operation. Defaults to a no-op.
167
+ * @returns {Promise<MasterCertificate>} - A signed MasterCertificate instance containing the encrypted fields and subject specific keyring.
168
+ *
169
+ * @throws {Error} Throws an error if any operation (e.g., encryption, signing) fails during certificate issuance.
170
+ */
171
+ static async issueCertificateForSubject(
172
+ certifierWallet: ProtoWallet,
173
+ subject: WalletCounterparty,
174
+ fields: Record<CertificateFieldNameUnder50Bytes, string>,
175
+ certificateType: string,
176
+ getRevocationOutpoint = async (
177
+ serialNumber: string
178
+ ): Promise<string> => { return 'Certificate revocation not tracked.' }
179
+ ): 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)
197
+ }
198
+
199
+ // 3. Obtain a revocation outpoint (ex. certifier can call wallet.createAction())
200
+ const revocationOutpoint = await getRevocationOutpoint(serialNumber)
201
+ // TODO: Validate revocation outpoint format
202
+
203
+ // 4. Create new MasterCertificate instance
204
+ const certificate = new MasterCertificate(
205
+ certificateType,
206
+ serialNumber,
207
+ subject,
208
+ (await certifierWallet.getPublicKey({ identityKey: true })).publicKey,
209
+ revocationOutpoint,
210
+ encryptedCertificateFields,
211
+ masterKeyringForSubject
212
+ )
213
+
214
+ // 5. Sign and return the new MasterCertificate certifying the subject.
215
+ await certificate.sign(certifierWallet)
216
+ return certificate
217
+ }
107
218
  }
@@ -6,7 +6,8 @@ import {
6
6
  HexString,
7
7
  OutpointString,
8
8
  PubKeyHex,
9
- Wallet
9
+ Wallet,
10
+ WalletError
10
11
  } from '../../../mod.js'
11
12
  import Certificate from './Certificate.js'
12
13
 
@@ -43,7 +44,7 @@ export class VerifiableCertificate extends Certificate {
43
44
  }
44
45
 
45
46
  /**
46
- * Decrypts certificate fields using the provided keyring and verifier wallet
47
+ * Decrypts selectively revealed certificate fields using the provided keyring and verifier wallet
47
48
  * @param {Wallet} verifierWallet - The wallet instance of the certificate's verifier, used to decrypt field keys.
48
49
  * @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.
49
50
  * @throws {Error} Throws an error if any of the decryption operations fail, with a message indicating the failure context.
@@ -53,12 +54,11 @@ export class VerifiableCertificate extends Certificate {
53
54
  throw new Error('A keyring is required to decrypt certificate fields for the verifier.')
54
55
  }
55
56
  try {
56
- const decryptedFields = {}
57
+ const decryptedFields: Record<CertificateFieldNameUnder50Bytes, string> = {}
57
58
  for (const fieldName in this.keyring) {
58
59
  const { plaintext: fieldRevelationKey } = await verifierWallet.decrypt({
59
60
  ciphertext: Utils.toArray(this.keyring[fieldName], 'base64'),
60
- protocolID: [2, 'certificate field encryption'],
61
- keyID: `${this.serialNumber} ${fieldName}`,
61
+ ...Certificate.getCertificateFieldEncryptionDetails(this.serialNumber, fieldName),
62
62
  counterparty: this.subject
63
63
  })
64
64
 
@@ -67,7 +67,7 @@ export class VerifiableCertificate extends Certificate {
67
67
  }
68
68
  return decryptedFields
69
69
  } catch (error) {
70
- throw new Error(`Failed to decrypt certificate fields using keyring: ${error instanceof Error ? error.message : error}`)
70
+ throw new Error(`Failed to decrypt selectively revealed certificate fields using keyring: ${error instanceof Error ? error.message : error}`)
71
71
  }
72
72
  }
73
73
  }