Skip to content

Commit 7656d58

Browse files
sam-githubaddaleax
authored andcommitted
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>
1 parent d02ad40 commit 7656d58

10 files changed

+148
-49
lines changed

doc/api/tls.md

+46-3
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,9 @@ will create a new session. See [RFC 2246][] for more information, page 23 and
152152
Resumption using session identifiers is supported by most web browsers when
153153
making HTTPS requests.
154154

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

617+
### Event: 'session'
618+
<!-- YAML
619+
added: REPLACEME
620+
-->
621+
622+
* `session` {Buffer}
623+
624+
The `'session'` event is emitted on a client `tls.TLSSocket` when a new session
625+
or TLS ticket is available. This may or may not be before the handshake is
626+
complete, depending on the TLS protocol version that was negotiated. The event
627+
is not emitted on the server, or if a new session was not created, for example,
628+
when the connection was resumed. For some TLS protocol versions the event may be
629+
emitted multiple times, in which case all the sessions can be used for
630+
resumption.
631+
632+
On the client, the `session` can be provided to the `session` option of
633+
[`tls.connect()`][] to resume the connection.
634+
635+
See [Session Resumption][] for more information.
636+
637+
Note: For TLS1.2 and below, [`tls.TLSSocket.getSession()`][] can be called once
638+
the handshake is complete. For TLS1.3, only ticket based resumption is allowed
639+
by the protocol, multiple tickets are sent, and the tickets aren't sent until
640+
later, after the handshake completes, so it is necessary to wait for the
641+
`'session'` event to get a resumable session. Future-proof applications are
642+
recommended to use the `'session'` event instead of `getSession()` to ensure
643+
they will work for all TLS protocol versions. Applications that only expect to
644+
get or use 1 session should listen for this event only once:
645+
646+
```js
647+
tlsSocket.once('session', (session) => {
648+
// The session can be used immediately or later.
649+
tls.connect({
650+
session: session,
651+
// Other connect options...
652+
});
653+
});
654+
```
655+
617656
### tlsSocket.address()
618657
<!-- YAML
619658
added: v0.11.4
@@ -880,6 +919,9 @@ for debugging.
880919

881920
See [Session Resumption][] for more information.
882921

922+
Note: `getSession()` works only for TLS1.2 and below. Future-proof applications
923+
should use the [`'session'`][] event.
924+
883925
### tlsSocket.getTLSTicket()
884926
<!-- YAML
885927
added: v0.11.4
@@ -1538,6 +1580,7 @@ where `secureSocket` has the same API as `pair.cleartext`.
15381580
[`'resumeSession'`]: #tls_event_resumesession
15391581
[`'secureConnect'`]: #tls_event_secureconnect
15401582
[`'secureConnection'`]: #tls_event_secureconnection
1583+
[`'session'`]: #tls_event_session
15411584
[`--tls-cipher-list`]: cli.html#cli_tls_cipher_list_list
15421585
[`NODE_OPTIONS`]: cli.html#cli_node_options_options
15431586
[`crypto.getCurves()`]: crypto.html#crypto_crypto_getcurves

lib/_tls_wrap.js

+21
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,12 @@ function requestOCSPDone(socket) {
212212
}
213213

214214

215+
function onnewsessionclient(sessionId, session) {
216+
debug('client onnewsessionclient', sessionId, session);
217+
const owner = this[owner_symbol];
218+
owner.emit('session', session);
219+
}
220+
215221
function onnewsession(sessionId, session) {
216222
const owner = this[owner_symbol];
217223

@@ -512,6 +518,21 @@ TLSSocket.prototype._init = function(socket, wrap) {
512518

513519
if (options.session)
514520
ssl.setSession(options.session);
521+
522+
ssl.onnewsession = onnewsessionclient;
523+
524+
// Only call .onnewsession if there is a session listener.
525+
this.on('newListener', newListener);
526+
527+
function newListener(event) {
528+
if (event !== 'session')
529+
return;
530+
531+
ssl.enableSessionCallbacks();
532+
533+
// Remover this listener since its no longer needed.
534+
this.removeListener('newListener', newListener);
535+
}
515536
}
516537

517538
ssl.onerror = onerror;

lib/https.js

+13-11
Original file line numberDiff line numberDiff line change
@@ -117,18 +117,20 @@ function createConnection(port, host, options) {
117117
}
118118
}
119119

