Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a868ebe
deps: update OpenSSL upgrade process
sam-github Mar 1, 2019
c80bff3
deps: upgrade openssl sources to 1.1.1b
sam-github Apr 25, 2019
63aa831
deps: openssl-1.1.1b no longer packages .gitignore
sam-github Feb 26, 2019
1cea121
deps: add ARM64 Windows support in openssl
shigeki Feb 23, 2019
c2310c7
deps: add s390 asm rules for OpenSSL-1.1.1
shigeki Mar 7, 2018
f54db0b
deps: update archs files for OpenSSL-1.1.1b
sam-github Apr 25, 2019
f47e208
tls: support changing credentials dynamically
cjihrig Oct 13, 2018
5f5d3c9
tls: get the local certificate after tls handshake
sam-github Nov 8, 2018
4a82835
tls: fix initRead socket argument name
sam-github Dec 19, 2018
78b42fc
tls: do not confuse session and session ID
sam-github Dec 19, 2018
a6635b2
src: use consistent names for JSStream
sam-github Dec 19, 2018
ae7c74c
tls: remove unused ocsp extension parsing
sam-github Dec 19, 2018
6b327e5
src: in-source comments and minor TLS cleanups
sam-github Jan 16, 2019
2d25b65
tls: introduce client 'session' event
sam-github Jan 30, 2019
8c7406f
tls: do not free cert in `.getCertificate()`
addaleax Jan 14, 2019
38838af
src: remove unused TLWrap::EnableTrace()
sam-github Jan 31, 2019
d3c7020
src: organize TLSWrap declarations by parent
sam-github Jan 31, 2019
1c3c9f3
tls: don't shadow the tls global with a local
sam-github Jan 31, 2019
750b906
src: const_cast is necessary for 1.1.1, not 0.9.7
sam-github Jan 31, 2019
5febe41
src: refactor SSLError case statement
sam-github Jan 31, 2019
1f65f18
tls: support "BEGIN TRUSTED CERTIFICATE" for ca:
sam-github Nov 30, 2018
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
Prev Previous commit
Next Next commit
tls: introduce client 'session' event
OpenSSL has supported async notification of sessions and tickets since
1.1.0 using SSL_CTX_sess_set_new_cb(), for all versions of TLS. Using
the async API is optional for TLS1.2 and below, but for TLS1.3 it will
be mandatory. Future-proof applications should start to use async
notification immediately. In the future, for TLS1.3, applications that
don't use the async API will silently, but gracefully, fail to resume
sessions and instead do a full handshake.

See: https://wiki.openssl.org/index.php/TLS1.3#Sessions

PR-URL: #25831
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Fedor Indutny <fedor.indutny@gmail.com>
  • Loading branch information
sam-github committed Apr 29, 2019
commit 2d25b6597b5ea5a2aaf3a0f77ebc7f517ce5200a
49 changes: 46 additions & 3 deletions doc/api/tls.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,9 @@ will create a new session. See [RFC 2246][] for more information, page 23 and
Resumption using session identifiers is supported by most web browsers when
making HTTPS requests.

For Node.js, clients must call [`tls.TLSSocket.getSession()`][] after the
[`'secureConnect'`][] event to get the session data, and provide the data to the
`session` option of [`tls.connect()`][] to reuse the session. Servers must
For Node.js, clients wait for the [`'session'`][] event to get the session data,
and provide the data to the `session` option of a subsequent [`tls.connect()`][]
to reuse the session. Servers must
implement handlers for the [`'newSession'`][] and [`'resumeSession'`][] events
to save and restore the session data using the session ID as the lookup key to
reuse sessions. To reuse sessions across load balancers or cluster workers,
Expand Down Expand Up @@ -614,6 +614,45 @@ determine if the server certificate was signed by one of the specified CAs. If
`tlsSocket.alpnProtocol` property can be checked to determine the negotiated
protocol.

### Event: 'session'
<!-- YAML
added: REPLACEME
-->

* `session` {Buffer}

The `'session'` event is emitted on a client `tls.TLSSocket` when a new session
or TLS ticket is available. This may or may not be before the handshake is
complete, depending on the TLS protocol version that was negotiated. The event
is not emitted on the server, or if a new session was not created, for example,
when the connection was resumed. For some TLS protocol versions the event may be
emitted multiple times, in which case all the sessions can be used for
resumption.

On the client, the `session` can be provided to the `session` option of
[`tls.connect()`][] to resume the connection.

See [Session Resumption][] for more information.

Note: For TLS1.2 and below, [`tls.TLSSocket.getSession()`][] can be called once
the handshake is complete. For TLS1.3, only ticket based resumption is allowed
by the protocol, multiple tickets are sent, and the tickets aren't sent until
later, after the handshake completes, so it is necessary to wait for the
`'session'` event to get a resumable session. Future-proof applications are
recommended to use the `'session'` event instead of `getSession()` to ensure
they will work for all TLS protocol versions. Applications that only expect to
get or use 1 session should listen for this event only once:

