Skip to content

Commit

Permalink
tls, crypto: add ALPN Support
Browse files Browse the repository at this point in the history
cherry-pick 802a2e7 from v6-staging.

ALPN is added to tls according to RFC7301, which supersedes NPN.
When the server receives both NPN and ALPN extensions from the client,
ALPN takes precedence over NPN and the server does not send NPN
extension to the client. alpnProtocol in TLSSocket always returns
false when no selected protocol exists by ALPN.
In https server, http/1.1 token is always set when no
options.ALPNProtocols exists.

PR-URL: #2564
Reviewed-By: Fedor Indutny <fedor@indutny.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
  • Loading branch information
Shigeki Ohtsu authored and MylesBorins committed Jan 24, 2017
1 parent bf3a67c commit 2b91385
Show file tree
Hide file tree
Showing 11 changed files with 769 additions and 33 deletions.
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
51 changes: 33 additions & 18 deletions lib/tls.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,27 +32,42 @@ 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)) {
protocols = convertProtocols(protocols);
}
// If it's already a Buffer - store it
if (protocols instanceof Buffer) {
out.NPNProtocols = protocols;
}
};

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

Expand Down
2 changes: 2 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 @@ -208,6 +209,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 @@ -2740,6 +2740,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

0 comments on commit 2b91385

Please sign in to comment.