@bsv/sdk 1.2.20 → 1.2.22

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 (160) hide show
  1. package/dist/cjs/package.json +3 -3
  2. package/dist/cjs/src/auth/Peer.js +536 -0
  3. package/dist/cjs/src/auth/Peer.js.map +1 -0
  4. package/dist/cjs/src/auth/SessionManager.js +66 -0
  5. package/dist/cjs/src/auth/SessionManager.js.map +1 -0
  6. package/dist/cjs/src/auth/{Certificate.js → certificates/Certificate.js} +22 -26
  7. package/dist/cjs/src/auth/certificates/Certificate.js.map +1 -0
  8. package/dist/cjs/src/auth/certificates/MasterCertificate.js +79 -0
  9. package/dist/cjs/src/auth/certificates/MasterCertificate.js.map +1 -0
  10. package/dist/cjs/src/auth/certificates/VerifiableCertificate.js +49 -0
  11. package/dist/cjs/src/auth/certificates/VerifiableCertificate.js.map +1 -0
  12. package/dist/cjs/src/auth/certificates/index.js +25 -0
  13. package/dist/cjs/src/auth/certificates/index.js.map +1 -0
  14. package/dist/cjs/src/auth/clients/AuthFetch.js +411 -0
  15. package/dist/cjs/src/auth/clients/AuthFetch.js.map +1 -0
  16. package/dist/cjs/src/auth/clients/index.js +18 -0
  17. package/dist/cjs/src/auth/clients/index.js.map +1 -0
  18. package/dist/cjs/src/auth/index.js +20 -5
  19. package/dist/cjs/src/auth/index.js.map +1 -1
  20. package/dist/cjs/src/auth/transports/SimplifiedFetchTransport.js +259 -0
  21. package/dist/cjs/src/auth/transports/SimplifiedFetchTransport.js.map +1 -0
  22. package/dist/cjs/src/auth/transports/index.js +18 -0
  23. package/dist/cjs/src/auth/transports/index.js.map +1 -0
  24. package/dist/cjs/src/auth/types.js +3 -0
  25. package/dist/cjs/src/auth/types.js.map +1 -0
  26. package/dist/cjs/src/auth/utils/certificateHelpers.js +51 -0
  27. package/dist/cjs/src/auth/utils/certificateHelpers.js.map +1 -0
  28. package/dist/cjs/src/auth/utils/createNonce.js +19 -0
  29. package/dist/cjs/src/auth/utils/createNonce.js.map +1 -0
  30. package/dist/cjs/src/auth/utils/getVerifiableCertificates.js +31 -0
  31. package/dist/cjs/src/auth/utils/getVerifiableCertificates.js.map +1 -0
  32. package/dist/cjs/src/auth/utils/index.js +22 -0
  33. package/dist/cjs/src/auth/utils/index.js.map +1 -0
  34. package/dist/cjs/src/auth/utils/validateCertificates.js +42 -0
  35. package/dist/cjs/src/auth/utils/validateCertificates.js.map +1 -0
  36. package/dist/cjs/src/auth/utils/verifyNonce.js +27 -0
  37. package/dist/cjs/src/auth/utils/verifyNonce.js.map +1 -0
  38. package/dist/cjs/src/primitives/Point.js +1 -1
  39. package/dist/cjs/src/primitives/Point.js.map +1 -1
  40. package/dist/cjs/src/wallet/substrates/WalletWireProcessor.js +1 -1
  41. package/dist/cjs/src/wallet/substrates/WalletWireProcessor.js.map +1 -1
  42. package/dist/cjs/src/wallet/substrates/WalletWireTransceiver.js +148 -148
  43. package/dist/cjs/src/wallet/substrates/WalletWireTransceiver.js.map +1 -1
  44. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  45. package/dist/esm/src/auth/Peer.js +533 -0
  46. package/dist/esm/src/auth/Peer.js.map +1 -0
  47. package/dist/esm/src/auth/SessionManager.js +63 -0
  48. package/dist/esm/src/auth/SessionManager.js.map +1 -0
  49. package/dist/esm/src/auth/{Certificate.js → certificates/Certificate.js} +1 -2
  50. package/dist/esm/src/auth/certificates/Certificate.js.map +1 -0
  51. package/dist/esm/src/auth/certificates/MasterCertificate.js +73 -0
  52. package/dist/esm/src/auth/certificates/MasterCertificate.js.map +1 -0
  53. package/dist/esm/src/auth/certificates/VerifiableCertificate.js +44 -0
  54. package/dist/esm/src/auth/certificates/VerifiableCertificate.js.map +1 -0
  55. package/dist/esm/src/auth/certificates/index.js +4 -0
  56. package/dist/esm/src/auth/certificates/index.js.map +1 -0
  57. package/dist/esm/src/auth/clients/AuthFetch.js +409 -0
  58. package/dist/esm/src/auth/clients/AuthFetch.js.map +1 -0
  59. package/dist/esm/src/auth/clients/index.js +2 -0
  60. package/dist/esm/src/auth/clients/index.js.map +1 -0
  61. package/dist/esm/src/auth/index.js +7 -1
  62. package/dist/esm/src/auth/index.js.map +1 -1
  63. package/dist/esm/src/auth/transports/SimplifiedFetchTransport.js +258 -0
  64. package/dist/esm/src/auth/transports/SimplifiedFetchTransport.js.map +1 -0
  65. package/dist/esm/src/auth/transports/index.js +2 -0
  66. package/dist/esm/src/auth/transports/index.js.map +1 -0
  67. package/dist/esm/src/auth/types.js +2 -0
  68. package/dist/esm/src/auth/types.js.map +1 -0
  69. package/dist/esm/src/auth/utils/certificateHelpers.js +47 -0
  70. package/dist/esm/src/auth/utils/certificateHelpers.js.map +1 -0
  71. package/dist/esm/src/auth/utils/createNonce.js +16 -0
  72. package/dist/esm/src/auth/utils/createNonce.js.map +1 -0
  73. package/dist/esm/src/auth/utils/getVerifiableCertificates.js +27 -0
  74. package/dist/esm/src/auth/utils/getVerifiableCertificates.js.map +1 -0
  75. package/dist/esm/src/auth/utils/index.js +6 -0
  76. package/dist/esm/src/auth/utils/index.js.map +1 -0
  77. package/dist/esm/src/auth/utils/validateCertificates.js +38 -0
  78. package/dist/esm/src/auth/utils/validateCertificates.js.map +1 -0
  79. package/dist/esm/src/auth/utils/verifyNonce.js +24 -0
  80. package/dist/esm/src/auth/utils/verifyNonce.js.map +1 -0
  81. package/dist/esm/src/primitives/Point.js +1 -1
  82. package/dist/esm/src/primitives/Point.js.map +1 -1
  83. package/dist/esm/src/wallet/substrates/WalletWireProcessor.js +1 -1
  84. package/dist/esm/src/wallet/substrates/WalletWireProcessor.js.map +1 -1
  85. package/dist/esm/src/wallet/substrates/WalletWireTransceiver.js +1 -1
  86. package/dist/esm/src/wallet/substrates/WalletWireTransceiver.js.map +1 -1
  87. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  88. package/dist/types/src/auth/Peer.d.ts +193 -0
  89. package/dist/types/src/auth/Peer.d.ts.map +1 -0
  90. package/dist/types/src/auth/SessionManager.d.ts +42 -0
  91. package/dist/types/src/auth/SessionManager.d.ts.map +1 -0
  92. package/dist/types/src/auth/{Certificate.d.ts → certificates/Certificate.d.ts} +1 -1
  93. package/dist/types/src/auth/certificates/Certificate.d.ts.map +1 -0
  94. package/dist/types/src/auth/certificates/MasterCertificate.d.ts +38 -0
  95. package/dist/types/src/auth/certificates/MasterCertificate.d.ts.map +1 -0
  96. package/dist/types/src/auth/certificates/VerifiableCertificate.d.ts +26 -0
  97. package/dist/types/src/auth/certificates/VerifiableCertificate.d.ts.map +1 -0
  98. package/dist/types/src/auth/certificates/index.d.ts +4 -0
  99. package/dist/types/src/auth/certificates/index.d.ts.map +1 -0
  100. package/dist/types/src/auth/clients/AuthFetch.d.ts +87 -0
  101. package/dist/types/src/auth/clients/AuthFetch.d.ts.map +1 -0
  102. package/dist/types/src/auth/clients/index.d.ts +2 -0
  103. package/dist/types/src/auth/clients/index.d.ts.map +1 -0
  104. package/dist/types/src/auth/index.d.ts +7 -1
  105. package/dist/types/src/auth/index.d.ts.map +1 -1
  106. package/dist/types/src/auth/transports/SimplifiedFetchTransport.d.ts +51 -0
  107. package/dist/types/src/auth/transports/SimplifiedFetchTransport.d.ts.map +1 -0
  108. package/dist/types/src/auth/transports/index.d.ts +2 -0
  109. package/dist/types/src/auth/transports/index.d.ts.map +1 -0
  110. package/dist/types/src/auth/types.d.ts +31 -0
  111. package/dist/types/src/auth/types.d.ts.map +1 -0
  112. package/dist/types/src/auth/utils/certificateHelpers.d.ts +26 -0
  113. package/dist/types/src/auth/utils/certificateHelpers.d.ts.map +1 -0
  114. package/dist/types/src/auth/utils/createNonce.d.ts +8 -0
  115. package/dist/types/src/auth/utils/createNonce.d.ts.map +1 -0
  116. package/dist/types/src/auth/utils/getVerifiableCertificates.d.ts +13 -0
  117. package/dist/types/src/auth/utils/getVerifiableCertificates.d.ts.map +1 -0
  118. package/dist/types/src/auth/utils/index.d.ts +6 -0
  119. package/dist/types/src/auth/utils/index.d.ts.map +1 -0
  120. package/dist/types/src/auth/utils/validateCertificates.d.ts +12 -0
  121. package/dist/types/src/auth/utils/validateCertificates.d.ts.map +1 -0
  122. package/dist/types/src/auth/utils/verifyNonce.d.ts +9 -0
  123. package/dist/types/src/auth/utils/verifyNonce.d.ts.map +1 -0
  124. package/dist/types/src/primitives/Point.d.ts.map +1 -1
  125. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  126. package/dist/umd/bundle.js +1 -1
  127. package/docs/README.md +1 -0
  128. package/docs/auth.md +1193 -0
  129. package/package.json +13 -3
  130. package/src/auth/Peer.ts +600 -0
  131. package/src/auth/SessionManager.ts +71 -0
  132. package/src/auth/__tests/Peer.test.ts +599 -0
  133. package/src/auth/__tests/SessionManager.test.ts +87 -0
  134. package/src/auth/{Certificate.ts → certificates/Certificate.ts} +15 -8
  135. package/src/auth/certificates/MasterCertificate.ts +106 -0
  136. package/src/auth/certificates/VerifiableCertificate.ts +73 -0
  137. package/src/auth/certificates/__tests/Certificate.test.ts +282 -0
  138. package/src/auth/certificates/index.ts +3 -0
  139. package/src/auth/clients/AuthFetch.ts +482 -0
  140. package/src/auth/clients/index.ts +1 -0
  141. package/src/auth/index.ts +7 -1
  142. package/src/auth/transports/SimplifiedFetchTransport.ts +288 -0
  143. package/src/auth/transports/index.ts +1 -0
  144. package/src/auth/types.ts +41 -0
  145. package/src/auth/utils/__tests/cryptononce.test.ts +84 -0
  146. package/src/auth/utils/__tests/getVerifiableCertificates.test.ts +126 -0
  147. package/src/auth/utils/__tests/validateCertificates.test.ts +142 -0
  148. package/src/auth/utils/certificateHelpers.ts +86 -0
  149. package/src/auth/utils/createNonce.ts +16 -0
  150. package/src/auth/utils/getVerifiableCertificates.ts +40 -0
  151. package/src/auth/utils/index.ts +5 -0
  152. package/src/auth/utils/validateCertificates.ts +54 -0
  153. package/src/auth/utils/verifyNonce.ts +27 -0
  154. package/src/primitives/Point.ts +59 -59
  155. package/src/wallet/substrates/WalletWireProcessor.ts +1 -1
  156. package/src/wallet/substrates/WalletWireTransceiver.ts +1 -1
  157. package/dist/cjs/src/auth/Certificate.js.map +0 -1
  158. package/dist/esm/src/auth/Certificate.js.map +0 -1
  159. package/dist/types/src/auth/Certificate.d.ts.map +0 -1
  160. package/src/auth/__tests/Certificate.test.ts +0 -282
