@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.
- package/dist/cjs/package.json +3 -3
- package/dist/cjs/src/auth/Peer.js +536 -0
- package/dist/cjs/src/auth/Peer.js.map +1 -0
- package/dist/cjs/src/auth/SessionManager.js +66 -0
- package/dist/cjs/src/auth/SessionManager.js.map +1 -0
- package/dist/cjs/src/auth/{Certificate.js → certificates/Certificate.js} +22 -26
- package/dist/cjs/src/auth/certificates/Certificate.js.map +1 -0
- package/dist/cjs/src/auth/certificates/MasterCertificate.js +79 -0
- package/dist/cjs/src/auth/certificates/MasterCertificate.js.map +1 -0
- package/dist/cjs/src/auth/certificates/VerifiableCertificate.js +49 -0
- package/dist/cjs/src/auth/certificates/VerifiableCertificate.js.map +1 -0
- package/dist/cjs/src/auth/certificates/index.js +25 -0
- package/dist/cjs/src/auth/certificates/index.js.map +1 -0
- package/dist/cjs/src/auth/clients/AuthFetch.js +411 -0
- package/dist/cjs/src/auth/clients/AuthFetch.js.map +1 -0
- package/dist/cjs/src/auth/clients/index.js +18 -0
- package/dist/cjs/src/auth/clients/index.js.map +1 -0
- package/dist/cjs/src/auth/index.js +20 -5
- package/dist/cjs/src/auth/index.js.map +1 -1
- package/dist/cjs/src/auth/transports/SimplifiedFetchTransport.js +259 -0
- package/dist/cjs/src/auth/transports/SimplifiedFetchTransport.js.map +1 -0
- package/dist/cjs/src/auth/transports/index.js +18 -0
- package/dist/cjs/src/auth/transports/index.js.map +1 -0
- package/dist/cjs/src/auth/types.js +3 -0
- package/dist/cjs/src/auth/types.js.map +1 -0
- package/dist/cjs/src/auth/utils/certificateHelpers.js +51 -0
- package/dist/cjs/src/auth/utils/certificateHelpers.js.map +1 -0
- package/dist/cjs/src/auth/utils/createNonce.js +19 -0
- package/dist/cjs/src/auth/utils/createNonce.js.map +1 -0
- package/dist/cjs/src/auth/utils/getVerifiableCertificates.js +31 -0
- package/dist/cjs/src/auth/utils/getVerifiableCertificates.js.map +1 -0
- package/dist/cjs/src/auth/utils/index.js +22 -0
- package/dist/cjs/src/auth/utils/index.js.map +1 -0
- package/dist/cjs/src/auth/utils/validateCertificates.js +42 -0
- package/dist/cjs/src/auth/utils/validateCertificates.js.map +1 -0
- package/dist/cjs/src/auth/utils/verifyNonce.js +27 -0
- package/dist/cjs/src/auth/utils/verifyNonce.js.map +1 -0
- package/dist/cjs/src/primitives/Point.js +1 -1
- package/dist/cjs/src/primitives/Point.js.map +1 -1
- package/dist/cjs/src/wallet/substrates/WalletWireProcessor.js +1 -1
- package/dist/cjs/src/wallet/substrates/WalletWireProcessor.js.map +1 -1
- package/dist/cjs/src/wallet/substrates/WalletWireTransceiver.js +148 -148
- package/dist/cjs/src/wallet/substrates/WalletWireTransceiver.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/auth/Peer.js +533 -0
- package/dist/esm/src/auth/Peer.js.map +1 -0
- package/dist/esm/src/auth/SessionManager.js +63 -0
- package/dist/esm/src/auth/SessionManager.js.map +1 -0
- package/dist/esm/src/auth/{Certificate.js → certificates/Certificate.js} +1 -2
- package/dist/esm/src/auth/certificates/Certificate.js.map +1 -0
- package/dist/esm/src/auth/certificates/MasterCertificate.js +73 -0
- package/dist/esm/src/auth/certificates/MasterCertificate.js.map +1 -0
- package/dist/esm/src/auth/certificates/VerifiableCertificate.js +44 -0
- package/dist/esm/src/auth/certificates/VerifiableCertificate.js.map +1 -0
- package/dist/esm/src/auth/certificates/index.js +4 -0
- package/dist/esm/src/auth/certificates/index.js.map +1 -0
- package/dist/esm/src/auth/clients/AuthFetch.js +409 -0
- package/dist/esm/src/auth/clients/AuthFetch.js.map +1 -0
- package/dist/esm/src/auth/clients/index.js +2 -0
- package/dist/esm/src/auth/clients/index.js.map +1 -0
- package/dist/esm/src/auth/index.js +7 -1
- package/dist/esm/src/auth/index.js.map +1 -1
- package/dist/esm/src/auth/transports/SimplifiedFetchTransport.js +258 -0
- package/dist/esm/src/auth/transports/SimplifiedFetchTransport.js.map +1 -0
- package/dist/esm/src/auth/transports/index.js +2 -0
- package/dist/esm/src/auth/transports/index.js.map +1 -0
- package/dist/esm/src/auth/types.js +2 -0
- package/dist/esm/src/auth/types.js.map +1 -0
- package/dist/esm/src/auth/utils/certificateHelpers.js +47 -0
- package/dist/esm/src/auth/utils/certificateHelpers.js.map +1 -0
- package/dist/esm/src/auth/utils/createNonce.js +16 -0
- package/dist/esm/src/auth/utils/createNonce.js.map +1 -0
- package/dist/esm/src/auth/utils/getVerifiableCertificates.js +27 -0
- package/dist/esm/src/auth/utils/getVerifiableCertificates.js.map +1 -0
- package/dist/esm/src/auth/utils/index.js +6 -0
- package/dist/esm/src/auth/utils/index.js.map +1 -0
- package/dist/esm/src/auth/utils/validateCertificates.js +38 -0
- package/dist/esm/src/auth/utils/validateCertificates.js.map +1 -0
- package/dist/esm/src/auth/utils/verifyNonce.js +24 -0
- package/dist/esm/src/auth/utils/verifyNonce.js.map +1 -0
- package/dist/esm/src/primitives/Point.js +1 -1
- package/dist/esm/src/primitives/Point.js.map +1 -1
- package/dist/esm/src/wallet/substrates/WalletWireProcessor.js +1 -1
- package/dist/esm/src/wallet/substrates/WalletWireProcessor.js.map +1 -1
- package/dist/esm/src/wallet/substrates/WalletWireTransceiver.js +1 -1
- package/dist/esm/src/wallet/substrates/WalletWireTransceiver.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/auth/Peer.d.ts +193 -0
- package/dist/types/src/auth/Peer.d.ts.map +1 -0
- package/dist/types/src/auth/SessionManager.d.ts +42 -0
- package/dist/types/src/auth/SessionManager.d.ts.map +1 -0
- package/dist/types/src/auth/{Certificate.d.ts → certificates/Certificate.d.ts} +1 -1
- package/dist/types/src/auth/certificates/Certificate.d.ts.map +1 -0
- package/dist/types/src/auth/certificates/MasterCertificate.d.ts +38 -0
- package/dist/types/src/auth/certificates/MasterCertificate.d.ts.map +1 -0
- package/dist/types/src/auth/certificates/VerifiableCertificate.d.ts +26 -0
- package/dist/types/src/auth/certificates/VerifiableCertificate.d.ts.map +1 -0
- package/dist/types/src/auth/certificates/index.d.ts +4 -0
- package/dist/types/src/auth/certificates/index.d.ts.map +1 -0
- package/dist/types/src/auth/clients/AuthFetch.d.ts +87 -0
- package/dist/types/src/auth/clients/AuthFetch.d.ts.map +1 -0
- package/dist/types/src/auth/clients/index.d.ts +2 -0
- package/dist/types/src/auth/clients/index.d.ts.map +1 -0
- package/dist/types/src/auth/index.d.ts +7 -1
- package/dist/types/src/auth/index.d.ts.map +1 -1
- package/dist/types/src/auth/transports/SimplifiedFetchTransport.d.ts +51 -0
- package/dist/types/src/auth/transports/SimplifiedFetchTransport.d.ts.map +1 -0
- package/dist/types/src/auth/transports/index.d.ts +2 -0
- package/dist/types/src/auth/transports/index.d.ts.map +1 -0
- package/dist/types/src/auth/types.d.ts +31 -0
- package/dist/types/src/auth/types.d.ts.map +1 -0
- package/dist/types/src/auth/utils/certificateHelpers.d.ts +26 -0
- package/dist/types/src/auth/utils/certificateHelpers.d.ts.map +1 -0
- package/dist/types/src/auth/utils/createNonce.d.ts +8 -0
- package/dist/types/src/auth/utils/createNonce.d.ts.map +1 -0
- package/dist/types/src/auth/utils/getVerifiableCertificates.d.ts +13 -0
- package/dist/types/src/auth/utils/getVerifiableCertificates.d.ts.map +1 -0
- package/dist/types/src/auth/utils/index.d.ts +6 -0
- package/dist/types/src/auth/utils/index.d.ts.map +1 -0
- package/dist/types/src/auth/utils/validateCertificates.d.ts +12 -0
- package/dist/types/src/auth/utils/validateCertificates.d.ts.map +1 -0
- package/dist/types/src/auth/utils/verifyNonce.d.ts +9 -0
- package/dist/types/src/auth/utils/verifyNonce.d.ts.map +1 -0
- package/dist/types/src/primitives/Point.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +1 -1
- package/docs/README.md +1 -0
- package/docs/auth.md +1193 -0
- package/package.json +13 -3
- package/src/auth/Peer.ts +600 -0
- package/src/auth/SessionManager.ts +71 -0
- package/src/auth/__tests/Peer.test.ts +599 -0
- package/src/auth/__tests/SessionManager.test.ts +87 -0
- package/src/auth/{Certificate.ts → certificates/Certificate.ts} +15 -8
- package/src/auth/certificates/MasterCertificate.ts +106 -0
- package/src/auth/certificates/VerifiableCertificate.ts +73 -0
- package/src/auth/certificates/__tests/Certificate.test.ts +282 -0
- package/src/auth/certificates/index.ts +3 -0
- package/src/auth/clients/AuthFetch.ts +482 -0
- package/src/auth/clients/index.ts +1 -0
- package/src/auth/index.ts +7 -1
- package/src/auth/transports/SimplifiedFetchTransport.ts +288 -0
- package/src/auth/transports/index.ts +1 -0
- package/src/auth/types.ts +41 -0
- package/src/auth/utils/__tests/cryptononce.test.ts +84 -0
- package/src/auth/utils/__tests/getVerifiableCertificates.test.ts +126 -0
- package/src/auth/utils/__tests/validateCertificates.test.ts +142 -0
- package/src/auth/utils/certificateHelpers.ts +86 -0
- package/src/auth/utils/createNonce.ts +16 -0
- package/src/auth/utils/getVerifiableCertificates.ts +40 -0
- package/src/auth/utils/index.ts +5 -0
- package/src/auth/utils/validateCertificates.ts +54 -0
- package/src/auth/utils/verifyNonce.ts +27 -0
- package/src/primitives/Point.ts +59 -59
- package/src/wallet/substrates/WalletWireProcessor.ts +1 -1
- package/src/wallet/substrates/WalletWireTransceiver.ts +1 -1
- package/dist/cjs/src/auth/Certificate.js.map +0 -1
- package/dist/esm/src/auth/Certificate.js.map +0 -1
- package/dist/types/src/auth/Certificate.d.ts.map +0 -1
- 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 {
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
})
|