Skip to content

Commit 4c64bd0

Browse files
authored
fix: reuse WebRTC certificates between restarts (#3071)
To ensure WebRTC addresses are stable between restarts, store the generated certiciates in the datastore and the key that they are generated from in the keychain, if one has been configured.
1 parent da4e9da commit 4c64bd0

File tree

8 files changed

+479
-37
lines changed

8 files changed

+479
-37
lines changed

packages/transport-webrtc/README.md

+54
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,60 @@ await pipe(
230230
)
231231
```
232232

233+
## WebRTC Direct certificate hashes
234+
235+
WebRTC Direct listeners publish the hash of their TLS certificate as part of
236+
the listening multiaddr.
237+
238+
By default these certificates are generated at start up using an ephemeral
239+
keypair that only exists while the node is running.
240+
241+
This means that the certificate hashes change when the node is restarted,
242+
which can be undesirable if multiaddrs are intended to be long lived (e.g.
243+
if the node is used as a network bootstrapper).
244+
245+
To reuse the same certificate and keypair, configure a persistent datastore
246+
and the [@libp2p/keychain](https://www.npmjs.com/package/@libp2p/keychain)
247+
service as part of your service map:
248+
249+
## Example - Reuse TLS certificates after restart
250+
251+
```ts
252+
import { LevelDatastore } from 'datastore-level'
253+
import { webRTCDirect } from '@libp2p/webrtc'
254+
import { keychain } from '@libp2p/keychain'
255+
import { createLibp2p } from 'libp2p'
256+
257+
// store data on disk between restarts
258+
const datastore = new LevelDatastore('/path/to/store')
259+
260+
const listener = await createLibp2p({
261+
addresses: {
262+
listen: [
263+
'/ip4/0.0.0.0/udp/0/webrtc-direct'
264+
]
265+
},
266+
datastore,
267+
transports: [
268+
webRTCDirect()
269+
],
270+
services: {
271+
keychain: keychain()
272+
}
273+
})
274+
275+
await listener.start()
276+
277+
console.info(listener.getMultiaddrs())
278+
// /ip4/...../udp/../webrtc-direct/certhash/foo
279+
280+
await listener.stop()
281+
await listener.start()
282+
283+
console.info(listener.getMultiaddrs())
284+
// /ip4/...../udp/../webrtc-direct/certhash/foo
285+
```
286+
233287
# Install
234288

235289
```console

packages/transport-webrtc/package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,10 @@
5454
"@chainsafe/is-ip": "^2.0.2",
5555
"@chainsafe/libp2p-noise": "^16.0.0",
5656
"@ipshipyard/node-datachannel": "^0.26.4",
57+
"@libp2p/crypto": "^5.0.15",
5758
"@libp2p/interface": "^2.7.0",
5859
"@libp2p/interface-internal": "^2.3.9",
60+
"@libp2p/keychain": "^5.1.4",
5961
"@libp2p/peer-id": "^5.1.0",
6062
"@libp2p/utils": "^6.6.0",
6163
"@multiformats/multiaddr": "^12.4.0",
@@ -65,6 +67,7 @@
6567
"any-signal": "^4.1.1",
6668
"detect-browser": "^5.3.0",
6769
"get-port": "^7.1.0",
70+
"interface-datastore": "^8.3.1",
6871
"it-length-prefixed": "^10.0.1",
6972
"it-protobuf-stream": "^2.0.1",
7073
"it-pushable": "^3.2.3",
@@ -83,11 +86,11 @@
8386
"uint8arrays": "^5.1.0"
8487
},
8588
"devDependencies": {
86-
"@libp2p/crypto": "^5.0.15",
8789
"@libp2p/interface-compliance-tests": "^6.4.2",
8890
"@libp2p/logger": "^5.1.13",
8991
"@types/sinon": "^17.0.3",
9092
"aegir": "^45.1.1",
93+
"datastore-core": "^10.0.2",
9194
"delay": "^6.0.0",
9295
"it-length": "^3.0.6",
9396
"it-pair": "^2.0.6",

packages/transport-webrtc/src/constants.ts

+20
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,23 @@ export const MUXER_PROTOCOL = '/webrtc'
110110
* The protocol used for the signalling stream protocol
111111
*/
112112
export const SIGNALING_PROTOCOL = '/webrtc-signaling/0.0.1'
113+
114+
/**
115+
* Used to store generated certificates in the datastore
116+
*/
117+
export const DEFAULT_CERTIFICATE_DATASTORE_KEY = '/libp2p/webrtc-direct/certificate'
118+
119+
/**
120+
* Used to store the certificate private key in the keychain
121+
*/
122+
export const DEFAULT_CERTIFICATE_PRIVATE_KEY_NAME = 'webrtc-direct-certificate-private-key'
123+
124+
/**
125+
* The default type of certificate private key
126+
*/
127+
export const DEFAULT_CERTIFICATE_PRIVATE_KEY_TYPE = 'ECDSA'
128+
129+
/**
130+
* How long the certificate is valid for
131+
*/
132+
export const DEFAULT_CERTIFICATE_LIFESPAN = 365

packages/transport-webrtc/src/index.ts

+54
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,60 @@
206206
* }
207207
* )
208208
* ```
209+
*
210+
* ## WebRTC Direct certificate hashes
211+
*
212+
* WebRTC Direct listeners publish the hash of their TLS certificate as part of
213+
* the listening multiaddr.
214+
*
215+
* By default these certificates are generated at start up using an ephemeral
216+
* keypair that only exists while the node is running.
217+
*
218+
* This means that the certificate hashes change when the node is restarted,
219+
* which can be undesirable if multiaddrs are intended to be long lived (e.g.
220+
* if the node is used as a network bootstrapper).
221+
*
222+
* To reuse the same certificate and keypair, configure a persistent datastore
223+
* and the [@libp2p/keychain](https://www.npmjs.com/package/@libp2p/keychain)
224+
* service as part of your service map:
225+
*
226+
* @example Reuse TLS certificates after restart
227+
*
228+
* ```ts
229+
* import { LevelDatastore } from 'datastore-level'
230+
* import { webRTCDirect } from '@libp2p/webrtc'
231+
* import { keychain } from '@libp2p/keychain'
232+
* import { createLibp2p } from 'libp2p'
233+
*
234+
* // store data on disk between restarts
235+
* const datastore = new LevelDatastore('/path/to/store')
236+
*
237+
* const listener = await createLibp2p({
238+
* addresses: {
239+
* listen: [
240+
* '/ip4/0.0.0.0/udp/0/webrtc-direct'
241+
* ]
242+
* },
243+
* datastore,
244+
* transports: [
245+
* webRTCDirect()
246+
* ],
247+
* services: {
248+
* keychain: keychain()
249+
* }
250+
* })
251+
*
252+
* await listener.start()
253+
*
254+
* console.info(listener.getMultiaddrs())
255+
* // /ip4/...../udp/../webrtc-direct/certhash/foo
256+
*
257+
* await listener.stop()
258+
* await listener.start()
259+
*
260+
* console.info(listener.getMultiaddrs())
261+
* // /ip4/...../udp/../webrtc-direct/certhash/foo
262+
* ```
209263
*/
210264

211265
import { WebRTCTransport } from './private-to-private/transport.js'

packages/transport-webrtc/src/private-to-public/listener.ts

+16-26
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,38 @@ import { InvalidParametersError, TypedEventEmitter } from '@libp2p/interface'
33
import { getThinWaistAddresses } from '@libp2p/utils/get-thin-waist-addresses'
44
import { multiaddr, fromStringTuples } from '@multiformats/multiaddr'
55
import { WebRTCDirect } from '@multiformats/multiaddr-matcher'
6-
import { Crypto } from '@peculiar/webcrypto'
76
import getPort from 'get-port'
87
import pWaitFor from 'p-wait-for'
98
import { CODEC_CERTHASH, CODEC_WEBRTC_DIRECT } from '../constants.js'
109
import { connect } from './utils/connect.js'
11-
import { generateTransportCertificate } from './utils/generate-certificates.js'
1210
import { createDialerRTCPeerConnection } from './utils/get-rtcpeerconnection.js'
1311
import { stunListener } from './utils/stun-listener.js'
1412
import type { DataChannelOptions, TransportCertificate } from '../index.js'
13+
import type { WebRTCDirectTransportCertificateEvents } from './transport.js'
1514
import type { DirectRTCPeerConnection } from './utils/get-rtcpeerconnection.js'
1615
import type { StunServer } from './utils/stun-listener.js'
17-
import type { PeerId, ListenerEvents, Listener, Upgrader, ComponentLogger, Logger, CounterGroup, Metrics, PrivateKey } from '@libp2p/interface'
16+
import type { PeerId, ListenerEvents, Listener, Upgrader, ComponentLogger, Logger, CounterGroup, Metrics, PrivateKey, TypedEventTarget } from '@libp2p/interface'
17+
import type { Keychain } from '@libp2p/keychain'
1818
import type { Multiaddr } from '@multiformats/multiaddr'
19-
20-
const crypto = new Crypto()
19+
import type { Datastore } from 'interface-datastore'
2120

2221
export interface WebRTCDirectListenerComponents {
2322
peerId: PeerId
2423
privateKey: PrivateKey
2524
logger: ComponentLogger
2625
upgrader: Upgrader
26+
keychain?: Keychain
27+
datastore: Datastore
2728
metrics?: Metrics
2829
}
2930

3031
export interface WebRTCDirectListenerInit {
3132
upgrader: Upgrader
32-
certificates?: TransportCertificate[]
33+
certificate: TransportCertificate
3334
maxInboundStreams?: number
3435
dataChannel?: DataChannelOptions
3536
rtcConfiguration?: RTCConfiguration | (() => RTCConfiguration | Promise<RTCConfiguration>)
36-
useLibjuice?: boolean
37+
emitter: TypedEventTarget<WebRTCDirectTransportCertificateEvents>
3738
}
3839

3940
export interface WebRTCListenerMetrics {
@@ -53,7 +54,7 @@ let UDP_MUX_LISTENERS: UDPMuxServer[] = []
5354

5455
export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> implements Listener {
5556
private listeningMultiaddr?: Multiaddr
56-
private certificate?: TransportCertificate
57+
private certificate: TransportCertificate
5758
private stunServer?: StunServer
5859
private readonly connections: Map<string, DirectRTCPeerConnection>
5960
private readonly log: Logger
@@ -69,8 +70,8 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
6970
this.components = components
7071
this.connections = new Map()
7172
this.log = components.logger.forComponent('libp2p:webrtc-direct:listener')
72-
this.certificate = init.certificates?.[0]
7373
this.shutdownController = new AbortController()
74+
this.certificate = init.certificate
7475

7576
if (components.metrics != null) {
7677
this.metrics = {
@@ -80,6 +81,12 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
8081
})
8182
}
8283
}
84+
85+
// inform the transport manager our addresses have changed
86+
init.emitter.addEventListener('certificate:renew', evt => {
87+
this.certificate = evt.detail
88+
this.safeDispatchEvent('listening')
89+
})
8390
}
8491

8592
async listen (ma: Multiaddr): Promise<void> {
@@ -132,23 +139,6 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
132139
isIPv6: family === 6,
133140
server: Promise.resolve()
134141
.then(async (): Promise<StunServer> => {
135-
// ensure we have a certificate
136-
if (this.certificate == null) {
137-
this.log.trace('creating TLS certificate')
138-
const keyPair = await crypto.subtle.generateKey({
139-
name: 'ECDSA',
140-
namedCurve: 'P-256'
141-
}, true, ['sign', 'verify'])
142-
143-
const certificate = await generateTransportCertificate(keyPair, {
144-
days: 365 * 10
145-
})
146-
147-
if (this.certificate == null) {
148-
this.certificate = certificate
149-
}
150-
}
151-
152142
if (port === 0) {
153143
// libjuice doesn't map 0 to a random free port so we have to do it
154144
// ourselves

0 commit comments

Comments
 (0)