120-
const socket = tls.connect(options, () => {
121-
if (!options._agentKey)
122-
return;
120+
const socket = tls.connect(options);
123121

124-
this._cacheSession(options._agentKey, socket.getSession());
125-
});
126-
127-
// Evict session on error
128-
socket.once('close', (err) => {
129-
if (err)
130-
this._evictSession(options._agentKey);
131-
});
122+
if (options._agentKey) {
123+
// Cache new session for reuse
124+
socket.on('session', (session) => {
125+
this._cacheSession(options._agentKey, session);
126+
});
127+
128+
// Evict session on error
129+
socket.once('close', (err) => {
130+
if (err)
131+
this._evictSession(options._agentKey);
132+
});
133+
}
132134

133135
return socket;
134136
}

src/node_crypto.cc

+5-1
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@ void SecureContext::Init(const FunctionCallbackInfo<Value>& args) {
495495

496496
// SSL session cache configuration
497497
SSL_CTX_set_session_cache_mode(sc->ctx_.get(),
498+
SSL_SESS_CACHE_CLIENT |
498499
SSL_SESS_CACHE_SERVER |
499500
SSL_SESS_CACHE_NO_INTERNAL |
500501
SSL_SESS_CACHE_NO_AUTO_CLEAR);
@@ -1513,7 +1514,10 @@ int SSLWrap<Base>::NewSessionCallback(SSL* s, SSL_SESSION* sess) {
15131514
reinterpret_cast<const char*>(session_id_data),
15141515
session_id_length).ToLocalChecked();
15151516
Local<Value> argv[] = { session_id, session };
1516-
w->awaiting_new_session_ = true;
1517+
// On servers, we pause the handshake until callback of 'newSession', which
1518+
// calls NewSessionDoneCb(). On clients, there is no callback to wait for.
1519+
if (w->is_server())
1520+
w->awaiting_new_session_ = true;
15171521
w->MakeCallback(env->onnewsession_string(), arraysize(argv), argv);
15181522

15191523
return 0;

src/tls_wrap.cc

+5
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,11 @@ void TLSWrap::EnableSessionCallbacks(
756756
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
757757
CHECK_NOT_NULL(wrap->ssl_);
758758
wrap->enable_session_callbacks();
759+
760+
// Clients don't use the HelloParser.
761+
if (wrap->is_client())
762+
return;
763+
759764
crypto::NodeBIO::FromBIO(wrap->enc_in_)->set_initial(kMaxHelloLength);
760765
wrap->hello_parser_.Start(SSLWrap<TLSWrap>::OnClientHello,
761766
OnClientHelloParseEnd,

test/parallel/test-https-client-resume.js

+12-15
Original file line numberDiff line numberDiff line change
@@ -43,45 +43,42 @@ const server = https.createServer(options, common.mustCall((req, res) => {
4343
}, 2));
4444

4545
// start listening
46-
server.listen(0, function() {
47-
48-
let session1 = null;
46+
server.listen(0, common.mustCall(function() {
4947
const client1 = tls.connect({
5048
port: this.address().port,
5149
rejectUnauthorized: false
52-
}, () => {
50+
}, common.mustCall(() => {
5351
console.log('connect1');
54-
assert.ok(!client1.isSessionReused(), 'Session *should not* be reused.');
55-
session1 = client1.getSession();
52+
assert.strictEqual(client1.isSessionReused(), false);
5653
client1.write('GET / HTTP/1.0\r\n' +
5754
'Server: 127.0.0.1\r\n' +
5855
'\r\n');
59-
});
56+
}));
6057

61-
client1.on('close', () => {
62-
console.log('close1');
58+
client1.on('session', common.mustCall((session) => {
59+
console.log('session');
6360

6461
const opts = {
6562
port: server.address().port,
6663
rejectUnauthorized: false,
67-
session: session1
64+
session,
6865
};
6966

70-
const client2 = tls.connect(opts, () => {
67+
const client2 = tls.connect(opts, common.mustCall(() => {
7168
console.log('connect2');
72-
assert.ok(client2.isSessionReused(), 'Session *should* be reused.');
69+
assert.strictEqual(client2.isSessionReused(), true);
7370
client2.write('GET / HTTP/1.0\r\n' +
7471
'Server: 127.0.0.1\r\n' +
7572
'\r\n');
76-
});
73+
}));
7774

7875
client2.on('close', () => {
7976
console.log('close2');
8077
server.close();
8178
});
8279

8380
client2.resume();
84-
});
81+
}));
8582

8683
client1.resume();
87-
});
84+
}));

test/parallel/test-tls-async-cb-after-socket-end.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@ const fixtures = require('../common/fixtures');
66
const SSL_OP_NO_TICKET = require('crypto').constants.SSL_OP_NO_TICKET;
77
const tls = require('tls');
88

9-
// Check tls async callback after socket ends
9+
// Check that TLS1.2 session resumption callbacks don't explode when made after
10+
// the tls socket is destroyed. Disable TLS ticket support to force the legacy
11+
// session resumption mechanism to be used.
12+
13+
// TLS1.2 is the last protocol version to support TLS sessions, after that the
14+
// new and resume session events will never be emitted on the server.
1015

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

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

test/parallel/test-tls-client-resume.js

+25-17
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
// USE OR OTHER DEALINGS IN THE SOFTWARE.
2121

2222
'use strict';
23-
// Create an ssl server. First connection, validate that not resume.
24-
// Cache session and close connection. Use session on second connection.
25-
// ASSERT resumption.
23+
24+
// Check that the ticket from the first connection causes session resumption
25+
// when used to make a second connection.
2626

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

4545
// start listening
46-
server.listen(0, function() {
46+
server.listen(0, common.mustCall(function() {
4747

48+
let sessionx = null;
4849
let session1 = null;
4950
const client1 = tls.connect({
5051
port: this.address().port,
5152
rejectUnauthorized: false
52-
}, () => {
53+
}, common.mustCall(() => {
5354
console.log('connect1');
54-
assert.ok(!client1.isSessionReused(), 'Session *should not* be reused.');
55-
session1 = client1.getSession();
56-
});
55+
assert.strictEqual(client1.isSessionReused(), false);
56+
sessionx = client1.getSession();
57+
}));
58+
59+
client1.once('session', common.mustCall((session) => {
60+
console.log('session1');
61+
session1 = session;
62+
}));
5763

58-
client1.on('close', () => {
59-
console.log('close1');
64+
client1.on('close', common.mustCall(() => {
65+
assert(sessionx);
66+
assert(session1);
67+
assert.strictEqual(sessionx.compare(session1), 0);
6068

6169
const opts = {
6270
port: server.address().port,
6371
rejectUnauthorized: false,
6472
session: session1
6573
};
6674

67-
const client2 = tls.connect(opts, () => {
75+
const client2 = tls.connect(opts, common.mustCall(() => {
6876
console.log('connect2');
69-
assert.ok(client2.isSessionReused(), 'Session *should* be reused.');
70-
});
77+
assert.strictEqual(client2.isSessionReused(), true);
78+
}));
7179

72-
client2.on('close', () => {
80+
client2.on('close', common.mustCall(() => {
7381
console.log('close2');
7482
server.close();
75-
});
83+
}));
7684

7785
client2.resume();
78-
});
86+
}));
7987

8088
client1.resume();
81-
});
89+
}));

