← 中文资源

简体中文版本

OCSP 封套

OCSP 封套让服务器把签名且带有效期的证书状态证明附加到 TLS 握手中,客户端无需再向 CA 查询吊销状态,兼顾隐私与延迟。

阅读英文原文

概念定义

传统吊销检查要求浏览器在握手期间或之后查询 CA,既增加往返又向 CA 泄露用户访问了哪些站点。封套反转了流程:服务器周期性取得签名状态,并在握手时随证书一起呈上,客户端本地验证签名与时间戳。

工作机制

服务器为叶证书请求 OCSP 响应,缓存到临近过期,并在 CertificateStatus 握手消息中携带;Multiple 协议扩展可为中间证书附带响应。证书中的 must-staple 标志会强制客户端在缺少封套时硬失败。

使用场景

任何公开 HTTPS 端点都受益:握手延迟下降、CA 查询量减少、隐私改善。CDN 在边缘按证书封套。搭配短周期证书与自动化续期,可保证封套永不携带过期证明。

常见误区

封套只负责分发吊销状态,本身不吊销证书;刷新器故障时服务器会给出过期或缺席的封套,浏览器退回软失败,把“未知”当“有效”。部分客户端栈会静默忽略封套数据。运维应对封套年龄与刷新失败设置告警,而不是指望握自我报告。

代码示例

export function parseContentDisposition(header) {
  if (typeof header !== "string" || header.trim() === "") {
    throw new TypeError("header must be a non-empty string");
  }
  const segments = splitParameters(header);
  const type = segments.shift().trim().toLowerCase();
  if (type !== "inline" && type !== "attachment") {
    throw new Error(`unsupported disposition type "${type}"`);
  }
  const parameters = new Map();
  for (const segment of segments) {
    const equals = segment.indexOf("=");
    if (equals === -1) continue;
    const name = segment.slice(0, equals).trim().toLowerCase();
    if (name === "") continue;
    parameters.set(name, unquote(segment.slice(equals + 1).trim()));
  }
  return { type, filename: resolveFilename(parameters), parameters };
}

function splitParameters(header) {
  const segments = [];
  let current = "";
  let quoted = false;
  for (const character of header) {
    if (character === '"') quoted = !quoted;
    if (character === ";" && !quoted) {
      segments.push(current);
      current = "";
      continue;
    }
    current += character;
  }
  segments.push(current);
  return segments;
}

function unquote(value) {
  if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
    return value.slice(1, -1).replace(/\\(.)/g, "$1");
  }
  return value;
}

function resolveFilename(parameters) {
  const extended = parameters.get("filename*");
  if (extended !== undefined) {
    const match = /^([\w-]+)'([\w-]*)'(.+)$/.exec(extended);
    if (match !== null) {
      try {
        return sanitize(decodeURIComponent(match[3]));
      } catch {
        return sanitize(extended);
      }
    }
  }
  const plain = parameters.get("filename");
  return plain === undefined ? null : sanitize(plain);
}

function sanitize(filename) {
  return filename.split(/[\\/]/).pop() || null;
}

console.log(parseContentDisposition('attachment; filename="report.pdf"'));
console.log(parseContentDisposition("attachment; filename*=UTF-8''%E4%B8%AD%E6%96%87.pdf"));
console.log(parseContentDisposition('inline; filename="../../etc/passwd"'));
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";

export interface CsrfCheckInput {
  headerToken?: string;
  cookieToken?: string;
  sessionId: string;
}

export function createCsrfToken(sessionId: string, secret: string): string {
  if (!sessionId || !secret) throw new TypeError("sessionId and secret are required");
  const nonce = randomBytes(18).toString("base64url");
  const signature = createHmac("sha256", secret)
    .update(sessionId + "." + nonce)
    .digest("base64url");
  return nonce + "." + signature;
}

export function verifyCsrfToken(input: CsrfCheckInput, secret: string): boolean {
  const { headerToken, cookieToken, sessionId } = input;
  if (!headerToken || !cookieToken || !sessionId) return false;
  const header = Buffer.from(headerToken);
  const cookie = Buffer.from(cookieToken);
  if (header.length !== cookie.length || !timingSafeEqual(header, cookie)) return false;
  const separator = headerToken.lastIndexOf(".");
  if (separator <= 0) return false;
  const nonce = headerToken.slice(0, separator);
  const signature = headerToken.slice(separator + 1);
  const expected = createHmac("sha256", secret)
    .update(sessionId + "." + nonce)
    .digest("base64url");
  const provided = Buffer.from(signature);
  const expectedBuffer = Buffer.from(expected);
  return provided.length === expectedBuffer.length
    && timingSafeEqual(provided, expectedBuffer);
}

export function csrfCookie(token: string, secure = true): string {
  const flags = ["Path=/", "SameSite=Lax", secure ? "Secure" : ""].filter(Boolean);
  return "csrf_token=" + encodeURIComponent(token) + "; " + flags.join("; ");
}