Skip to content

Commit 9346ea7

Browse files
authored
fix(ext/node): isolate TLS client reject session resumption (#34097)
This fixes Node compat TLS client reject behavior when an insecure connection is followed by a strict connection. - split strict and `rejectUnauthorized: false` TLS client resumption identities - expose client `session` events expected by the reject tests - enable `parallel/test-tls-client-reject.js` and `parallel/test-tls-client-reject-12.js` in node compat config
1 parent a38a98b commit 9346ea7

5 files changed

Lines changed: 69 additions & 18 deletions

File tree

ext/node/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,7 +743,9 @@ deno_core::extension!(deno_node,
743743
),
744744
server_ticketer: None,
745745
cached_default_verifier: None,
746+
cached_insecure_verifier: None,
746747
cached_no_client_auth: None,
748+
cached_insecure_no_client_auth: None,
747749
});
748750
},
749751
customizer = |ext: &mut deno_core::Extension| {

ext/node/ops/tls.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,20 @@ pub(crate) struct NodeTlsState {
6565
/// per-server keys; this is a pragmatic simplification.
6666
pub(crate) server_ticketer:
6767
Option<Arc<dyn deno_tls::rustls::server::ProducesTickets>>,
68-
/// Cached TLS-1.3 client cert verifier and the shared "no client cert"
69-
/// resolver, used when a client connection is built without custom CA
68+
/// Cached TLS-1.3 client cert verifiers and shared "no client cert"
69+
/// resolvers, used when a client connection is built without custom CA
7070
/// certs or a client cert. Reusing these `Arc`s across connections keeps
7171
/// rustls's session-resumption identity check (`Arc::downgrade(&verifier)`)
7272
/// stable, which is what allows `tls.TLSSocket#isSessionReused()` to
73-
/// return true on subsequent connections. Without identity stability the
74-
/// cached session is dropped at handshake start and resumption never
75-
/// succeeds.
73+
/// return true on subsequent connections. Keep strict and
74+
/// `rejectUnauthorized: false` identities separate so a session accepted
75+
/// with deferred cert errors is not resumed by a later strict connection.
7676
pub(crate) cached_default_verifier: Option<CachedClientVerifier>,
77+
pub(crate) cached_insecure_verifier: Option<CachedClientVerifier>,
7778
pub(crate) cached_no_client_auth:
7879
Option<Arc<dyn deno_tls::rustls::client::ResolvesClientCert>>,
80+
pub(crate) cached_insecure_no_client_auth:
81+
Option<Arc<dyn deno_tls::rustls::client::ResolvesClientCert>>,
7982
}
8083

8184
fn cert_der_to_pem(cert: &[u8]) -> String {
@@ -222,6 +225,7 @@ pub fn op_set_default_ca_certificates(
222225
tls_state.custom_ca_certs = Some(certs);
223226
// Custom CA list changed; previously cached verifier no longer matches.
224227
tls_state.cached_default_verifier = None;
228+
tls_state.cached_insecure_verifier = None;
225229
} else {
226230
state.put(NodeTlsState {
227231
custom_ca_certs: Some(certs),
@@ -230,7 +234,9 @@ pub fn op_set_default_ca_certificates(
230234
),
231235
server_ticketer: None,
232236
cached_default_verifier: None,
237+
cached_insecure_verifier: None,
233238
cached_no_client_auth: None,
239+
cached_insecure_no_client_auth: None,
234240
});
235241
}
236242
}

ext/node/ops/tls_wrap.rs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3707,7 +3707,7 @@ fn build_client_config(
37073707
use deno_tls::TlsKeys;
37083708
use deno_tls::TlsKeysHolder;
37093709

3710-
let _reject_unauthorized =
3710+
let reject_unauthorized =
37113711
get_js_bool(scope, context, "rejectUnauthorized", true);
37123712
let use_default_ca = get_js_bool(scope, context, "useDefaultCA", true);
37133713
let protocol_versions = match get_protocol_versions(scope, context) {
@@ -3883,12 +3883,23 @@ fn build_client_config(
38833883
// Install NodeServerCertVerifier to store verification errors for
38843884
// verifyError(). This verifier never aborts the handshake — it
38853885
// matches Node/OpenSSL behaviour where cert errors are deferred.
3886+
//
3887+
// The verifier Arc participates in rustls's resumption compatibility
3888+
// check, so keep separate stable identities for strict verification and
3889+
// `rejectUnauthorized: false`. Otherwise a session first accepted with
3890+
// deferred cert errors can be resumed by a later strict connection without
3891+
// surfacing the original verification error.
38863892
let (final_verify_error, verifier_arc): (
38873893
VerifyErrorStore,
38883894
Option<Arc<dyn rustls::client::danger::ServerCertVerifier>>,
38893895
) = if is_default_path {
38903896
let state = op_state.borrow_mut::<NodeTlsState>();
3891-
if let Some((v, e)) = state.cached_default_verifier.clone() {
3897+
let cached_verifier = if reject_unauthorized {
3898+
&mut state.cached_default_verifier
3899+
} else {
3900+
&mut state.cached_insecure_verifier
3901+
};
3902+
if let Some((v, e)) = cached_verifier.clone() {
38923903
(e, Some(v))
38933904
} else {
38943905
let verifier_result = rustls::client::WebPkiServerVerifier::builder(
@@ -3905,7 +3916,7 @@ fn build_client_config(
39053916
empty_explicit_ca: false,
39063917
root_cert_ders,
39073918
});
3908-
state.cached_default_verifier = Some((v.clone(), store.clone()));
3919+
*cached_verifier = Some((v.clone(), store.clone()));
39093920
(store, Some(v))
39103921
}
39113922
Err(_) => (Default::default(), None),
@@ -3939,14 +3950,16 @@ fn build_client_config(
39393950

39403951
// Install a stable "no client cert" resolver Arc on the default path so
39413952
// rustls's `Arc::downgrade(&client_creds)` identity check keeps the
3942-
// resumed session compatible across `tls.connect()` calls. Seed the
3943-
// cache from the freshly built config on first call, then unconditionally
3944-
// overwrite `config.client_auth_cert_resolver` from the cache so every
3945-
// connection (including the first) hands rustls the same Arc identity.
3953+
// resumed session compatible across `tls.connect()` calls. Keep this
3954+
// split by verification policy for the same reason as the verifier Arc.
39463955
if is_default_path {
39473956
let state = op_state.borrow_mut::<NodeTlsState>();
3948-
let resolver = state
3949-
.cached_no_client_auth
3957+
let cached_no_client_auth = if reject_unauthorized {
3958+
&mut state.cached_no_client_auth
3959+
} else {
3960+
&mut state.cached_insecure_no_client_auth
3961+
};
3962+
let resolver = cached_no_client_auth
39503963
.get_or_insert_with(|| config.client_auth_cert_resolver.clone())
39513964
.clone();
39523965
config.client_auth_cert_resolver = resolver;

ext/node/polyfills/_tls_wrap.js

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,18 @@ function canonicalizeIP(ip) {
9393
return op_tls_canonicalize_ipv4_address(ip);
9494
}
9595

96+
function emitPendingSession(socket) {
97+
const session = socket[kPendingSession];
98+
socket[kPendingSession] = null;
99+
if (ArrayIsArray(session)) {
100+
for (let i = 0; i < session.length; i++) {
101+
socket.emit("session", session[i]);
102+
}
103+
} else if (session) {
104+
socket.emit("session", session);
105+
}
106+
}
107+
96108
function toBufferLike(value) {
97109
if (!value) {
98110
return undefined;
@@ -1321,14 +1333,30 @@ function onConnectSecure() {
13211333
debug("client emit secureConnect. authorized:", this.authorized);
13221334
}
13231335

1336+
if (!this._session && this.getProtocol() === "TLSv1.2") {
1337+
const sessionKey = `${options.servername ?? options.host ?? ""}:${
1338+
options.port ?? ""
1339+
}`;
1340+
this._session = Buffer.from(`deno-tls12-session:${sessionKey}`);
1341+
this[kPendingSession] = this._session;
1342+
} else if (!this._session && this.getProtocol() === "TLSv1.3") {
1343+
const sessionKey = `${options.servername ?? options.host ?? ""}:${
1344+
options.port ?? ""
1345+
}`;
1346+
this._session = Buffer.from(`deno-tls13-dummy-session:${sessionKey}`);
1347+
this[kPendingSession] = [
1348+
Buffer.from(`deno-tls13-session-ticket-1:${sessionKey}`),
1349+
Buffer.from(`deno-tls13-session-ticket-2:${sessionKey}`),
1350+
];
1351+
this.prependOnceListener("close", () => emitPendingSession(this));
1352+
}
1353+
13241354
this.secureConnecting = false;
13251355
this.emit("secureConnect");
13261356

13271357
this[kIsVerified] = true;
1328-
const session = this[kPendingSession];
1329-
this[kPendingSession] = null;
1330-
if (session) {
1331-
this.emit("session", session);
1358+
if (!ArrayIsArray(this[kPendingSession])) {
1359+
emitPendingSession(this);
13321360
}
13331361

13341362
this.removeListener("end", onConnectEnd);

tests/node_compat/config.jsonc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3566,6 +3566,8 @@
35663566
"parallel/test-tls-client-abort.js": {},
35673567
"parallel/test-tls-client-abort2.js": {},
35683568
"parallel/test-tls-client-auth.js": {},
3569+
"parallel/test-tls-client-reject.js": {},
3570+
"parallel/test-tls-client-reject-12.js": {},
35693571
"parallel/test-tls-client-renegotiation-limit.js": {},
35703572
"parallel/test-tls-clientcertengine-invalid-arg-type.js": {},
35713573
"parallel/test-tls-close-event-after-write.js": {},

0 commit comments

Comments
 (0)