test/parallel/test-tls-ticket-cluster.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ if (cluster.isMaster) {
4545
session: lastSession,
4646
rejectUnauthorized: false
4747
}, () => {
48-
lastSession = c.getSession();
4948
c.end();
5049

5150
if (++reqCount === expectedReqCount) {
@@ -55,6 +54,8 @@ if (cluster.isMaster) {
5554
} else {
5655
shoot();
5756
}
57+
}).once('session', (session) => {
58+
lastSession = session;
5859
});
5960
}
6061

test/parallel/test-tls-ticket.js

+10
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ const shared = net.createServer(function(c) {
8181
});
8282
});
8383

84+
// 'session' events only occur for new sessions. The first connection is new.
85+
// After, for each set of 3 connections, the middle connection is made when the
86+
// server has random keys set, so the client's ticket is silently ignored, and a
87+
// new ticket is sent.
88+
const onNewSession = common.mustCall((s, session) => {
89+
assert(session);
90+
assert.strictEqual(session.compare(s.getSession()), 0);
91+
}, 4);
92+
8493
function start(callback) {
8594
let sess = null;
8695
let left = servers.length;
@@ -99,6 +108,7 @@ function start(callback) {
99108
else
100109
connect();
101110
});
111+
s.once('session', (session) => onNewSession(s, session));
102112
}
103113

104114
connect();

0 commit comments

Comments
 (0)