@libp2p/webrtc 5.2.9-0b9090aea → 5.2.9-4c64bd06d

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.
@@ -1,20 +1,30 @@
1
- import { serviceCapabilities, transportSymbol } from '@libp2p/interface'
1
+ import { generateKeyPair, privateKeyToCryptoKeyPair } from '@libp2p/crypto/keys'
2
+ import { NotFoundError, NotStartedError, TypedEventEmitter, serviceCapabilities, transportSymbol } from '@libp2p/interface'
2
3
  import { peerIdFromString } from '@libp2p/peer-id'
3
4
  import { WebRTCDirect } from '@multiformats/multiaddr-matcher'
5
+ import { BasicConstraintsExtension, X509Certificate, X509CertificateGenerator } from '@peculiar/x509'
6
+ import { Key } from 'interface-datastore'
7
+ import { base64url } from 'multiformats/bases/base64'
8
+ import { sha256 } from 'multiformats/hashes/sha2'
4
9
  import { raceSignal } from 'race-signal'
10
+ import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
11
+ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
12
+ import { DEFAULT_CERTIFICATE_DATASTORE_KEY, DEFAULT_CERTIFICATE_LIFESPAN, DEFAULT_CERTIFICATE_PRIVATE_KEY_NAME } from '../constants.js'
5
13
  import { genUfrag } from '../util.js'
6
14
  import { WebRTCDirectListener } from './listener.js'
7
15
  import { connect } from './utils/connect.js'
8
16
  import { createDialerRTCPeerConnection } from './utils/get-rtcpeerconnection.js'
17
+ import { formatAsPem } from './utils/pem.js'
9
18
  import type { DataChannelOptions, TransportCertificate } from '../index.js'
10
19
  import type { WebRTCDialEvents } from '../private-to-private/transport.js'
11
- import type { CreateListenerOptions, Transport, Listener, ComponentLogger, Logger, Connection, CounterGroup, Metrics, PeerId, DialTransportOptions, PrivateKey, Upgrader } from '@libp2p/interface'
20
+ import type { CreateListenerOptions, Transport, Listener, ComponentLogger, Logger, Connection, CounterGroup, Metrics, PeerId, DialTransportOptions, PrivateKey, Upgrader, Startable, TypedEventTarget } from '@libp2p/interface'
12
21
  import type { TransportManager } from '@libp2p/interface-internal'
22
+ import type { Keychain } from '@libp2p/keychain'
13
23
  import type { Multiaddr } from '@multiformats/multiaddr'
24
+ import type { Datastore } from 'interface-datastore'
25
+
26
+ const ONE_DAY_MS = 86_400_000
14
27
 
