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

Backport ALPN to v4.x LTS #10831

Closed
wants to merge 5 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions doc/api/tls.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,15 @@ of an application. The `--tls-cipher-list` switch should by used only if
absolutely necessary.


## NPN and SNI
## ALPN, NPN and SNI

<!-- type=misc -->

NPN (Next Protocol Negotiation) and SNI (Server Name Indication) are TLS
ALPN (Application-Layer Protocol Negotiation Extension), NPN (Next
Protocol Negotiation) and SNI (Server Name Indication) are TLS
handshake extensions allowing you:

* NPN - to use one TLS server for multiple protocols (HTTP, SPDY)
* ALPN/NPN - to use one TLS server for multiple protocols (HTTP, SPDY, HTTP/2)
* SNI - to use one TLS server for multiple hostnames with different SSL
certificates.

Expand Down Expand Up @@ -305,7 +306,13 @@ server. If `socket.authorized` is false, then
`socket.authorizationError` is set to describe how authorization
failed. Implied but worth mentioning: depending on the settings of the TLS
server, you unauthorized connections may be accepted.
`socket.npnProtocol` is a string containing selected NPN protocol.

`socket.npnProtocol` is a string containing the selected NPN protocol
and `socket.alpnProtocol` is a string containing the selected ALPN
protocol, When both NPN and ALPN extensions are received, ALPN takes
precedence over NPN and the next protocol is selected by ALPN. When
ALPN has no selected protocol, this returns false.

`socket.servername` is a string containing servername requested with
SNI.

Expand Down Expand Up @@ -429,6 +436,8 @@ Construct a new TLSSocket object from existing TCP socket.

- `NPNProtocols`: Optional, see [`tls.createServer()`][]

- `ALPNProtocols`: Optional, see [tls.createServer][]

- `SNICallback`: Optional, see [`tls.createServer()`][]

- `session`: Optional, a `Buffer` instance, containing TLS session
Expand Down Expand Up @@ -460,8 +469,9 @@ The listener will be called no matter if the server's certificate was
authorized or not. It is up to the user to test `tlsSocket.authorized`
to see if the server certificate was signed by one of the specified CAs.
If `tlsSocket.authorized === false` then the error can be found in
`tlsSocket.authorizationError`. Also if NPN was used you can check
`tlsSocket.npnProtocol` for negotiated protocol.
`tlsSocket.authorizationError`. Also if ALPN or NPN was used - you can
check `tlsSocket.alpnProtocol` or `tlsSocket.npnProtocol` for the
negotiated protocol.

### tlsSocket.address()
<!-- YAML
Expand Down Expand Up @@ -684,6 +694,12 @@ Creates a new client connection to the given `port` and `host` (old API) or
where first byte is next protocol name's length. (Passing array should
usually be much simpler: `['hello', 'world']`.)

- `ALPNProtocols`: An array of strings or `Buffer`s containing
supported ALPN protocols. `Buffer`s should have following format:
`0x05hello0x05world`, where the first byte is the next protocol
name's length. (Passing array should usually be much simpler:
`['hello', 'world']`.)

- `servername`: Servername for SNI (Server Name Indication) TLS extension.

- `checkServerIdentity(servername, cert)`: Provide an override for checking
Expand Down Expand Up @@ -925,6 +941,12 @@ automatically set as a listener for the [`'secureConnection'`][] event. The
- `NPNProtocols`: An array or `Buffer` of possible NPN protocols. (Protocols
should be ordered by their priority).

- `ALPNProtocols`: An array or `Buffer` of possible ALPN
protocols. (Protocols should be ordered by their priority). When
the server receives both NPN and ALPN extensions from the client,
ALPN takes precedence over NPN and the server does not send an NPN
extension to the client.

