mirror of
https://github.com/zebrajr/node.git
synced 2026-01-15 12:15:26 +00:00
tls: add ALPNCallback server option for dynamic ALPN negotiation
PR-URL: https://github.com/nodejs/node/pull/45190 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Debadree Chatterjee <debadree333@gmail.com>
This commit is contained in:
@@ -2746,6 +2746,20 @@ This error represents a failed test. Additional information about the failure
|
||||
is available via the `cause` property. The `failureType` property specifies
|
||||
what the test was doing when the failure occurred.
|
||||
|
||||
<a id="ERR_TLS_ALPN_CALLBACK_INVALID_RESULT"></a>
|
||||
|
||||
### `ERR_TLS_ALPN_CALLBACK_INVALID_RESULT`
|
||||
|
||||
This error is thrown when an `ALPNCallback` returns a value that is not in the
|
||||
list of ALPN protocols offered by the client.
|
||||
|
||||
<a id="ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS"></a>
|
||||
|
||||
### `ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS`
|
||||
|
||||
This error is thrown when creating a `TLSServer` if the TLS options include
|
||||
both `ALPNProtocols` and `ALPNCallback`. These options are mutually exclusive.
|
||||
|
||||
<a id="ERR_TLS_CERT_ALTNAME_FORMAT"></a>
|
||||
|
||||
### `ERR_TLS_CERT_ALTNAME_FORMAT`
|
||||
|
||||
@@ -2049,6 +2049,9 @@ where `secureSocket` has the same API as `pair.cleartext`.
|
||||
<!-- YAML
|
||||
added: v0.3.2
|
||||
changes:
|
||||
- version: REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/45190
|
||||
description: The `options` parameter can now include `ALPNCallback`.
|
||||
- version: v19.0.0
|
||||
pr-url: https://github.com/nodejs/node/pull/44031
|
||||
description: If `ALPNProtocols` is set, incoming connections that send an
|
||||
@@ -2079,6 +2082,17 @@ changes:
|
||||
e.g. `0x05hello0x05world`, where the first byte is the length of the next
|
||||
protocol name. Passing an array is usually much simpler, e.g.
|
||||
`['hello', 'world']`. (Protocols should be ordered by their priority.)
|
||||
* `ALPNCallback`: {Function} If set, this will be called when a
|
||||
client opens a connection using the ALPN extension. One argument will
|
||||
be passed to the callback: an object containing `servername` and
|
||||
`protocols` fields, respectively containing the server name from
|
||||
the SNI extension (if any) and an array of ALPN protocol name strings. The
|
||||
callback must return either one of the strings listed in
|
||||
`protocols`, which will be returned to the client as the selected
|
||||
ALPN protocol, or `undefined`, to reject the connection with a fatal alert.
|
||||
If a string is returned that does not match one of the client's ALPN
|
||||
protocols, an error will be thrown. This option cannot be used with the
|
||||
`ALPNProtocols` option, and setting both options will throw an error.
|
||||
* `clientCertEngine` {string} Name of an OpenSSL engine which can provide the
|
||||
client certificate.
|
||||
* `enableTrace` {boolean} If `true`, [`tls.TLSSocket.enableTrace()`][] will be
|
||||
|
||||
@@ -72,6 +72,8 @@ const {
|
||||
ERR_INVALID_ARG_VALUE,
|
||||
ERR_MULTIPLE_CALLBACK,
|
||||
ERR_SOCKET_CLOSED,
|
||||
ERR_TLS_ALPN_CALLBACK_INVALID_RESULT,
|
||||
ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS,
|
||||
ERR_TLS_DH_PARAM_SIZE,
|
||||
ERR_TLS_HANDSHAKE_TIMEOUT,
|
||||
ERR_TLS_INVALID_CONTEXT,
|
||||
@@ -108,6 +110,7 @@ const kErrorEmitted = Symbol('error-emitted');
|
||||
const kHandshakeTimeout = Symbol('handshake-timeout');
|
||||
const kRes = Symbol('res');
|
||||
const kSNICallback = Symbol('snicallback');
|
||||
const kALPNCallback = Symbol('alpncallback');
|
||||
const kEnableTrace = Symbol('enableTrace');
|
||||
const kPskCallback = Symbol('pskcallback');
|
||||
const kPskIdentityHint = Symbol('pskidentityhint');
|
||||
@@ -234,6 +237,45 @@ function loadSNI(info) {
|
||||
}
|
||||
|
||||
|
||||
function callALPNCallback(protocolsBuffer) {
|
||||
const handle = this;
|
||||
const socket = handle[owner_symbol];
|
||||
|
||||
const servername = handle.getServername();
|
||||
|
||||
// Collect all the protocols from the given buffer:
|
||||
const protocols = [];
|
||||
let offset = 0;
|
||||
while (offset < protocolsBuffer.length) {
|
||||
const protocolLen = protocolsBuffer[offset];
|
||||
offset += 1;
|
||||
|
||||
const protocol = protocolsBuffer.slice(offset, offset + protocolLen);
|
||||
offset += protocolLen;
|
||||
|
||||
protocols.push(protocol.toString('ascii'));
|
||||
}
|
||||
|
||||
const selectedProtocol = socket[kALPNCallback]({
|
||||
servername,
|
||||
protocols,
|
||||
});
|
||||
|
||||
// Undefined -> all proposed protocols rejected
|
||||
if (selectedProtocol === undefined) return undefined;
|
||||
|
||||
const protocolIndex = protocols.indexOf(selectedProtocol);
|
||||
if (protocolIndex === -1) {
|
||||
throw new ERR_TLS_ALPN_CALLBACK_INVALID_RESULT(selectedProtocol, protocols);
|
||||
}
|
||||
let protocolOffset = 0;
|
||||
for (let i = 0; i < protocolIndex; i++) {
|
||||
protocolOffset += 1 + protocols[i].length;
|
||||
}
|
||||
|
||||
return protocolOffset;
|
||||
}
|
||||
|
||||
function requestOCSP(socket, info) {
|
||||
if (!info.OCSPRequest || !socket.server)
|
||||
return requestOCSPDone(socket);
|
||||
@@ -493,6 +535,7 @@ function TLSSocket(socket, opts) {
|
||||
this._controlReleased = false;
|
||||
this.secureConnecting = true;
|
||||
this._SNICallback = null;
|
||||
this[kALPNCallback] = null;
|
||||
this.servername = null;
|
||||
this.alpnProtocol = null;
|
||||
this.authorized = false;
|
||||
@@ -755,6 +798,16 @@ TLSSocket.prototype._init = function(socket, wrap) {
|
||||
ssl.lastHandshakeTime = 0;
|
||||
ssl.handshakes = 0;
|
||||
|
||||
if (options.ALPNCallback) {
|
||||
if (typeof options.ALPNCallback !== 'function') {
|
||||
throw new ERR_INVALID_ARG_TYPE('options.ALPNCallback', 'Function', options.ALPNCallback);
|
||||
}
|
||||
assert(typeof options.ALPNCallback === 'function');
|
||||
this[kALPNCallback] = options.ALPNCallback;
|
||||
ssl.ALPNCallback = callALPNCallback;
|
||||
ssl.enableALPNCb();
|
||||
}
|
||||
|
||||
if (this.server) {
|
||||
if (this.server.listenerCount('resumeSession') > 0 ||
|
||||
this.server.listenerCount('newSession') > 0) {
|
||||
@@ -1133,6 +1186,7 @@ function tlsConnectionListener(rawSocket) {
|
||||
rejectUnauthorized: this.rejectUnauthorized,
|
||||
handshakeTimeout: this[kHandshakeTimeout],
|
||||
ALPNProtocols: this.ALPNProtocols,
|
||||
ALPNCallback: this.ALPNCallback,
|
||||
SNICallback: this[kSNICallback] || SNICallback,
|
||||
enableTrace: this[kEnableTrace],
|
||||
pauseOnConnect: this.pauseOnConnect,
|
||||
@@ -1232,6 +1286,11 @@ function Server(options, listener) {
|
||||
this.requestCert = options.requestCert === true;
|
||||
this.rejectUnauthorized = options.rejectUnauthorized !== false;
|
||||
|
||||
this.ALPNCallback = options.ALPNCallback;
|
||||
if (this.ALPNCallback && options.ALPNProtocols) {
|
||||
throw new ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS();
|
||||
}
|
||||
|
||||
if (options.sessionTimeout)
|
||||
this.sessionTimeout = options.sessionTimeout;
|
||||
|
||||
|
||||
@@ -1628,6 +1628,16 @@ E('ERR_TEST_FAILURE', function(error, failureType) {
|
||||
this.cause = error;
|
||||
return msg;
|
||||
}, Error);
|
||||
E('ERR_TLS_ALPN_CALLBACK_INVALID_RESULT', (value, protocols) => {
|
||||
return `ALPN callback returned a value (${
|
||||
value
|
||||
}) that did not match any of the client's offered protocols (${
|
||||
protocols.join(', ')
|
||||
})`;
|
||||
}, TypeError);
|
||||
E('ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS',
|
||||
'The ALPNCallback and ALPNProtocols TLS options are mutually exclusive',
|
||||
TypeError);
|
||||
E('ERR_TLS_CERT_ALTNAME_FORMAT', 'Invalid subject alternative name string',
|
||||
SyntaxError);
|
||||
E('ERR_TLS_CERT_ALTNAME_INVALID', function(reason, host, cert) {
|
||||
|
||||
@@ -224,6 +224,44 @@ int SelectALPNCallback(
|
||||
unsigned int inlen,
|
||||
void* arg) {
|
||||
TLSWrap* w = static_cast<TLSWrap*>(arg);
|
||||
if (w->alpn_callback_enabled_) {
|
||||
Environment* env = w->env();
|
||||
HandleScope handle_scope(env->isolate());
|
||||
|
||||
Local<Value> callback_arg =
|
||||
Buffer::Copy(env, reinterpret_cast<const char*>(in), inlen)
|
||||
.ToLocalChecked();
|
||||
|
||||
MaybeLocal<Value> maybe_callback_result =
|
||||
w->MakeCallback(env->alpn_callback_string(), 1, &callback_arg);
|
||||
|
||||
if (UNLIKELY(maybe_callback_result.IsEmpty())) {
|
||||
// Implies the callback didn't return, because some exception was thrown
|
||||
// during processing, e.g. if callback returned an invalid ALPN value.
|
||||
return SSL_TLSEXT_ERR_ALERT_FATAL;
|
||||
}
|
||||
|
||||
Local<Value> callback_result = maybe_callback_result.ToLocalChecked();
|
||||
|
||||
if (callback_result->IsUndefined()) {
|
||||
// If you set an ALPN callback, but you return undefined for an ALPN
|
||||
// request, you're rejecting all proposed ALPN protocols, and so we send
|
||||
// a fatal alert:
|
||||
return SSL_TLSEXT_ERR_ALERT_FATAL;
|
||||
}
|
||||
|
||||
CHECK(callback_result->IsNumber());
|
||||
unsigned int result_int = callback_result.As<v8::Number>()->Value();
|
||||
|
||||
// The callback returns an offset into the given buffer, for the selected
|
||||
// protocol that should be returned. We then set outlen & out to point
|
||||
// to the selected input length & value directly:
|
||||
*outlen = *(in + result_int);
|
||||
*out = (in + result_int + 1);
|
||||
|
||||
return SSL_TLSEXT_ERR_OK;
|
||||
}
|
||||
|
||||
const std::vector<unsigned char>& alpn_protos = w->alpn_protos_;
|
||||
|
||||
if (alpn_protos.empty()) return SSL_TLSEXT_ERR_NOACK;
|
||||
@@ -1224,6 +1262,15 @@ void TLSWrap::OnClientHelloParseEnd(void* arg) {
|
||||
c->Cycle();
|
||||
}
|
||||
|
||||
void TLSWrap::EnableALPNCb(const FunctionCallbackInfo<Value>& args) {
|
||||
TLSWrap* wrap;
|
||||
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
|
||||
wrap->alpn_callback_enabled_ = true;
|
||||
|
||||
SSL* ssl = wrap->ssl_.get();
|
||||
SSL_CTX_set_alpn_select_cb(SSL_get_SSL_CTX(ssl), SelectALPNCallback, wrap);
|
||||
}
|
||||
|
||||
void TLSWrap::GetServername(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
|
||||
@@ -2034,6 +2081,7 @@ void TLSWrap::Initialize(
|
||||
SetProtoMethod(isolate, t, "certCbDone", CertCbDone);
|
||||
SetProtoMethod(isolate, t, "destroySSL", DestroySSL);
|
||||
SetProtoMethod(isolate, t, "enableCertCb", EnableCertCb);
|
||||
SetProtoMethod(isolate, t, "enableALPNCb", EnableALPNCb);
|
||||
SetProtoMethod(isolate, t, "endParser", EndParser);
|
||||
SetProtoMethod(isolate, t, "enableKeylogCallback", EnableKeylogCallback);
|
||||
SetProtoMethod(isolate, t, "enableSessionCallbacks", EnableSessionCallbacks);
|
||||
@@ -2099,6 +2147,7 @@ void TLSWrap::RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
||||
registry->Register(CertCbDone);
|
||||
registry->Register(DestroySSL);
|
||||
registry->Register(EnableCertCb);
|
||||
registry->Register(EnableALPNCb);
|
||||
registry->Register(EndParser);
|
||||
registry->Register(EnableKeylogCallback);
|
||||
registry->Register(EnableSessionCallbacks);
|
||||
|
||||
@@ -172,6 +172,7 @@ class TLSWrap : public AsyncWrap,
|
||||
static void CertCbDone(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void DestroySSL(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void EnableCertCb(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void EnableALPNCb(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void EnableKeylogCallback(
|
||||
const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void EnableSessionCallbacks(
|
||||
@@ -285,6 +286,7 @@ class TLSWrap : public AsyncWrap,
|
||||
|
||||
public:
|
||||
std::vector<unsigned char> alpn_protos_; // Accessed by SelectALPNCallback.
|
||||
bool alpn_callback_enabled_ = false; // Accessed by SelectALPNCallback.
|
||||
};
|
||||
|
||||
} // namespace crypto
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
V(ack_string, "ack") \
|
||||
V(address_string, "address") \
|
||||
V(aliases_string, "aliases") \
|
||||
V(alpn_callback_string, "ALPNCallback") \
|
||||
V(args_string, "args") \
|
||||
V(asn1curve_string, "asn1Curve") \
|
||||
V(async_ids_stack_string, "async_ids_stack") \
|
||||
|
||||
@@ -41,9 +41,8 @@ function runTest(clientsOptions, serverOptions, cb) {
|
||||
opt.rejectUnauthorized = false;
|
||||
|
||||
results[clientIndex] = {};
|
||||
const client = tls.connect(opt, function() {
|
||||
results[clientIndex].client = { ALPN: client.alpnProtocol };
|
||||
client.end();
|
||||
|
||||
function startNextClient() {
|
||||
if (options.length) {
|
||||
clientIndex++;
|
||||
connectClient(options);
|
||||
@@ -53,6 +52,15 @@ function runTest(clientsOptions, serverOptions, cb) {
|
||||
cb(results);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const client = tls.connect(opt, function() {
|
||||
results[clientIndex].client = { ALPN: client.alpnProtocol };
|
||||
client.end();
|
||||
startNextClient();
|
||||
}).on('error', function(err) {
|
||||
results[clientIndex].client = { error: err };
|
||||
startNextClient();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -200,12 +208,73 @@ function TestFatalAlert() {
|
||||
.on('close', common.mustCall(() => {
|
||||
assert.match(stderr, /SSL alert number 120/);
|
||||
server.close();
|
||||
TestALPNCallback();
|
||||
}));
|
||||
} else {
|
||||
server.close();
|
||||
TestALPNCallback();
|
||||
}
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
function TestALPNCallback() {
|
||||
// Server always selects the client's 2nd preference:
|
||||
const serverOptions = {
|
||||
ALPNCallback: common.mustCall(({ protocols }) => {
|
||||
return protocols[1];
|
||||
}, 2)
|
||||
};
|
||||
|
||||
const clientsOptions = [{
|
||||
ALPNProtocols: ['a', 'b', 'c'],
|
||||
}, {
|
||||
ALPNProtocols: ['a'],
|
||||
}];
|
||||
|
||||
runTest(clientsOptions, serverOptions, function(results) {
|
||||
// Callback picks 2nd preference => picks 'b'
|
||||
checkResults(results[0],
|
||||
{ server: { ALPN: 'b' },
|
||||
client: { ALPN: 'b' } });
|
||||
|
||||
// Callback picks 2nd preference => undefined => ALPN rejected:
|
||||
assert.strictEqual(results[1].server, undefined);
|
||||
assert.strictEqual(results[1].client.error.code, 'ECONNRESET');
|
||||
|
||||
TestBadALPNCallback();
|
||||
});
|
||||
}
|
||||
|
||||
function TestBadALPNCallback() {
|
||||
// Server always returns a fixed invalid value:
|
||||
const serverOptions = {
|
||||
ALPNCallback: common.mustCall(() => 'http/5')
|
||||
};
|
||||
|
||||
const clientsOptions = [{
|
||||
ALPNProtocols: ['http/1', 'h2'],
|
||||
}];
|
||||
|
||||
process.once('uncaughtException', common.mustCall((error) => {
|
||||
assert.strictEqual(error.code, 'ERR_TLS_ALPN_CALLBACK_INVALID_RESULT');
|
||||
}));
|
||||
|
||||
runTest(clientsOptions, serverOptions, function(results) {
|
||||
// Callback returns 'http/5' => doesn't match client ALPN => error & reset
|
||||
assert.strictEqual(results[0].server, undefined);
|
||||
assert.strictEqual(results[0].client.error.code, 'ECONNRESET');
|
||||
|
||||
TestALPNOptionsCallback();
|
||||
});
|
||||
}
|
||||
|
||||
function TestALPNOptionsCallback() {
|
||||
// Server sets two incompatible ALPN options:
|
||||
assert.throws(() => tls.createServer({
|
||||
ALPNCallback: () => 'a',
|
||||
ALPNProtocols: ['b', 'c']
|
||||
}), (error) => error.code === 'ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS');
|
||||
}
|
||||
|
||||
Test1();
|
||||
|
||||
Reference in New Issue
Block a user