Skip to content

Commit a2cfcc4

Browse files
pimterrypanva
andcommitted
tls: don't trigger SNICallback or OCSPRequest from the TLS lib stack
Both events (backed by oncertcb) could potentially write to the socket synchronously, re-entering SSL mid-handshake and breaking the connection, so we defer them just like the new 'resumeSession' behaviour. Also fixes a small bug in the error path of EmitClientHello, which now bails out more aggressively instead of resuming handshakes in a V8 teardown scenario. Co-authored-by: Filip Skokan <panva.ip@gmail.com> Signed-off-by: Tim Perry <pimterry@gmail.com>
1 parent 5875e8e commit a2cfcc4

3 files changed

Lines changed: 96 additions & 25 deletions

File tree

src/crypto/crypto_tls.cc

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -218,32 +218,18 @@ int SSLCertCallback(SSL* s, void* arg) {
218218
// handshake will continue after certcb is done.
219219
return -1;
220220

221-
Environment* env = w->env();
222-
HandleScope handle_scope(env->isolate());
223-
Context::Scope context_scope(env->context());
224221
w->set_cert_cb_running();
225222

226-
Local<Object> info = Object::New(env->isolate());
223+
// The view points into SSL-owned memory, so copy it before deferring.
224+
std::string servername;
225+
if (auto name = SSLPointer::GetServerName(s)) servername = *name;
227226

228-
auto servername = SSLPointer::GetServerName(s);
229-
Local<String> servername_str =
230-
!servername.has_value()
231-
? String::Empty(env->isolate())
232-
: OneByteString(env->isolate(), servername.value());
233-
234-
Local<Value> ocsp = Boolean::New(
235-
env->isolate(), SSL_get_tlsext_status_type(s) == TLSEXT_STATUSTYPE_ocsp);
227+
w->ScheduleCertCb(std::move(servername),
228+
SSL_get_tlsext_status_type(s) == TLSEXT_STATUSTYPE_ocsp);
236229

237-
if (info->Set(env->context(), env->servername_string(), servername_str)
238-
.IsNothing() ||
239-
info->Set(env->context(), env->ocsp_request_string(), ocsp).IsNothing()) {
240-
return 1;
241-
}
242-
243-
Local<Value> argv[] = { info };
244-
w->MakeCallback(env->oncertcb_string(), arraysize(argv), argv);
245-
246-
return w->is_cert_cb_running() ? -1 : 1;
230+
// Suspend handshake with SSL_ERROR_WANT_X509_LOOKUP, and handshake will
231+
// continue after certcb is done.
232+
return -1;
247233
}
248234

249235
int SelectALPNCallback(
@@ -519,16 +505,48 @@ void TLSWrap::EmitClientHello(const std::vector<unsigned char>& session_id,
519505
env->tls_ticket_string(),
520506
Boolean::New(env->isolate(), has_ticket))
521507
.IsNothing()) {
522-
// Continue the handshake unresumed rather than leaving it suspended.
523-
hello_answered_ = true;
524-
Cycle();
508+
// An exception is pending, so don't re-enter SSL or JS to resume.
525509
return;
526510
}
527511

528512
Local<Value> argv[] = {hello_obj};
529513
MakeCallback(env->onclienthello_string(), arraysize(argv), argv);
530514
}
531515

516+
// As with the ClientHello, JS must not run on the library's stack: 'oncertcb'
517+
// handlers synchronously call back into the handle to resume the handshake.
518+
void TLSWrap::ScheduleCertCb(std::string servername, bool ocsp) {
519+
Debug(this, "Scheduling oncertcb");
520+
BaseObjectPtr<TLSWrap> strong_ref{this};
521+
env()->SetImmediate(
522+
[this, strong_ref, servername = std::move(servername), ocsp](
523+
Environment* env) {
524+
if (ssl_) EmitCertCb(servername, ocsp);
525+
});
526+
}
527+
528+
void TLSWrap::EmitCertCb(const std::string& servername, bool ocsp) {
529+
Debug(this, "Emitting oncertcb");
530+
Environment* env = this->env();
531+
HandleScope handle_scope(env->isolate());
532+
Context::Scope context_scope(env->context());
533+
534+
Local<Object> info = Object::New(env->isolate());
535+
if (info->Set(env->context(),
536+
env->servername_string(),
537+
OneByteString(env->isolate(), servername))
538+
.IsNothing() ||
539+
info->Set(env->context(),
540+
env->ocsp_request_string(),
541+
Boolean::New(env->isolate(), ocsp))
542+
.IsNothing()) {
543+
return;
544+
}
545+
546+
Local<Value> argv[] = {info};
547+
MakeCallback(env->oncertcb_string(), arraysize(argv), argv);
548+
}
549+
532550
void TLSWrap::InitSSL() {
533551
// Initialize SSL – OpenSSL takes ownership of these.
534552
enc_in_ = NodeBIO::New(env()).release();

src/crypto/crypto_tls.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ class TLSWrap : public AsyncWrap,
115115
size_t session_id_len,
116116
bool has_ticket);
117117

118+
// Schedules 'oncertcb'. The handshake stays suspended until certCbDone().
119+
void ScheduleCertCb(std::string servername, bool ocsp);
120+
118121
// Implement MemoryRetainer:
119122
void MemoryInfo(MemoryTracker* tracker) const override;
120123
SET_MEMORY_INFO_NAME(TLSWrap)
@@ -149,6 +152,7 @@ class TLSWrap : public AsyncWrap,
149152
void WaitForCertCb(CertCb cb, void* arg);
150153
void EmitClientHello(const std::vector<unsigned char>& session_id,
151154
bool has_ticket);
155+
void EmitCertCb(const std::string& servername, bool ocsp);
152156

153157
TLSWrap(Environment* env,
154158
v8::Local<v8::Object> obj,
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
'use strict';
2+
3+
// Writing to a server TLSSocket synchronously from inside an SNICallback,
4+
// while the handshake is still waiting on the certificate callback, must not
5+
// break the connection; the data must be delivered once the handshake ends.
6+
7+
const common = require('../common');
8+
9+
if (!common.hasCrypto)
10+
common.skip('missing crypto');
11+
12+
const assert = require('assert');
13+
const fixtures = require('../common/fixtures');
14+
const net = require('net');
15+
const tls = require('tls');
16+
17+
const secureContext = tls.createSecureContext({
18+
key: fixtures.readKey('rsa_private.pem'),
19+
cert: fixtures.readKey('rsa_cert.crt'),
20+
});
21+
22+
let serverSocket;
23+
const server = net.createServer(common.mustCall((raw) => {
24+
serverSocket = new tls.TLSSocket(raw, {
25+
isServer: true,
26+
secureContext,
27+
SNICallback: common.mustCall((servername, callback) => {
28+
assert.strictEqual(servername, 'localhost');
29+
serverSocket.write('from-mid-handshake');
30+
callback(null, null);
31+
}),
32+
});
33+
serverSocket.on('error', common.mustNotCall());
34+
}));
35+
36+
server.listen(0, common.mustCall(() => {
37+
const client = tls.connect({
38+
port: server.address().port,
39+
servername: 'localhost',
40+
rejectUnauthorized: false,
41+
}, common.mustCall(() => {
42+
client.on('data', common.mustCall((data) => {
43+
assert.strictEqual(data.toString(), 'from-mid-handshake');
44+
client.end();
45+
server.close();
46+
}));
47+
}));
48+
client.on('error', common.mustNotCall());
49+
}));

0 commit comments

Comments
 (0)