Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

quic: refactor clientHello #34541

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
68 changes: 36 additions & 32 deletions doc/api/quic.md
Expand Up @@ -1274,38 +1274,6 @@ The `QuicServerSession` class implements the server side of a QUIC connection.
Instances are created internally and are emitted using the `QuicSocket`
`'session'` event.

#### Event: `'clientHello'`
<!-- YAML
added: REPLACEME
-->

Emitted at the start of the TLS handshake when the `QuicServerSession` receives
the initial TLS Client Hello.

The event handler is given a callback function that *must* be invoked for the
handshake to continue.

The callback is invoked with four arguments:

* `alpn` {string} The ALPN protocol identifier requested by the client.
* `servername` {string} The SNI servername requested by the client.
* `ciphers` {string[]} The list of TLS cipher algorithms requested by the
client.
* `callback` {Function} A callback function that must be called in order for
the TLS handshake to continue.

The `'clientHello'` event will not be emitted more than once.

#### `quicserversession.addContext(servername\[, context\])`
<!-- YAML
added: REPLACEME
-->

* `servername` {string} A DNS name to associate with the given context.
* `context` {tls.SecureContext} A TLS SecureContext to associate with the `servername`.

TBD

### Class: `QuicSocket`
<!-- YAML
added: REPLACEME
Expand Down Expand Up @@ -1766,6 +1734,9 @@ added: REPLACEME
uppercased in order for OpenSSL to accept them.
* `clientCertEngine` {string} Name of an OpenSSL engine which can provide the
client certificate.
* `clientHelloHandler` {Function} An async function that may be used to
set a {tls.SecureContext} for the given server name at the start of the
TLS handshake. See [Handling client hello][] for details.
* `crl` {string|string[]|Buffer|Buffer[]} PEM formatted CRLs (Certificate
Revocation Lists).
* `defaultEncoding` {string} The default encoding that is used when no
Expand Down Expand Up @@ -2479,6 +2450,38 @@ async function ocspClientHandler(type, { data }) {
sock.connect({ ocspHandler: ocspClientHandler });
```

### Handling client hello

When `quicsocket.listen()` is called, a {tls.SecureContext} is created and used
by default for all new `QuicServerSession` instances. There are times, however,
when the {tls.SecureContext} to be used for a `QuicSession` can only be
determined once the client initiates a connection. This is accomplished using
the `clientHelloHandler` option when calling `quicsocket.listen()`.

The value of `clientHelloHandler` is an async function that is called at the
start of a new `QuicServerSession`. It is invoked with three arguments:

* `alpn` {string} The ALPN protocol identifier specified by the client.
* `servername` {string} The SNI server name specified by the client.
* `ciphers` {string[]} The array of TLS 1.3 ciphers specified by the client.

The `clientHelloHandler` *may* return a new {tls.SecureContext} object that will
be used to continue the TLS handshake. If the function returns `undefined`, the
default {tls.SecureContext} will be used. Returning any other value will cause
an error to be thrown that will destroy the `QuicServerSession` instance.

```js
const server = createQuicSocket();

server.listen({
async clientHelloHandler(alpn, servername, ciphers) {
console.log(alpn);
console.log(servername);
console.log(ciphers);
}
});
```

[`crypto.getCurves()`]: crypto.html#crypto_crypto_getcurves
[`stream.Readable`]: #stream_class_stream_readable
[`tls.DEFAULT_ECDH_CURVE`]: #tls_tls_default_ecdh_curve
Expand All @@ -2487,6 +2490,7 @@ sock.connect({ ocspHandler: ocspClientHandler });
[RFC 4007]: https://tools.ietf.org/html/rfc4007
[Certificate Object]: https://nodejs.org/dist/latest-v12.x/docs/api/tls.html#tls_certificate_object
[custom DNS lookup function]: #quic_custom_dns_lookup_functions
[Handling client hello]: #quic_handling_client_hello
[modifying the default cipher suite]: tls.html#tls_modifying_the_default_tls_cipher_suite
[OCSP requests]: #quic_online_certificate_status_protocol_ocsp
[OCSP responses]: #quic_online_certificate_status_protocol_ocsp
Expand Down
107 changes: 40 additions & 67 deletions lib/internal/quic/core.js
Expand Up @@ -20,7 +20,6 @@ const {
PromiseAll,
PromiseReject,
PromiseResolve,
RegExp,
Set,
Symbol,
} = primordials;
Expand Down Expand Up @@ -200,7 +199,6 @@ const {
validateBoolean,
validateInteger,
validateObject,
validateString,
} = require('internal/validators');

const emit = EventEmitter.prototype.emit;
Expand Down Expand Up @@ -297,30 +295,18 @@ function onSessionClose(code, family, silent, statelessReset) {
this[owner_symbol][kDestroy](code, family, silent, statelessReset);
}

// Used only within the onSessionClientHello function. Invoked
// to complete the client hello process.
function clientHelloCallback(err, ...args) {
if (err) {
this[owner_symbol].destroy(err);
return;
}
try {
this.onClientHelloDone(...args);
} catch (err) {
this[owner_symbol].destroy(err);
}
}

// This callback is invoked at the start of the TLS handshake to provide
// some basic information about the ALPN, SNI, and Ciphers that are
// being requested. It is only called if the 'clientHello' event is
// listened for.
// being requested. It is only called if the 'clientHelloHandler' option is
// specified on listen().
function onSessionClientHello(alpn, servername, ciphers) {
this[owner_symbol][kClientHello](
alpn,
servername,
ciphers,
clientHelloCallback.bind(this));
this[owner_symbol][kClientHello](alpn, servername, ciphers)
.then((context) => {
if (context !== undefined && !context?.context)
throw new ERR_INVALID_ARG_TYPE('context', 'SecureContext', context);
this.onClientHelloDone(context?.context);
})
.catch((error) => this[owner_symbol].destroy(error));
}

// This callback is only ever invoked for QuicServerSession instances,
Expand All @@ -329,9 +315,7 @@ function onSessionClientHello(alpn, servername, ciphers) {
// TLS handshake to continue.
function onSessionCert(servername) {
this[owner_symbol][kHandleOcsp](servername)
.then(({ context, data }) => {
if (context !== undefined && !context?.context)
throw new ERR_INVALID_ARG_TYPE('context', 'SecureContext', context);
.then((data) => {
if (data !== undefined) {
if (typeof data === 'string')
data = Buffer.from(data);
Expand All @@ -342,7 +326,7 @@ function onSessionCert(servername) {
data);
}
}
this.onCertDone(context?.context, data);
this.onCertDone(data);
})
.catch((error) => this[owner_symbol].destroy(error));
}
Expand Down Expand Up @@ -919,6 +903,7 @@ class QuicSocket extends EventEmitter {
listenPromise: undefined,
lookup: undefined,
ocspHandler: undefined,
clientHelloHandler: undefined,
server: undefined,
serverSecureContext: undefined,
sessions: new Set(),
Expand Down Expand Up @@ -1010,15 +995,14 @@ class QuicSocket extends EventEmitter {
this.destroy(err);
}

// Returns the default QuicStream options for peer-initiated
// streams. These are passed on to new client and server
// QuicSession instances when they are created.
get [kStreamOptions]() {
const state = this[kInternalState];
return {
highWaterMark: state.highWaterMark,
defaultEncoding: state.defaultEncoding,
ocspHandler: state.ocspHandler,
clientHelloHandler: state.clientHelloHandler,
context: state.serverSecureContext,
};
}

Expand Down Expand Up @@ -1212,11 +1196,10 @@ class QuicSocket extends EventEmitter {
defaultEncoding,
highWaterMark,
ocspHandler,
clientHelloHandler,
transportParams,
} = validateQuicSocketListenOptions(options);

// Store the secure context so that it is not garbage collected
// while we still need to make use of it.
state.serverSecureContext =
createSecureContext({
...options,
Expand All @@ -1228,6 +1211,7 @@ class QuicSocket extends EventEmitter {
state.alpn = alpn;
state.listenPending = true;
state.ocspHandler = ocspHandler;
state.clientHelloHandler = clientHelloHandler;

await this[kMaybeBind]();

Expand Down Expand Up @@ -1484,9 +1468,6 @@ class QuicSocket extends EventEmitter {
return Array.from(this[kInternalState].endpoints);
}

// The sever secure context is the SecureContext specified when calling
// listen. It is the context that will be used with all new server
// QuicSession instances.
get serverSecureContext() {
return this[kInternalState].serverSecureContext;
}
Expand Down Expand Up @@ -1639,6 +1620,7 @@ class QuicSession extends EventEmitter {
alpn: undefined,
cipher: undefined,
cipherVersion: undefined,
clientHelloHandler: undefined,
closeCode: NGTCP2_NO_ERROR,
closeFamily: QUIC_ERROR_APPLICATION,
closePromise: undefined,
Expand Down Expand Up @@ -1676,6 +1658,7 @@ class QuicSession extends EventEmitter {
highWaterMark,
defaultEncoding,
ocspHandler,
clientHelloHandler,
} = options;
super({ captureRejections: true });
this.on('newListener', onNewListener);
Expand All @@ -1687,6 +1670,7 @@ class QuicSession extends EventEmitter {
state.highWaterMark = highWaterMark;
state.defaultEncoding = defaultEncoding;
state.ocspHandler = ocspHandler;
state.clientHelloHandler = clientHelloHandler;
socket[kAddSession](this);
}

Expand Down Expand Up @@ -1751,6 +1735,7 @@ class QuicSession extends EventEmitter {
state.handshakeAckHistogram = new Histogram(handle.ack);
state.handshakeContinuationHistogram = new Histogram(handle.rate);
state.state.ocspEnabled = state.ocspHandler !== undefined;
state.state.clientHelloEnabled = state.clientHelloHandler !== undefined;
if (handle.qlogStream !== undefined) {
this[kSetQLogStream](handle.qlogStream);
handle.qlogStream = undefined;
Expand Down Expand Up @@ -2270,59 +2255,47 @@ class QuicSession extends EventEmitter {
}
class QuicServerSession extends QuicSession {
[kInternalServerState] = {
contexts: []
context: undefined
};

constructor(socket, handle, options) {
const {
highWaterMark,
defaultEncoding,
ocspHandler,
clientHelloHandler,
context,
} = options;
super(socket, { highWaterMark, defaultEncoding, ocspHandler });
super(socket, {
highWaterMark,
defaultEncoding,
ocspHandler,
clientHelloHandler
});
this[kInternalServerState].context = context;
this[kSetHandle](handle);
}

// Called only when a clientHello event handler is registered.
// Allows user code an opportunity to interject into the start
// of the TLS handshake.
[kClientHello](alpn, servername, ciphers, callback) {
this.emit(
'clientHello',
alpn,
servername,
ciphers,
callback.bind(this[kHandle]));
async [kClientHello](alpn, servername, ciphers) {
const internalState = this[kInternalState];
return internalState.clientHelloHandler?.(alpn, servername, ciphers);
}

async [kHandleOcsp](servername) {
const internalState = this[kInternalState];
const state = this[kInternalServerState];
const { context } = this.socket.serverSecureContext;

return internalState.ocspHandler?.(
'request',
{
servername,
context,
contexts: Array.from(state.contexts)
});
const { context } = this[kInternalServerState];
if (!internalState.ocspHandler || !context) return undefined;
return internalState.ocspHandler('request', {
servername,
certificate: context.context.getCertificate(),
issuer: context.context.getIssuer()
});
}

get allowEarlyData() { return false; }

addContext(servername, context = {}) {
validateString(servername, 'servername');
validateObject(context, 'context');

// TODO(@jasnell): Consider unrolling regex
const re = new RegExp('^' +
servername.replace(/([.^$+?\-\\[\]{}])/g, '\\$1')
.replace(/\*/g, '[^.]*') +
'$');
this[kInternalServerState].contexts.push(
[re, _createSecureContext(context)]);
}
}

class QuicClientSession extends QuicSession {
Expand Down
14 changes: 10 additions & 4 deletions lib/internal/quic/util.js
Expand Up @@ -612,6 +612,7 @@ function validateQuicSocketListenOptions(options = {}) {
defaultEncoding,
highWaterMark,
ocspHandler,
clientHelloHandler,
requestCert,
rejectUnauthorized,
lookup,
Expand All @@ -626,9 +627,16 @@ function validateQuicSocketListenOptions(options = {}) {
if (ocspHandler !== undefined && typeof ocspHandler !== 'function') {
throw new ERR_INVALID_ARG_TYPE(
'options.ocspHandler',
'functon',
'function',
ocspHandler);
}
if (clientHelloHandler !== undefined &&
typeof clientHelloHandler !== 'function') {
throw new ERR_INVALID_ARG_TYPE(
'options.clientHelloHandler',
'function',
clientHelloHandler);
}
const transportParams =
validateTransportParams(
options,
Expand All @@ -639,6 +647,7 @@ function validateQuicSocketListenOptions(options = {}) {
alpn,
lookup,
ocspHandler,
clientHelloHandler,
rejectUnauthorized,
requestCert,
transportParams,
Expand Down Expand Up @@ -812,9 +821,6 @@ function toggleListeners(state, event, on) {
case 'keylog':
state.keylogEnabled = on;
break;
case 'clientHello':
state.clientHelloEnabled = on;
break;
case 'pathValidation':
state.pathValidatedEnabled = on;
break;
Expand Down