15
- /**
16
- * The peer for this transport
17
- */
18
28
  export interface WebRTCDirectTransportComponents {
19
29
  peerId: PeerId
20
30
  privateKey: PrivateKey
@@ -22,6 +32,8 @@ export interface WebRTCDirectTransportComponents {
22
32
  logger: ComponentLogger
23
33
  transportManager: TransportManager
24
34
  upgrader: Upgrader
35
+ keychain?: Keychain
36
+ datastore: Datastore
25
37
  }
26
38
 
27
39
  export interface WebRTCMetrics {
@@ -29,26 +41,85 @@ export interface WebRTCMetrics {
29
41
  }
30
42
 
31
43
  export interface WebRTCTransportDirectInit {
44
+ /**
45
+ * The default configuration used by all created RTCPeerConnections
46
+ */
32
47
  rtcConfiguration?: RTCConfiguration | (() => RTCConfiguration | Promise<RTCConfiguration>)
48
+
49
+ /**
50
+ * The default configuration used by all created RTCDataChannels
51
+ */
33
52
  dataChannel?: DataChannelOptions
53
+
54
+ /**
55
+ * @deprecated use `certificate` instead - this option will be removed in a future release
56
+ */
34
57
  certificates?: TransportCertificate[]
35
58
 
59
+ /**
60
+ * Use an existing TLS certificate to secure incoming connections or supply
61
+ * settings to generate one.
62
+ *
63
+ * This must be an ECDSA certificate using the P-256 curve.
64
+ *
65
+ * From our testing we find that P-256 elliptic curve is supported by Pion,
66
+ * webrtc-rs, as well as Chromium (P-228 and P-384 was not supported in
67
+ * Chromium).
68
+ */
69
+ certificate?: TransportCertificate
70
+
36
71
  /**
37
72
  * @deprecated this setting is ignored and will be removed in a future release
38
73
  */
39
74
  useLibjuice?: boolean
75
+
76
+ /**
77
+ * The key the certificate is stored in the datastore under
78
+ *
79
+ * @default '/libp2p/webrtc-direct/certificate'
80
+ */
81
+ certificateDatastoreKey?: string
82
+
83
+ /**
84
+ * The name the certificate private key is stored in the keychain with
85
+ *
86
+ * @default 'webrtc-direct-certificate-private-key'
87
+ */
88
+ certificateKeychainName?: string
89
+
90
+ /**
91
+ * Number of days a certificate should be valid for
92
+ *
93
+ * @default 365
94
+ */
95
+ certificateLifespan?: number
96
+
97
+ /**
98
+ * Certificates will be renewed this many days before their expiry
99
+ *
100
+ * @default 5
101
+ */
102
+ certificateRenewalThreshold?: number
103
+ }
104
+
105
+ export interface WebRTCDirectTransportCertificateEvents {
106
+ 'certificate:renew': CustomEvent<TransportCertificate>
40
107
  }
41
108
 
42
- export class WebRTCDirectTransport implements Transport {
109
+ export class WebRTCDirectTransport implements Transport, Startable {
43
110
  private readonly log: Logger
44
111
  private readonly metrics?: WebRTCMetrics
45
112
  private readonly components: WebRTCDirectTransportComponents
46
113
  private readonly init: WebRTCTransportDirectInit
114
+ private certificate?: TransportCertificate
115
+ private privateKey?: PrivateKey
116
+ private readonly emitter: TypedEventTarget<WebRTCDirectTransportCertificateEvents>
47
117
 
48
118
  constructor (components: WebRTCDirectTransportComponents, init: WebRTCTransportDirectInit = {}) {
49
119
  this.log = components.logger.forComponent('libp2p:webrtc-direct')
50
120
  this.components = components
51
121
  this.init = init
122
+ this.emitter = new TypedEventEmitter()
52
123
 
53
124
  if (components.metrics != null) {
54
125
  this.metrics = {
@@ -68,6 +139,14 @@ export class WebRTCDirectTransport implements Transport {
68
139
  '@libp2p/transport'
69
140
  ]
70
141
 
142
+ async start (): Promise<void> {
143
+ this.certificate = await this.getCertificate()
144
+ }
145
+
146
+ async stop (): Promise<void> {
147
+
148
+ }
149
+
71
150
  /**
72
151
  * Dial a given multiaddr
73
152
  */
@@ -81,9 +160,15 @@ export class WebRTCDirectTransport implements Transport {
81
160
  * Create transport listeners no supported by browsers
82
161
  */
83
162
  createListener (options: CreateListenerOptions): Listener {
163
+ if (this.certificate == null) {
164
+ throw new NotStartedError()
165
+ }
166
+
84
167
  return new WebRTCDirectListener(this.components, {
85
168
  ...this.init,
86
- ...options
169
+ ...options,
170
+ certificate: this.certificate,
171
+ emitter: this.emitter
87
172
  })
88
173
  }
89
174
 
@@ -139,4 +224,154 @@ export class WebRTCDirectTransport implements Transport {
139
224
  throw err
140
225
  }
141
226
  }
227
+
228
+ private async getCertificate (): Promise<TransportCertificate> {
229
+ if (isTransportCertificate(this.init.certificate)) {
230
+ this.log.trace('using provided TLS certificate')
231
+ return this.init.certificate
232
+ }
233
+
234
+ const privateKey = await this.loadOrCreatePrivateKey()
235
+ const { pem, certhash } = await this.loadOrCreateCertificate(privateKey)
236
+
237
+ return {
238
+ privateKey: await formatAsPem(privateKey),
239
+ pem,
240
+ certhash
241
+ }
242
+ }
243
+
244
+ private async loadOrCreatePrivateKey (): Promise<PrivateKey> {
245
+ if (this.privateKey != null) {
246
+ return this.privateKey
247
+ }
248
+
249
+ const keychainName = this.init.certificateKeychainName ?? DEFAULT_CERTIFICATE_PRIVATE_KEY_NAME
250
+ const keychain = this.getKeychain()
251
+
252
+ try {
253
+ if (keychain == null) {
254
+ this.log('no keychain configured - not checking for stored private key')
255
+ throw new NotFoundError()
256
+ }
257
+
258
+ this.log.trace('checking for stored private key')
259
+ this.privateKey = await keychain.exportKey(keychainName)
260
+ } catch (err: any) {
261
+ if (err.name !== 'NotFoundError') {
262
+ throw err
263
+ }
264
+
265
+ this.log.trace('generating private key')
266
+ this.privateKey = await generateKeyPair('ECDSA', 'P-256')
267
+
268
+ if (keychain != null) {
269
+ this.log.trace('storing private key')
270
+ await keychain.importKey(keychainName, this.privateKey)
271
+ } else {
272
+ this.log('no keychain configured - not storing private key')
273
+ }
274
+ }
275
+
276
+ return this.privateKey
277
+ }
278
+
279
+ private async loadOrCreateCertificate (privateKey: PrivateKey): Promise<{ pem: string, certhash: string }> {
280
+ if (this.certificate != null) {
281
+ return this.certificate
282
+ }
283
+
284
+ let cert: X509Certificate
285
+ const dsKey = new Key(this.init.certificateDatastoreKey ?? DEFAULT_CERTIFICATE_DATASTORE_KEY)
286
+ const keyPair = await privateKeyToCryptoKeyPair(privateKey)
287
+
288
+ try {
289
+ this.log.trace('checking for stored TLS certificate')
290
+ cert = await this.loadCertificate(dsKey, keyPair)
291
+ } catch (err: any) {
292
+ if (err.name !== 'NotFoundError') {
293
+ throw err
294
+ }
295
+
296
+ this.log('generating TLS certificate using private key')
297
+ cert = await this.createCertificate(dsKey, keyPair)
298
+ }
299
+
300
+ return {
301
+ pem: cert.toString('pem'),
302
+ certhash: base64url.encode((await sha256.digest(new Uint8Array(cert.rawData))).bytes)
303
+ }
304
+ }
305
+
306
+ async loadCertificate (dsKey: Key, keyPair: CryptoKeyPair): Promise<X509Certificate> {
307
+ const buf = await this.components.datastore.get(dsKey)
308
+ const cert = new X509Certificate(buf)
309
+
310
+ // check expiry date
311
+ const threshold = Date.now() - ((this.init.certificateLifespan ?? DEFAULT_CERTIFICATE_LIFESPAN) * ONE_DAY_MS)
312
+
313
+ if (cert.notAfter.getTime() < threshold) {
314
+ this.log('stored TLS certificate has expired')
315
+ // act as if no certificate was present
316
+ throw new NotFoundError()
317
+ }
318
+
319
+ // check public keys match
320
+ const exportedCertKey = await cert.publicKey.export(crypto)
321
+ const rawCertKey = await crypto.subtle.exportKey('raw', exportedCertKey)
322
+ const rawKeyPairKey = await crypto.subtle.exportKey('raw', keyPair.publicKey)
323
+
324
+ if (!uint8ArrayEquals(
325
+ new Uint8Array(rawCertKey, 0, rawCertKey.byteLength),
326
+ new Uint8Array(rawKeyPairKey, 0, rawKeyPairKey.byteLength)
327
+ )) {
328
+ this.log('stored TLS certificate public key did not match public key from private key')
329
+ throw new NotFoundError()
330
+ }
331
+
332
+ return cert
333
+ }
334
+
335
+ async createCertificate (dsKey: Key, keyPair: CryptoKeyPair): Promise<X509Certificate> {
336
+ const notBefore = new Date()
337
+ const notAfter = new Date(notBefore.getTime() + ((this.init.certificateLifespan ?? DEFAULT_CERTIFICATE_LIFESPAN) * ONE_DAY_MS))
338
+
339
+ // have to set ms to 0 to work around https://github.com/PeculiarVentures/x509/issues/73
340
+ notBefore.setMilliseconds(0)
341
+ notAfter.setMilliseconds(0)
342
+
343
+ const cert = await X509CertificateGenerator.createSelfSigned({
344
+ serialNumber: (BigInt(Math.random().toString().replace('.', '')) * 100000n).toString(16),
345
+ name: 'CN=example.com, C=US, L=CA, O=example, ST=CA',
346
+ notBefore,
347
+ notAfter,
348
+ keys: keyPair,
349
+ extensions: [
350
+ new BasicConstraintsExtension(false, undefined, true)
351
+ ]
352
+ }, crypto)
353
+
354
+ if (this.getKeychain() != null) {
355
+ this.log.trace('storing TLS certificate')
356
+ await this.components.datastore.put(dsKey, uint8ArrayFromString(cert.toString('pem')))
357
+ } else {
358
+ this.log('no keychain is configured so not storing TLS certificate since the private key will not be reused')
359
+ }
360
+
361
+ return cert
362
+ }
363
+
364
+ private getKeychain (): Keychain | undefined {
365
+ try {
366
+ return this.components.keychain
367
+ } catch {}
368
+ }
369
+ }
370
+
371
+ function isTransportCertificate (obj?: any): obj is TransportCertificate {
372
+ if (obj == null) {
373
+ return false
374
+ }
375
+
376
+ return typeof obj.privateKey === 'string' && typeof obj.pem === 'string' && typeof obj.certhash === 'string'
142
377
  }
@@ -0,0 +1,18 @@
1
+ import { privateKeyToCryptoKeyPair } from '@libp2p/crypto/keys'
2
+ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
3
+ import type { PrivateKey } from '@libp2p/interface'
4
+
5
+ export function toBuffer (uint8Array: Uint8Array): Buffer {
6
+ return Buffer.from(uint8Array.buffer, uint8Array.byteOffset, uint8Array.byteLength)
7
+ }
8
+
9
+ export async function formatAsPem (privateKey: PrivateKey): Promise<string> {
10
+ const keyPair = await privateKeyToCryptoKeyPair(privateKey)
11
+ const exported = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey)
12
+
13
+ return [
14
+ '-----BEGIN PRIVATE KEY-----',
15
+ ...uint8ArrayToString(new Uint8Array(exported), 'base64pad').split(/(.{64})/).filter(Boolean),
16
+ '-----END PRIVATE KEY-----'
17
+ ].join('\n')
18
+ }