- `SNICallback(servername, cb)`: A function that will be called if client
supports SNI TLS extension. Two argument will be passed to it: `servername`,
and `cb`. `SNICallback` should invoke `cb(null, ctx)`, where `ctx` is a
Expand Down
15 changes: 13 additions & 2 deletions lib/_tls_legacy.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ CryptoStream.prototype._write = function _write(data, encoding, cb) {
if (this.pair.encrypted._internallyPendingBytes())
this.pair.encrypted.read(0);

// Get NPN and Server name when ready
// Get ALPN, NPN and Server name when ready
this.pair.maybeInitFinished();

// Whole buffer was written
Expand Down Expand Up @@ -273,7 +273,7 @@ CryptoStream.prototype._read = function _read(size) {
bytesRead < size &&
this.pair.ssl !== null);

// Get NPN and Server name when ready
// Get ALPN, NPN and Server name when ready
this.pair.maybeInitFinished();

// Create new buffer if previous was filled up
Expand Down Expand Up @@ -729,6 +729,13 @@ function SecurePair(context, isServer, requestCert, rejectUnauthorized,
this.npnProtocol = null;
}

if (process.features.tls_alpn && options.ALPNProtocols) {
// keep reference in secureContext not to be GC-ed
this.ssl._secureContext.alpnBuffer = options.ALPNProtocols;
this.ssl.setALPNrotocols(this.ssl._secureContext.alpnBuffer);
this.alpnProtocol = null;
}

/* Acts as a r/w stream to the cleartext side of the stream. */
this.cleartext = new CleartextStream(this, options.cleartext);

Expand Down Expand Up @@ -781,6 +788,10 @@ SecurePair.prototype.maybeInitFinished = function() {
this.npnProtocol = this.ssl.getNegotiatedProtocol();
}

if (process.features.tls_alpn) {
this.alpnProtocol = this.ssl.getALPNNegotiatedProtocol();
}

if (process.features.tls_sni) {
this.servername = this.ssl.getServername();
}
Expand Down
27 changes: 22 additions & 5 deletions lib/_tls_wrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ function TLSSocket(socket, options) {
this._SNICallback = null;
this.servername = null;
this.npnProtocol = null;
this.alpnProtocol = null;
this.authorized = false;
this.authorizationError = null;

Expand Down Expand Up @@ -486,6 +487,12 @@ TLSSocket.prototype._init = function(socket, wrap) {
if (process.features.tls_npn && options.NPNProtocols)
ssl.setNPNProtocols(options.NPNProtocols);

if (process.features.tls_alpn && options.ALPNProtocols) {
// keep reference in secureContext not to be GC-ed
ssl._secureContext.alpnBuffer = options.ALPNProtocols;
ssl.setALPNProtocols(ssl._secureContext.alpnBuffer);
}

if (options.handshakeTimeout > 0)
this.setTimeout(options.handshakeTimeout, this._handleTimeout);

Expand Down Expand Up @@ -592,6 +599,10 @@ TLSSocket.prototype._finishInit = function() {
this.npnProtocol = this._handle.getNegotiatedProtocol();
}

if (process.features.tls_alpn) {
this.alpnProtocol = this.ssl.getALPNNegotiatedProtocol();
}

if (process.features.tls_sni && this._tlsOptions.isServer) {
this.servername = this._handle.getServername();
}
Expand Down Expand Up @@ -792,6 +803,7 @@ function Server(/* [options], listener */) {
rejectUnauthorized: self.rejectUnauthorized,
handshakeTimeout: timeout,
NPNProtocols: self.NPNProtocols,
ALPNProtocols: self.ALPNProtocols,
SNICallback: options.SNICallback || SNICallback
});

Expand Down Expand Up @@ -902,6 +914,8 @@ Server.prototype.setOptions = function(options) {
this.honorCipherOrder = true;
if (secureOptions) this.secureOptions = secureOptions;
if (options.NPNProtocols) tls.convertNPNProtocols(options.NPNProtocols, this);
if (options.ALPNProtocols)
tls.convertALPNProtocols(options.ALPNProtocols, this);
if (options.sessionIdContext) {
this.sessionIdContext = options.sessionIdContext;
} else {
Expand Down Expand Up @@ -986,12 +1000,14 @@ exports.connect = function(/* [port, host], options, cb */) {
assert(typeof options.checkServerIdentity === 'function');

var hostname = options.servername ||
options.host ||
(options.socket && options.socket._host) ||
'localhost';
const NPN = {};
const context = options.secureContext || tls.createSecureContext(options);
options.host ||
(options.socket && options.socket._host) ||
'localhost';
var NPN = {};
var ALPN = {};
var context = options.secureContext || tls.createSecureContext(options);
tls.convertNPNProtocols(options.NPNProtocols, NPN);
tls.convertALPNProtocols(options.ALPNProtocols, ALPN);

var socket = new TLSSocket(options.socket, {
pipe: options.path && !options.port,
Expand All @@ -1001,6 +1017,7 @@ exports.connect = function(/* [port, host], options, cb */) {
rejectUnauthorized: options.rejectUnauthorized,
session: options.session,
NPNProtocols: NPN.NPNProtocols,
ALPNProtocols: ALPN.ALPNProtocols,
requestOCSP: options.requestOCSP
});

Expand Down
7 changes: 7 additions & 0 deletions lib/https.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ function Server(opts, requestListener) {
opts.NPNProtocols = ['http/1.1', 'http/1.0'];
}

if (process.features.tls_alpn && !opts.ALPNProtocols) {
// http/1.0 is not defined as Protocol IDs in IANA
// http://www.iana.org/assignments/tls-extensiontype-values
// /tls-extensiontype-values.xhtml#alpn-protocol-ids
opts.ALPNProtocols = ['http/1.1'];
}

tls.Server.call(this, opts, http._connectionListener);

this.httpAllowHalfOpen = false;
Expand Down
50 changes: 31 additions & 19 deletions lib/tls.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,27 +32,39 @@ exports.getCiphers = function() {

// Convert protocols array into valid OpenSSL protocols list
// ("\x06spdy/2\x08http/1.1\x08http/1.0")
exports.convertNPNProtocols = function convertNPNProtocols(NPNProtocols, out) {
// If NPNProtocols is Array - translate it into buffer
if (Array.isArray(NPNProtocols)) {
var buff = new Buffer(NPNProtocols.reduce(function(p, c) {
return p + 1 + Buffer.byteLength(c);
}, 0));

NPNProtocols.reduce(function(offset, c) {
var clen = Buffer.byteLength(c);
buff[offset] = clen;
buff.write(c, offset + 1);

return offset + 1 + clen;
}, 0);

NPNProtocols = buff;
function convertProtocols(protocols) {
var buff = new Buffer(protocols.reduce(function(p, c) {
return p + 1 + Buffer.byteLength(c);
}, 0));

protocols.reduce(function(offset, c) {
var clen = Buffer.byteLength(c);
buff[offset] = clen;
buff.write(c, offset + 1);

return offset + 1 + clen;
}, 0);

return buff;
}

exports.convertNPNProtocols = function(protocols, out) {
// If protocols is Array - translate it into buffer
if (Array.isArray(protocols)) {
out.NPNProtocols = convertProtocols(protocols);
} else if (protocols instanceof Buffer) {
// Copy new buffer not to be modified by user.
out.NPNProtocols = Buffer.from(protocols);
}
};

// If it's already a Buffer - store it
if (NPNProtocols instanceof Buffer) {
out.NPNProtocols = Buffer.from(NPNProtocols);
exports.convertALPNProtocols = function(protocols, out) {
// If protocols is Array - translate it into buffer
if (Array.isArray(protocols)) {
out.ALPNProtocols = convertProtocols(protocols);
} else if (protocols instanceof Buffer) {
// Copy new buffer not to be modified by user.
out.ALPNProtocols = Buffer.from(protocols);
}
};

Expand Down
4 changes: 4 additions & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ namespace node {
// for the sake of convenience. Strings should be ASCII-only.
#define PER_ISOLATE_STRING_PROPERTIES(V) \
V(address_string, "address") \
V(alpn_buffer_string, "alpnBuffer") \
V(args_string, "args") \
V(argv_string, "argv") \
V(arrow_message_string, "arrowMessage") \
Expand Down Expand Up @@ -133,6 +134,7 @@ namespace node {
V(netmask_string, "netmask") \
V(nice_string, "nice") \
V(nlink_string, "nlink") \
V(npn_buffer_string, "npnBuffer") \
V(nsname_string, "nsname") \
V(ocsp_request_string, "OCSPRequest") \
V(offset_string, "offset") \
Expand Down Expand Up @@ -183,6 +185,7 @@ namespace node {
V(serial_string, "serial") \
V(scavenge_string, "scavenge") \
V(scopeid_string, "scopeid") \
V(selected_npn_buffer_string, "selectedNpnBuffer") \
V(sent_shutdown_string, "sentShutdown") \
V(serial_number_string, "serialNumber") \
V(service_string, "service") \
Expand All @@ -208,6 +211,7 @@ namespace node {
V(timestamp_string, "timestamp") \
V(title_string, "title") \
V(tls_npn_string, "tls_npn") \
V(tls_alpn_string, "tls_alpn") \
V(tls_ocsp_string, "tls_ocsp") \
V(tls_sni_string, "tls_sni") \
V(tls_string, "tls") \
Expand Down
7 changes: 7 additions & 0 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2707,6 +2707,13 @@ static Local<Object> GetFeatures(Environment* env) {
#endif
obj->Set(env->tls_npn_string(), tls_npn);

#ifdef TLSEXT_TYPE_application_layer_protocol_negotiation
Local<Boolean> tls_alpn = True(env->isolate());
#else
Local<Boolean> tls_alpn = False(env->isolate());
#endif
obj->Set(env->tls_alpn_string(), tls_alpn);

#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB
Local<Boolean> tls_sni = True(env->isolate());
#else
Expand Down
5 changes: 5 additions & 0 deletions src/node_constants.cc
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,11 @@ void DefineOpenSSLConstants(Local<Object> target) {
NODE_DEFINE_CONSTANT(target, NPN_ENABLED);
#endif

#ifdef TLSEXT_TYPE_application_layer_protocol_negotiation
#define ALPN_ENABLED 1
NODE_DEFINE_CONSTANT(target, ALPN_ENABLED);
#endif

#ifdef RSA_PKCS1_PADDING
NODE_DEFINE_CONSTANT(target, RSA_PKCS1_PADDING);
#endif
Expand Down
Loading