```js
tlsSocket.once('session', (session) => {
// The session can be used immediately or later.
tls.connect({
session: session,
// Other connect options...
});
});
```

### tlsSocket.address()
<!-- YAML
added: v0.11.4
Expand Down Expand Up @@ -822,6 +861,9 @@ for debugging.

See [Session Resumption][] for more information.

Note: `getSession()` works only for TLS1.2 and below. Future-proof applications
should use the [`'session'`][] event.

### tlsSocket.getTLSTicket()
<!-- YAML
added: v0.11.4
Expand Down Expand Up @@ -1501,6 +1543,7 @@ where `secureSocket` has the same API as `pair.cleartext`.
[`'resumeSession'`]: #tls_event_resumesession
[`'secureConnect'`]: #tls_event_secureconnect
[`'secureConnection'`]: #tls_event_secureconnection
[`'session'`]: #tls_event_session
[`--tls-cipher-list`]: cli.html#cli_tls_cipher_list_list
[`NODE_OPTIONS`]: cli.html#cli_node_options_options
[`crypto.getCurves()`]: crypto.html#crypto_crypto_getcurves
Expand Down
21 changes: 21 additions & 0 deletions lib/_tls_wrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,12 @@ function requestOCSPDone(socket) {
}


function onnewsessionclient(sessionId, session) {
debug('client onnewsessionclient', sessionId, session);
const owner = this[owner_symbol];
owner.emit('session', session);
}

function onnewsession(sessionId, session) {
const owner = this[owner_symbol];

Expand Down Expand Up @@ -514,6 +520,21 @@ TLSSocket.prototype._init = function(socket, wrap) {

if (options.session)
ssl.setSession(options.session);

ssl.onnewsession = onnewsessionclient;

// Only call .onnewsession if there is a session listener.
this.on('newListener', newListener);

function newListener(event) {
if (event !== 'session')
return;

ssl.enableSessionCallbacks();

// Remover this listener since its no longer needed.
this.removeListener('newListener', newListener);
}
}

ssl.onerror = onerror;
Expand Down
24 changes: 13 additions & 11 deletions lib/https.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,18 +117,20 @@ function createConnection(port, host, options) {
}
}

const socket = tls.connect(options, () => {
if (!options._agentKey)
return;
const socket = tls.connect(options);

this._cacheSession(options._agentKey, socket.getSession());
});

// Evict session on error
socket.once('close', (err) => {
if (err)
this._evictSession(options._agentKey);
});
if (options._agentKey) {
// Cache new session for reuse
socket.on('session', (session) => {
this._cacheSession(options._agentKey, session);
});

// Evict session on error
socket.once('close', (err) => {
if (err)
this._evictSession(options._agentKey);
});
}

