Skip to content

Commit e219b6b

Browse files
committed
feat: configuratle threshold and timeout before certificate expiry
1 parent 2c545d8 commit e219b6b

File tree

4 files changed

+62
-26
lines changed

4 files changed

+62
-26
lines changed

packages/transport-webrtc/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ export interface TransportCertificate {
280280
* The hash of the certificate
281281
*/
282282
certhash: string
283-
notAfter: string
283+
notAfter: number
284284
}
285285

286286
export type { WebRTCTransportDirectInit, WebRTCDirectTransportComponents }

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

+36-14
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export interface WebRTCDirectListenerInit {
3434
rtcConfiguration?: RTCConfiguration | (() => RTCConfiguration | Promise<RTCConfiguration>)
3535
useLibjuice?: boolean
3636
certificateDuration?: number
37+
certificateExpiryThreshold?: number
3738
}
3839

3940
export interface WebRTCListenerMetrics {
@@ -86,10 +87,10 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
8687

8788
private isCertificateExpiring (): boolean {
8889
if (this.certificate == null) return true
89-
const expiryDate = new Date(this.certificate.notAfter)
90+
const expiryDate = this.certificate.notAfter
9091
const now = new Date()
91-
const timeToExpiry = expiryDate.getTime() - now.getTime()
92-
const threshold = 30 * 24 * 60 * 60 * 1000
92+
const timeToExpiry = expiryDate - now.getTime()
93+
const threshold = this.init.certificateExpiryThreshold ?? 7 * 86400000
9394
return timeToExpiry < threshold
9495
}
9596

@@ -166,17 +167,7 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
166167
// ensure we have a certificate
167168
if (this.certificate == null || this.isCertificateExpiring()) {
168169
this.log.trace('creating TLS certificate')
169-
const keyPair = await crypto.subtle.generateKey({
170-
name: 'ECDSA',
171-
namedCurve: 'P-256'
172-
}, true, ['sign', 'verify'])
173-
174-
const certificate = await generateTransportCertificate(keyPair, {
175-
days: this.init.certificateDuration ?? 365 * 10
176-
})
177-
this.safeDispatchEvent('listening')
178-
179-
this.certificate = certificate
170+
await this.createAndSetCertificate()
180171
}
181172

182173
if (port === 0) {
@@ -196,6 +187,37 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
196187
}
197188
}
198189

190+
private async createAndSetCertificate (): Promise<void> {
191+
const keyPair = await crypto.subtle.generateKey({
192+
name: 'ECDSA',
193+
namedCurve: 'P-256'
194+
}, true, ['sign', 'verify'])
195+
196+
const certificate = await generateTransportCertificate(keyPair, {
197+
days: this.init.certificateDuration ?? 365 * 10
198+
})
199+
200+
this.certificate = certificate
201+
this.setCertificateExpiryTimeout()
202+
}
203+
204+
private setCertificateExpiryTimeout (): void {
205+
if (this.certificate == null) {
206+
return
207+
}
208+
209+
const expiryDate = new Date(this.certificate.notAfter)
210+
const now = new Date()
211+
const timeToExpiry = expiryDate.getTime() - now.getTime()
212+
const timeoutDuration = timeToExpiry - 7 * 86400000
213+
214+
setTimeout(async () => {
215+
this.log.trace('renewing TLS certificate')
216+
await this.createAndSetCertificate()
217+
this.safeDispatchEvent('listening')
218+
}, timeoutDuration)
219+
}
220+
199221
private async incomingConnection (ufrag: string, remoteHost: string, remotePort: number): Promise<void> {
200222
const key = `${remoteHost}:${remotePort}:${ufrag}`
201223
let peerConnection = this.connections.get(key)

packages/transport-webrtc/src/private-to-public/utils/generate-certificates.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,6 @@ export async function generateTransportCertificate (keyPair: CryptoKeyPair, opti
4747
privateKey: privateKeyPem,
4848
pem: cert.toString('pem'),
4949
certhash: base64url.encode((await sha256.digest(new Uint8Array(cert.rawData))).bytes),
50-
notAfter: notAfter.toISOString()
50+
notAfter: notAfter.getTime()
5151
}
5252
}

packages/transport-webrtc/test/certificate.spec.ts

+24-10
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import { defaultLogger } from '@libp2p/logger'
12
import { Crypto } from '@peculiar/webcrypto'
23
import { expect } from 'aegir/utils/chai.js'
34
import sinon from 'sinon'
5+
import { stubInterface } from 'sinon-ts'
46
import { WebRTCDirectListener, type WebRTCDirectListenerInit } from '../src/private-to-public/listener.js'
57
import { type WebRTCDirectTransportComponents } from '../src/private-to-public/transport.js'
68
import { generateTransportCertificate } from '../src/private-to-public/utils/generate-certificates.js'
79
import type { TransportCertificate } from '../src/index.js'
10+
import type { TransportManager } from '@libp2p/interface-internal'
811

912
const crypto = new Crypto()
1013

@@ -17,13 +20,8 @@ describe('WebRTCDirectListener', () => {
1720
components = {
1821
peerId: { toB58String: () => 'QmPeerId' } as any,
1922
privateKey: {} as any,
20-
logger: {
21-
forComponent: () => ({
22-
trace: () => {},
23-
error: () => {}
24-
})
25-
} as any,
26-
transportManager: {} as any
23+
logger: defaultLogger(),
24+
transportManager: stubInterface<TransportManager>()
2725
}
2826

2927
init = {
@@ -64,13 +62,29 @@ describe('WebRTCDirectListener', () => {
6462
expect(emitSpy.calledWith('listening')).to.be.true
6563
})
6664

67-
it('should generate a new certificate before expiry', async () => {
65+
it('should generate a new certificate before expiry with default threshold', async () => {
66+
init.certificateExpiryThreshold = undefined
67+
listener = new WebRTCDirectListener(components, init)
68+
;(listener as any).certificate = {
69+
notAfter: new Date(Date.now() + 5 * 86400000).getTime()
70+
}
71+
72+
const isCertificateExpiringSpy = sinon.spy(listener as any, 'isCertificateExpiring')
73+
const generateCertificateSpy = sinon.spy(listener as any, 'createAndSetCertificate')
74+
75+
await (listener as any).startUDPMuxServer('127.0.0.1', 0)
76+
77+
expect(isCertificateExpiringSpy.returned(true)).to.be.true
78+
expect(generateCertificateSpy.called).to.be.true
79+
})
80+
81+
it('should generate a new certificate before expiry with custom threshold', async () => {
6882
(listener as any).certificate = {
69-
notAfter: new Date(Date.now() + 5 * 86400000).toISOString()
83+
notAfter: new Date(Date.now() + 4 * 86400000).getTime()
7084
}
7185

7286
const isCertificateExpiringSpy = sinon.spy(listener as any, 'isCertificateExpiring')
73-
const generateCertificateSpy = sinon.spy(generateTransportCertificate)
87+
const generateCertificateSpy = sinon.spy(listener as any, 'createAndSetCertificate')
7488

7589
await (listener as any).startUDPMuxServer('127.0.0.1', 0)
7690

0 commit comments

Comments
 (0)