nodejs / nodejs/node

Re-implement TLS 1.3 session resumption

Đang mở
#60,847 3 bình luận 2 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

Ngôn ngữ chính
JavaScript
Star
122k
Fork
37.3k
Merge trung bình
4 ngày 2 giờ
Pull request đã merge (30 ngày)
283

Mô tả

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).

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Bắt đầu bằng cách chạy gist index.js được đề cập với Node v25.2.1 và đọc tài liệu node:tls về việc tiếp tục phiên. Theo dõi cách các kết nối TLS 1.2 và TLS 1.3 xử lý newSession, resumeSession và isSessionReused trong các chế độ stateful và stateless. Hoàn tất có nghĩa là TLS 1.3 tiếp tục các phiên như mong đợi mà không có callback phiên không chính xác, trong khi hành vi của TLS 1.2 vẫn không thay đổi.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
javascript
Lĩnh vực
networking, security
Loại issue
Lỗi
Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức độ hoạt động
Ít trao đổi
Độ rõ ràng
Khá rõ ràng
Mức phù hợp với người mới
40/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.