🛡️ 第五章 什么是密码、数字签名、数字证书
📄 一句话概括

代码签名回答的是"这段程序是谁发布的、有没有被人改过"这两个问题,它不回答"这个程序安全吗"——而它一开始就撞上一个死结:签名证书只活 1~3 年,可一个安装包的签名要管十年二十年;时间戳就是为了解开这个死结;2026 年正在发生的两件事是:私钥不再长期保管(无密钥签名),以及"怎么构建的"本身也变成要证明的东西(SLSA)。

📝 本节和前面几节的关系

5-6 讲了数字签名的通用流程,5-10 讲了"证书到期就得处理"。

这一节是把签名用在一个最贴近我们日常的场景上:你下载的那个安装包。 它也接上了 4-12 的话题——当软件是由一大堆第三方组件拼起来的时候,"谁签的"这个问题会变得非常复杂。

一、代码签名回答两个问题,不回答第三个

你双击一个安装包之前,系统其实替你问过两个问题:

问题 签名怎么回答它
这段程序是谁发布的 证书里的身份(公司名 / 组织)
有没有被人改过 内容哈希与签名里的哈希是否一致

第二条是签名最本质的价值:开发者对安装包算一次哈希,再用自己的私钥把哈希签掉。你下载之后,系统重新算一次哈希,再用证书里的公钥去验签名——两个哈希对得上,说明这个安装包一个字节都没被人改过(这正是一句话不讲完的那个"防止篡改"的能力,参见 5-5)。

但注意它不回答第三个问题:

代码签名不回答【这个程序安全吗】:签名只保证来源与完整,不保证代码里没有漏洞。

这一点极其重要,因为它经常被误读。微软签名的驱动程序、某个大厂签名的更新程序——它们确实"来源可信、内容完整",但它们同样可能带着漏洞,甚至可能被攻击者用来做提权。攻击者能拿到合法签名的情况有两种:一是通过收购或仿冒获得签名证书(2012 年就有恶意软件家族用偷来的证书签名);二是"白利用"——利用一个合法的、有漏洞的已签名程序来加载自己的恶意代码。换句话说,签名通过的软件可以放心"它是它自己",但不能放心"它是好人"。

二、时间戳:让"证书今天过期"不影响"昨天签的软件"

代码签名一开始就撞上一个死结,值得单独讲清楚,因为它是"时间戳"这个技术存在的唯一理由。

这个问题不解决,代码签名根本没法用。解法就是时间戳。

步 发生什么
1 开发者对软件算哈希并签名
2 把【签名值的哈希】发给时间戳服务(TSA)
3 TSA 盖上一个可信时间戳,返回时间戳令牌
4 把【软件 + 签名 + 时间戳】一起发出去
5 日后验证:只要能证明【签名当时证书是有效的】就够了

结果:证书今天过期,也不会让昨天签好的软件失效。 这就是为什么下载页面会写【已签名并加时间戳】。

注意第 2 步一个容易被忽略的细节:发给时间戳服务的不是"软件本身",而是签名的哈希。这么做有两个好处:一是开发者不必把源码或安装包交给第三方(只交了一个哈希,对方算不出内容);二是流量很小,一次请求几毫秒就能完成。

而第 5 步是整个机制的精髓:验证的判断标准从"证书现在有效吗"变成了"签名的时候证书有效吗"。 时间戳在这里扮演的是一个"可信的时钟"——它在替未来的验证者作证:"这一笔签名,确实发生在证书还有效的那个时刻。" 没有这个证人,你手里那份"2016 年的签名"永远无法自证清白。

三、动手 Demo:签名、时间戳与两代做法

下面这段输出把上面两节连起来,并交代了 2026 年正在发生的两代做法的更替——最值得看的是第四节的对照表:它把"长期保管私钥"这个最难的环节干脆取消了。

一、代码签名回答两个问题
  问题                          签名怎么回答它
  这段程序是谁发布的            证书里的身份(公司名 / 组织)
  有没有被人改过                内容哈希与签名里的哈希是否一致
  → 注意它不回答【这个程序安全吗】:签名只保证来源与完整,
     不保证代码里没有漏洞。

二、签名有一个和【证书寿命】冲突的死结
  · 代码签名证书有效期只有 1~3 年(TLS 证书 2026 起更是压到 200 天以内)
  · 但一个安装包的签名要管十年、二十年
  · 如果只按【验证时的当前时间】判断,证书一过期,
     所有历史版本的签名会集体失效——连五年前的安装包都装不上

