GeezerVison is showing github.com — tls: add ALPNCallback server option for dynamic ALPN negotiation · nodejs/node@504d1d7
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/errors.md +14Lines changed: 14 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -2729,6 +2729,20 @@ This error represents a failed test. Additional information about the failure | 2729 | 2729 | is available via the `cause` property. The `failureType` property specifies | 2730 | 2730 | what the test was doing when the failure occurred. | 2731 | 2731 |
| | 2732 | +<a id="ERR_TLS_ALPN_CALLBACK_INVALID_RESULT"></a> | | 2733 | + | | 2734 | +### `ERR_TLS_ALPN_CALLBACK_INVALID_RESULT` | | 2735 | + | | 2736 | +This error is thrown when an `ALPNCallback` returns a value that is not in the | | 2737 | +list of ALPN protocols offered by the client. | | 2738 | + | | 2739 | +<a id="ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS"></a> | | 2740 | + | | 2741 | +### `ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS` | | 2742 | + | | 2743 | +This error is thrown when creating a `TLSServer` if the TLS options include | | 2744 | +both `ALPNProtocols` and `ALPNCallback`. These options are mutually exclusive. | | 2745 | + | 2732 | 2746 | <a id="ERR_TLS_CERT_ALTNAME_FORMAT"></a> | 2733 | 2747 |
| 2734 | 2748 | ### `ERR_TLS_CERT_ALTNAME_FORMAT` | |
Collapse file Copy file name to clipboardExpand all lines: doc/api/tls.md +14Lines changed: 14 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -2047,6 +2047,9 @@ where `secureSocket` has the same API as `pair.cleartext`. | 2047 | 2047 | <!-- YAML | 2048 | 2048 | added: v0.3.2 | 2049 | 2049 | changes: | | 2050 | + - version: REPLACEME | | 2051 | + pr-url: https://github.com/nodejs/node/pull/45190 | | 2052 | + description: The `options` parameter can now include `ALPNCallback`. | 2050 | 2053 | - version: v19.0.0 | 2051 | 2054 | pr-url: https://github.com/nodejs/node/pull/44031 | 2052 | 2055 | description: If `ALPNProtocols` is set, incoming connections that send an | @@ -2077,6 +2080,17 @@ changes: | 2077 | 2080 | e.g. `0x05hello0x05world`, where the first byte is the length of the next | 2078 | 2081 | protocol name. Passing an array is usually much simpler, e.g. | 2079 | 2082 | `['hello', 'world']`. (Protocols should be ordered by their priority.) | | 2083 | + * `ALPNCallback`: {Function} If set, this will be called when a | | 2084 | + client opens a connection using the ALPN extension. One argument will | | 2085 | + be passed to the callback: an object containing `servername` and | | 2086 | + `protocols` fields, respectively containing the server name from | | 2087 | + the SNI extension (if any) and an array of ALPN protocol name strings. The | | 2088 | + callback must return either one of the strings listed in | | 2089 | + `protocols`, which will be returned to the client as the selected | | 2090 | + ALPN protocol, or `undefined`, to reject the connection with a fatal alert. | | 2091 | + If a string is returned that does not match one of the client's ALPN | | 2092 | + protocols, an error will be thrown. This option cannot be used with the | | 2093 | + `ALPNProtocols` option, and setting both options will throw an error. | 2080 | 2094 | * `clientCertEngine` {string} Name of an OpenSSL engine which can provide the | 2081 | 2095 | client certificate. | 2082 | 2096 | * `enableTrace` {boolean} If `true`, [`tls.TLSSocket.enableTrace()`][] will be | |
Collapse file Copy file name to clipboardExpand all lines: lib/_tls_wrap.js +59Lines changed: 59 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -72,6 +72,8 @@ const { | 72 | 72 | ERR_INVALID_ARG_VALUE, | 73 | 73 | ERR_MULTIPLE_CALLBACK, | 74 | 74 | ERR_SOCKET_CLOSED, | | 75 | + ERR_TLS_ALPN_CALLBACK_INVALID_RESULT, | | 76 | + ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS, | 75 | 77 | ERR_TLS_DH_PARAM_SIZE, | 76 | 78 | ERR_TLS_HANDSHAKE_TIMEOUT, | 77 | 79 | ERR_TLS_INVALID_CONTEXT, | @@ -108,6 +110,7 @@ const kErrorEmitted = Symbol('error-emitted'); | 108 | 110 | const kHandshakeTimeout = Symbol('handshake-timeout'); | 109 | 111 | const kRes = Symbol('res'); | 110 | 112 | const kSNICallback = Symbol('snicallback'); | | 113 | +const kALPNCallback = Symbol('alpncallback'); | 111 | 114 | const kEnableTrace = Symbol('enableTrace'); | 112 | 115 | const kPskCallback = Symbol('pskcallback'); | 113 | 116 | const kPskIdentityHint = Symbol('pskidentityhint'); | @@ -234,6 +237,45 @@ function loadSNI(info) { | 234 | 237 | } | 235 | 238 |
| 236 | 239 |
| | 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 | + | 237 | 279 | function requestOCSP(socket, info) { | 238 | 280 | if (!info.OCSPRequest || !socket.server) | 239 | 281 | return requestOCSPDone(socket); | @@ -493,6 +535,7 @@ function TLSSocket(socket, opts) { | 493 | 535 | this._controlReleased = false; | 494 | 536 | this.secureConnecting = true; | 495 | 537 | this._SNICallback = null; | | 538 | + this[kALPNCallback] = null; | 496 | 539 | this.servername = null; | 497 | 540 | this.alpnProtocol = null; | 498 | 541 | this.authorized = false; | @@ -755,6 +798,16 @@ TLSSocket.prototype._init = function(socket, wrap) { | 755 | 798 | ssl.lastHandshakeTime = 0; | 756 | 799 | ssl.handshakes = 0; | 757 | 800 |
| | 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 | + | 758 | 811 | if (this.server) { | 759 | 812 | if (this.server.listenerCount('resumeSession') > 0 || | 760 | 813 | this.server.listenerCount('newSession') > 0) { | @@ -1133,6 +1186,7 @@ function tlsConnectionListener(rawSocket) { | 1133 | 1186 | rejectUnauthorized: this.rejectUnauthorized, | 1134 | 1187 | handshakeTimeout: this[kHandshakeTimeout], | 1135 | 1188 | ALPNProtocols: this.ALPNProtocols, | | 1189 | + ALPNCallback: this.ALPNCallback, | 1136 | 1190 | SNICallback: this[kSNICallback] || SNICallback, | 1137 | 1191 | enableTrace: this[kEnableTrace], | 1138 | 1192 | pauseOnConnect: this.pauseOnConnect, | @@ -1232,6 +1286,11 @@ function Server(options, listener) { | 1232 | 1286 | this.requestCert = options.requestCert === true; | 1233 | 1287 | this.rejectUnauthorized = options.rejectUnauthorized !== false; | 1234 | 1288 |
| | 1289 | + this.ALPNCallback = options.ALPNCallback; | | 1290 | + if (this.ALPNCallback && options.ALPNProtocols) { | | 1291 | + throw new ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS(); | | 1292 | + } | | 1293 | + | 1235 | 1294 | if (options.sessionTimeout) | 1236 | 1295 | this.sessionTimeout = options.sessionTimeout; | 1237 | 1296 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/errors.js +10Lines changed: 10 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -1623,6 +1623,16 @@ E('ERR_TEST_FAILURE', function(error, failureType) { | 1623 | 1623 | this.cause = error; | 1624 | 1624 | return msg; | 1625 | 1625 | }, Error); | | 1626 | +E('ERR_TLS_ALPN_CALLBACK_INVALID_RESULT', (value, protocols) => { | | 1627 | + return `ALPN callback returned a value (${ | | 1628 | + value | | 1629 | + }) that did not match any of the client's offered protocols (${ | | 1630 | + protocols.join(', ') | | 1631 | + })`; | | 1632 | +}, TypeError); | | 1633 | +E('ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS', | | 1634 | + 'The ALPNCallback and ALPNProtocols TLS options are mutually exclusive', | | 1635 | + TypeError); | 1626 | 1636 | E('ERR_TLS_CERT_ALTNAME_FORMAT', 'Invalid subject alternative name string', | 1627 | 1637 | SyntaxError); | 1628 | 1638 | E('ERR_TLS_CERT_ALTNAME_INVALID', function(reason, host, cert) { | |
Collapse file Copy file name to clipboardExpand all lines: src/crypto/crypto_tls.cc +49Lines changed: 49 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -224,6 +224,44 @@ int SelectALPNCallback( | 224 | 224 | unsigned int inlen, | 225 | 225 | void* arg) { | 226 | 226 | 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 | + | 227 | 265 | const std::vector<unsigned char>& alpn_protos = w->alpn_protos_; | 228 | 266 |
| 229 | 267 | if (alpn_protos.empty()) return SSL_TLSEXT_ERR_NOACK; | @@ -1224,6 +1262,15 @@ void TLSWrap::OnClientHelloParseEnd(void* arg) { | 1224 | 1262 | c->Cycle(); | 1225 | 1263 | } | 1226 | 1264 |
| | 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 | + | 1227 | 1274 | void TLSWrap::GetServername(const FunctionCallbackInfo<Value>& args) { | 1228 | 1275 | Environment* env = Environment::GetCurrent(args); | 1229 | 1276 |
| @@ -2034,6 +2081,7 @@ void TLSWrap::Initialize( | 2034 | 2081 | SetProtoMethod(isolate, t, "certCbDone", CertCbDone); | 2035 | 2082 | SetProtoMethod(isolate, t, "destroySSL", DestroySSL); | 2036 | 2083 | SetProtoMethod(isolate, t, "enableCertCb", EnableCertCb); | | 2084 | + SetProtoMethod(isolate, t, "enableALPNCb", EnableALPNCb); | 2037 | 2085 | SetProtoMethod(isolate, t, "endParser", EndParser); | 2038 | 2086 | SetProtoMethod(isolate, t, "enableKeylogCallback", EnableKeylogCallback); | 2039 | 2087 | SetProtoMethod(isolate, t, "enableSessionCallbacks", EnableSessionCallbacks); | @@ -2099,6 +2147,7 @@ void TLSWrap::RegisterExternalReferences(ExternalReferenceRegistry* registry) { | 2099 | 2147 | registry->Register(CertCbDone); | 2100 | 2148 | registry->Register(DestroySSL); | 2101 | 2149 | registry->Register(EnableCertCb); | | 2150 | + registry->Register(EnableALPNCb); | 2102 | 2151 | registry->Register(EndParser); | 2103 | 2152 | registry->Register(EnableKeylogCallback); | 2104 | 2153 | registry->Register(EnableSessionCallbacks); | |
Collapse file Copy file name to clipboardExpand all lines: src/crypto/crypto_tls.h +2Lines changed: 2 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -172,6 +172,7 @@ class TLSWrap : public AsyncWrap, | 172 | 172 | static void CertCbDone(const v8::FunctionCallbackInfo<v8::Value>& args); | 173 | 173 | static void DestroySSL(const v8::FunctionCallbackInfo<v8::Value>& args); | 174 | 174 | static void EnableCertCb(const v8::FunctionCallbackInfo<v8::Value>& args); | | 175 | + static void EnableALPNCb(const v8::FunctionCallbackInfo<v8::Value>& args); | 175 | 176 | static void EnableKeylogCallback( | 176 | 177 | const v8::FunctionCallbackInfo<v8::Value>& args); | 177 | 178 | static void EnableSessionCallbacks( | @@ -285,6 +286,7 @@ class TLSWrap : public AsyncWrap, | 285 | 286 |
| 286 | 287 | public: | 287 | 288 | std::vector<unsigned char> alpn_protos_; // Accessed by SelectALPNCallback. | | 289 | + bool alpn_callback_enabled_ = false; // Accessed by SelectALPNCallback. | 288 | 290 | }; | 289 | 291 |
| 290 | 292 | } // namespace crypto | |
Collapse file Copy file name to clipboardExpand all lines: src/env_properties.h +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 50 | 50 | V(ack_string, "ack") \ | 51 | 51 | V(address_string, "address") \ | 52 | 52 | V(aliases_string, "aliases") \ | | 53 | + V(alpn_callback_string, "ALPNCallback") \ | 53 | 54 | V(args_string, "args") \ | 54 | 55 | V(asn1curve_string, "asn1Curve") \ | 55 | 56 | V(async_ids_stack_string, "async_ids_stack") \ | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-tls-alpn-server-client.js +72-3Lines changed: 72 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -41,9 +41,8 @@ function runTest(clientsOptions, serverOptions, cb) { | 41 | 41 | opt.rejectUnauthorized = false; | 42 | 42 |
| 43 | 43 | results[clientIndex] = {}; | 44 | | - const client = tls.connect(opt, function() { | 45 | | - results[clientIndex].client = { ALPN: client.alpnProtocol }; | 46 | | - client.end(); | | 44 | + | | 45 | + function startNextClient() { | 47 | 46 | if (options.length) { | 48 | 47 | clientIndex++; | 49 | 48 | connectClient(options); | @@ -53,6 +52,15 @@ function runTest(clientsOptions, serverOptions, cb) { | 53 | 52 | cb(results); | 54 | 53 | }); | 55 | 54 | } | | 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(); | 56 | 64 | }); | 57 | 65 | } | 58 | 66 |
| @@ -200,12 +208,73 @@ function TestFatalAlert() { | 200 | 208 | .on('close', common.mustCall(() => { | 201 | 209 | assert.match(stderr, /SSL alert number 120/); | 202 | 210 | server.close(); | | 211 | + TestALPNCallback(); | 203 | 212 | })); | 204 | 213 | } else { | 205 | 214 | server.close(); | | 215 | + TestALPNCallback(); | 206 | 216 | } | 207 | 217 | })); | 208 | 218 | })); | 209 | 219 | } | 210 | 220 |
| | 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 | + | 211 | 280 | Test1(); |
|