@@ -0,0 +1,87 @@
1
+ import { SessionManager } from '../SessionManager'
2
+ import { PeerSession } from '../types'
3
+
4
+ describe('SessionManager', () => {
5
+ let sessionManager: SessionManager
6
+ let validSession: PeerSession
7
+
8
+ beforeEach(() => {
9
+ sessionManager = new SessionManager()
10
+ validSession = {
11
+ isAuthenticated: false,
12
+ sessionNonce: 'testSessionNonce',
13
+ peerIdentityKey: 'testPeerIdentityKey',
14
+ }
15
+ })
16
+
17
+ describe('addSession', () => {
18
+ it('should add a session when sessionNonce and peerIdentityKey are present', () => {
19
+ sessionManager.addSession(validSession)
20
+
21
+ expect(sessionManager.getSession(validSession.sessionNonce!)).toBe(validSession)
22
+ expect(sessionManager.getSession(validSession.peerIdentityKey!)).toBe(validSession)
23
+ })
24
+
25
+ it('should throw an error if sessionNonce and peerIdentityKey are missing', () => {
26
+ const invalidSession = { ...validSession, sessionNonce: undefined, peerIdentityKey: undefined }
27
+
28
+ expect(() => sessionManager.addSession(invalidSession)).toThrow('Invalid session: at least one of sessionNonce or peerIdentityKey is required.')
29
+ })
30
+
31
+ it('should not throw an error if just peerIdentityKey is missing', () => {
32
+ const invalidSession = { ...validSession, peerIdentityKey: undefined }
33
+
34
+ expect(() => sessionManager.addSession(invalidSession)).not.toThrow('Invalid session: peerIdentityKey is required.')
35
+ })
36
+ })
37
+
38
+ describe('getSession', () => {
39
+ it('should retrieve a session by sessionNonce', () => {
40
+ sessionManager.addSession(validSession)
41
+
42
+ const retrievedSession = sessionManager.getSession(validSession.sessionNonce!)
43
+ expect(retrievedSession).toBe(validSession)
44
+ })
45
+
46
+ it('should retrieve a session by peerIdentityKey', () => {
47
+ sessionManager.addSession(validSession)
48
+
49
+ const retrievedSession = sessionManager.getSession(validSession.peerIdentityKey!)
50
+ expect(retrievedSession).toBe(validSession)
51
+ })
52
+
53
+ it('should return undefined for a non-existent identifier', () => {
54
+ const retrievedSession = sessionManager.getSession('nonExistentIdentifier')
55
+ expect(retrievedSession).toBeUndefined()
56
+ })
57
+ })
58
+
59
+ describe('removeSession', () => {
60
+ it('should remove a session by both sessionNonce and peerIdentityKey', () => {
61
+ sessionManager.addSession(validSession)
62
+
63
+ sessionManager.removeSession(validSession)
64
+ expect(sessionManager.getSession(validSession.sessionNonce!)).toBeUndefined()
65
+ expect(sessionManager.getSession(validSession.peerIdentityKey!)).toBeUndefined()
66
+ })
67
+
68
+ it('should not throw an error when removing a session with undefined identifiers', () => {
69
+ const sessionWithUndefinedIdentifiers = { ...validSession, sessionNonce: undefined, peerIdentityKey: undefined }
70
+
71
+ expect(() => sessionManager.removeSession(sessionWithUndefinedIdentifiers)).not.toThrow()
72
+ })
73
+ })
74
+
75
+ describe('hasSession', () => {
76
+ it('should return true if a session exists for the identifier', () => {
77
+ sessionManager.addSession(validSession)
78
+
79
+ expect(sessionManager.hasSession(validSession.sessionNonce!)).toBe(true)
80
+ expect(sessionManager.hasSession(validSession.peerIdentityKey!)).toBe(true)
81
+ })
82
+
83
+ it('should return false if no session exists for the identifier', () => {
84
+ expect(sessionManager.hasSession('nonExistentIdentifier')).toBe(false)
85
+ })
86
+ })
87
+ })
@@ -1,6 +1,13 @@
1
- import { Utils } from '../primitives/index.js'
2
- import { Wallet, Base64String, PubKeyHex, HexString, OutpointString, CertificateFieldNameUnder50Bytes } from '../wallet/Wallet.interfaces.js'
3
- import ProtoWallet from '../wallet/ProtoWallet.js'
1
+ import {
2
+ Utils,
3
+ Wallet,
4
+ Base64String,
5
+ PubKeyHex,
6
+ HexString,
7
+ OutpointString,
8
+ CertificateFieldNameUnder50Bytes,
9
+ ProtoWallet
10
+ } from '../../../mod.js'
4
11
 