三、时间戳就是来解这个死结的
  步    发生什么
  1     开发者对软件算哈希并签名
  2     把【签名值的哈希】发给时间戳服务(TSA)
  3     TSA 盖上一个可信时间戳,返回时间戳令牌
  4     把【软件 + 签名 + 时间戳】一起发出去
  5     日后验证:只要能证明【签名当时证书是有效的】就够了
  → 结果:证书今天过期,也不会让昨天签好的软件失效。
     这就是为什么下载页面会写【已签名并加时间戳】。

四、2026 年正在改变的做法:无密钥签名(Sigstore)
  环节                      传统做法  →  无密钥做法(Sigstore)
  身份从哪来                花钱买的长期证书 → 用 GitHub / Google 身份登录换取
  私钥放哪                  保险库/硬件模块,长期保管 → 只存在内存,签完立刻丢弃
  证书有效期                1~3 年 → 约 10 分钟
  怎么查审计                各家自己记 → 写入公开透明日志(Rekor)
  → 核心思路:既然最难的是【长期保管私钥】,那就干脆不要长期私钥。
     2026 年 npm、PyPI 等主流仓库已开始展示这类签名证明。

五、SLSA:把【怎么构建的】也变成可验证的证据
  等级    要求
  L1      有机器可读的构建来源信息
  L2      来源信息被签名,且构建跑在托管服务上
  L3      构建环境隔离,连项目维护者也无法篡改构建过程
  (当前 SLSA v1.0 只有 L1~L3;旧版 v0.x 里还有 L4 的说法)
  → 为什么需要它:SolarWinds(2020)、XZ Utils(2024)这类事故
     攻击的都是【构建过程】,而不是源码本身。
     只验签名不够,还得证明【它是用可信流程构建出来的】。

六、2026 年最大的缺口:签名普及了,验证没跟上
  · 自己给自己签很容易,难的是【每一个间接依赖都有可验证的来源】
  · 很多项目签了名,但下载端从不验证——等于门上装了锁却不锁
  · 正确顺序:先签自己的产物 → 对直接依赖强制验证 → 再逐步覆盖全链路

第五节的 SLSA 等级表值得说明一下"为什么 L3 这么重要":L3 的要求是"构建环境隔离,连项目维护者也无法篡改构建过程"。这句话针对的正是 SolarWinds(2020)和 XZ Utils(2024)这两类事故——它们的源代码仓库看起来没问题,问题出在"代码变成产物"这一步。所以只验"这个安装包有没有被改"不够,还得能证明"它是用可信的流程构建出来的",而且这个证明要由构建系统自己签(这就是 L2 与 L3 的区别)。

四、2026 正在改变的两件事:无密钥签名与构建来源证明

第一件事:把"长期保管私钥"这个最难的环节取消掉。

传统做法的软肋其实不在密码学,而在"保管":你要买一张 1~3 年的证书,把私钥放在保险库或硬件模块里长期保存——只要这段时间里任何一次泄露、任何一次误操作、任何一次人员离职带走密钥,整套签名体系的可信度就完了(对照 5-10:私钥泄露正是证书必须被吊销的首要原因)。

Sigstore 的思路是"既然最难的是保管,那就不要长期私钥":

环节 传统做法 无密钥做法(Sigstore)
身份从哪来 花钱买的长期证书 用 GitHub / Google 身份登录换取
私钥放哪 保险库/硬件模块,长期保管 只存在内存,签完立刻丢弃
证书有效期 1~3 年 约 10 分钟
怎么查审计 各家自己记 写入公开透明日志(Rekor)

整套流程是这样的:开发者用 GitHub(或 Google、微软)的身份登录 → 签名服务据此签发一张存活约 10 分钟的短命证书 → 私钥只在内存里存在,签完立刻丢弃 → 这次签名被写进一个公开的透明日志(Rekor)。日后验证时,只要检查"那次签名确实被记进了日志、而且当时的身份确实是那个 GitHub 账号"就够了——不再有任何需要长期保管的秘密。

第二件事:把"来源证明"变成上下游之间的通用要求。

这类机制今天已经落到开发者的日常里:cosign 用来给容器镜像签名,gitsign 用来给 Git 提交签名,而 npm、PyPI 这类主流包仓库已经开始展示这类签名证明——你在 PyPI 上一个包的页面上,就能看到它"是在哪个 GitHub Actions 工作流里构建出来的"。Kubernetes 这类平台甚至能在部署时强制校验:镜像没有合格签名就直接拒绝启动。