return socket;
}
Expand Down
6 changes: 5 additions & 1 deletion src/node_crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@ void SecureContext::Init(const FunctionCallbackInfo<Value>& args) {

// SSL session cache configuration
SSL_CTX_set_session_cache_mode(sc->ctx_.get(),
SSL_SESS_CACHE_CLIENT |
SSL_SESS_CACHE_SERVER |
SSL_SESS_CACHE_NO_INTERNAL |
SSL_SESS_CACHE_NO_AUTO_CLEAR);
Expand Down Expand Up @@ -1476,7 +1477,10 @@ int SSLWrap<Base>::NewSessionCallback(SSL* s, SSL_SESSION* sess) {
reinterpret_cast<const char*>(session_id_data),
session_id_length).ToLocalChecked();
Local<Value> argv[] = { session_id, session };
w->awaiting_new_session_ = true;
// On servers, we pause the handshake until callback of 'newSession', which
// calls NewSessionDoneCb(). On clients, there is no callback to wait for.
if (w->is_server())
w->awaiting_new_session_ = true;
w->MakeCallback(env->onnewsession_string(), arraysize(argv), argv);

return 0;
Expand Down
5 changes: 5 additions & 0 deletions src/tls_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,11 @@ void TLSWrap::EnableSessionCallbacks(
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
CHECK_NOT_NULL(wrap->ssl_);
wrap->enable_session_callbacks();

// Clients don't use the HelloParser.
if (wrap->is_client())
return;

crypto::NodeBIO::FromBIO(wrap->enc_in_)->set_initial(kMaxHelloLength);
wrap->hello_parser_.Start(SSLWrap<TLSWrap>::OnClientHello,
OnClientHelloParseEnd,
Expand Down
27 changes: 12 additions & 15 deletions test/parallel/test-https-client-resume.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,45 +43,42 @@ const server = https.createServer(options, common.mustCall((req, res) => {
}, 2));

// start listening
server.listen(0, function() {

let session1 = null;
server.listen(0, common.mustCall(function() {
const client1 = tls.connect({
port: this.address().port,
rejectUnauthorized: false
}, () => {
}, common.mustCall(() => {
console.log('connect1');
assert.ok(!client1.isSessionReused(), 'Session *should not* be reused.');
session1 = client1.getSession();
assert.strictEqual(client1.isSessionReused(), false);
client1.write('GET / HTTP/1.0\r\n' +
'Server: 127.0.0.1\r\n' +
'\r\n');
});
}));

client1.on('close', () => {
console.log('close1');
client1.on('session', common.mustCall((session) => {
console.log('session');

const opts = {
port: server.address().port,
rejectUnauthorized: false,
session: session1
session,
};

const client2 = tls.connect(opts, () => {
const client2 = tls.connect(opts, common.mustCall(() => {
console.log('connect2');
assert.ok(client2.isSessionReused(), 'Session *should* be reused.');
assert.strictEqual(client2.isSessionReused(), true);
client2.write('GET / HTTP/1.0\r\n' +
'Server: 127.0.0.1\r\n' +
'\r\n');
});
}));

client2.on('close', () => {
console.log('close2');
server.close();
});

client2.resume();
});
}));

client1.resume();
});
}));
10 changes: 9 additions & 1 deletion test/parallel/test-tls-async-cb-after-socket-end.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ const fixtures = require('../common/fixtures');
const SSL_OP_NO_TICKET = require('crypto').constants.SSL_OP_NO_TICKET;
const tls = require('tls');

// Check tls async callback after socket ends
// Check that TLS1.2 session resumption callbacks don't explode when made after
// the tls socket is destroyed. Disable TLS ticket support to force the legacy
// session resumption mechanism to be used.

// TLS1.2 is the last protocol version to support TLS sessions, after that the
// new and resume session events will never be emitted on the server.

const options = {
maxVersion: 'TLSv1.2',
secureOptions: SSL_OP_NO_TICKET,
key: fixtures.readSync('test_key.pem'),
cert: fixtures.readSync('test_cert.pem')
Expand All @@ -25,6 +31,8 @@ server.on('newSession', common.mustCall((key, session, done) => {

server.on('resumeSession', common.mustCall((id, cb) => {
sessionCb = cb;
// Destroy the client and then call the session cb, to check that the cb
// doesn't explode when called after the handle has been destroyed.
next();
}));

Expand Down
42 changes: 25 additions & 17 deletions test/parallel/test-tls-client-resume.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
// USE OR OTHER DEALINGS IN THE SOFTWARE.

'use strict';
// Create an ssl server. First connection, validate that not resume.
// Cache session and close connection. Use session on second connection.
// ASSERT resumption.

// Check that the ticket from the first connection causes session resumption
// when used to make a second connection.

const common = require('../common');
if (!common.hasCrypto)
Expand All @@ -43,39 +43,47 @@ const server = tls.Server(options, common.mustCall((socket) => {
}, 2));

// start listening
server.listen(0, function() {
server.listen(0, common.mustCall(function() {

let sessionx = null;
let session1 = null;
const client1 = tls.connect({
port: this.address().port,
rejectUnauthorized: false
}, () => {
}, common.mustCall(() => {
console.log('connect1');
assert.ok(!client1.isSessionReused(), 'Session *should not* be reused.');
session1 = client1.getSession();
});
assert.strictEqual(client1.isSessionReused(), false);
sessionx = client1.getSession();
}));

client1.once('session', common.mustCall((session) => {
console.log('session1');
session1 = session;
}));

client1.on('close', () => {
console.log('close1');
client1.on('close', common.mustCall(() => {
assert(sessionx);
assert(session1);
assert.strictEqual(sessionx.compare(session1), 0);

const opts = {
port: server.address().port,
rejectUnauthorized: false,
session: session1
};

const client2 = tls.connect(opts, () => {
const client2 = tls.connect(opts, common.mustCall(() => {
console.log('connect2');
assert.ok(client2.isSessionReused(), 'Session *should* be reused.');
});
assert.strictEqual(client2.isSessionReused(), true);
}));

client2.on('close', () => {
client2.on('close', common.mustCall(() => {
console.log('close2');
server.close();
});
}));

client2.resume();
});
}));

client1.resume();
});
}));
3 changes: 2 additions & 1 deletion test/parallel/test-tls-ticket-cluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ if (cluster.isMaster) {
session: lastSession,
rejectUnauthorized: false
}, () => {
lastSession = c.getSession();
c.end();

if (++reqCount === expectedReqCount) {
Expand All @@ -55,6 +54,8 @@ if (cluster.isMaster) {
} else {
shoot();
}
}).once('session', (session) => {
lastSession = session;
});
}

Expand Down
10 changes: 10 additions & 0 deletions test/parallel/test-tls-ticket.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ const shared = net.createServer(function(c) {
});
});

// 'session' events only occur for new sessions. The first connection is new.
// After, for each set of 3 connections, the middle connection is made when the
// server has random keys set, so the client's ticket is silently ignored, and a
// new ticket is sent.
const onNewSession = common.mustCall((s, session) => {
assert(session);
assert.strictEqual(session.compare(s.getSession()), 0);
}, 4);

function start(callback) {
let sess = null;
let left = servers.length;
Expand All @@ -99,6 +108,7 @@ function start(callback) {
else
connect();
});
s.once('session', (session) => onNewSession(s, session));
}

connect();
Expand Down