5
12
  /**
6
13
  * Represents an Identity Certificate as per the Wallet interface specifications.
@@ -54,7 +61,7 @@ export default class Certificate {
54
61
  * @param {Record<CertificateFieldNameUnder50Bytes, string>} fields - All the fields present in the certificate.
55
62
  * @param {HexString} signature - Certificate signature by the certifier's private key, DER encoded hex string.
56
63
  */
57
- constructor (
64
+ constructor(
58
65
  type: Base64String,
59
66
  serialNumber: Base64String,
60
67
  subject: PubKeyHex,
@@ -78,7 +85,7 @@ export default class Certificate {
78
85
  * @param {boolean} [includeSignature=true] - Whether to include the signature in the serialization.
79
86
  * @returns {number[]} - The serialized certificate in binary format.
80
87
  */
81
- toBin (includeSignature: boolean = true): number[] {
88
+ toBin(includeSignature: boolean = true): number[] {
82
89
  const writer = new Utils.Writer()
83
90
 
84
91
  // Write type (Base64String, 32 bytes)
@@ -134,7 +141,7 @@ export default class Certificate {
134
141
  * @param {number[]} bin - The binary data representing the certificate.
135
142
  * @returns {Certificate} - The deserialized Certificate object.
136
143
  */
137
- static fromBin (bin: number[]): Certificate {
144
+ static fromBin(bin: number[]): Certificate {
138
145
  const reader = new Utils.Reader(bin)
139
146
 
140
147
  // Read type
@@ -200,7 +207,7 @@ export default class Certificate {
200
207
  *
201
208
  * @returns {Promise<boolean>} - A promise that resolves to true if the signature is valid.
202
209
  */
203
- async verify (): Promise<boolean> {
210
+ async verify(): Promise<boolean> {
204
211
  // A verifier can be any wallet capable of verifying signatures
205
212
  const verifier = new ProtoWallet('anyone')
206
213
  const verificationData = this.toBin(false) // Exclude the signature from the verification data
@@ -221,7 +228,7 @@ export default class Certificate {
221
228
  * @param {Wallet} certifier - The wallet representing the certifier.
222
229
  * @returns {Promise<void>}
223
230
  */
224
- async sign (certifier: Wallet): Promise<void> {
231
+ async sign(certifier: Wallet): Promise<void> {
225
232
  const preimage = this.toBin(false) // Exclude the signature when signing
226
233
  const { signature } = await certifier.createSignature({
227
234
  data: preimage,
@@ -0,0 +1,106 @@
1
+ import {
2
+ SymmetricKey,
3
+ Utils,
4
+ Base64String,
5
+ CertificateFieldNameUnder50Bytes,
6
+ HexString,
7
+ OutpointString,
8
+ PubKeyHex,
9
+ Wallet
10
+ } from '../../../mod.js'
11
+ import Certificate from './Certificate.js'
12
+
13
+ /**
14
+ * MasterCertificate extends the base Certificate class to manage a master keyring, enabling the creation of verifiable certificates.
15
+ *
16
+ * It allows for the selective disclosure of certificate fields by creating a `VerifiableCertificate` for a specific verifier.
17
+ * The `MasterCertificate` can securely decrypt each master key and re-encrypt it for a verifier, creating a customized
18
+ * keyring containing only the keys necessary for the verifier to access designated fields.
19
+ *
20
+ */
21
+ export class MasterCertificate extends Certificate {
22
+ declare type: Base64String
23
+ declare serialNumber: Base64String
24
+ declare subject: PubKeyHex
25
+ declare certifier: PubKeyHex
26
+ declare revocationOutpoint: OutpointString
27
+ declare fields: Record<CertificateFieldNameUnder50Bytes, string>
28
+ declare signature?: HexString
29
+
30
+ masterKeyring: Record<CertificateFieldNameUnder50Bytes, string>
31
+
32
+ constructor(
33
+ type: Base64String,
34
+ serialNumber: Base64String,
35
+ subject: PubKeyHex,
36
+ certifier: PubKeyHex,
37
+ revocationOutpoint: OutpointString,
38
+ fields: Record<CertificateFieldNameUnder50Bytes, string>,
39
+ masterKeyring: Record<CertificateFieldNameUnder50Bytes, string>,
40
+ signature?: HexString
41
+ ) {
42
+ super(type, serialNumber, subject, certifier, revocationOutpoint, fields, signature)
43
+ this.masterKeyring = masterKeyring
44
+ }
45
+
46
+ /**
47
+ * Creates a verifiable certificate structure for a specific verifier, allowing them access to specified fields.
48
+ * This method decrypts the master field keys for each field specified in `fieldsToReveal` and re-encrypts them
49
+ * for the verifier's identity key. The resulting certificate structure includes only the fields intended to be
50
+ * revealed and a verifier-specific keyring for field decryption.
51
+ *
52
+ * @param {Wallet} subjectWallet - The wallet instance of the subject, used to decrypt and re-encrypt field keys.
53
+ * @param {string} verifierIdentityKey - The public identity key of the verifier who will receive access to the specified fields.
54
+ * @param {string[]} fieldsToReveal - An array of field names to be revealed to the verifier. Must be a subset of the certificate's fields.
55
+ * @param {string} [originator] - Optional originator identifier, used if additional context is needed for decryption and encryption operations.
56
+ * @returns {Promise<Object>} - A new certificate structure containing the original encrypted fields, the verifier-specific field decryption keyring, and essential certificate metadata.
57
+ * @throws {Error} Throws an error if:
58
+ * - fieldsToReveal is empty or a field in `fieldsToReveal` does not exist in the certificate.
59
+ * - The decrypted master field key fails to decrypt the corresponding field (indicating an invalid key).
60
+ */
61
+ async createKeyringForVerifier(subjectWallet: Wallet, verifierIdentityKey: string, fieldsToReveal: string[], originator?: string): Promise<Record<CertificateFieldNameUnder50Bytes, string>> {
62
+ if (!Array.isArray(fieldsToReveal)) {
63
+ throw new Error('fieldsToReveal must be an array of strings')
64
+ }
65
+ const fieldRevelationKeyring = {}
66
+ for (const fieldName of fieldsToReveal) {
67
+ // Make sure that fields to reveal is a subset of the certificate fields
68
+ if (!this.fields[fieldName]) {
69
+ throw new Error(`Fields to reveal must be a subset of the certificate fields. Missing the "${fieldName}" field.`)
70
+ }
71
+
72
+ // Create a keyID
73
+ const keyID = `${this.serialNumber} ${fieldName}`
74
+ const encryptedMasterFieldKey = this.masterKeyring[fieldName]
75
+
76
+ // Decrypt the master field key
77
+ const { plaintext: masterFieldKey } = await subjectWallet.decrypt({
78
+ ciphertext: Utils.toArray(encryptedMasterFieldKey, 'base64'),
79
+ protocolID: [2, 'certificate field encryption'],
80
+ keyID,
81
+ counterparty: 'self'
82
+ }, originator)
83
+
84
+ // Verify that derived key actually decrypts requested field
85
+ try {
86
+ new SymmetricKey(masterFieldKey).decrypt(Utils.toArray(this.fields[fieldName], 'base64'))
87
+ } catch (_) {
88
+ throw new Error(`Decryption of the "${fieldName}" field with its revelation key failed.`)
89
+ }
90
+
91
+ // Encrypt derived fieldRevelationKey for verifier
92
+ const { ciphertext: encryptedFieldRevelationKey } = await subjectWallet.encrypt({
93
+ plaintext: masterFieldKey,
94
+ protocolID: [2, 'certificate field encryption'],
95
+ keyID: `${this.serialNumber} ${fieldName}`,
96
+ counterparty: verifierIdentityKey
97
+ }, originator)
98
+
99
+ // Add encryptedFieldRevelationKey to fieldRevelationKeyring
100
+ fieldRevelationKeyring[fieldName] = Utils.toBase64(encryptedFieldRevelationKey)
101
+ }
102
+
103
+ // Return the field revelation keyring which can be used to create a verifiable certificate for a verifier.
104
+ return fieldRevelationKeyring
105
+ }
106
+ }
@@ -0,0 +1,73 @@
1
+ import {
2
+ SymmetricKey,
3
+ Utils,
4
+ Base64String,
5
+ CertificateFieldNameUnder50Bytes,
6
+ HexString,
7
+ OutpointString,
8
+ PubKeyHex,
9
+ Wallet
10
+ } from '../../../mod.js'
11
+ import Certificate from './Certificate.js'
12
+
13
+ /**
14
+ * VerifiableCertificate extends the Certificate class, adding functionality to manage a verifier-specific keyring.
15
+ * This keyring allows selective decryption of certificate fields for authorized verifiers.
16
+ */
17
+ export class VerifiableCertificate extends Certificate {
18
+ declare type: Base64String
19
+ declare serialNumber: Base64String
20
+ declare subject: PubKeyHex
21
+ declare certifier: PubKeyHex
22
+ declare revocationOutpoint: OutpointString
23
+ declare fields: Record<CertificateFieldNameUnder50Bytes, string>
24
+ declare signature?: HexString
25
+
26
+ keyring: Record<CertificateFieldNameUnder50Bytes, string>
27
+ decryptedFields?: Record<CertificateFieldNameUnder50Bytes, Base64String>
28
+
29
+ constructor(
30
+ type: Base64String,
31
+ serialNumber: Base64String,
32
+ subject: PubKeyHex,
33
+ certifier: PubKeyHex,
34
+ revocationOutpoint: OutpointString,
35
+ fields: Record<CertificateFieldNameUnder50Bytes, string>,
36
+ signature?: HexString,
37
+ keyring?: Record<CertificateFieldNameUnder50Bytes, string>,
38
+ decryptedFields?: Record<CertificateFieldNameUnder50Bytes, Base64String>
39
+ ) {
40
+ super(type, serialNumber, subject, certifier, revocationOutpoint, fields, signature)
41
+ this.keyring = keyring
42
+ this.decryptedFields = decryptedFields
43
+ }
44
+
45
+ /**
46
+ * Decrypts certificate fields using the provided keyring and verifier wallet
47
+ * @param {Wallet} verifierWallet - The wallet instance of the certificate's verifier, used to decrypt field keys.
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.
49
+ * @throws {Error} Throws an error if any of the decryption operations fail, with a message indicating the failure context.
50
+ */
51
+ async decryptFields(verifierWallet: Wallet): Promise<Record<CertificateFieldNameUnder50Bytes, string>> {
52
+ if (!this.keyring || Object.keys(this.keyring).length === 0) {
53
+ throw new Error('A keyring is required to decrypt certificate fields for the verifier.')
54
+ }
55
+ try {
56
+ const decryptedFields = {}
57
+ for (const fieldName in this.keyring) {
58
+ const { plaintext: fieldRevelationKey } = await verifierWallet.decrypt({
59
+ ciphertext: Utils.toArray(this.keyring[fieldName], 'base64'),
60
+ protocolID: [2, 'certificate field encryption'],
61
+ keyID: `${this.serialNumber} ${fieldName}`,
62
+ counterparty: this.subject
63
+ })
64
+
65
+ const fieldValue = new SymmetricKey(fieldRevelationKey).decrypt(Utils.toArray(this.fields[fieldName], 'base64'))
66
+ decryptedFields[fieldName] = Utils.toUTF8(fieldValue as number[])
67
+ }
68
+ return decryptedFields
69
+ } catch (error) {
70
+ throw new Error(`Failed to decrypt certificate fields using keyring: ${error instanceof Error ? error.message : error}`)
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,282 @@
1
+ import { Certificate } from '../../../../dist/cjs/src/auth/index.js'
2
+ import { ProtoWallet } from '../../../../dist/cjs/src/wallet/index.js'
3
+ import { Utils, PrivateKey } from '../../../../dist/cjs/src/primitives/index.js'
4
+
5
+ describe('Certificate', () => {
6
+ // Sample data for testing
7
+ const sampleType = Utils.toBase64(new Array(32).fill(1))
8
+ const sampleSerialNumber = Utils.toBase64(new Array(32).fill(2))
9
+ const sampleSubjectPrivateKey = PrivateKey.fromRandom()
10
+ const sampleSubjectPubKey = sampleSubjectPrivateKey.toPublicKey().toString()
11
+ const sampleCertifierPrivateKey = PrivateKey.fromRandom()
12
+ const sampleCertifierPubKey = sampleCertifierPrivateKey.toPublicKey().toString()
13
+ const sampleRevocationOutpoint = 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef.1'
14
+ const sampleFields = {
15
+ name: 'Alice',
16
+ email: 'alice@example.com',
17
+ organization: 'Example Corp'
18
+ }
19
+ const sampleFieldsEmpty = {}
20
+
21
+ it('should construct a Certificate with valid data', () => {
22
+ const certificate = new Certificate(
23
+ sampleType,
24
+ sampleSerialNumber,
25
+ sampleSubjectPubKey,
26
+ sampleCertifierPubKey,
27
+ sampleRevocationOutpoint,
28
+ sampleFields,
29
+ undefined // No signature
30
+ )
31
+
32
+ expect(certificate.type).toEqual(sampleType)
33
+ expect(certificate.serialNumber).toEqual(sampleSerialNumber)
34
+ expect(certificate.subject).toEqual(sampleSubjectPubKey)
35
+ expect(certificate.certifier).toEqual(sampleCertifierPubKey)
36
+ expect(certificate.revocationOutpoint).toEqual(sampleRevocationOutpoint)
37
+ expect(certificate.signature).toBeUndefined()
38
+ expect(certificate.fields).toEqual(sampleFields)
39
+ })
40
+
41
+ it('should serialize and deserialize the Certificate without signature', () => {
42
+ const certificate = new Certificate(
43
+ sampleType,
44
+ sampleSerialNumber,
45
+ sampleSubjectPubKey,
46
+ sampleCertifierPubKey,
47
+ sampleRevocationOutpoint,
48
+ sampleFields,
49
+ undefined // No signature
50
+ )
51
+
52
+ const serialized = certificate.toBin(false) // Exclude signature
53
+ const deserializedCertificate = Certificate.fromBin(serialized)
54
+
55
+ expect(deserializedCertificate.type).toEqual(sampleType)
56
+ expect(deserializedCertificate.serialNumber).toEqual(sampleSerialNumber)
57
+ expect(deserializedCertificate.subject).toEqual(sampleSubjectPubKey)
58
+ expect(deserializedCertificate.certifier).toEqual(sampleCertifierPubKey)
59
+ expect(deserializedCertificate.revocationOutpoint).toEqual(sampleRevocationOutpoint)
60
+ expect(deserializedCertificate.signature).toBeUndefined()
61
+ expect(deserializedCertificate.fields).toEqual(sampleFields)
62
+ })
63
+
64
+ it('should serialize and deserialize the Certificate with signature', async () => {
65
+ const certificate = new Certificate(
66
+ sampleType,
67
+ sampleSerialNumber,
68
+ sampleSubjectPubKey,
69
+ sampleCertifierPubKey,
70
+ sampleRevocationOutpoint,
71
+ sampleFields,
72
+ undefined // No signature
73
+ )
74
+
75
+ // Sign the certificate
76
+ const certifierWallet = new ProtoWallet(sampleCertifierPrivateKey)
77
+ await certificate.sign(certifierWallet)
78
+
79
+ const serialized = certificate.toBin(true) // Include signature
80
+ const deserializedCertificate = Certificate.fromBin(serialized)
81
+
82
+ expect(deserializedCertificate.type).toEqual(sampleType)
83
+ expect(deserializedCertificate.serialNumber).toEqual(sampleSerialNumber)
84
+ expect(deserializedCertificate.subject).toEqual(sampleSubjectPubKey)
85
+ expect(deserializedCertificate.certifier).toEqual(sampleCertifierPubKey)
86
+ expect(deserializedCertificate.revocationOutpoint).toEqual(sampleRevocationOutpoint)
87
+ expect(deserializedCertificate.signature).toEqual(certificate.signature)
88
+ expect(deserializedCertificate.fields).toEqual(sampleFields)
89
+ })
90
+
91
+ it('should sign the Certificate and verify the signature successfully', async () => {
92
+ const certificate = new Certificate(
93
+ sampleType,
94
+ sampleSerialNumber,
95
+ sampleSubjectPubKey,
96
+ sampleCertifierPubKey,
97
+ sampleRevocationOutpoint,
98
+ sampleFields,
99
+ undefined // No signature
100
+ )
101
+
102
+ // Sign the certificate
103
+ const certifierWallet = new ProtoWallet(sampleCertifierPrivateKey)
104
+ await certificate.sign(certifierWallet)
105
+
106
+ // Verify the signature
107
+ const isValid = await certificate.verify()
108
+ expect(isValid).toBe(true)
109
+ })
110
+
111
+ it('should fail verification if the Certificate is tampered with', async () => {
112
+ const certificate = new Certificate(
113
+ sampleType,
114
+ sampleSerialNumber,
115
+ sampleSubjectPubKey,
116
+ sampleCertifierPubKey,
117
+ sampleRevocationOutpoint,
118
+ sampleFields,
119
+ undefined // No signature
120
+ )
121
+
122
+ // Sign the certificate
123
+ const certifierWallet = new ProtoWallet(sampleCertifierPrivateKey)
124
+ await certificate.sign(certifierWallet)
125
+
126
+ // Tamper with the certificate (modify a field)
127
+ certificate.fields.email = 'attacker@example.com'
128
+
129
+ // Verify the signature
130
+ await expect(certificate.verify()).rejects.toThrow()
131
+ })
132
+
133
+ it('should fail verification if the signature is missing', async () => {
134
+ const certificate = new Certificate(
135
+ sampleType,
136
+ sampleSerialNumber,
137
+ sampleSubjectPubKey,
138
+ sampleCertifierPubKey,
139
+ sampleRevocationOutpoint,
140
+ sampleFields,
141
+ undefined // No signature
142
+ )
143
+
144
+ // Verify the signature
145
+ await expect(certificate.verify()).rejects.toThrow()
146
+ })
147
+
148
+ it('should fail verification if the signature is incorrect', async () => {
149
+ const certificate = new Certificate(
150
+ sampleType,
151
+ sampleSerialNumber,
152
+ sampleSubjectPubKey,
153
+ sampleCertifierPubKey,
154
+ sampleRevocationOutpoint,
155
+ sampleFields,
156
+ '3045022100cde229279465bb91992ccbc30bf6ed4eb8cdd9d517f31b30ff778d500d5400010220134f0e4065984f8668a642a5ad7a80886265f6aaa56d215d6400c216a4802177' // Incorrect signature
157
+ )
158
+
159
+ // Verify the signature
160
+ await expect(certificate.verify()).rejects.toThrowErrorMatchingInlineSnapshot(`"Signature is not valid"`)
161
+ })
162
+
163
+ it('should handle certificates with empty fields', async () => {
164
+ const certificate = new Certificate(
165
+ sampleType,
166
+ sampleSerialNumber,
167
+ sampleSubjectPubKey,
168
+ sampleCertifierPubKey,
169
+ sampleRevocationOutpoint,
170
+ sampleFieldsEmpty,
171
+ undefined // No signature
172
+ )
173
+
174
+ // Sign the certificate
175
+ const certifierWallet = new ProtoWallet(sampleCertifierPrivateKey)
176
+ await certificate.sign(certifierWallet)
177
+
178
+ // Serialize and deserialize
179
+ const serialized = certificate.toBin(true)
180
+ const deserializedCertificate = Certificate.fromBin(serialized)
181
+
182
+ expect(deserializedCertificate.fields).toEqual(sampleFieldsEmpty)
183
+
184
+ // Verify the signature
185
+ const isValid = await deserializedCertificate.verify()
186
+ expect(isValid).toBe(true)
187
+ })
188
+
189
+ it('should correctly handle serialization/deserialization when signature is excluded', () => {
190
+ const certificate = new Certificate(
191
+ sampleType,
192
+ sampleSerialNumber,
193
+ sampleSubjectPubKey,
194
+ sampleCertifierPubKey,
195
+ sampleRevocationOutpoint,
196
+ sampleFields,
197
+ 'deadbeef1234', // Placeholder signature
198
+ )
199
+
200
+ // Serialize without signature
201
+ const serialized = certificate.toBin(false)
202
+ const deserializedCertificate = Certificate.fromBin(serialized)
203
+
204
+ expect(deserializedCertificate.signature).toBeUndefined() // Signature should be empty
205
+ expect(deserializedCertificate.fields).toEqual(sampleFields)
206
+ })
207
+
208
+ it('should correctly handle certificates with long field names and values', async () => {
209
+ const longFieldName = 'longFieldName_'.repeat(10) as any // Exceeding typical lengths
210
+ const longFieldValue = 'longFieldValue_'.repeat(20)
211
+ const fields = {
212
+ [longFieldName]: longFieldValue
213
+ }
214
+
215
+ const certificate = new Certificate(
216
+ sampleType,
217
+ sampleSerialNumber,
218
+ sampleSubjectPubKey,
219
+ sampleCertifierPubKey,
220
+ sampleRevocationOutpoint,
221
+ fields,
222
+ undefined // No signature
223
+ )
224
+
225
+ // Sign the certificate
226
+ const certifierWallet = new ProtoWallet(sampleCertifierPrivateKey)
227
+ await certificate.sign(certifierWallet)
228
+
229
+ // Serialize and deserialize
230
+ const serialized = certificate.toBin(true)
231
+ const deserializedCertificate = Certificate.fromBin(serialized)
232
+
233
+ expect(deserializedCertificate.fields).toEqual(fields)
234
+
235
+ // Verify the signature
236
+ const isValid = await deserializedCertificate.verify()
237
+ expect(isValid).toBe(true)
238
+ })
239
+
240
+ it('should correctly serialize and deserialize the revocationOutpoint', () => {
241
+ const certificate = new Certificate(
242
+ sampleType,
243
+ sampleSerialNumber,
244
+ sampleSubjectPubKey,
245
+ sampleCertifierPubKey,
246
+ sampleRevocationOutpoint,
247
+ sampleFields,
248
+ undefined // No signature
249
+ )
250
+
251
+ const serialized = certificate.toBin(false)
252
+ const deserializedCertificate = Certificate.fromBin(serialized)
253
+
254
+ expect(deserializedCertificate.revocationOutpoint).toEqual(sampleRevocationOutpoint)
255
+ })
256
+
257
+ it('should correctly handle certificates with no fields', async () => {
258
+ const certificate = new Certificate(
259
+ sampleType,
260
+ sampleSerialNumber,
261
+ sampleSubjectPubKey,
262
+ sampleCertifierPubKey,
263
+ sampleRevocationOutpoint,
264
+ {}, // No fields
265
+ undefined // No signature
266
+ )
267
+
268
+ // Sign the certificate
269
+ const certifierWallet = new ProtoWallet(sampleCertifierPrivateKey)
270
+ await certificate.sign(certifierWallet)
271
+
272
+ // Serialize and deserialize
273
+ const serialized = certificate.toBin(true)
274
+ const deserializedCertificate = Certificate.fromBin(serialized)
275
+
276
+ expect(deserializedCertificate.fields).toEqual({})
277
+
278
+ // Verify the signature
279
+ const isValid = await deserializedCertificate.verify()
280
+ expect(isValid).toBe(true)
281
+ })
282
+ })
@@ -0,0 +1,3 @@
1
+ export { default as Certificate } from './Certificate.js'
2
+ export * from './MasterCertificate.js'
3
+ export * from './VerifiableCertificate.js'