但这两件事都撞上同一个尴尬:第六节那段话说得很直白——"很多项目签了名,但下载端从不验证,等于门上装了锁却不锁"。而且"自己给自己签"极其容易,难的是"每一个间接依赖都有可验证的来源":你的项目直接依赖 10 个包,这 10 个包各自又依赖几十个包——真正被攻击者盯上、也最难验证的,恰恰是那些你从来没听说过的间接依赖。 所以正确的推进顺序是渐进的:先签自己的产物 → 对直接依赖强制验证 → 再逐步覆盖全链路。

五、⚠️ 2026 现状

1. 无密钥签名已经从"新鲜玩意"变成主流仓库的默认展示项。 OIDC 身份(GitHub、Google、微软)换取短命证书、私钥签完即丢、签名写入公开透明日志(Rekor)——这套流程在 2026 年已被 PyPI 与 npm 在元数据里展示,容器镜像与 Git 提交也各有对应工具。它的价值不只是"少管一个私钥":透明日志是 append-only 的,也就是说任何人都能查到"某个时间点谁签了什么",而后来的篡改会留下痕迹——这和证书透明度(CT)日志的思路是同一个(对照 5-10 里 CT 那一行)。

2. SLSA 的等级和版本要分清,容易记混。 当前 SLSA v1.0 只有 L1~L3 三个等级(L1 有机器可读的构建来源信息、L2 来源信息被签名且构建跑在托管服务上、L3 构建环境隔离到连项目维护者也篡改不了);而旧版 v0.x 里还有 L4 的说法——你在老文章里看到"SLSA L4"就是指那个版本。GitHub Actions、Google Cloud Build、GitLab CI 都能配置到 L3。

3. 供应链攻击打的是构建过程,这是 2026 年最需要重新认识的一点。 SolarWinds(2020)与 XZ Utils(2024)这两起标志性事件,攻击的位置都不在源码仓库,而在"代码变成产物"这一步——这也直接推动了 SLSA 这类标准、以及 SBOM(软件物料清单)的普及(美国 EO 14028 与欧盟 CRA 都在要求披露依赖组成)。换句话说,只验"安装包有没有被改过"已经不够了,还必须能回答"它是用什么流程、由谁、在什么样的环境里构建出来的"。

4. 2026 年最大的缺口是"签名普及了,验证没跟上"。 这件事有两个层面的含义:一是下载端普遍不验证——很多项目老老实实签了名,用户和自动部署流程却从不检查,等于门锁装了不锁;二是间接依赖的来源验证目前"还不可行"——你很难要求几十层深的依赖树里每一个包都提供可验证的来源证明。所以现阶段最实际的做法是分步推进,而不是"一步到位要求全链路"。这一点和 4-12 的结论一致:组件用得越方便,交付链条上就越要先想清楚"谁在替你打包"。

30 秒实验

  1. 看一个 Windows 程序有没有带时间戳。 打开 PowerShell 敲:

Get-AuthenticodeSignature C:\Windows\System32\notepad.exe | Format-List Status,SignerCertificate,TimeStamperCertificate

Status 是 Valid 表示签名有效;SignerCertificate 里能看到是谁签的;而最关键的是看有没有 TimeStamperCertificate 这一项——如果有,说明它带了时间戳,那么这张证书将来过期也不会导致这个程序签名失效。顺手把路径换成你常用的某个软件的可执行文件再跑一遍,比较一下。

  1. 看一个 Python 包是在哪儿构建的。 打开 pypi.org/project/requests/(或任意一个你常用的包),在「Release history」区域里找一个版本,点它的「数字证明 / Provenance」入口。你会看到一段"这是由 GitHub Actions 工作流 XXX 构建"的记录,并且可以点进 Rekor 透明日志查那次签名的原始记录。 这就是本节讲的"无密钥签名 + 透明日志"在实际仓库里的样子——注意它是"证明它是怎么来的",而不是"证明它安全"。

