后量子密码迁移:现在要做的不是换算法,而是盘清楚债务
古董级程序员,大厂出来后一直在创业公司,现在仍在一线做 AI 相关的工程。更完整的技术记录写在微信公众号「字与码」:工作经历、对新工具的看法,以及这些年踩过的坑,会不定期发在那里。若这篇对你有用,欢迎顺手关注。
后量子密码迁移最容易被讲成一个算法故事:RSA 会被 Shor 算法威胁,ECC 也会受影响,NIST 给了 ML-KEM、ML-DSA、SLH-DSA,于是大家应该尽快换新算法。
这个说法没有错,但它太像安全培训课了。真正进到公司系统里,问题完全不是“把 A 算法替换成 B 算法”这么干净。证书在哪里签发,移动 App 包是谁签的,备份密钥放在哪个 KMS 里,合作伙伴接口验签有没有文档,旧 SDK 还能不能升级,这些细碎问题才会决定迁移能不能做下去。
下面我用一个虚构但很贴近现实的 SaaS 公司做主线。公司叫 Northwind Notes,一家给企业做协作文档和审批流的 B2B SaaS。它没有国家级机密,也没有神秘硬件;它只是有公网 TLS、移动 App、每天备份、合作伙伴开放 API、一些历史包袱。安全负责人给了团队一个目标:90 天内做完后量子迁移试点,不追求全面替换,但要把账盘清楚,并跑通一条可回滚的真实链路。

