Skip to content

Commit d8e7109

Browse files
committed
WIP
1 parent 286a006 commit d8e7109

12 files changed

+1783
-141
lines changed

src/QUICServer.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type {
1010
} from './types';
1111
import type { Header } from './native/types';
1212
import type QUICConnectionMap from './QUICConnectionMap';
13-
import type { QUICConfig, TlsConfig } from './config';
13+
import type { QUICConfig } from './config';
1414
import type { QUICServerConnectionEvent } from './events';
1515
import Logger from '@matrixai/logger';
1616
import { running } from '@matrixai/async-init';
@@ -70,8 +70,8 @@ class QUICServer extends EventTarget {
7070

7171
public constructor({
7272
crypto,
73-
socket,
7473
config,
74+
socket,
7575
resolveHostname = utils.resolveHostname,
7676
reasonToCode,
7777
codeToReason,
@@ -84,11 +84,11 @@ class QUICServer extends EventTarget {
8484
key: ArrayBuffer;
8585
ops: Crypto;
8686
};
87+
config: Partial<QUICConfig> & {
88+
key: string | Array<string> | Uint8Array | Array<Uint8Array>;
89+
cert: string | Array<string> | Uint8Array | Array<Uint8Array>;
90+
};
8791
socket?: QUICSocket;
88-
// This actually requires TLS
89-
// You have to specify these some how
90-
// We can force it
91-
config: Partial<QUICConfig> & { tlsConfig: TlsConfig };
9292
resolveHostname?: (hostname: Hostname) => Host | PromiseLike<Host>;
9393
reasonToCode?: StreamReasonToCode;
9494
codeToReason?: StreamCodeToReason;
@@ -146,15 +146,17 @@ class QUICServer extends EventTarget {
146146
public async start({
147147
host = '::' as Host,
148148
port = 0 as Port,
149+
reuseAddr,
149150
}: {
150151
host?: Host | Hostname;
151152
port?: Port;
153+
reuseAddr?: boolean;
152154
} = {}) {
153155
let address: string;
154156
if (!this.isSocketShared) {
155157
address = utils.buildAddress(host, port);
156158
this.logger.info(`Start ${this.constructor.name} on ${address}`);
157-
await this.socket.start({ host, port });
159+
await this.socket.start({ host, port, reuseAddr });
158160
this.socket.addEventListener('error', this.handleQUICSocketError);
159161
address = utils.buildAddress(this.socket.host, this.socket.port);
160162
} else {
@@ -193,6 +195,9 @@ class QUICServer extends EventTarget {
193195
this.logger.info(`Stopped ${this.constructor.name} on ${address}`);
194196
}
195197

198+
/**
199+
* @internal
200+
*/
196201
public async connectionNew(
197202
data: Buffer,
198203
remoteInfo: RemoteInfo,

src/QUICSocket.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,8 @@ class QUICSocket extends EventTarget {
7272
}
7373
return;
7474
}
75-
76-
// Apparently if it is a UDP datagram
77-
// it could be a QUIC datagram, and not part of any connection
78-
// However I don't know how the DCID would work in a QUIC dgram
79-
// We have not explored this yet
80-
8175
// Destination Connection ID is the ID the remote peer chose for us.
8276
const dcid = new QUICConnectionId(header.dcid);
83-
8477
// Derive our SCID using HMAC signing.
8578
const scid = new QUICConnectionId(
8679
await this.crypto.ops.sign(
@@ -90,12 +83,10 @@ class QUICSocket extends EventTarget {
9083
0,
9184
quiche.MAX_CONN_ID_LEN,
9285
);
93-
9486
const remoteInfo_ = {
9587
host: remoteInfo.address as Host,
9688
port: remoteInfo.port as Port,
9789
};
98-
9990
// Now both must be checked
10091
let conn: QUICConnection;
10192
if (!this.connectionMap.has(dcid) && !this.connectionMap.has(scid)) {
@@ -207,10 +198,12 @@ class QUICSocket extends EventTarget {
207198
public async start({
208199
host = '::' as Host,
209200
port = 0 as Port,
201+
reuseAddr = false,
210202
ipv6Only = false,
211203
}: {
212204
host?: Host | Hostname;
213205
port?: Port;
206+
reuseAddr?: boolean;
214207
ipv6Only?: boolean;
215208
} = {}): Promise<void> {
216209
let address = utils.buildAddress(host, port);
@@ -223,7 +216,7 @@ class QUICSocket extends EventTarget {
223216
);
224217
this.socket = dgram.createSocket({
225218
type: udpType,
226-
reuseAddr: false,
219+
reuseAddr,
227220
ipv6Only,
228221
});
229222
this.socketBind = utils.promisify(this.socket.bind).bind(this.socket);

src/config.ts

Lines changed: 167 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,67 @@
11
import type { Config as QuicheConfig } from './native/types';
22
import { quiche } from './native';
3+
import * as errors from './errors';
34

4-
// All the algos chrome supports + ed25519
5-
const supportedPrivateKeyAlgosDefault =
6-
'ed25519:RSA+SHA256:RSA+SHA384:RSA+SHA512:ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA512:RSA-PSS+SHA256:RSA-PSS+SHA384:RSA-PSS+SHA512';
5+
type QUICConfig = {
6+
/**
7+
* Certificate authority certificate in PEM format or Uint8Array buffer
8+
* containing PEM formatted certificate. Each string or Uint8Array can be
9+
* one certificate or multiple certificates concatenated together. The order
10+
* does not matter, each is an independent certificate authority. Multiple
11+
* concatenated certificate authorities can be passed. They are all
12+
* concatenated together.
13+
*
14+
* When this is not set, this defaults to the operating system's CA
15+
* certificates. OpenSSL (and forks of OpenSSL) all support the
16+
* environment variables `SSL_CERT_DIR` and `SSL_CERT_FILE`.
17+
*/
18+
ca?: string | Array<string> | Uint8Array | Array<Uint8Array>;
719

8-
export type TlsConfig =
9-
| {
10-
certChainPem: string | null;
11-
privKeyPem: string | null;
12-
}
13-
| {
14-
certChainFromPemFile: string | null;
15-
privKeyFromPemFile: string | null;
16-
};
20+
/**
21+
* Private key as a PEM string or Uint8Array buffer containing PEM formatted
22+
* key. You can pass multiple keys. The number of keys must match the number
23+
* of certs. Each key must be associated to the the corresponding cert chain.
24+
*
25+
* Currently multiple key and certificate chains is not supported.
26+
*/
27+
key?: string | Array<string> | Uint8Array | Array<Uint8Array>;
1728

18-
type QUICConfig = {
19-
tlsConfig: TlsConfig | undefined;
20-
verifyPem: string | undefined;
21-
verifyFromPemFile: string | undefined;
22-
supportedPrivateKeyAlgos: string | undefined;
29+
/**
30+
* X.509 certificate chain in PEM format or Uint8Array buffer containing
31+
* PEM formatted certificate chain. Each string or Uint8Array is a
32+
* certificate chain in subject to issuer order. Multiple certificate chains
33+
* can be passed. The number of certificate chains must match the number of
34+
* keys. Each certificate chain must be associated to the corresponding key.
35+
*
36+
* Currently multiple key and certificate chains is not supported.
37+
*/
38+
cert?: string | Array<string> | Uint8Array | Array<Uint8Array>;
39+
40+
/**
41+
* Colon separated list of supported signature algorithms.
42+
*
43+
* When this is not set, this defaults to the following list:
44+
* - rsa_pkcs1_sha256
45+
* - rsa_pkcs1_sha384
46+
* - rsa_pkcs1_sha512
47+
* - rsa_pss_rsae_sha256
48+
* - rsa_pss_rsae_sha384
49+
* - rsa_pss_rsae_sha512
50+
* - ecdsa_secp256r1_sha256
51+
* - ecdsa_secp384r1_sha384
52+
* - ecdsa_secp521r1_sha512
53+
* - ed25519
54+
*/
55+
sigalgs?: string;
56+
57+
/**
58+
* Verify the other peer.
59+
* Clients by default set this to true.
60+
* Servers by default set this to false.
61+
*/
2362
verifyPeer: boolean;
24-
logKeys: string | undefined;
63+
64+
logKeys?: string;
2565
grease: boolean;
2666
maxIdleTimeout: number;
2767
maxRecvUdpPayloadSize: number;
@@ -36,12 +76,28 @@ type QUICConfig = {
3676
enableEarlyData: boolean;
3777
};
3878

79+
/**
80+
* BoringSSL does not support:
81+
* - rsa_pss_pss_sha256
82+
* - rsa_pss_pss_sha384
83+
* - rsa_pss_pss_sha512
84+
* - ed448
85+
*/
86+
const sigalgs = [
87+
'rsa_pkcs1_sha256',
88+
'rsa_pkcs1_sha384',
89+
'rsa_pkcs1_sha512',
90+
'rsa_pss_rsae_sha256',
91+
'rsa_pss_rsae_sha384',
92+
'rsa_pss_rsae_sha512',
93+
'ecdsa_secp256r1_sha256',
94+
'ecdsa_secp384r1_sha384',
95+
'ecdsa_secp521r1_sha512',
96+
'ed25519',
97+
].join(':');
98+
3999
const clientDefault: QUICConfig = {
40-
tlsConfig: undefined,
41-
verifyPem: undefined,
42-
verifyFromPemFile: undefined,
43-
supportedPrivateKeyAlgos: supportedPrivateKeyAlgosDefault,
44-
logKeys: undefined,
100+
sigalgs,
45101
verifyPeer: true,
46102
grease: true,
47103
maxIdleTimeout: 5000,
@@ -53,16 +109,13 @@ const clientDefault: QUICConfig = {
53109
initialMaxStreamsBidi: 100,
54110
initialMaxStreamsUni: 100,
55111
disableActiveMigration: true,
112+
// Test if this is needed
56113
applicationProtos: ['http/0.9'],
57114
enableEarlyData: true,
58115
};
59116

60117
const serverDefault: QUICConfig = {
61-
tlsConfig: undefined,
62-
verifyPem: undefined,
63-
verifyFromPemFile: undefined,
64-
supportedPrivateKeyAlgos: supportedPrivateKeyAlgosDefault,
65-
logKeys: undefined,
118+
sigalgs,
66119
verifyPeer: false,
67120
grease: true,
68121
maxIdleTimeout: 5000,
@@ -74,40 +127,103 @@ const serverDefault: QUICConfig = {
74127
initialMaxStreamsBidi: 100,
75128
initialMaxStreamsUni: 100,
76129
disableActiveMigration: true,
130+
// Test if this is needed
77131
applicationProtos: ['http/0.9'],
78132
enableEarlyData: true,
79133
};
80134

135+
const textDecoder = new TextDecoder('utf-8');
136+
const textEncoder = new TextEncoder();
137+
81138
function buildQuicheConfig(config: QUICConfig): QuicheConfig {
82-
let certChainPem: Buffer | null = null;
83-
let privKeyPem: Buffer | null = null;
84-
if (config.tlsConfig != null && 'certChainPem' in config.tlsConfig) {
85-
if (config.tlsConfig.certChainPem != null) {
86-
certChainPem = Buffer.from(config.tlsConfig.certChainPem);
139+
if (config.key != null && config.cert == null) {
140+
throw new errors.ErrorQUICConfig(
141+
'The cert option must be set when key is set',
142+
);
143+
} else if (config.key == null && config.cert != null) {
144+
throw new errors.ErrorQUICConfig(
145+
'The key option must be set when cert is set',
146+
);
147+
} else if (config.key != null && config.cert != null) {
148+
if (Array.isArray(config.key) && Array.isArray(config.cert)) {
149+
if (config.key.length !== config.cert.length) {
150+
throw new errors.ErrorQUICConfig(
151+
'The number of keys must match the number of certs',
152+
);
153+
}
87154
}
88-
if (config.tlsConfig.privKeyPem != null) {
89-
privKeyPem = Buffer.from(config.tlsConfig.privKeyPem);
155+
}
156+
// This is a concatenated CA certificates in PEM format
157+
let caPEMBuffer: Uint8Array | undefined;
158+
if (config.ca != null) {
159+
let caPEMString = '';
160+
if (typeof config.ca === 'string') {
161+
caPEMString = config.ca.trim() + '\n';
162+
} else if (config.ca instanceof Uint8Array) {
163+
caPEMString = textDecoder.decode(config.ca).trim() + '\n';
164+
} else if (Array.isArray(config.ca)) {
165+
for (const c of config.ca) {
166+
if (typeof c === 'string') {
167+
caPEMString += c.trim() + '\n';
168+
} else {
169+
caPEMString += textDecoder.decode(c).trim() + '\n';
170+
}
171+
}
90172
}
173+
caPEMBuffer = textEncoder.encode(caPEMString);
91174
}
92-
const quicheConfig: QuicheConfig = quiche.Config.withBoringSslCtx(
93-
certChainPem,
94-
privKeyPem,
95-
config.supportedPrivateKeyAlgos ?? null,
96-
config.verifyPem != null ? Buffer.from(config.verifyPem) : null,
97-
config.verifyPeer,
98-
);
99-
if (config.tlsConfig != null && 'certChainFromPemFile' in config.tlsConfig) {
100-
if (config.tlsConfig?.certChainFromPemFile != null) {
101-
quicheConfig.loadCertChainFromPemFile(
102-
config.tlsConfig.certChainFromPemFile,
103-
);
175+
// This is an array of private keys in PEM format
176+
let keyPEMBuffers: Array<Uint8Array> | undefined;
177+
if (config.key != null) {
178+
const keyPEMs: Array<string> = [];
179+
if (typeof config.key === 'string') {
180+
keyPEMs.push(config.key.trim() + '\n');
181+
} else if (config.key instanceof Uint8Array) {
182+
keyPEMs.push(textDecoder.decode(config.key).trim() + '\n');
183+
} else if (Array.isArray(config.key)) {
184+
for (const k of config.key) {
185+
if (typeof k === 'string') {
186+
keyPEMs.push(k.trim() + '\n');
187+
} else {
188+
keyPEMs.push(textDecoder.decode(k).trim() + '\n');
189+
}
190+
}
104191
}
105-
if (config.tlsConfig?.privKeyFromPemFile != null) {
106-
quicheConfig.loadPrivKeyFromPemFile(config.tlsConfig.privKeyFromPemFile);
192+
keyPEMBuffers = keyPEMs.map((k) => textEncoder.encode(k));
193+
}
194+
// This is an array of certificate chains in PEM format
195+
let certChainPEMBuffers: Array<Uint8Array> | undefined;
196+
if (config.cert != null) {
197+
const certChainPEMs: Array<string> = [];
198+
if (typeof config.cert === 'string') {
199+
certChainPEMs.push(config.cert.trim() + '\n');
200+
} else if (config.cert instanceof Uint8Array) {
201+
certChainPEMs.push(textDecoder.decode(config.cert).trim() + '\n');
202+
} else if (Array.isArray(config.cert)) {
203+
for (const c of config.cert) {
204+
if (typeof c === 'string') {
205+
certChainPEMs.push(c.trim() + '\n');
206+
} else {
207+
certChainPEMs.push(textDecoder.decode(c).trim() + '\n');
208+
}
209+
}
107210
}
211+
certChainPEMBuffers = certChainPEMs.map((c) => textEncoder.encode(c));
108212
}
109-
if (config.verifyFromPemFile != null) {
110-
quicheConfig.loadVerifyLocationsFromFile(config.verifyFromPemFile);
213+
let quicheConfig: QuicheConfig;
214+
try {
215+
quicheConfig = quiche.Config.withBoringSslCtx(
216+
config.verifyPeer,
217+
caPEMBuffer,
218+
keyPEMBuffers,
219+
certChainPEMBuffers,
220+
config.sigalgs,
221+
);
222+
} catch (e) {
223+
throw new errors.ErrorQUICConfig(
224+
`Failed to build Quiche config with custom SSL context: ${e.message}`,
225+
{ cause: e }
226+
);
111227
}
112228
if (config.logKeys != null) {
113229
quicheConfig.logKeys();

src/errors.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ class ErrorQUIC<T> extends AbstractError<T> {
44
static description = 'QUIC error';
55
}
66

7+
class ErrorQUICConfig<T> extends ErrorQUIC<T> {
8+
static description = 'QUIC config error';
9+
}
10+
711
class ErrorQUICSocket<T> extends ErrorQUIC<T> {
812
static description = 'QUIC Socket error';
913
}
@@ -105,6 +109,7 @@ class ErrorQUICUndefinedBehaviour<T> extends ErrorQUIC<T> {
105109

106110
export {
107111
ErrorQUIC,
112+
ErrorQUICConfig,
108113
ErrorQUICSocket,
109114
ErrorQUICSocketNotRunning,
110115
ErrorQUICSocketServerDuplicate,

0 commit comments

Comments
 (0)