Skip to content

Commit 8f00b54

Browse files
committed
feat: added QUICServer.setTLSConfig() allowing for TLS certificate rotation
* Related #3 [ci skip]
1 parent aa447ae commit 8f00b54

File tree

3 files changed

+95
-14
lines changed

3 files changed

+95
-14
lines changed

src/QUICServer.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ConnectionId, Crypto, Host, Hostname, Port, RemoteInfo } from './types';
22
import type { Header, Config } from './native/types';
33
import type QUICConnectionMap from './QUICConnectionMap';
4-
import type { QUICConfig } from './config';
4+
import type { QUICConfig, TlsConfig } from "./config";
55
import QUICConnectionId from './QUICConnectionId';
66
import Logger from '@matrixai/logger';
77
import { running } from '@matrixai/async-init';
@@ -70,10 +70,7 @@ class QUICServer extends EventTarget {
7070
// This actually requires TLS
7171
// You have to specify these some how
7272
// We can force it
73-
config: Partial<QUICConfig> & Pick<
74-
QUICConfig,
75-
'tlsConfig'
76-
>;
73+
config: Partial<QUICConfig> & {TlsConfig?: TlsConfig};
7774
resolveHostname?: (hostname: Hostname) => Host | PromiseLike<Host>;
7875
logger?: Logger;
7976
}) {
@@ -282,6 +279,16 @@ class QUICServer extends EventTarget {
282279
return connection;
283280
}
284281

282+
/**
283+
* This updates the `tlsConfig` used when new connections are established.
284+
* It will not affect existing connections, they will keep using the old `tlsconfig`
285+
* @param tlsConfig
286+
*/
287+
public setTLSConfig(tlsConfig: TlsConfig): void {
288+
// tlsConfig is an object, spread to copy and avoid object mutation
289+
this.config.tlsConfig = { ...tlsConfig };
290+
};
291+
285292
/**
286293
* Creates a retry token.
287294
* This will embed peer host IP and DCID into the token.

src/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Config as QuicheConfig } from './native/types';
22
import { quiche } from './native';
33

4-
type TlsConfig = {
4+
export type TlsConfig = {
55
certChainPem: string | null;
66
privKeyPem: string | null;
77
} | {

tests/QUICClient.test.ts

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as tls from 'tls';
1111
import * as errors from '@/errors';
1212
import { fc } from '@fast-check/jest';
1313
import * as tlsUtils from './tlsUtils';
14+
import * as certFixtures from './fixtures/certFixtures';
1415

1516

1617
const privKeyPem = `
@@ -65,11 +66,8 @@ jti9iwz2QT6q1s+PjS/gbflIO3j4FP4XOEQGtWm9iqPbVhoUIB9PBED3
6566
-----END CERTIFICATE-----
6667
`
6768

68-
// const tlsArb = fc.constant({
69-
// certChainFromPemFile: './tmp/localhost.crt',
70-
// privKeyFromPemFile: './tmp/localhost.key',
71-
// });
72-
const tlsArb = tlsUtils.tlsConfigArb(tlsUtils.keyPairsArb(1));
69+
const tlsArb = fc.constant(certFixtures.tlsConfigFileRSA1);
70+
// const tlsArb = tlsUtils.tlsConfigArb(tlsUtils.keyPairsArb(1));
7371
// const tlsArb = fc.constant({
7472
// certChainPem,
7573
// privKeyPem,
@@ -207,8 +205,6 @@ describe(QUICClient.name, () => {
207205
await server.stop();
208206
});
209207
});
210-
211-
212208
test('times out when there is no server', async () => {
213209
// QUICClient repeatedly dials until the connection timesout
214210
await expect(QUICClient.createQUICClient({
@@ -222,7 +218,85 @@ describe(QUICClient.name, () => {
222218
}
223219
})).rejects.toThrow(errors.ErrorQUICConnectionTimeout);
224220
});
225-
221+
describe('TLS rotation', () => {
222+
let connectionEventP;
223+
let resolveConnectionEventP;
224+
let handleConnectionEventP;
225+
beforeEach(async () => {
226+
const {
227+
p,
228+
resolveP
229+
} = utils.promise<events.QUICServerConnectionEvent>();
230+
connectionEventP = p;
231+
resolveConnectionEventP = resolveP;
232+
handleConnectionEventP = (e: events.QUICServerConnectionEvent) => {
233+
resolveConnectionEventP(e);
234+
};
235+
});
236+
test.todo('existing connections still function');
237+
test('existing connections config is unchanged and still function', async () => {
238+
const server = new QUICServer({
239+
crypto,
240+
logger: logger.getChild(QUICServer.name),
241+
config: {
242+
tlsConfig: certFixtures.tlsConfigFileRSA1
243+
}
244+
});
245+
server.addEventListener('connection', handleConnectionEventP);
246+
await server.start({
247+
host: '127.0.0.1' as Host,
248+
});
249+
const client1 = await QUICClient.createQUICClient({
250+
host: '::ffff:127.0.0.1' as Host,
251+
port: server.port,
252+
localHost: '::' as Host,
253+
crypto,
254+
logger: logger.getChild(QUICClient.name),
255+
});
256+
const peerCertChainInitial = client1.connection.conn.peerCertChain()
257+
server.setTLSConfig(certFixtures.tlsConfigFileRSA2)
258+
// The existing connection's certs should be unchanged
259+
const peerCertChainNew = client1.connection.conn.peerCertChain()
260+
expect(peerCertChainNew![0].toString()).toStrictEqual(peerCertChainInitial![0].toString());
261+
await client1.destroy();
262+
await server.stop();
263+
});
264+
test('new connections use new config', async () => {
265+
const server = new QUICServer({
266+
crypto,
267+
logger: logger.getChild(QUICServer.name),
268+
config: {
269+
tlsConfig: certFixtures.tlsConfigFileRSA1
270+
}
271+
});
272+
server.addEventListener('connection', handleConnectionEventP);
273+
await server.start({
274+
host: '127.0.0.1' as Host,
275+
});
276+
const client1 = await QUICClient.createQUICClient({
277+
host: '::ffff:127.0.0.1' as Host,
278+
port: server.port,
279+
localHost: '::' as Host,
280+
crypto,
281+
logger: logger.getChild(QUICClient.name),
282+
});
283+
const peerCertChainInitial = client1.connection.conn.peerCertChain()
284+
server.setTLSConfig(certFixtures.tlsConfigFileRSA2)
285+
// Starting a new connection has a different peerCertChain
286+
const client2 = await QUICClient.createQUICClient({
287+
host: '::ffff:127.0.0.1' as Host,
288+
port: server.port,
289+
localHost: '::' as Host,
290+
crypto,
291+
logger: logger.getChild(QUICClient.name),
292+
});
293+
const peerCertChainNew = client2.connection.conn.peerCertChain()
294+
expect(peerCertChainNew![0].toString()).not.toStrictEqual(peerCertChainInitial![0].toString());
295+
await client1.destroy();
296+
await client2.destroy();
297+
await server.stop();
298+
});
299+
})
226300

227301
// test('dual stack to dual stack', async () => {
228302

0 commit comments

Comments
 (0)