第一天的误会:我们不是已经全站 HTTPS 了吗
Northwind Notes 的第一场会开得很快。业务团队听到“后量子密码迁移”,第一反应是:公网服务都在 HTTPS 后面,证书也是云厂商托管,应该找网关团队看一眼就行。
网关团队也确实很快拉出了一张清单:三个公网域名、两个区域的负载均衡、一个 CDN 配置、一个内部 mTLS 网格。清单看起来很整齐,直到移动端负责人问了一句:“App 安装包签名算不算?”备份负责人又问:“备份加密用的 RSA 包装密钥算不算?”生态合作团队补了一刀:“我们还有几个伙伴接口是 RSA-SHA256 签名,合同里写了签名串格式。”
这才是迁移的真实开头。PQC 不是 TLS 专项治理,而是密码资产治理。密码资产不只在安全团队手里,也不只在基础设施里。它藏在发布流水线、移动端证书、归档任务、合作伙伴 SDK、老的批处理脚本和一堆“当年赶时间写的签名工具”里。
这家公司后来把 90 天目标改成了三句话:
| 目标 | 不是为了 | 真正要交付 |
|---|---|---|
| 盘点密码资产 | 写一份漂亮报告 | 找出长期保密、升级慢、责任不清的资产 |
| 做混合试点 | 证明新算法很先进 | 验证兼容性、观测、回滚和供应商边界 |
| 建密码敏捷性 | 一次性替换所有算法 | 以后算法变化时不用再全公司挖代码 |
这个转向很重要。后量子迁移如果一开始就喊“全面升级”,很容易变成大而虚的安全项目。反过来,如果先承认自己不知道哪里用了密码,项目就会落到可执行的清单、接口和流程上。
资产清单不是 CMDB,它要能让人排序
第一周,Northwind Notes 没有买平台,也没有先引入新库。安全团队只是发了一份很朴素的表格,让每个系统负责人填。表格字段不是“系统名、负责人、备注”这种行政口径,而是围绕迁移决策设计的。
| 字段 | 示例 | 用来回答的问题 |
|---|---|---|
| 资产编号 | CRYPTO-API-017 | 后续测试、风险和工单能不能指向同一件事 |
| 场景 | 合作伙伴订单回调验签 | 这是传输加密、静态加密,还是数字签名 |
| 算法与参数 | RSA-2048 / SHA-256 / PKCS#1 v1.5 | 受量子风险影响多大,是否还有传统弱点 |
| 数据保密期 | 7 年 | 是否存在 harvest now, decrypt later 风险 |
| 密钥或证书生命周期 | 2 年轮换 | 下一次自然窗口在哪里 |
| 依赖方 | 8 个合作伙伴、3 个内部服务 | 谁会被迁移影响 |
| 所在代码或配置 | api-signature-service / v2 signer | 能不能找到真实实现 |
| 供应商约束 | HSM 固件暂不支持 PQC | 迁移卡点是不是外部能力 |
| 回滚办法 | feature flag 切回 RSA | 试点失败时是否能退 |
| 当前观测 | 只有 401 计数,无算法维度 | 上线后能不能定位失败 |
表格很快暴露出几个问题。公网 TLS 倒是清楚,因为证书管理集中;移动 App 签名只知道证书到期日,不知道老设备验签策略;备份加密用了云 KMS,但 KMS 内部如何做密钥封装,团队没看过文档;合作伙伴 RSA 签名最麻烦,签名代码散在一个老服务和两个 SDK 里,甚至还有伙伴按字段排序的私有差异。
第一周结束时,他们得到的不是“迁移完成 12%”这种数字,而是一张可以排序的风险图。
| 资产 | 当前做法 | 主要风险 | 90 天动作 |
|---|---|---|---|
| 公网 API TLS | 云 LB,ECDHE + ECDSA/RSA 证书 | 长期敏感请求可能被采集;中间设备兼容未知 | 内部入口做混合 KEM 试点,不直接动全站 |
| 移动 App 签名 | Android/iOS 平台签名 + 内部包签名 | 旧版本验证链长期存在;离线验签周期长 | 补齐签名链文档,先做双签名演练 |
| 备份加密 | AES 数据密钥,RSA 包装历史归档密钥 | 备份保密期 7 到 10 年,典型先收集后解密风险 | 新备份改成可插拔 envelope,旧备份建立重加密计划 |
| 合作伙伴 API 签名 | RSA-SHA256,请求级签名 | 外部改造慢,协议写死,验签失败难排查 | 设计 v3 签名协议,先支持双验签 |
| 内部 mTLS | service mesh 管理 | 客户端版本相对统一,但流量大 | 选低风险内部链路做握手观测试点 |
这张表比算法选型更早出现,也更有价值。它让会议从“是不是要换 ML-KEM”变成“哪条链路最值得在 90 天内跑通”。
不同密码用途,迁移节奏完全不同
Northwind Notes 后来把资产分成四类:密钥交换、传输层证书、静态数据加密、数字签名。它们都和后量子有关,但迁移难点不一样。
TLS 和 mTLS 主要看协议栈、客户端、网关和中间设备。试点可以走混合密钥交换,失败可以按流量切回旧配置。它有风险,但至少是在线系统,可观测、可灰度。
备份加密看起来更简单,因为没有浏览器兼容问题。实际难点在保密期。备份是冷的,但价值很长。攻击者今天拿到密文,几年后有能力解密,业务损失仍然成立。这个场景比一些公网短会话更应该早处理。
移动 App 签名和合作伙伴签名又是另一种麻烦。签名的验证方很多,验证时间可能很久以后发生。一个 App 包、一个审计归档、一个合同回调日志,可能几年后还要被验证。签名迁移不能只看“今天能不能验”,还要看“未来如何证明当时的签名有效”。这会引入双签名、时间戳、旧证书保留、验证器版本管理。
团队一开始想把 90 天试点放在公网 TLS。后来放弃了。原因很现实:公网客户端不可控,云厂商对混合 KEM 的生产支持还在排期,直接做容易把项目变成供应商等待。最后他们选了两条线并行:
| 试点线 | 为什么选它 | 为什么不是终局 |
|---|---|---|
| 内部备份加密 envelope 重构 | 数据保密期长,链路可控,能真实降低长期风险 | 只覆盖新备份,旧备份还要分批处理 |
| 合作伙伴签名 v3 双验签 | 代表外部协调难点,能提前暴露协议治理问题 | 90 天内只接入两个沙盒伙伴 |
这个选择比“挑一个最好展示的新算法”更稳。一个试点验证静态加密的密码敏捷性,一个试点验证签名迁移的外部兼容性。TLS 仍然在路线图里,但不再硬塞进 90 天。
备份线:把算法从代码里拔出来
Northwind Notes 的备份系统历史不复杂:每天从数据库和对象存储导出快照,用随机 AES-256-GCM 数据密钥加密,数据密钥再用一个 RSA 公钥包装,最后把密文、包装后的数据密钥和元数据一起写到归档桶。
老格式类似这样:
{
"version": "backup.v1",
"data_cipher": "AES-256-GCM",
"key_wrap": "RSA-OAEP-SHA256",
"wrapped_data_key": "base64...",
"nonce": "base64...",
"created_at": "2026-01-04T03:10:12Z"
}
这个格式的问题不在 AES,而在 key wrap 写死了 RSA。更麻烦的是恢复工具也写死了同一个假设:看到 backup.v1 就按 RSA-OAEP 解包。要做 PQC 或混合封装,第一步不是马上换算法,而是把 envelope 版本化。
第二版元数据改成了这样:
{
"version": "backup.v2",
"data_cipher": "AES-256-GCM",
"key_envelopes": [
{
"id": "rsa-2026-q1",
"type": "rsa-oaep-sha256",
"recipient": "backup-rsa-prod-01",
"wrapped_data_key": "base64..."
},
{
"id": "mlkem-2026-q1",
"type": "ml-kem-768-hybrid-x25519",
"recipient": "backup-pqc-pilot-01",
"encapsulated_key": "base64...",
"wrapped_data_key": "base64..."
}
],
"nonce": "base64...",
"aad": {
"tenant_region": "us-east-1",
"retention_class": "seven_years"
},
"created_at": "2026-03-18T03:10:12Z"
}
这里的关键不是某个字段名,而是设计原则:数据加密层继续用对称加密;密钥封装层允许多个 envelope 并存;恢复工具按能力选择 envelope;每个 envelope 都有版本、接收者和审计信息。这样即使第一版 PQC 方案要回滚,也不会破坏备份主体。
对应的配置没有放在业务代码里,而是进入了一个受控配置:
backup_crypto_profile:
name: seven-year-retention-pilot
applies_to:
retention_class: seven_years
regions:
- us-east-1
data_cipher: AES-256-GCM
envelopes:
- id: rsa-2026-q1
type: rsa-oaep-sha256
required_for_restore: true
recipient_key_ref: kms://backup/rsa/prod-01
- id: pqc-hybrid-2026-q1
type: ml-kem-768-hybrid-x25519
required_for_restore: false
recipient_key_ref: kms://backup/pqc/pilot-01
rollout:
mode: shadow
tenant_percent: 5
stop_on_restore_failure: true
第一阶段 mode: shadow 的意思是:写入新 envelope,但恢复路径仍以 RSA envelope 为准,同时在后台定期用 PQC envelope 做恢复演练。这样可以真实产生数据,又不会把生产恢复能力押在新路径上。
这个设计也给后续留下空间。等运行足够久,required_for_restore 可以调整;等供应商和审计都准备好,RSA envelope 可以从“主路径”退成“兼容路径”;旧备份也可以按保密期分批重加密,而不是一次性做一个危险的大迁移。
备份线的第一次失败:恢复工具没错,值班手册错了
第 32 天,团队做第一次恢复演练。加密和解密单元测试都过了,影子恢复也能跑。但演练还是失败了,原因很不起眼:值班手册里写的是“复制 wrapped_data_key 到恢复命令”,而新格式里有多个 envelope。值班同学按旧手册拿了 RSA envelope 的字段,却在新恢复工具里指定了 PQC profile,工具报了一个很底层的 “recipient mismatch”。
这类失败很典型。密码迁移经常不是算法实现失败,而是周边操作假设失败。工具的错误信息、手册、审计字段、监控面板都默认“只有一个密钥包装方式”。当格式变成多 envelope,这些默认假设会一起浮出来。
他们做了三个小改动:
backupctl restore \
--archive s3://backup-archive/2026/03/18/tenant-1042.snapshot \
--crypto-profile seven-year-retention-pilot \
--envelope auto \
--dry-run
--envelope auto 会列出可用 envelope,并解释选择原因。--dry-run 会验证元数据和密钥访问权限,但不真正写回数据库。错误信息也从底层异常改成了面向值班人员的话:
cannot use envelope pqc-hybrid-2026-q1:
plugin can read archive metadata, but KMS recipient backup-pqc-pilot-01 is not granted to this restore role.
suggestion:
run with --envelope rsa-2026-q1, or request restore role backup-pqc-restore-pilot.
这不是密码学突破,但它决定试点能不能进生产。安全项目经常低估这些人机接口。真正的回滚和恢复不是写在架构图里的箭头,而是半夜值班的人能不能按手册做对。
合作伙伴签名线:先做双验签,不急着让别人改
合作伙伴 API 是另一个世界。Northwind Notes 有一个开放接口,伙伴推送审批状态变更。旧协议大概长这样:
POST /partner/v2/callback HTTP/1.1
X-Partner-Id: acme-sandbox
X-Timestamp: 1771272000
X-Signature-Alg: RSA-SHA256
X-Signature: base64...
Content-Type: application/json
签名串是:
method + "\n" +
path + "\n" +
timestamp + "\n" +
sha256(body)
这个协议有几个老问题:算法字段只是字符串,没有版本协商;伙伴公钥更新靠人工邮件;错误日志只记录“验签失败”;SDK 里有伙伴自定义排序逻辑。PQC 迁移只是把这些问题照亮了。
新协议没有要求伙伴马上换后量子签名。团队先设计了 v3 manifest,让伙伴声明自己支持的签名能力,宿主支持双验签和灰度拒绝。
{
"partner_id": "partner-sandbox-17",
"protocol": "northwind.partner-signature.v3",
"valid_from": "2026-04-01T00:00:00Z",
"keys": [
{
"kid": "rsa-2026-01",
"alg": "rsa-pss-sha256",
"use": "verify",
"public_key": "pem-or-jwk-ref",
"expires_at": "2026-12-31T23:59:59Z"
},
{
"kid": "mldsa-pilot-01",
"alg": "ml-dsa-65",
"use": "verify",
"public_key": "jwk-ref",
"expires_at": "2026-09-30T23:59:59Z",
"pilot": true
}
],
"policy": {
"accept": "rsa_or_pqc",
"require_timestamp_skew_seconds": 300,
"log_signature_material": false
}
}
请求头也改成支持多个签名:
X-Partner-Id: partner-sandbox-17
X-Signature-Input: method path timestamp body-sha256
X-Signature-RSA: kid=rsa-2026-01; sig=base64...
X-Signature-PQC: kid=mldsa-pilot-01; sig=base64...
试点期策略是 rsa_or_pqc。只要 RSA 通过,请求仍然被接受;PQC 验签结果进入日志和看板,但不影响业务。等两个沙盒伙伴连续 30 天通过率达标,再切一个低风险接口到 rsa_and_pqc,要求双签名都通过。最终目标才是某些新接口只接受 PQC 或混合签名。
这里有个容易被忽略的点:双签名不是简单加一个字段。签名输入必须完全一致,错误码要能说明是哪一个签名失败,重放保护不能因为两个签名出现歧义,SDK 要能生成测试向量。否则伙伴只会看到 401,然后双方开始在群里贴日志。
合作伙伴线的失败样本:验签失败不是一种错误
第 51 天,一个沙盒伙伴接入 v3,PQC 签名一直失败。最开始大家怀疑是算法库版本不一致,后来发现是签名输入里的 path 规范化不同。伙伴 SDK 签的是 /callback,网关验的是 /partner/v3/callback。旧 RSA SDK 里也有这个问题,只是旧服务在验签前做了一个隐式 rewrite,没人写进文档。
这件事让团队把验签错误拆成了更细的类型:
| 错误码 | 含义 | 是否计入算法失败 |
|---|---|---|
signature.input_mismatch | 双方签名输入不一致 | 否,协议接入问题 |
signature.key_not_found | 找不到 kid 对应公钥 | 否,配置或轮换问题 |
signature.unsupported_alg | 宿主不支持该算法 | 是,能力协商问题 |
signature.verify_failed | 输入和 key 都匹配,但验签失败 | 是,签名实现或数据篡改 |
signature.clock_skew | 时间戳超出窗口 | 否,重放保护问题 |
他们还生成了一组公开测试向量,放进 SDK 仓库和伙伴文档:
{
"case": "v3-path-normalization",
"method": "POST",
"path": "/partner/v3/callback",
"timestamp": "1771272000",
"body_sha256": "d4735e3a265e16eee03f59718b9b5d03...",
"signature_input": "POST\n/partner/v3/callback\n1771272000\nd4735e3a...",
"expected": {
"rsa_pss_sha256": "base64...",
"ml_dsa_65": "base64..."
}
}
测试向量不包含真实伙伴名、真实密钥、真实请求体,只保留协议行为。它的作用是把“你们库是不是不兼容”的争论,变成双方都能本地复现的输入输出。
TLS 没有消失,只是被放回正确位置
这篇文章不是说 TLS 不重要。Northwind Notes 也做了 TLS 盘点,只是没有把它当成 90 天唯一战场。
TLS 盘点包括这些项:
| 项目 | 检查点 |
|---|---|
| 公网域名 | 证书算法、证书链、有效期、CDN 和 LB 支持路线 |
| 内部 mTLS | service mesh 版本、sidecar 更新节奏、失败指标 |
| 客户端分布 | 浏览器、移动 App、老 SDK、企业代理 |
| 中间设备 | WAF、API 网关、企业代理是否会丢弃未知扩展 |
| 观测 | 握手失败是否能按协议、cipher、客户端版本聚合 |
他们在第 60 天选了一条内部链路做预研:从文档服务到审计归档服务的 mTLS。链路流量稳定,客户端都是受控 sidecar,数据有长期审计价值,失败可以切回旧目标。
试点配置没有直接上“全量 PQC”,而是提供了明确开关:
mesh_tls_policy:
service: audit-archive
client_selector:
namespace: docs
key_exchange:
mode: hybrid
classical: x25519
post_quantum: ml-kem-768
rollout:
percent: 10
fallback: classical_only
abort_if:
handshake_error_rate_gt: 0.2%
p95_handshake_ms_increase_gt: 15
telemetry:
label_algorithm: true
label_client_version: true
真正有价值的是 telemetry。以前握手失败就是一条 TLS error,现在至少能看到是哪个 sidecar 版本、哪种协商模式、哪个错误族。没有这个维度,混合模式上线后出了问题,只能全局回滚。
测试办法:别只测“能不能加解密”
90 天试点里,Northwind Notes 把测试分成四层。第一层才是算法正确性,后面三层更接近工程现实。
| 测试层 | 备份线样例 | 签名线样例 | TLS 线样例 |
|---|---|---|---|
| 正确性 | 同一备份用 RSA 和 PQC envelope 都能恢复 | 测试向量验签通过 | sidecar 能完成混合握手 |
| 兼容性 | 旧恢复工具能拒绝新格式并给清楚错误 | v2 伙伴不受 v3 manifest 影响 | 旧 sidecar 自动走 classical |
| 故障注入 | KMS PQC key 不可用时能用 RSA 恢复 | kid 过期、path 不一致、时钟偏移 | 中间代理丢扩展、握手超时 |
| 操作演练 | 值班按手册恢复一份样本 | 伙伴按文档本地生成签名 | SRE 按指标触发回滚 |
他们没有把测试写成“全部自动化才算完成”。有些测试必须自动化,比如测试向量和恢复 dry-run;有些测试反而要人参与,比如值班手册演练。密码迁移会改变操作流程,人也在系统里。
每周例会看四个指标:
| 指标 | 目标 |
|---|---|
| 高优先级密码资产 owner 覆盖率 | 100% |
| 新备份 v2 envelope 写入成功率 | 99.99% 以上 |
| PQC shadow restore 成功率 | 连续 14 天 100% |
| v3 签名错误可分类比例 | 95% 以上 |
这里没有“已经迁移百分之多少”的大数字,因为 90 天试点不该假装完成全公司迁移。它要证明的是:资产能被看见,失败能被解释,路径能被回滚。
发布和回滚:后量子迁移要允许“不成功但不事故”
发布计划写得很保守。
备份线分四步:
| 阶段 | 行为 | 回滚 |
|---|---|---|
| 0 | 只读取旧 v1,不写新格式 | 无需回滚 |
| 1 | 5% 租户写 v2,RSA envelope 仍为主恢复路径 | 停止写 v2,新备份回到 v1 |
| 2 | 全量写 v2,后台 shadow restore PQC envelope | 禁用 PQC envelope,保留 RSA |
| 3 | 指定租户恢复演练优先用 PQC envelope | 恢复命令切回 RSA envelope |
合作伙伴签名线也分四步:
| 阶段 | 行为 | 回滚 |
|---|---|---|
| 0 | v3 manifest 只在沙盒注册 | 删除 manifest,不影响 v2 |
| 1 | 生产接受 v2;v3 双签只记录 | 关闭 v3 验签 worker |
| 2 | 两个低风险伙伴启用 rsa_or_pqc | partner policy 改回 rsa_only |
| 3 | 单个低风险接口启用 rsa_and_pqc | policy 回退并保留失败样本 |
这个发布计划的核心是:每一步都能回退到已知安全的传统路径,同时不丢失观测数据。回滚不是承认失败,而是试点设计的一部分。没有回滚的密码迁移,会让所有人倾向于拖延,因为没人愿意背一个“全站验签失败”的锅。
90 天结束时,真正交付了什么
第 90 天,Northwind Notes 没有宣布“我们已经完成后量子迁移”。这反而是一个成熟的结论。他们交付的是一组更朴素但可持续的东西:
| 交付物 | 价值 |
|---|---|
| 72 条密码资产清单,其中 28 条高优先级 | 后续预算和排期有了依据 |
| 备份 v2 envelope 格式和恢复工具 | 长期保密数据有了迁移入口 |
| 合作伙伴 v3 签名协议、manifest、SDK 测试向量 | 外部协调从口头改成契约 |
| mTLS 混合握手观测字段 | 以后扩大 TLS 试点时能看懂失败 |
| 供应商问题清单 | HSM、KMS、CDN、移动平台支持节奏被记录 |
| 发布和回滚模板 | 其他系统能复用同一套变更方式 |
也有没完成的部分。旧备份没有全部重加密;公网 TLS 没有启用 PQC;移动 App 签名还停留在双签名演练;合作伙伴只接了两个沙盒。这些都不丢人。90 天要解决的是“不知道怎么开始”,不是把未来几年的工作一口吞下。
我更看重他们留下来的几个工程习惯:
| 习惯 | 为什么重要 |
|---|---|
| 算法不写死在业务代码里 | 以后标准、库、供应商变化时可切换 |
| 密钥和证书有 owner | 没有人认领的密码资产最危险 |
| 新协议先支持双轨 | 外部生态迁移需要缓冲 |
| 错误要能分类 | 只有“失败”两个字无法推动合作 |
| 值班手册和工具一起改 | 迁移会影响真实操作,不只是代码 |
这就是我理解的密码敏捷性。它不是一个框架名,也不是某个厂商产品。它是一种组织能力:知道自己在哪里用了密码,知道谁负责,知道怎么试,知道怎么退。
给自己的一个小检查表
如果你现在也要启动类似项目,不妨先问这些问题。它们比“我们什么时候全面换成 PQC”更容易得到真实答案。
| 问题 | 如果答不上来,说明什么 |
|---|---|
| 哪些数据今天被抓走,五年后仍然有价值? | 风险排序还没建立 |
| 哪些签名需要几年后仍可验证? | 签名迁移没有考虑长期证据 |
| 哪些密码能力依赖供应商? | 时间表可能不是内部说了算 |
| 哪些算法写死在业务代码里? | 迁移会变成逐仓库改造 |
| 哪些失败现在只能看到 401 或 TLS error? | 上线后无法定位兼容问题 |
| 有没有一条链路可以真实试点并快速回滚? | 项目可能只能停留在文档 |
后量子密码当然有算法问题,但公司里的后量子迁移首先是资产问题、协议问题、发布问题和协作问题。现在最该做的不是到处替换 RSA,也不是等量子计算机真的可用才开会,而是把密码债务摊开,挑一条真实链路跑起来。
等那一天真的需要大规模切换时,准备好的团队不会临时翻代码找签名函数。他们会打开清单,调整 profile,灰度发布,看指标,必要时回滚。那才是这件事最值得提前建设的部分。