1
1
import { generateKeyPair , privateKeyToCryptoKeyPair } from '@libp2p/crypto/keys'
2
- import { NotFoundError , NotStartedError , TypedEventEmitter , serviceCapabilities , transportSymbol } from '@libp2p/interface'
2
+ import { InvalidParametersError , NotFoundError , NotStartedError , TypedEventEmitter , serviceCapabilities , transportSymbol } from '@libp2p/interface'
3
3
import { peerIdFromString } from '@libp2p/peer-id'
4
4
import { WebRTCDirect } from '@multiformats/multiaddr-matcher'
5
5
import { BasicConstraintsExtension , X509Certificate , X509CertificateGenerator } from '@peculiar/x509'
@@ -9,7 +9,7 @@ import { sha256 } from 'multiformats/hashes/sha2'
9
9
import { raceSignal } from 'race-signal'
10
10
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
11
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'
12
+ import { DEFAULT_CERTIFICATE_DATASTORE_KEY , DEFAULT_CERTIFICATE_LIFESPAN , DEFAULT_CERTIFICATE_PRIVATE_KEY_NAME , DEFAULT_CERTIFICATE_RENEWAL_THRESHOLD } from '../constants.js'
13
13
import { genUfrag } from '../util.js'
14
14
import { WebRTCDirectListener } from './listener.js'
15
15
import { connect } from './utils/connect.js'
@@ -23,8 +23,6 @@ import type { Keychain } from '@libp2p/keychain'
23
23
import type { Multiaddr } from '@multiformats/multiaddr'
24
24
import type { Datastore } from 'interface-datastore'
25
25
26
- const ONE_DAY_MS = 86_400_000
27
-
28
26
export interface WebRTCDirectTransportComponents {
29
27
peerId : PeerId
30
28
privateKey : PrivateKey
@@ -88,16 +86,16 @@ export interface WebRTCTransportDirectInit {
88
86
certificateKeychainName ?: string
89
87
90
88
/**
91
- * Number of days a certificate should be valid for
89
+ * Number of ms a certificate should be valid for (defaults to 14 days)
92
90
*
93
- * @default 365
91
+ * @default 2_592_000_000
94
92
*/
95
93
certificateLifespan ?: number
96
94
97
95
/**
98
- * Certificates will be renewed this many days before their expiry
96
+ * Certificates will be renewed this many ms before expiry (defaults to 1 day)
99
97
*
100
- * @default 5
98
+ * @default 86_400_000
101
99
*/
102
100
certificateRenewalThreshold ?: number
103
101
}
@@ -114,13 +112,18 @@ export class WebRTCDirectTransport implements Transport, Startable {
114
112
private certificate ?: TransportCertificate
115
113
private privateKey ?: PrivateKey
116
114
private readonly emitter : TypedEventTarget < WebRTCDirectTransportCertificateEvents >
115
+ private renewCertificateTask ?: ReturnType < typeof setTimeout >
117
116
118
117
constructor ( components : WebRTCDirectTransportComponents , init : WebRTCTransportDirectInit = { } ) {
119
118
this . log = components . logger . forComponent ( 'libp2p:webrtc-direct' )
120
119
this . components = components
121
120
this . init = init
122
121
this . emitter = new TypedEventEmitter ( )
123
122
123
+ if ( init . certificateLifespan != null && init . certificateRenewalThreshold != null && init . certificateRenewalThreshold >= init . certificateLifespan ) {
124
+ throw new InvalidParametersError ( 'Certificate renewal threshold must be less than certificate lifespan' )
125
+ }
126
+
124
127
if ( components . metrics != null ) {
125
128
this . metrics = {
126
129
dialerEvents : components . metrics . registerCounterGroup ( 'libp2p_webrtc-direct_dialer_events_total' , {
@@ -144,7 +147,11 @@ export class WebRTCDirectTransport implements Transport, Startable {
144
147
}
145
148
146
149
async stop ( ) : Promise < void > {
150
+ if ( this . renewCertificateTask != null ) {
151
+ clearTimeout ( this . renewCertificateTask )
152
+ }
147
153
154
+ this . certificate = undefined
148
155
}
149
156
150
157
/**
@@ -225,14 +232,14 @@ export class WebRTCDirectTransport implements Transport, Startable {
225
232
}
226
233
}
227
234
228
- private async getCertificate ( ) : Promise < TransportCertificate > {
235
+ private async getCertificate ( forceRenew ?: boolean ) : Promise < TransportCertificate > {
229
236
if ( isTransportCertificate ( this . init . certificate ) ) {
230
- this . log . trace ( 'using provided TLS certificate' )
237
+ this . log ( 'using provided TLS certificate' )
231
238
return this . init . certificate
232
239
}
233
240
234
241
const privateKey = await this . loadOrCreatePrivateKey ( )
235
- const { pem, certhash } = await this . loadOrCreateCertificate ( privateKey )
242
+ const { pem, certhash } = await this . loadOrCreateCertificate ( privateKey , forceRenew )
236
243
237
244
return {
238
245
privateKey : await formatAsPem ( privateKey ) ,
@@ -276,8 +283,8 @@ export class WebRTCDirectTransport implements Transport, Startable {
276
283
return this . privateKey
277
284
}
278
285
279
- private async loadOrCreateCertificate ( privateKey : PrivateKey ) : Promise < { pem : string , certhash : string } > {
280
- if ( this . certificate != null ) {
286
+ private async loadOrCreateCertificate ( privateKey : PrivateKey , forceRenew ?: boolean ) : Promise < { pem : string , certhash : string } > {
287
+ if ( this . certificate != null && forceRenew !== true ) {
281
288
return this . certificate
282
289
}
283
290
@@ -286,17 +293,45 @@ export class WebRTCDirectTransport implements Transport, Startable {
286
293
const keyPair = await privateKeyToCryptoKeyPair ( privateKey )
287
294
288
295
try {
296
+ if ( forceRenew === true ) {
297
+ this . log . trace ( 'forcing renewal of TLS certificate' )
298
+ throw new NotFoundError ( )
299
+ }
300
+
289
301
this . log . trace ( 'checking for stored TLS certificate' )
290
302
cert = await this . loadCertificate ( dsKey , keyPair )
291
303
} catch ( err : any ) {
292
304
if ( err . name !== 'NotFoundError' ) {
293
305
throw err
294
306
}
295
307
296
- this . log ( 'generating TLS certificate using private key ' )
308
+ this . log . trace ( 'generating new TLS certificate' )
297
309
cert = await this . createCertificate ( dsKey , keyPair )
298
310
}
299
311
312
+ // set timeout to renew certificate
313
+ let renewTime = ( cert . notAfter . getTime ( ) - ( this . init . certificateRenewalThreshold ?? DEFAULT_CERTIFICATE_RENEWAL_THRESHOLD ) ) - Date . now ( )
314
+
315
+ if ( renewTime < 0 ) {
316
+ renewTime = 100
317
+ }
318
+
319
+ this . log ( 'will renew TLS certificate after %d ms' , renewTime )
320
+
321
+ this . renewCertificateTask = setTimeout ( ( ) => {
322
+ this . log ( 'renewing TLS certificate' )
323
+ this . getCertificate ( true )
324
+ . then ( cert => {
325
+ this . certificate = cert
326
+ this . emitter . safeDispatchEvent ( 'certificate:renew' , {
327
+ detail : cert
328
+ } )
329
+ } )
330
+ . catch ( err => {
331
+ this . log . error ( 'could not renew certificate - %e' , err )
332
+ } )
333
+ } , renewTime )
334
+
300
335
return {
301
336
pem : cert . toString ( 'pem' ) ,
302
337
certhash : base64url . encode ( ( await sha256 . digest ( new Uint8Array ( cert . rawData ) ) ) . bytes )
@@ -308,14 +343,16 @@ export class WebRTCDirectTransport implements Transport, Startable {
308
343
const cert = new X509Certificate ( buf )
309
344
310
345
// check expiry date
311
- const threshold = Date . now ( ) - ( ( this . init . certificateLifespan ?? DEFAULT_CERTIFICATE_LIFESPAN ) * ONE_DAY_MS )
346
+ const expiryTime = cert . notAfter . getTime ( ) - ( this . init . certificateRenewalThreshold ?? DEFAULT_CERTIFICATE_RENEWAL_THRESHOLD )
312
347
313
- if ( cert . notAfter . getTime ( ) < threshold ) {
348
+ if ( Date . now ( ) > expiryTime ) {
314
349
this . log ( 'stored TLS certificate has expired' )
315
350
// act as if no certificate was present
316
351
throw new NotFoundError ( )
317
352
}
318
353
354
+ this . log ( 'loaded certificate, expires in %d ms' , expiryTime )
355
+
319
356
// check public keys match
320
357
const exportedCertKey = await cert . publicKey . export ( crypto )
321
358
const rawCertKey = await crypto . subtle . exportKey ( 'raw' , exportedCertKey )
@@ -329,12 +366,14 @@ export class WebRTCDirectTransport implements Transport, Startable {
329
366
throw new NotFoundError ( )
330
367
}
331
368
369
+ this . log ( 'loaded certificate, expiry time is %o' , expiryTime )
370
+
332
371
return cert
333
372
}
334
373
335
374
async createCertificate ( dsKey : Key , keyPair : CryptoKeyPair ) : Promise < X509Certificate > {
336
375
const notBefore = new Date ( )
337
- const notAfter = new Date ( notBefore . getTime ( ) + ( ( this . init . certificateLifespan ?? DEFAULT_CERTIFICATE_LIFESPAN ) * ONE_DAY_MS ) )
376
+ const notAfter = new Date ( Date . now ( ) + ( this . init . certificateLifespan ?? DEFAULT_CERTIFICATE_LIFESPAN ) )
338
377
339
378
// have to set ms to 0 to work around https://github.com/PeculiarVentures/x509/issues/73
340
379
notBefore . setMilliseconds ( 0 )
0 commit comments