@libp2p/tls 0.0.0-0321812e7

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/src/utils.ts ADDED
@@ -0,0 +1,319 @@
1
+ import { Duplex as DuplexStream } from 'node:stream'
2
+ import { Ed25519PublicKey, Secp256k1PublicKey, marshalPublicKey, supportedKeys, unmarshalPrivateKey, unmarshalPublicKey } from '@libp2p/crypto/keys'
3
+ import { CodeError, InvalidCryptoExchangeError, UnexpectedPeerError } from '@libp2p/interface'
4
+ import { peerIdFromKeys } from '@libp2p/peer-id'
5
+ import { AsnConvert } from '@peculiar/asn1-schema'
6
+ import * as asn1X509 from '@peculiar/asn1-x509'
7
+ import { Crypto } from '@peculiar/webcrypto'
8
+ import * as x509 from '@peculiar/x509'
9
+ import * as asn1js from 'asn1js'
10
+ import { pushable } from 'it-pushable'
11
+ import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
12
+ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
13
+ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
14
+ import { KeyType, PublicKey } from '../src/pb/index.js'
15
+ import type { PeerId, PublicKey as Libp2pPublicKey, Logger } from '@libp2p/interface'
16
+ import type { Duplex } from 'it-stream-types'
17
+ import type { Uint8ArrayList } from 'uint8arraylist'
18
+
19
+ const crypto = new Crypto()
20
+ x509.cryptoProvider.set(crypto)
21
+
22
+ const LIBP2P_PUBLIC_KEY_EXTENSION = '1.3.6.1.4.1.53594.1.1'
23
+ const CERT_PREFIX = 'libp2p-tls-handshake:'
24
+ // https://github.com/libp2p/go-libp2p/blob/28c0f6ab32cd69e4b18e9e4b550ef6ce059a9d1a/p2p/security/tls/crypto.go#L265
25
+ const CERT_VALIDITY_PERIOD_FROM = 60 * 60 * 1000 // ~1 hour
26
+ // https://github.com/libp2p/go-libp2p/blob/28c0f6ab32cd69e4b18e9e4b550ef6ce059a9d1a/p2p/security/tls/crypto.go#L24C28-L24C44
27
+ const CERT_VALIDITY_PERIOD_TO = 100 * 365 * 24 * 60 * 60 * 1000 // ~100 years
28
+
29
+ export async function verifyPeerCertificate (rawCertificate: Uint8Array, expectedPeerId?: PeerId, log?: Logger): Promise<PeerId> {
30
+ const now = Date.now()
31
+ const x509Cert = new x509.X509Certificate(rawCertificate)
32
+
33
+ if (x509Cert.notBefore.getTime() > now) {
34
+ log?.error('the certificate was not valid yet')
35
+ throw new CodeError('The certificate is not valid yet', 'ERR_INVALID_CERTIFICATE')
36
+ }
37
+
38
+ if (x509Cert.notAfter.getTime() < now) {
39
+ log?.error('the certificate has expired')
40
+ throw new CodeError('The certificate has expired', 'ERR_INVALID_CERTIFICATE')
41
+ }
42
+
43
+ const certSignatureValid = await x509Cert.verify()
44
+
45
+ if (!certSignatureValid) {
46
+ log?.error('certificate self signature was invalid')
47
+ throw new InvalidCryptoExchangeError('Invalid certificate self signature')
48
+ }
49
+
50
+ const certIsSelfSigned = await x509Cert.isSelfSigned()
51
+
52
+ if (!certIsSelfSigned) {
53
+ log?.error('certificate must be self signed')
54
+ throw new InvalidCryptoExchangeError('Certificate must be self signed')
55
+ }
56
+
57
+ const libp2pPublicKeyExtension = x509Cert.extensions[0]
58
+
59
+ if (libp2pPublicKeyExtension == null || libp2pPublicKeyExtension.type !== LIBP2P_PUBLIC_KEY_EXTENSION) {
60
+ log?.error('the certificate did not include the libp2p public key extension')
61
+ throw new CodeError('The certificate did not include the libp2p public key extension', 'ERR_INVALID_CERTIFICATE')
62
+ }
63
+
64
+ const { result: libp2pKeySequence } = asn1js.fromBER(libp2pPublicKeyExtension.value)
65
+
66
+ // @ts-expect-error deep chain
67
+ const remotePeerIdPb = libp2pKeySequence.valueBlock.value[0].valueBlock.valueHex
68
+ const marshalledPeerId = new Uint8Array(remotePeerIdPb, 0, remotePeerIdPb.byteLength)
69
+ const remotePublicKey = PublicKey.decode(marshalledPeerId)
70
+ const remotePublicKeyData = remotePublicKey.data ?? new Uint8Array(0)
71
+ let remoteLibp2pPublicKey: Libp2pPublicKey
72
+
73
+ if (remotePublicKey.type === KeyType.Ed25519) {
74
+ remoteLibp2pPublicKey = new Ed25519PublicKey(remotePublicKeyData)
75
+ } else if (remotePublicKey.type === KeyType.Secp256k1) {
76
+ remoteLibp2pPublicKey = new Secp256k1PublicKey(remotePublicKeyData)
77
+ } else if (remotePublicKey.type === KeyType.RSA) {
78
+ remoteLibp2pPublicKey = supportedKeys.rsa.unmarshalRsaPublicKey(remotePublicKeyData)
79
+ } else {
80
+ log?.error('unknown or unsupported key type', remotePublicKey.type)
81
+ throw new InvalidCryptoExchangeError('Unknown or unsupported key type')
82
+ }
83
+
84
+ // @ts-expect-error deep chain
85
+ const remoteSignature = libp2pKeySequence.valueBlock.value[1].valueBlock.valueHex
86
+ const dataToVerify = encodeSignatureData(x509Cert.publicKey.rawData)
87
+ const result = await remoteLibp2pPublicKey.verify(dataToVerify, new Uint8Array(remoteSignature, 0, remoteSignature.byteLength))
88
+
89
+ if (!result) {
90
+ log?.error('invalid libp2p signature')
91
+ throw new InvalidCryptoExchangeError('Could not verify signature')
92
+ }
93
+
94
+ const marshalled = marshalPublicKey(remoteLibp2pPublicKey)
95
+ const remotePeerId = await peerIdFromKeys(marshalled)
96
+
97
+ if (expectedPeerId?.equals(remotePeerId) === false) {
98
+ log?.error('invalid peer id')
99
+ throw new UnexpectedPeerError()
100
+ }
101
+
102
+ return remotePeerId
103
+ }
104
+
105
+ export async function generateCertificate (peerId: PeerId): Promise<{ cert: string, key: string }> {
106
+ const now = Date.now()
107
+
108
+ const alg = {
109
+ name: 'ECDSA',
110
+ namedCurve: 'P-256',
111
+ hash: 'SHA-256'
112
+ }
113
+
114
+ const keys = await crypto.subtle.generateKey(alg, true, ['sign'])
115
+
116
+ const certPublicKeySpki = await crypto.subtle.exportKey('spki', keys.publicKey)
117
+ const dataToSign = encodeSignatureData(certPublicKeySpki)
118
+
119
+ if (peerId.privateKey == null) {
120
+ throw new InvalidCryptoExchangeError('Private key was missing from PeerId')
121
+ }
122
+
123
+ const privateKey = await unmarshalPrivateKey(peerId.privateKey)
124
+ const sig = await privateKey.sign(dataToSign)
125
+
126
+ let keyType: KeyType
127
+ let keyData: Uint8Array
128
+
129
+ if (peerId.publicKey == null) {
130
+ throw new CodeError('Public key missing from PeerId', 'ERR_INVALID_PEER_ID')
131
+ }
132
+
133
+ const publicKey = unmarshalPublicKey(peerId.publicKey)
134
+
135
+ if (peerId.type === 'Ed25519') {
136
+ // Ed25519: Only the 32 bytes of the public key
137
+ keyType = KeyType.Ed25519
138
+ keyData = publicKey.marshal()
139
+ } else if (peerId.type === 'secp256k1') {
140
+ // Secp256k1: Only the compressed form of the public key. 33 bytes.
141
+ keyType = KeyType.Secp256k1
142
+ keyData = publicKey.marshal()
143
+ } else if (peerId.type === 'RSA') {
144
+ // The rest of the keys are encoded as a SubjectPublicKeyInfo structure in PKIX, ASN.1 DER form.
145
+ keyType = KeyType.RSA
146
+ keyData = publicKey.marshal()
147
+ } else {
148
+ throw new CodeError('Unknown PeerId type', 'ERR_UNKNOWN_PEER_ID_TYPE')
149
+ }
150
+
151
+ const selfCert = await x509.X509CertificateGenerator.createSelfSigned({
152
+ serialNumber: uint8ArrayToString(crypto.getRandomValues(new Uint8Array(9)), 'base16'),
153
+ name: '',
154
+ notBefore: new Date(now - CERT_VALIDITY_PERIOD_FROM),
155
+ notAfter: new Date(now + CERT_VALIDITY_PERIOD_TO),
156
+ signingAlgorithm: alg,
157
+ keys,
158
+ extensions: [
159
+ new x509.Extension(LIBP2P_PUBLIC_KEY_EXTENSION, true, new asn1js.Sequence({
160
+ value: [
161
+ // publicKey
162
+ new asn1js.OctetString({
163
+ valueHex: PublicKey.encode({
164
+ type: keyType,
165
+ data: keyData
166
+ })
167
+ }),
168
+ // signature
169
+ new asn1js.OctetString({
170
+ valueHex: sig
171
+ })
172
+ ]
173
+ }).toBER())
174
+ ]
175
+ })
176
+
177
+ const certPrivateKeySpki = await crypto.subtle.exportKey('spki', keys.privateKey)
178
+
179
+ return {
180
+ cert: selfCert.toString(),
181
+ key: spkiToPEM(certPrivateKeySpki)
182
+ }
183
+ }
184
+
185
+ /**
186
+ * @see https://github.com/libp2p/specs/blob/master/tls/tls.md#libp2p-public-key-extension
187
+ */
188
+ export function encodeSignatureData (certPublicKey: ArrayBuffer): Uint8Array {
189
+ const keyInfo = AsnConvert.parse(certPublicKey, asn1X509.SubjectPublicKeyInfo)
190
+ const bytes = AsnConvert.serialize(keyInfo)
191
+
192
+ return uint8ArrayConcat([
193
+ uint8ArrayFromString(CERT_PREFIX),
194
+ new Uint8Array(bytes, 0, bytes.byteLength)
195
+ ])
196
+ }
197
+
198
+ function spkiToPEM (keydata: ArrayBuffer): string {
199
+ return formatAsPem(uint8ArrayToString(new Uint8Array(keydata), 'base64'))
200
+ }
201
+
202
+ function formatAsPem (str: string): string {
203
+ let finalString = '-----BEGIN PRIVATE KEY-----\n'
204
+
205
+ while (str.length > 0) {
206
+ finalString += str.substring(0, 64) + '\n'
207
+ str = str.substring(64)
208
+ }
209
+
210
+ finalString = finalString + '-----END PRIVATE KEY-----'
211
+
212
+ return finalString
213
+ }
214
+
215
+ export function itToStream (conn: Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>>): DuplexStream {
216
+ const output = pushable()
217
+ const iterator = conn.source[Symbol.asyncIterator]() as AsyncGenerator<Uint8Array>
218
+
219
+ const stream = new DuplexStream({
220
+ autoDestroy: false,
221
+ allowHalfOpen: true,
222
+ write (chunk, encoding, callback) {
223
+ output.push(chunk)
224
+ callback()
225
+ },
226
+ read () {
227
+ iterator.next()
228
+ .then(result => {
229
+ if (result.done === true) {
230
+ this.push(null)
231
+ } else {
232
+ this.push(result.value)
233
+ }
234
+ }, (err) => {
235
+ this.destroy(err)
236
+ })
237
+ }
238
+ })
239
+
240
+ // @ts-expect-error return type of sink is unknown
241
+ conn.sink(output)
242
+ .catch((err: any) => {
243
+ stream.destroy(err)
244
+ })
245
+
246
+ return stream
247
+ }
248
+
249
+ export function streamToIt (stream: DuplexStream): Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> {
250
+ const output: Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = {
251
+ source: (async function * () {
252
+ const output = pushable<Uint8Array>()
253
+
254
+ stream.addListener('data', (buf) => {
255
+ output.push(buf.subarray())
256
+ })
257
+ // both ends closed
258
+ stream.addListener('close', () => {
259
+ output.end()
260
+ })
261
+ stream.addListener('error', (err) => {
262
+ output.end(err)
263
+ })
264
+ // just writable end closed
265
+ stream.addListener('finish', () => {
266
+ output.end()
267
+ })
268
+
269
+ try {
270
+ yield * output
271
+ } catch (err: any) {
272
+ stream.destroy(err)
273
+ throw err
274
+ }
275
+ })(),
276
+ sink: async (source) => {
277
+ try {
278
+ for await (const buf of source) {
279
+ const sendMore = stream.write(buf.subarray())
280
+
281
+ if (!sendMore) {
282
+ await waitForBackpressure(stream)
283
+ }
284
+ }
285
+
286
+ // close writable end
287
+ stream.end()
288
+ } catch (err: any) {
289
+ stream.destroy(err)
290
+ throw err
291
+ }
292
+ }
293
+ }
294
+
295
+ return output
296
+ }
297
+
298
+ async function waitForBackpressure (stream: DuplexStream): Promise<void> {
299
+ await new Promise<void>((resolve, reject) => {
300
+ const continueListener = (): void => {
301
+ cleanUp()
302
+ resolve()
303
+ }
304
+ const stopListener = (err?: Error): void => {
305
+ cleanUp()
306
+ reject(err ?? new Error('Stream ended'))
307
+ }
308
+
309
+ const cleanUp = (): void => {
310
+ stream.removeListener('drain', continueListener)
311
+ stream.removeListener('end', stopListener)
312
+ stream.removeListener('error', stopListener)
313
+ }
314
+
315
+ stream.addListener('drain', continueListener)
316
+ stream.addListener('end', stopListener)
317
+ stream.addListener('error', stopListener)
318
+ })
319
+ }