Re-implement TLS 1.3 session resumption
还没有人认领这个 Issue。
- 主要语言
- JavaScript
- 星标
- 122k
- 派生
- 37.4k
- 平均合并
- 4 天 3 小时
- 30 天内合并 PR
- 272
描述
Version
v25.2.1
Platform
Linux ... 5.15.0-161-generic #171-Ubuntu SMP Sat Oct 11 08:17:01 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux
Subsystem
tls
What steps will reproduce the bug?
run this gist with node:
# test TLS1.2 and TLS1.3 stateless resumption (session identifiers)
node ./index.js --stateless
# test TLS 1.2 and TLS 1.3 stateful resumption (session tickets)
node ./index.js --stateful
#! node
// SPDX-License-Identifier: 0BSD
import { execSync } from "child_process";
import { SSL_OP_NO_TICKET } from "node:constants";
import fs from "node:fs";
import tls from "node:tls";
import { isMainThread, Worker, workerData } from "node:worker_threads";
((_main) => {
generateCertKeyIfNeeded();
})(this);
function generateCertKeyIfNeeded() {
if (fs.existsSync("key.pem") && fs.existsSync("cert.pem")) {
return;
}
execSync(
'openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 3000 -nodes -subj "/CN=localhost"'
);
}
// run this openssl command in another worker
// openssl s_client -connect localhost:8000 -reconnect -tls1_3
function tls13(noTicket) {
const cmd = `openssl s_client -connect localhost:8000 -reconnect -tls1_3 ${
noTicket ? "-no_ticket" : ""
}`;
execSync(cmd, { stdio: ["ignore", "ignore", "ignore"] });
}
function tls12(noTicket) {
const cmd = `openssl s_client -connect localhost:8000 -reconnect -tls1_2 ${
noTicket ? "-no_ticket" : ""
}`;
execSync(cmd, { stdio: ["ignore", "ignore", "ignore"] });
}
const stats = {
// get node version
node: process.versions.node,
openssl: process.versions.openssl,
tot: 0,
stateful: {
saved: 0,
resumed: 0,
missed: 0,
nodata: 0,
},
stateless: {
saved: "?",
resumed: 0,
missed: 0,
},
tls13: {
new: 0,
resumed: 0,
},
tls12: {
new: 0,
resumed: 0,
},
};
const noTicket =
process.argv.includes("--no-ticket") ||
process.argv.includes("--stateful") ||
(process.argv.includes("--session-id") &&
!(
process.argv.includes("--stateless") ||
process.argv.includes("--ticket") ||
process.argv.includes("--session-ticket")
));
const noState = !noTicket;
if (isMainThread) {
// retrieve command line args to know if SSL_OP_NO_TICKET should be used
const w1 = new Worker(import.meta.filename, {
workerData: { func: "tls13", opt: noTicket },
});
const w2 = new Worker(import.meta.filename, {
workerData: { func: "tls12", opt: noTicket },
});
/** @type {import("node:tls").TlsOptions} */
const options = {
key: fs.readFileSync("key.pem"),
cert: fs.readFileSync("cert.pem"),
honorCipherOrder: true,
// allow only stateful session resumption in TLSv1.2
// and treat stateless resumption as stateful in TLSv1.3
secureOptions: noTicket ? SSL_OP_NO_TICKET : undefined,
minVersion: "TLSv1.2",
dhparam: "auto",
sessionIdContext: "tls13-resumption-test",
sessionTimeout: 24 * 60 * 60, // 1 day
ticketKeys: noTicket
? undefined
: crypto.getRandomValues(new Uint8Array(48)),
};
const server = tls.createServer(options, (socket) => {
const p = socket.getProtocol();
const r = socket.isSessionReused();
recordStats(p, r);
socket.end();
socket.on("error", (_err) => {});
});
server.listen(8000, () => {
console.log(
"server listening on port 8000",
noTicket ? "without" : "with",
"tickets;",
noState ? "stateless" : "stateful",
"session resumption"
);
});
server.on("error", (_err) => {});
if (noTicket) {
const sess = new Map();
server.on("newSession", (id, data, cb) => {
if (data) stats.stateful.saved++;
else stats.stateful.nodata++;
sess.set(id.toString("hex"), data);
cb();
});
server.on("resumeSession", (id, cb) => {
const data = sess.get(id.toString("hex")) || null;
if (data) stats.stateful.resumed++;
else stats.stateful.missed++;
cb(null, data);
});
}
// terminate the workers and server after 10 seconds
setTimeout(async () => {
server.close();
console.log("Stats:", stats);
await w1.terminate();
await w2.terminate();
}, 1000);
} else {
if (workerData) {
if (workerData.func === "tls13") {
tls13(workerData.opt);
} else if (workerData.func === "tls12") {
tls12(workerData.opt);
}
}
}
function recordStats(p, r) {
stats.tot++;
console.log(p, r ? "✅ Renewed" : "❌ New");
if (p === "TLSv1.3") {
if (r) stats.tls13.resumed++;
else stats.tls13.new++;
if (noState)
if (r) stats.stateless.resumed++;
else stats.stateless.missed++;
} else if (p === "TLSv1.2") {
if (r) stats.tls12.resumed++;
else stats.tls12.new++;
if (noState)
if (r) stats.stateless.resumed++;
else stats.stateless.missed++;
}
}
How often does it reproduce? Is there a required condition?
100%
What is the expected behavior? Why is that the expected behavior?
server listening on port 8000 with tickets; stateless session resumption
TLSv1.3 ❌ New
TLSv1.2 ❌ New
TLSv1.2 ✅ Renewed
TLSv1.3 ✅ Renewed
TLSv1.2 ✅ Renewed
TLSv1.2 ✅ Renewed
TLSv1.3 ✅ Renewed
TLSv1.2 ✅ Renewed
TLSv1.3 ✅ Renewed
TLSv1.2 ✅ Renewed
TLSv1.3 ✅ Renewed
TLSv1.3 ✅ Renewed
Stats: {
node: '25.2.1',
openssl: '3.5.4',
tot: 12,
stateful: { saved: 0, resumed: 0, missed: 0, nodata: 0 },
stateless: { saved: '?', resumed: 5, missed: 2 },
tls13: { new: 1, resumed: 5 },
tls12: { new: 1, resumed: 5 }
}
What do you see instead?
server listening on port 8000 with tickets; stateless session resumption
TLSv1.3 ❌ New
TLSv1.2 ❌ New
TLSv1.2 ✅ Renewed
TLSv1.3 ❌ New
TLSv1.2 ✅ Renewed
TLSv1.2 ✅ Renewed
TLSv1.3 ❌ New
TLSv1.2 ✅ Renewed
TLSv1.3 ❌ New
TLSv1.2 ✅ Renewed
TLSv1.3 ❌ New
TLSv1.3 ❌ New
Stats: {
node: '25.2.1',
openssl: '3.5.4',
tot: 12,
stateful: { saved: 0, resumed: 0, missed: 0, nodata: 0 },
stateless: { saved: '?', resumed: 5, missed: 7 },
tls13: { new: 6, resumed: 0 },
tls12: { new: 1, resumed: 5 }
}
Additional information
Node's TLS docs could be updated to reflect this immediately until this issue is fixed.
Per OpenSSL docs, with SecureContextOptions.secureOptions: SSL_OP_NO_TICKET, TLS 1.3 session resumption (which is stateless) should behave like TLS 1.2 session tickets (stateful); that is, newSession & resumeSession should be called for both TLS 1.3 and TLS 1.2 connections. Without it set, OpenSSL should resume TLS 1.2 and TLS 1.3 sessions under the hood, without client code interaction, when resumption is "stateless" (using session tickets). This means, newSession & resumeSession must only be called for TLS 1.2 session identifiers (stateful resumptions).
Node incorrectly and always calls in to newSession and resumeSession for both TLS 1.3 and TLS 1.2 connections with (id, data) tuple, regardless of stateless (session tickets) / stateful (session identifier) modes.
Session resumption in TLS 1.3 reduces bandwidth overhead for handshakes (since the entire web PKI ceremony is side-stepped).
贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
调研方向
首先,使用 Node v25.2.1 运行提到的 index.js gist,并阅读 node:tls 的会话恢复文档。跟踪 TLS 1.2 和 TLS 1.3 连接在有状态和无状态模式下如何处理 newSession、resumeSession 和 isSessionReused。完成标准是 TLS 1.3 能按预期恢复会话,且不会触发错误的会话回调,同时 TLS 1.2 的行为保持不变。
由索引模型根据 Issue 内容生成。
评估
- 技术栈
- javascript
- 领域
- networking, security
- Issue 类型
- 缺陷
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 活跃度
- 冷清
- 描述清晰度
- 基本清楚
- 新手友好度
- 40/100