Skip to content

Commit 87bd47a

Browse files
pimterryCeres6
authored andcommitted
tls: add ALPNCallback server option for dynamic ALPN negotiation
PR-URL: nodejs#45190 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Debadree Chatterjee <[email protected]>
1 parent 3966b4f commit 87bd47a

File tree

8 files changed

+221
-3
lines changed

8 files changed

+221
-3
lines changed

doc/api/errors.md

+14
Original file line numberDiff line numberDiff line change
@@ -2746,6 +2746,20 @@ This error represents a failed test. Additional information about the failure
27462746
is available via the `cause` property. The `failureType` property specifies
27472747
what the test was doing when the failure occurred.
27482748

2749+
<a id="ERR_TLS_ALPN_CALLBACK_INVALID_RESULT"></a>
2750+
2751+
### `ERR_TLS_ALPN_CALLBACK_INVALID_RESULT`
2752+
2753+
This error is thrown when an `ALPNCallback` returns a value that is not in the
2754+
list of ALPN protocols offered by the client.
2755+
2756+
<a id="ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS"></a>
2757+
2758+
### `ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS`
2759+
2760+
This error is thrown when creating a `TLSServer` if the TLS options include
2761+
both `ALPNProtocols` and `ALPNCallback`. These options are mutually exclusive.
2762+
27492763
<a id="ERR_TLS_CERT_ALTNAME_FORMAT"></a>
27502764

27512765
### `ERR_TLS_CERT_ALTNAME_FORMAT`

doc/api/tls.md

+14
Original file line numberDiff line numberDiff line change
@@ -2049,6 +2049,9 @@ where `secureSocket` has the same API as `pair.cleartext`.
20492049
<!-- YAML
20502050
added: v0.3.2
20512051
changes:
2052+
- version: REPLACEME
2053+
pr-url: https://github.com/nodejs/node/pull/45190
2054+
description: The `options` parameter can now include `ALPNCallback`.
20522055
- version: v19.0.0
20532056
pr-url: https://github.com/nodejs/node/pull/44031
20542057
description: If `ALPNProtocols` is set, incoming connections that send an
@@ -2079,6 +2082,17 @@ changes:
20792082
e.g. `0x05hello0x05world`, where the first byte is the length of the next
20802083
protocol name. Passing an array is usually much simpler, e.g.
20812084
`['hello', 'world']`. (Protocols should be ordered by their priority.)
2085+
* `ALPNCallback`: {Function} If set, this will be called when a
2086+
client opens a connection using the ALPN extension. One argument will
2087+
be passed to the callback: an object containing `servername` and
2088+
`protocols` fields, respectively containing the server name from
2089+
the SNI extension (if any) and an array of ALPN protocol name strings. The
2090+
callback must return either one of the strings listed in
2091+
`protocols`, which will be returned to the client as the selected
2092+
ALPN protocol, or `undefined`, to reject the connection with a fatal alert.
2093+
If a string is returned that does not match one of the client's ALPN
2094+
protocols, an error will be thrown. This option cannot be used with the
2095+
`ALPNProtocols` option, and setting both options will throw an error.
20822096
* `clientCertEngine` {string} Name of an OpenSSL engine which can provide the
20832097
client certificate.
20842098
* `enableTrace` {boolean} If `true`, [`tls.TLSSocket.enableTrace()`][] will be

lib/_tls_wrap.js

