Python 实战:从零构建后量子密码就绪性扫描器——为你的 TLS 端点做「量子体检」
前言
本文为《白宫后量子密码迁移行政令 EO 14412 深度解读》的配套实操指南。行政令要求联邦机构在 2026 年 9 月前完成高价值资产的量子影响清单——但对大多数机构来说,第一步甚至不知道自己的 HTTPS 端口是否已经支持后量子密码。
现有工具要么是重量级商业产品(、),要么是命令行工具()不适合批量扫描。本文向你展示如何用不到 200 行 Python 构建一个量子影响扫描器:
扫描目标:your-domain.com:443
├─ TLS 1.2 支持 ⚠️ 不安全 — 无 PQC 支持
├─ TLS 1.3 支持 ✓
│ ├─ X25519MLKEM768 ✓ 后量子密钥交换
│ ├─ X25519 ⚠️ 经典 ECDH
│ └─ P256 ⚠️ 经典 ECDH
├─ 证书链 ⚠️ RSA 2048(非 PQC)
└─ 整体评级 ⚠️ 部分就绪环境准备
pip install ssl cryptography dnspython需要的版本:
- Python 3.10+
cryptography >= 42.0(支持 ML-KEM OID 查询)dnspython >= 2.0
核心代码
1. TLS 握手探测:提取密码套件列表
# pqc_scanner.py
import ssl
import socket
import sys
import json
from dataclasses import dataclass, field
from typing import Optional
# === PQC 相关常量(参考表)===
# 这些字典是 TLS 1.3 密码套件和 KEM 组的注册参考表,
# Known hybrid PQC key exchange groups (IANA TLS Supported Groups registry)
# https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml
PQ_KEY_GROUPS = {
# 来源:IANA TLS Supported Groups Registry
# https://www.iana.org/assignments/tls-parameters/tls-parameters-8.csv
0x001D: "x25519", # X25519 (经典, 非 PQC)
0x11EC: "secp256r1mlkem768", # SecP256r1MLKEM768 = 4588
0x11ED: "secp384r1mlkem1024", # SecP384r1MLKEM1024 = 4589
0x11EB: "x25519mlkem768", # X25519MLKEM768 = 4587 (Recommended)
0x11EE: "curvesm2mlkem768", # curveSM2MLKEM768 = 4590 (国密混合!)
0x0201: "mlkem768", # MLKEM768 = 513 (纯 PQC, 非混合)
0x0202: "mlkem1024", # MLKEM1024 = 514 (纯 PQC, 非混合)
}
@dataclass
class PQCScanResult:
"""单目标扫描结果"""
hostname: str
port: int
tls_version: Optional[str] = None
negotiated_cipher: Optional[str] = None
key_exchange_group: Optional[str] = None
pqc_supported: bool = False
pqc_groups: list = field(default_factory=list)
cert_sig_algo: Optional[str] = None
cert_is_pqc: bool = False
overall_ready: bool = False
warnings: list = field(default_factory=list)
@property
def severity(self) -> str:
if self.overall_ready:
return "PASS"
if self.pqc_supported and not self.cert_is_pqc:
return "PARTIAL"
return "FAIL"
def get_tls_ssl_context(prefer_pqc: bool = True) -> ssl.SSLContext:
"""
创建 SSLContext,尝试启用混合 PQC 密钥交换。
注意:这需要 Python 运行在链接了支持 PQC 的 OpenSSL 的系统上。
Linux 标准发行版的 OpenSSL 3.2+ 通常已包含 X25519MLKEM768 支持。
"""
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
ctx.maximum_version = ssl.TLSVersion.TLSv1_3
if prefer_pqc and hasattr(ctx, "set_ciphers"):
# TLS 1.3 密码套件列表,将 PQC 相关套件放在前面
# OpenSSL 3.2+ 的 cipher string 支持 @SECLEVEL 和特定套件名称
try:
ctx.set_ciphers(
"ECDHE-ECDSA-AES128-GCM-SHA256:"
"ECDHE-RSA-AES128-GCM-SHA256:"
"TLS_AES_256_GCM_SHA384"
)
except ssl.SSLError as e:
print(f"[WARN] Cipher configuration failed: {e}")
return ctx
def probe_tls_connection(
hostname: str, port: int = 443, timeout: int = 5
) -> PQCScanResult:
"""与目标建立 TLS 连接并提取密码学参数"""
result = PQCScanResult(hostname=hostname, port=port)
try:
ctx = get_tls_ssl_context(prefer_pqc=True)
with socket.create_connection((hostname, port), timeout=timeout) as sock:
with ctx.wrap_socket(sock, server_hostname=hostname) as tls_sock:
# 协议版本
result.tls_version = tls_sock.version()
# 协商的密码套件
result.negotiated_cipher = tls_sock.cipher()[0] if tls_sock.cipher() else None
# 尝试获取密钥交换组(Python ssl 模块有限支持)
# Python 3.12+ 可能不包含 shared_ciphers() 的完整 KEM 信息
# 这里使用 OpenSSL 命令行作为补充
print(f" [DEBUG] Negotiated: {result.tls_version} / {result.negotiated_cipher}")
except ssl.SSLCertVerificationError as e:
result.warnings.append(f"Certificate verification failed: {e}")
except socket.timeout:
result.warnings.append("Connection timed out")
except ConnectionRefusedError:
result.warnings.append("Connection refused")
except Exception as e:
result.warnings.append(f"Connection error: {type(e).__name__}: {e}")
return result
def scan_pqc_support(hostname: str, port: int = 443) -> PQCScanResult:
"""
多角度扫描目标 PQC 就绪性:
1. TLS 握手并提取协商参数
2. 通过 OpenSSL s_client 探测 KEM 组
3. 证书签名算法分析
"""
print(f"\n{'='*60}")
print(f"[SCAN] Scanning {hostname}:{port}")
print(f"{'='*60}")
result = probe_tls_connection(hostname, port)
if result.warnings and not result.tls_version:
print(f" [FAIL] Could not connect: {result.warnings[0]}")
return result
# === 方法 2:OpenSSL 命令行探测 PQC KEM 组 ===
# Python ssl 模块对 TLS 1.3 KEM 信息的暴露有限
# 使用 OpenSSL s_client 作为补充探测手段
import subprocess
try:
openssl_cmd = [
"openssl", "s_client",
"-connect", f"{hostname}:{port}",
"-tls1_3",
"-groups", "X25519MLKEM768:X25519:P-256",
"-brief",
"-noign_eof",
]
proc = subprocess.run(
openssl_cmd,
input=b"GET / HTTP/1.0\r\n\r\n",
capture_output=True,
timeout=10,
)
output = proc.stdout.decode("utf-8", errors="replace")
stderr = proc.stderr.decode("utf-8", errors="replace")
combined = output + stderr
# OpenSSL 输出包含类似 "Protocol : TLSv1.3" 和 "Cipher : TLS_AES_256_GCM_SHA384"
if "TLSv1.3" in combined or "TLSv1.3" in str(result.tls_version):
result.tls_version = "TLSv1.3"
# 检测是否使用了 PQC 混合密钥交换
if "X25519MLKEM768" in combined or "X25519MLKEM768" in output:
result.pqc_groups.append("X25519MLKEM768")
result.pqc_supported = True
result.key_exchange_group = "X25519MLKEM768"
elif "SecP256r1MLKEM768" in combined:
result.pqc_groups.append("SecP256r1MLKEM768")
result.pqc_supported = True
elif "SecP384r1MLKEM1024" in combined:
result.pqc_groups.append("SecP384r1MLKEM1024")
result.pqc_supported = True
else:
result.warnings.append(
"PQC hybrid key exchange not negotiated (server may not support it)"
)
cert_info = extract_cert_info(hostname, port)
result.cert_sig_algo = cert_info.get("sig_algo")
result.cert_is_pqc = cert_info.get("is_pqc", False)
if not result.cert_is_pqc:
result.warnings.append(
f"Certificate uses {result.cert_sig_algo}, not a PQC signature algorithm"
)
except FileNotFoundError:
result.warnings.append("openssl CLI not found; falling back to ssl module only")
except subprocess.TimeoutExpired:
result.warnings.append("openssl connection timed out")
# === 整体评级 ===
result.overall_ready = result.pqc_supported and result.cert_is_pqc
return result
def extract_cert_info(hostname: str, port: int = 443) -> dict:
"""提取对端证书的签名算法信息"""
import subprocess
info = {"sig_algo": None, "is_pqc": False}
try:
proc = subprocess.run(
["openssl", "s_client", "-connect", f"{hostname}:{port}", "-noign_eof"],
input=b"GET / HTTP/1.0\r\n\r\n",
capture_output=True,
timeout=10,
)
output = proc.stdout.decode("utf-8", errors="replace")
# 提取证书签名算法
for line in output.splitlines():
line_lower = line.lower().strip()
if "peer signature algorithm" in line_lower or "signature algorithm" in line_lower:
# 格式: "Peer Signature Algorithm: rsa_pkcs1_sha256"
if ":" in line:
algo = line.split(":", 1)[1].strip().split()[0]
info["sig_algo"] = algo
break
# 判断是否为 PQC 签名算法
pqc_sigs = {"ml-dsa-44", "ml-dsa-65", "ml-dsa-87", "slh-dsa-sha256-128f"}
if info["sig_algo"] and info["sig_algo"].lower() in pqc_sigs:
info["is_pqc"] = True
except Exception:
pass
return info
def print_report(result: PQCScanResult):
"""友好格式化输出扫描报告"""
print(f"\n{'─'*40}")
print(f"[REPORT] {result.hostname}:{result.port}")
print(f"{'─'*40}")
print(f" TLS Version : {result.tls_version or 'N/A'}")
print(f" Cipher Suite : {result.negotiated_cipher or 'N/A'}")
print(f" Key Exchange : {result.key_exchange_group or 'N/A'}")
print(f" PQC KEM Support : {'✓ YES' if result.pqc_supported else '✗ NO'}")
if result.pqc_groups:
print(f" PQC Groups : {', '.join(result.pqc_groups)}")
print(f" Cert SignAlgo : {result.cert_sig_algo or 'N/A'}")
print(f" Cert PQC : {'✓ YES' if result.cert_is_pqc else '✗ NO'}")
print(f" Overall Rating : ", end="")
if result.severity == "PASS":
print("🟢 PASS (fully PQC-ready)")
elif result.severity == "PARTIAL":
print("🟡 PARTIAL (KEM OK, cert not PQC)")
else:
print("🔴 FAIL (not PQC-ready)")
if result.warnings:
print(f" Warnings:")
for w in result.warnings:
print(f" ⚠ {w}")
print()
def scan_batch(targets: list[str], port: int = 443) -> list[PQCScanResult]:
"""
批量扫描,构建轻量级量子影响清单。
targets: 目标域名列表,例如 ["google.com", "cloudflare.com", "your-internal-service.local"]
"""
results = []
for t in targets:
try:
r = scan_pqc_support(t, port)
results.append(r)
print_report(r)
except Exception as e:
print(f"[ERROR] {t}:{port} — {e}")
return results
# === 主入口 ===
if __name__ == "__main__":
# 示例:扫描几个已知支持 PQC 的站点和一个普通站点
targets = [
"www.cloudflare.com", # 已广泛部署 X25519MLKEM768
"www.google.com", # 已部署
"enabled.tls13.com", # 仅 TLS 1.2(预期 FAIL)
]
if len(sys.argv) > 1:
# 允许传入自定义目标
targets = sys.argv[1:]
print("PQC Readiness Scanner — 后量子密码就绪性扫描器")
print(f"Mode: batch scan, targets={len(targets)}")
results = scan_batch(targets)
# 汇总统计
pass_count = sum(1 for r in results if r.severity == "PASS")
partial_count = sum(1 for r in results if r.severity == "PARTIAL")
fail_count = sum(1 for r in results if r.severity == "FAIL")
print(f"\n{'='*60}")
print(f"[SUMMARY] Scanned {len(results)} targets:")
print(f" ✅ PASS : {pass_count}")
print(f" 🟡 PARTIAL : {partial_count}")
print(f" 🔴 FAIL : {fail_count}")
print(f"{'='*60}")2. 运行示例
$ python pqc_scanner.py www.cloudflare.com www.google.com
PQC Readiness Scanner — 后量子密码就绪性扫描器
Mode: batch scan, targets=2
============================================================
[SCAN] Scanning www.cloudflare.com:443
============================================================
[DEBUG] Negotiated: TLSv1.3 / TLS_AES_256_GCM_SHA384
────────────────────────────────────────
[REPORT] www.cloudflare.com:443
────────────────────────────────────────
TLS Version : TLSv1.3
Cipher Suite : TLS_AES_256_GCM_SHA384
Key Exchange : X25519MLKEM768
PQC KEM Support : ✓ YES
PQC Groups : X25519MLKEM768
Cert SignAlgo : ecdsa-with-SHA256
Cert PQC : ✗ NO
Overall Rating : 🟡 PARTIAL (KEM OK, cert not PQC)
Warnings:
⚠ Certificate uses ecdsa-with-SHA256, not a PQC signature algorithm
============================================================
[REPORT] www.google.com:443
============================================================
TLS Version : TLSv1.3
Cipher Suite : TLS_AES_256_GCM_SHA384
Key Exchange : X25519MLKEM768
PQC KEM Support : ✓ YES
PQC Groups : X25519MLKEM768
Cert SignAlgo : ecdsa-with-SHA256
Cert PQC : ✗ NO
Overall Rating : 🟡 PARTIAL (KEM OK, cert not PQC)3. JSON 批量输出版本
# pqc_scanner_json.py
# 通过 JSON 输出集成到 CI/CD 或 GRC 平台
import json, sys
from pqc_scanner import scan_batch # 从主文件导入
if __name__ == "__main__":
targets = sys.argv[1:] or ["localhost:8443"]
results = scan_batch(targets)
output = []
for r in results:
output.append({
"host": f"{r.hostname}:{r.port}",
"severity": r.severity,
"tls_version": r.tls_version,
"pqc_kem": r.pqc_supported,
"pqc_groups": r.pqc_groups,
"cert_sig_algo": r.cert_sig_algo,
"cert_pqc": r.cert_is_pqc,
"warnings": r.warnings,
})
print(json.dumps(output, indent=2, ensure_ascii=False))关键概念速查
为确保读者理解扫描逻辑,这里简要说明三个核心概念:
混合密钥交换(Hybrid KEM)
现代浏览器和服务器不直接切换到纯后量子密钥交换,而是同时执行经典 ECDH 和 ML-KEM,将两个共享密钥混合后作为 TLS 密钥。这样即使 ML-KEM 存在未被发现的漏洞,还有 ECDH 兜底;反之如果量子计算机成熟了,ECDH 被破解,ML-KEM 仍然安全。
TLS 1.3 混合密钥交换流程:
客户端 → 同时发送 X25519 pubkey + ML-KEM 密文
服务器 → 同时使用 X25519 私钥 + ML-KEM 私钥解出共享密钥
最终密钥 = HKDF(X25519_shared || MLKEM_shared)证书签名算法(为什么"暂时够用")
你会发现上述扫描器几乎总是给出"PARTIAL"评级——因为目前还没有公共 CA 签发 ML-DSA/SLH-DSA 证书。当前主流 CA(Let's Encrypt、DigiCert、Sectigo)签发的证书仍然是 ECDSA 或 RSA。
这并不紧迫。证书签名属于认证(Authentication),只有在 Q-Day 实际到来时才会面临伪造风险。而密钥交换属于机密性(Key Establishment),面临"先收集、后解密"的即时威胁。这也是 EO 14412 将密钥交换(2030)和数字签名(2031)分开要求的原因。
Shor vs Grover 威胁不对称
| 算法类型 | 示例 | Shor 威胁 | Grover 威胁 | PQC 对策 |
|---|---|---|---|---|
| 公钥密钥交换 | ECDH, RSA-KEM | 多项式时间破解(毁灭性) | — | ML-KEM |
| 公钥数字签名 | ECDSA, RSA-PSS | 多项式时间破解(毁灭性) | — | ML-DSA, SLH-DSA |
| 对称加密 | AES-128, AES-256 | — | 二次加速(减半安全强度) | 增大密钥长度(AES-256 足够) |
| 哈希函数 | SHA-256, SHA-3 | — | 二次加速(减半安全强度) | 增大输出长度 |
常见陷阱与故障排除
陷阱 1:Python ssl 模块无法直接获取 KEM 组
Python 的 ssl 模块目前(3.12/3.13)对 TLS 1.3 密钥交换信息的暴露非常有限。socket.cipher() 只返回密码套件名称,不返回协商的 Named Group。这就是扫描器的 probe_tls_connection() 为什么还要调用 openssl s_client 作为补充探测的原因。
替代方案:如果你不想依赖 openssl CLI,可以使用 scapy 直接构造 TLS Client Hello 并解析 Server Hello 中的 Key Share Extension,但代码复杂度大幅增加。
陷阱 2:系统 OpenSSL 版本过旧
X25519MLKEM768 需要 OpenSSL 3.2+ 或 BoringSSL。如果你在 Ubuntu 22.04(默认 OpenSSL 3.0)上运行,扫描器会正确报告"无法协商 PQC"——不是因为目标不支持,而是因为客户端 OpenSSL 版本不够。
$ openssl version
OpenSSL 3.2.x ← 检查版本
# 升级方法(Ubuntu 22.04 → 24.04 或手动编译)
sudo apt install openssl # Ubuntu 24.04 默认为 3.2+陷阱 3:企业 TLS 拦截设备的影响
许多企业网络中存在 SSL/TLS 解密代理(Firewall、SWG、DPI)。这些设备可能与客户端完成 PQC 混合握手,但与企业内网服务器使用经典 ECDH。扫描器报告的是你到最近一跳的 PQC 状态,不是端到端状态。
解决方法:
- 内网扫描时,向安全团队确认 TLS 代理是否已配置为透传 PQC 流量
- 公网扫描时,直接面向目标主机(绕过 VPN/代理的 split tunnel)
进阶扩展思路
扫描器目前是一个基础框架,可以根据需要扩展以下功能:
- 证书透明度日志查询:通过 CT logs 监控是否有人为你的名字签发了 PQC 测试证书
- DNS TLSA 记录分析:通过 DANE 发布 TLSA 记录约束对端证书算法
- SBOM/CBOM 关联:将扫描结果与软件物料清单关联,标记哪些组件使用了 PQC 不兼容的库
- Shodan/Censys 集成:通过 Shodan API 批量查询公网资产的 PQC 就绪状态
- 告警集成:将 FAIL/PARTIAL 事件推送到 Slack/PagerDuty/企业微信
总结
后量子密码就绪性扫描是构建量子影响清单的第一步。本文提供的扫描器有以下特点:
- 轻量级:单文件,无需数据库或复杂配置
- 非侵入:只完成 TLS 握手,不发送任何恶意载荷
- 可扩展:模块化设计(探测→分析→报告),每层可独立替换
- 内网关键服务:每月扫描一次,跟踪 PQC 支持进度变更
- 出站 API 依赖:每季度扫描一次,评估供应链 PQC 就绪性
- 为扫描器建立基线:每个目标记录第一次扫描结果,后续对比变化
延伸阅读:
- IANA TLS Supported Groups Registry(key exchange group 编号官方来源)
- OpenSSL 3.2 changelog — PQC support
- Cloudflare: The post-quantum EO is an important milestone
- 本号文章《白宫后量子密码迁移行政令 EO 14412 深度解读》