常见误区

  1. 错误:这个软件有数字签名,所以它是安全的。——纠正:签名只回答"是谁发布的"和"有没有被人改过",不回答"这个程序安全吗"。一个来源可信、内容完整的程序,同样可能带着漏洞——更麻烦的是,攻击者有两种办法拿到合法签名:一是通过收购或仿冒获得签名证书(2012 年就有恶意软件家族用偷来的证书签名),二是"白利用"——利用一个合法的、有漏洞的已签名程序来加载自己的恶意代码。所以正确的解读是:签名通过的软件可以放心"它是它自己",但不能放心"它是好人"。

  2. 错误:证书过期了,那这个软件的历史签名就都作废了。——纠正:这正是时间戳要解决的死结。代码签名证书只活 1~3 年,而安装包要管十年二十年——如果只按"验证时的当前时间"判断,那五年前的安装包就都装不上了。加了时间戳之后,验证的判断标准变成"签名的时候证书是否有效":时间戳服务扮演的是"可信的时钟",替未来的验证者作证"这一笔签名确实发生在证书还有效的时刻"。这也是为什么今天的下载页面常常写着"已签名并加时间戳"——少了后半句,那些签名只能管到现在。

  3. 错误:无密钥签名(Sigstore)是把私钥交给云端保管了。——纠正:恰恰相反,它的目的是"不要任何长期私钥"。流程是:用 OIDC 身份(GitHub、Google、微软)登录换取一张约 10 分钟的短命证书 → 私钥只在内存中存在 → 签完立刻丢弃 → 这次签名被写进公开透明日志。日后验证靠的是"日志里确实有这一笔、而且当时的身份确实是那个账号",而不是靠任何长期保存的秘密。它的代价是"必须联网、必须依赖身份提供方",好处是把"长期保管私钥"这个最容易出事的环节整个删掉了。

  4. 错误:签了自己的软件就等于把供应链安全做完了。——纠正:"自己给自己签"是整件事里最容易的一步,难的是"每一个间接依赖都有可验证的来源"。你的项目直接依赖 10 个包,这 10 个包各自又依赖几十个包——真正被攻击者盯上、也最难验证的恰恰是那些你没听说过的间接依赖。而且 SolarWinds(2020)与 XZ Utils(2024)攻击的都是"构建过程"而不是源码本身,所以只验"安装包有没有被改过"不够,还要能证明"它是用什么流程构建出来的"(这就引出了 SLSA 这类标准)。另一方面,"签了但不验证"也很普遍——等于门锁装了不锁。

自测三问(点开对答案) 1. 代码签名回答哪两个问题?它不回答什么? —— **两个问题是:"这段程序是谁发布的"(由证书里的身份回答)和"有没有被人改过"(由内容哈希与签名里的哈希是否一致回答)**。**它不回答"这个程序安全吗"**——**签名只保证来源与完整,不保证代码里没有漏洞**。**这个区别很重要,因为攻击者有两种办法拿到合法签名**:**一是收购买卖或仿冒获得签名证书**;**二是"白利用"——用一个合法的、有漏洞的已签名程序去加载自己的恶意代码**。**所以看到签名通过,只能确信"它是它自己",不能确信"它是好人"。** 2. 为什么代码签名需要"时间戳"?它是怎么起作用的? —— **因为有一个死结:证书只活 1~3 年,而安装包要管十年二十年**。**如果只按"验证时的当前时间"判断,证书一过期,所有历史版本的签名会集体失效,连五年前的安装包都装不上**。**时间戳的解法是**:**开发者先对软件算哈希并签名,再把"签名值的哈希"发给时间戳服务(TSA),由 TSA 盖上一个可信时间戳、返回时间戳令牌,最后把"软件 + 签名 + 时间戳"一起发布**。**日后验证时,判断标准从"证书现在有效吗"换成"签名的时候证书有效吗"就够了**——**时间戳在这里扮演"可信的时钟",替未来的验证者作证那一笔签名发生的时刻**。**注意发给 TSA 的是哈希而不是软件本身,所以开发者不必把源码交给第三方,流量也极小。** 3. 无密钥签名(Sigstore)解决了什么问题?它为什么被认为更安全? —— **它解决的是"长期保管私钥"这个传统做法最脆弱的环节**。**传统做法要买一张 1~3 年的证书、把私钥长期放在保险库或硬件模块里——只要这段时间里有任何一次泄露、误操作或人员变动,整套签名体系的可信度就没了**(**私钥泄露也正是证书必须被吊销的首要原因**)。**Sigstore 的做法是:用 GitHub/Google/微软身份登录换取一张约 10 分钟的短命证书,私钥只在内存里、签完立刻丢弃,签名写入公开透明日志(Rekor)**。**日后验证靠"日志里确实有这一笔、当时的身份确实是那个账号",而不靠任何长期保存的秘密**——**所以它把"保管"这个最容易出事的动作整个删掉了,而不是换个地方保管**。**代价是必须联网、并依赖身份提供方。**

相关笔记