+59
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ const {
7272
ERR_INVALID_ARG_VALUE,
7373
ERR_MULTIPLE_CALLBACK,
7474
ERR_SOCKET_CLOSED,
75+
ERR_TLS_ALPN_CALLBACK_INVALID_RESULT,
76+
ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS,
7577
ERR_TLS_DH_PARAM_SIZE,
7678
ERR_TLS_HANDSHAKE_TIMEOUT,
7779
ERR_TLS_INVALID_CONTEXT,
@@ -108,6 +110,7 @@ const kErrorEmitted = Symbol('error-emitted');
108110
const kHandshakeTimeout = Symbol('handshake-timeout');
109111
const kRes = Symbol('res');
110112
const kSNICallback = Symbol('snicallback');
113+
const kALPNCallback = Symbol('alpncallback');
111114
const kEnableTrace = Symbol('enableTrace');
112115
const kPskCallback = Symbol('pskcallback');
113116
const kPskIdentityHint = Symbol('pskidentityhint');
@@ -234,6 +237,45 @@ function loadSNI(info) {
234237
}
235238

236239

240+
function callALPNCallback(protocolsBuffer) {
241+
const handle = this;
242+
const socket = handle[owner_symbol];
243+
244+
const servername = handle.getServername();
245+
246+
// Collect all the protocols from the given buffer:
247+
const protocols = [];
248+
let offset = 0;
249+
while (offset < protocolsBuffer.length) {
250+
const protocolLen = protocolsBuffer[offset];
251+
offset += 1;
252+
253+
const protocol = protocolsBuffer.slice(offset, offset + protocolLen);
254+
offset += protocolLen;
255+
256+
protocols.push(protocol.toString('ascii'));
257+
}
258+
259+
const selectedProtocol = socket[kALPNCallback]({
260+
servername,
261+
protocols,
262+
});
263+
264+
// Undefined -> all proposed protocols rejected
265+
if (selectedProtocol === undefined) return undefined;
266+
267+
const protocolIndex = protocols.indexOf(selectedProtocol);
268+
if (protocolIndex === -1) {
269+
throw new ERR_TLS_ALPN_CALLBACK_INVALID_RESULT(selectedProtocol, protocols);
270+
}
271+
let protocolOffset = 0;
272+
for (let i = 0; i < protocolIndex; i++) {
273+
protocolOffset += 1 + protocols[i].length;
274+
}
275+
276+
return protocolOffset;
277+
}
278+
237279
function requestOCSP(socket, info) {
238280
if (!info.OCSPRequest || !socket.server)
239281
return requestOCSPDone(socket);
@@ -493,6 +535,7 @@ function TLSSocket(socket, opts) {
493535
this._controlReleased = false;
494536
this.secureConnecting = true;
495537
this._SNICallback = null;
538+
this[kALPNCallback] = null;
496539
this.servername = null;
497540
this.alpnProtocol = null;
498541
this.authorized = false;
@@ -755,6 +798,16 @@ TLSSocket.prototype._init = function(socket, wrap) {
755798
ssl.lastHandshakeTime = 0;
756799
ssl.handshakes = 0;
757800

801+
if (options.ALPNCallback) {
802+
if (typeof options.ALPNCallback !== 'function') {
803+
throw new ERR_INVALID_ARG_TYPE('options.ALPNCallback', 'Function', options.ALPNCallback);
804+
}
805+
assert(typeof options.ALPNCallback === 'function');
806+
this[kALPNCallback] = options.ALPNCallback;
807+
ssl.ALPNCallback = callALPNCallback;
808+
ssl.enableALPNCb();
809+
}
810+
758811
if (this.server) {
759812
if (this.server.listenerCount('resumeSession') > 0 ||
760813
this.server.listenerCount('newSession') > 0) {
@@ -1133,6 +1186,7 @@ function tlsConnectionListener(rawSocket) {
11331186
rejectUnauthorized: this.rejectUnauthorized,
11341187
handshakeTimeout: this[kHandshakeTimeout],
11351188
ALPNProtocols: this.ALPNProtocols,
1189+
ALPNCallback: this.ALPNCallback,
11361190
SNICallback: this[kSNICallback] || SNICallback,
11371191
enableTrace: this[kEnableTrace],
11381192
pauseOnConnect: this.pauseOnConnect,
@@ -1232,6 +1286,11 @@ function Server(options, listener) {
12321286
this.requestCert = options.requestCert === true;
12331287
this.rejectUnauthorized = options.rejectUnauthorized !== false;
12341288

1289+
this.ALPNCallback = options.ALPNCallback;
1290+
if (this.ALPNCallback && options.ALPNProtocols) {
1291+
throw new ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS();
1292+
}
1293+
12351294
if (options.sessionTimeout)
12361295
this.sessionTimeout = options.sessionTimeout;
12371296

lib/internal/errors.js

+10
Original file line numberDiff line numberDiff line change
@@ -1628,6 +1628,16 @@ E('ERR_TEST_FAILURE', function(error, failureType) {
16281628
this.cause = error;
16291629
return msg;
16301630
}, Error);
1631+
E('ERR_TLS_ALPN_CALLBACK_INVALID_RESULT', (value, protocols) => {
1632+
return `ALPN callback returned a value (${
1633+
value
1634+
}) that did not match any of the client's offered protocols (${
1635+
protocols.join(', ')
1636+
})`;
1637+
}, TypeError);
1638+
E('ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS',
1639+
'The ALPNCallback and ALPNProtocols TLS options are mutually exclusive',
1640+
TypeError);
16311641
E('ERR_TLS_CERT_ALTNAME_FORMAT', 'Invalid subject alternative name string',
16321642
SyntaxError);
16331643
E('ERR_TLS_CERT_ALTNAME_INVALID', function(reason, host, cert) {

src/crypto/crypto_tls.cc

+49
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,44 @@ int SelectALPNCallback(
224224
unsigned int inlen,
225225
void* arg) {
226226
TLSWrap* w = static_cast<TLSWrap*>(arg);
227+
if (w->alpn_callback_enabled_) {
228+
Environment* env = w->env();
229+
HandleScope handle_scope(env->isolate());
230+
231+
Local<Value> callback_arg =
232+
Buffer::Copy(env, reinterpret_cast<const char*>(in), inlen)
233+
.ToLocalChecked();
234+
235+
MaybeLocal<Value> maybe_callback_result =
236+
w->MakeCallback(env->alpn_callback_string(), 1, &callback_arg);
237+
238+
if (UNLIKELY(maybe_callback_result.IsEmpty())) {
239+
// Implies the callback didn't return, because some exception was thrown
240+
// during processing, e.g. if callback returned an invalid ALPN value.
241+
return SSL_TLSEXT_ERR_ALERT_FATAL;
242+
}
243+
244+
Local<Value> callback_result = maybe_callback_result.ToLocalChecked();
245+
246+
if (callback_result->IsUndefined()) {
247+
// If you set an ALPN callback, but you return undefined for an ALPN
248+
// request, you're rejecting all proposed ALPN protocols, and so we send
249+
// a fatal alert:
250+
return SSL_TLSEXT_ERR_ALERT_FATAL;
251+
}
252+
253+
CHECK(callback_result->IsNumber());
254+
unsigned int result_int = callback_result.As<v8::Number>()->Value();
255+
256+
// The callback returns an offset into the given buffer, for the selected
257+
// protocol that should be returned. We then set outlen & out to point
258+
// to the selected input length & value directly:
259+
*outlen = *(in + result_int);
260+
*out = (in + result_int + 1);
261+
262+
return SSL_TLSEXT_ERR_OK;
263+
}
264+
227265
const std::vector<unsigned char>& alpn_protos = w->alpn_protos_;
228266

229267
if (alpn_protos.empty()) return SSL_TLSEXT_ERR_NOACK;
@@ -1224,6 +1262,15 @@ void TLSWrap::OnClientHelloParseEnd(void* arg) {
12241262
c->Cycle();
12251263
}
12261264

1265+
void TLSWrap::EnableALPNCb(const FunctionCallbackInfo<Value>& args) {
1266+
TLSWrap* wrap;
1267+
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
1268+
wrap->alpn_callback_enabled_ = true;
1269+
1270+
SSL* ssl = wrap->ssl_.get();
1271+
SSL_CTX_set_alpn_select_cb(SSL_get_SSL_CTX(ssl), SelectALPNCallback, wrap);
1272+
}
1273+
12271274
void TLSWrap::GetServername(const FunctionCallbackInfo<Value>& args) {
12281275
Environment* env = Environment::GetCurrent(args);
12291276

@@ -2034,6 +2081,7 @@ void TLSWrap::Initialize(
20342081
SetProtoMethod(isolate, t, "certCbDone", CertCbDone);
20352082
SetProtoMethod(isolate, t, "destroySSL", DestroySSL);
20362083
SetProtoMethod(isolate, t, "enableCertCb", EnableCertCb);
2084+
SetProtoMethod(isolate, t, "enableALPNCb", EnableALPNCb);
20372085
SetProtoMethod(isolate, t, "endParser", EndParser);
20382086
SetProtoMethod(isolate, t, "enableKeylogCallback", EnableKeylogCallback);
20392087
SetProtoMethod(isolate, t, "enableSessionCallbacks", EnableSessionCallbacks);
@@ -2099,6 +2147,7 @@ void TLSWrap::RegisterExternalReferences(ExternalReferenceRegistry* registry) {
20992147
registry->Register(CertCbDone);
21002148
registry->Register(DestroySSL);
21012149
registry->Register(EnableCertCb);
2150+
registry->Register(EnableALPNCb);
21022151
registry->Register(EndParser);
21032152
registry->Register(EnableKeylogCallback);
21042153
registry->Register(EnableSessionCallbacks);

src/crypto/crypto_tls.h

+2
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ class TLSWrap : public AsyncWrap,
172172
static void CertCbDone(const v8::FunctionCallbackInfo<v8::Value>& args);
173173
static void DestroySSL(const v8::FunctionCallbackInfo<v8::Value>& args);
174174
static void EnableCertCb(const v8::FunctionCallbackInfo<v8::Value>& args);
175+
static void EnableALPNCb(const v8::FunctionCallbackInfo<v8::Value>& args);
175176
static void EnableKeylogCallback(
176177
const v8::FunctionCallbackInfo<v8::Value>& args);
177178
static void EnableSessionCallbacks(
@@ -285,6 +286,7 @@ class TLSWrap : public AsyncWrap,
285286

286287
public:
287288
std::vector<unsigned char> alpn_protos_; // Accessed by SelectALPNCallback.
289+
bool alpn_callback_enabled_ = false; // Accessed by SelectALPNCallback.
288290
};
289291

290292
} // namespace crypto

src/env_properties.h

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
V(ack_string, "ack") \
5151
V(address_string, "address") \
5252
V(aliases_string, "aliases") \
53+
V(alpn_callback_string, "ALPNCallback") \
5354
V(args_string, "args") \
5455
V(asn1curve_string, "asn1Curve") \
5556
V(async_ids_stack_string, "async_ids_stack") \

test/parallel/test-tls-alpn-server-client.js

+72-3
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,8 @@ function runTest(clientsOptions, serverOptions, cb) {
4141
opt.rejectUnauthorized = false;
4242

4343
results[clientIndex] = {};
44-
const client = tls.connect(opt, function() {
45-
results[clientIndex].client = { ALPN: client.alpnProtocol };
46-
client.end();
44+
45+
function startNextClient() {
4746
if (options.length) {
4847
clientIndex++;
4948
connectClient(options);
@@ -53,6 +52,15 @@ function runTest(clientsOptions, serverOptions, cb) {
5352
cb(results);
5453
});
5554
}
55+
}
56+
57+
const client = tls.connect(opt, function() {
58+
results[clientIndex].client = { ALPN: client.alpnProtocol };
59+
client.end();
60+
startNextClient();
61+
}).on('error', function(err) {
62+
results[clientIndex].client = { error: err };
63+
startNextClient();
5664
});
5765
}
5866

@@ -200,12 +208,73 @@ function TestFatalAlert() {
200208
.on('close', common.mustCall(() => {
201209
assert.match(stderr, /SSL alert number 120/);
202210
server.close();
211+
TestALPNCallback();
203212
}));
204213
} else {
205214
server.close();
215+
TestALPNCallback();
206216
}
207217
}));
208218
}));
209219
}
210220

221+
function TestALPNCallback() {
222+
// Server always selects the client's 2nd preference:
223+
const serverOptions = {
224+
ALPNCallback: common.mustCall(({ protocols }) => {
225+
return protocols[1];
226+
}, 2)
227+
};
228+
229+
const clientsOptions = [{
230+
ALPNProtocols: ['a', 'b', 'c'],
231+
}, {
232+
ALPNProtocols: ['a'],
233+
}];
234+
235+
runTest(clientsOptions, serverOptions, function(results) {
236+
// Callback picks 2nd preference => picks 'b'
237+
checkResults(results[0],
238+
{ server: { ALPN: 'b' },
239+
client: { ALPN: 'b' } });
240+
241+
// Callback picks 2nd preference => undefined => ALPN rejected:
242+
assert.strictEqual(results[1].server, undefined);
243+
assert.strictEqual(results[1].client.error.code, 'ECONNRESET');
244+
245+
TestBadALPNCallback();
246+
});
247+
}
248+
249+
function TestBadALPNCallback() {
250+
// Server always returns a fixed invalid value:
251+
const serverOptions = {
252+
ALPNCallback: common.mustCall(() => 'http/5')
253+
};
254+
255+
const clientsOptions = [{
256+
ALPNProtocols: ['http/1', 'h2'],
257+
}];
258+
259+
process.once('uncaughtException', common.mustCall((error) => {
260+
assert.strictEqual(error.code, 'ERR_TLS_ALPN_CALLBACK_INVALID_RESULT');
261+
}));
262+
263+
runTest(clientsOptions, serverOptions, function(results) {
264+
// Callback returns 'http/5' => doesn't match client ALPN => error & reset
265+
assert.strictEqual(results[0].server, undefined);
266+
assert.strictEqual(results[0].client.error.code, 'ECONNRESET');
267+
268+
TestALPNOptionsCallback();
269+
});
270+
}
271+
272+
function TestALPNOptionsCallback() {
273+
// Server sets two incompatible ALPN options:
274+
assert.throws(() => tls.createServer({
275+
ALPNCallback: () => 'a',
276+
ALPNProtocols: ['b', 'c']
277+
}), (error) => error.code === 'ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS');
278+
}
279+
211280
Test1();

0 commit comments

Comments
 (0)