跳到主要内容
ecdsa.com
← 全部文章

Authenticode 代码签名:密钥、时间戳与验证

我们用 osslsigncode 2.14 为一个小型 Windows 可执行文件签名并加上 DigiCert 时间戳,然后做了三项测试:不带时间戳再签名一份副本、改动一个代码字节、在证书表中藏入数据。规则依据 CA/Browser Forum 的《代码签名基线要求》3.11.0 和 Microsoft 自己的文档,均于2026年10月核对。

简短回答:Authenticode 把一个 PKCS #7 SignedData 嵌入 Windows PE 文件末尾的一张表中;签名覆盖的是文件的哈希,计算时跳过校验和、指向这张表的指针以及表本身。自2023年6月1日起,公开受信任的代码签名密钥必须在硬件模块(硬件令牌、本地部署的 HSM,或带完整审计日志的云 HSM 服务)中生成和使用,因此构建服务器上常见的那种 .pfx 文件已经无法让你拿到证书了。2026年3月1日起签发的证书最长有效 460 天,而 Microsoft 的 Artifact Signing 签发的证书只有 72 小时有效期,这使 RFC 3161 时间戳成为让签名保持有效的关键:签名时始终使用 /fd SHA256 /tr <TSA URL> /td SHA256。EV 证书也不再能立即换来 SmartScreen 信誉。验证时,在 Windows 上用 signtool verify /pa /tw,在其他系统上用 osslsigncode verify。

签名覆盖了什么

PE 文件(.exe、.dll、.sys)的可选头中有一个数据目录;其中 4 号条目即证书表(Certificate Table),记录着追加在文件末尾的 WIN_CERTIFICATE 结构列表的文件偏移和大小。Authenticode 使用的条目修订版本为 0x0200,类型为 WIN_CERT_TYPE_PKCS_SIGNED_DATA(0x0002),其中包含一个 PKCS #7 SignedData(PE 格式)。

被签名的内容不是文件本身,而是一个包含文件哈希的 SpcIndirectDataContent。这个哈希不能覆盖签名自身,所以用 Microsoft 的话说,它“必须排除 PE 映像中的某些字段,例如可选头数据目录中的校验和与证书表条目”,此外还要排除证书表本身。其余的一切,即各个头、各个节以及最后一节之后的数据,都会计入哈希。

我们的样本是一个用 Go 1.27.3 交叉编译的 1.6 MB 程序(GOOS=windows GOARCH=amd64)。一个简短的 Python 脚本在签名前后分别读取证书表条目:

python3 pe.py hello.exe show; python3 pe.py hello-signed.exe show 的输出
file size 1651712, certificate table at offset 0, size 0
file size 1660952, certificate table at offset 1651712, size 9240
WIN_CERTIFICATE: dwLength 9240, wRevision 0x0200, wCertificateType 0x0002

这 9,240 字节就是签名、证书链和时间戳令牌。列出提取出的签名中的对象标识符,可以看到 Authenticode 特有的部分与常见的 CMS 部分并列其中:

openssl asn1parse -inform DER -in sig.der | grep -o 'OBJECT *:[^ ]*' | sort | uniq -c(节选)
      1 OBJECT            :1.3.6.1.4.1.311.2.1.11
      1 OBJECT            :1.3.6.1.4.1.311.2.1.12
      1 OBJECT            :1.3.6.1.4.1.311.2.1.15
      2 OBJECT            :1.3.6.1.4.1.311.2.1.4
      1 OBJECT            :1.3.6.1.4.1.311.3.3.1
      2 OBJECT            :id-smime-ct-TSTInfo
      2 OBJECT            :messageDigest
      2 OBJECT            :signingTime

1.3.6.1.4.1.311.2.1.4 是 SpcIndirectDataContent,…2.1.15 是其中的 PE 映像数据,…2.1.11 和 …2.1.12 分别是声明类型以及程序名称和 URL,1.3.6.1.4.1.311.3.3.1 则是作为未签名属性存放的 RFC 3161 时间戳令牌,其内容类型为 id-smime-ct-TSTInfo。

2026年的证书与密钥

公开受信任的代码签名证书受 CA/Browser Forum 的《代码签名基线要求》(CSBR)约束,版本为2026年6月16日发布的 3.11.0。以下规则会改变你的构建方式:

  • 所有人都必须使用硬件密钥。“自2023年6月1日起生效”,订户的私钥必须在至少通过 FIPS 140-2 Level 2 或 Common Criteria EAL 4+ 认证的硬件密码模块中生成、存储和使用(§6.2.7.4.1)。允许的方案有:你自己运营的硬件模块;HSM 达到这一标准、且订阅“配置为记录所有访问、操作和配置变更”的云密钥服务;或者签名服务。CA 必须核实这一点,例如寄送一个已预先生成密钥的硬件令牌,或者检查密钥证明(§6.2.7.4.2)。2023年之前,这项要求只适用于 EV 证书。
  • 密钥长度。RSA 密钥至少 3072 位,或者使用 P-256、P-384 或 P-521 上的 ECDSA(§6.1.5.2)。
  • 有效期。2026年3月1日及之后签发的证书最长 460 天;此前为 39 个月(§6.3.2)。
  • 时间戳机构。TSA 证书最长有效 135 个月,其密钥可在 notBefore 之后的 15 个月内使用;SHA-1 时间戳令牌自2022年4月起已被禁止。

Microsoft 在此之上还加了自己的要求。自 Windows 10 版本 1607 起,内核模式驱动程序必须由 Microsoft 通过 Windows 硬件开发人员中心签名,因此仅凭商业证书无法加载驱动程序(驱动程序签名策略)。EV 也不再意味着“没有 SmartScreen 警告”:Microsoft 的分发功能状态页面写道,自2024年受信任根证书计划(Trusted Root Program)变更以来,“任何证书类型(OV 或 EV)都无法立即绕过”;信誉按文件哈希和签名身份分别累积。

Microsoft 的托管服务 Artifact Signing(由 Trusted Signing 更名而来)把密钥保存在 Microsoft 运营的 HSM 中,签发的证书“每天续期,有效期只有 72 小时”(证书管理)。只有摘要会离开你的构建机器。证书有效期这么短,不带时间戳的签名三天后就没有用了。同一页面还提到两个细节:由于证书每天都在变化,每个证书配置文件都会获得一个位于 1.3.6.1.4.1.311.97. 之下的持久自定义 EKU,可以把信任锚定在它上面(例如在应用程序控制策略中);该服务的 CA 正在从 CCADB 中移除,也已不在 Microsoft 受信任根证书计划中,但“仍保留在 Windows 平台信任列表中”,所以 Windows 会继续信任它们,其他平台则未必。

用 osslsigncode 签名和验证

osslsigncode 在 Linux 和 macOS 上实现了 Authenticode。实验(debian:forky-20261005-slim 中的 Debian 软件包 2.14)创建了一套私有测试层级:一个根 CA、一个签发 CA,以及一张带 codeSigning EKU 的 RSA 3072 代码签名证书;然后对程序签名,并向 DigiCert 的公共 TSA 申请时间戳:

lab.sh(节选):带 RFC 3161 时间戳签名,然后验证
osslsigncode sign -certs chain.pem -key codesign.key -h sha256 -n "Hello" -i "https://example.test/hello" \
  -ts http://timestamp.digicert.com -in hello.exe -out hello-signed.exe
osslsigncode verify -CAfile root.pem -TSA-CAfile /etc/ssl/certs/ca-certificates.crt -in hello-signed.exe

发送给 TSA 的只有签名的哈希。验证输出如下,重复的证书链列表已缩略:

osslsigncode verify(已缩略)
Message digest algorithm  : SHA256
Current message digest    : ADEAAA5366F711E064A9D54630D1E45520727730ECA3B85423D7570B0D41F7A7 
Calculated message digest : ADEAAA5366F711E064A9D54630D1E45520727730ECA3B85423D7570B0D41F7A7 

Signer's certificate:
	------------------
	Signer #0:
		Subject: CN=Example Software GmbH,O=Example Software GmbH,C=DE
		Issuer : CN=Example Test Code Signing CA,O=Example Test
		Serial : 20ADC33D7219F573885B44765077E96312F72F6D
		Certificate expiration date:
			notBefore : Oct 22 06:32:28 2026 GMT
			notAfter : Nov 21 06:32:28 2026 GMT

Authenticated attributes:
	Signing time: Oct 22 06:32:28 2026 GMT
	Microsoft Individual Code Signing purpose
	Message digest: DE669CE86A064FAEB4DFE8E43E14090CE5912FA036C527C13D5558B0BF110BB7 
	URL description: https://example.test/hello
	Text description: Hello

Countersignatures:
	Timestamp time: Oct 22 06:32:29 2026 GMT
	Signing time: Oct 22 06:32:29 2026 GMT
	Hash Algorithm: sha256
	Issuer: CN=DigiCert Trusted G4 TimeStamping RSA4096 SHA256 2025 CA1,O=DigiCert\, Inc.,C=US
	Serial: 084FDC334F7E454EDBC30F8FF9921835
...
Timestamp Server Signature CRL verification: ok
Timestamp Server Signature verification: ok
Signature verification time: Oct 22 06:32:29 2026 GMT
...
Signature verification: ok

Number of verified signatures: 1
Succeeded

“Current message digest”是签名中存储的哈希,“Calculated”是根据文件重新计算出的哈希。签名者自己的“Signing time”只是签名者时钟给出的一个声明;“Timestamp time”由 TSA 签名,验证方依赖的正是它。随后,osslsigncode 按时间戳中的时间验证了签名,这正是整套机制的意义所在。

为什么时间戳至关重要

为了直观看到这一点,实验不带 -ts 把同一个程序再签名一次,然后假设当前已是 60 天之后、有效期 30 天的测试证书已经过期,对两个文件分别验证(osslsigncode verify -time):

在当前时间 + 60 天时验证(已筛选输出行,OpenSSL 错误位置已缩短)
hello-signed.exe   Signature verification time: Oct 22 06:32:29 2026 GMT Signature verification: ok Succeeded
hello-nots.exe     ...:PKCS7_verify:certificate verify error:...:Verify error: certificate has expired Signature verification time: Dec 21 06:32:29 2026 GMT Error: certificate has expired ... Signature verification: failed Failed

有了时间戳,检查就以经过证明的签名时间为准,因此签名证书过期后,签名依然有效(本次实验中 DigiCert 时间戳响应方证书本身的有效期到2037年11月)。没有时间戳时,验证使用当前时间,证书过期当天验证就会失败,与任何 TLS 客户端遇到的 certificate has expired 错误相同。Microsoft 的说法更进一步:只要时间戳落在签名证书和 TSA 证书两者的有效期之内,签名就有效,而且“在签名证书和 TSA 证书过期很久之后,它仍然有效(除非其中任何一个被吊销)”。

吊销同样与时间挂钩。CSBR 允许 CA 把代码签名证书的 revocationDate 设为该证书最早被认定无效的日期,对于密钥泄露则是“怀疑发生泄露的最早日期”;并且“软件应当继续将包含早于吊销日期的时间戳的对象视为有效”(§4.9.6)。因此,密钥泄露后,失效的只是泄露之后才加盖时间戳的签名,而不是你发布过的每一个版本。

哪些改动会破坏签名,哪些不会

两种改动,用同一条命令验证:

改动一个代码字节(偏移 4096),然后在证书表内追加 64 字节
### one byte of code changed
Signature verification: failed
Message digest algorithm  : SHA256
Current message digest    : ADEAAA5366F711E064A9D54630D1E45520727730ECA3B85423D7570B0D41F7A7 
Calculated message digest : E754D0A7C1AB0CA1716866C67046BA54D2C195CDCBA4A182D56DD5C57FCD37B6     MISMATCH!!!
Failed
### extra bytes inside the certificate table
appended 64 bytes inside the certificate table: size 9240 -> 9304
Warning: invalid PE checksum
Calculated message digest : ADEAAA5366F711E064A9D54630D1E45520727730ECA3B85423D7570B0D41F7A7 
Timestamp Server Signature verification: ok
Signature verification: ok
Succeeded

第二个文件把证书表条目和 WIN_CERTIFICATE 的长度各增加了 64 字节,并在其中写入数据。由于证书表不计入哈希,签名仍能通过验证;只有同样不计入哈希的 PE 校验和是错的。这正是 CVE-2013-3900 背后的布局:会从自身文件末尾读取配置的安装程序,可能被塞进攻击者的载荷,而签名并不会因此失效。Microsoft 在2013年以一个需要主动启用的注册表设置 EnableCertPaddingCheck 修复了这个问题,而它在2022年重新发布的公告中表示“不打算将更严格的验证行为作为默认设置强制执行”。CISA 于2022年1月将该 CVE 加入其已知被利用漏洞目录。给开发者的教训:切勿从自己可执行文件的证书表区域读取数据,并在你管理的机器上设置 EnableCertPaddingCheck。

在 Windows 上:signtool

Windows SDK 中的 SignTool 是参考实现。当前版本要求显式指定摘要算法:“SignTool sign 命令需要文件摘要算法选项(/fd)和时间戳摘要算法选项(/td)”(SignTool)。证书在 Windows 证书存储中、密钥在硬件令牌上时,常用命令如下:

signtool(摘自 Microsoft 文档;本文未实际运行)
signtool sign /n "Example Software GmbH" /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /d "Hello" MyApp.exe
signtool verify /pa /tw /v MyApp.exe
  • /n 或 /sha1 用于选择证书;密钥留在令牌或 HSM 中,通过其 CSP 或 KSP 访问(可用 /csp 和 /kc 显式指定)。
  • /tr 向 RFC 3161 服务器请求时间戳;较老的 /t 使用旧版 Authenticode 时间戳协议。
  • verify /pa 使用默认的 Authenticode 策略;不加它时,SignTool 会采用驱动程序策略,拒绝普通签名。/tw 会在签名没有时间戳时发出警告,每条发布流水线都应该包含这项检查。
  • /dlib 和 /dmdf 用于接入一个远程对摘要签名的库;Artifact Signing 就提供了这样的库,开源的 Jsign(Apache-2.0,版本 7.6)则能在任何操作系统上配合 AWS KMS、Google Cloud KMS、Azure Key Vault、PKCS #11 令牌和 YubiKey 完成同样的工作。

在验证端,PowerShell 的 Get-AuthenticodeSignature 会返回状态和签名者,Windows 资源管理器的“数字签名”选项卡会显示时间戳。

在构建流水线中签名

密钥绑定在硬件上之后,构建服务器上就不再有密钥文件。可行的方案有三种:

  1. 云密钥服务:在 AWS KMS、Google Cloud HSM 或 Azure Key Vault Premium 中生成密钥,向接受该服务密钥证明或审计报告的 CA 申请证书,再用 Jsign 或厂商工具远程对摘要签名。审计日志要求是 CSBR 的一部分;用云 KMS 签名一文说明了哪些日志默认是关闭的。
  2. 托管签名服务,例如 Artifact Signing 或 CA 自己提供的服务:由服务方持有密钥和证书,你只发送摘要。
  3. 专用签名主机上的硬件令牌,只有发布任务能访问这台主机。方案简单,但这样一来,单独一台机器就处在你的关键路径上。

无论采用哪种方案,都只对发布制品签名,只从受保护分支签名,始终加时间戳,并在版本说明中记录文件哈希和签名身份,以便用户和 EDR 工具核对你发布的内容。

常见错误

  • 没有时间戳。证书过期之前一切正常,过期之后,每一份已安装的副本都会显示签名无效。证书只有 72 小时或 460 天有效期,这一天很快就会到来。
  • 任何环节使用 SHA-1。过去省略 /fd 时默认使用 SHA-1;当前的 SignTool 会拒绝执行,但从论坛复制来的旧脚本里仍然有 /fd sha1 或 /t。
  • 验证策略用错。不加 /pa 的 signtool verify 会把有效的商业签名报告为不受信任。
  • 证书缺少 codeSigning EKU,或者拿 TLS 证书凑合充当代码签名证书。验证方会拒绝它,就像 OpenSSL 会报 unsuitable certificate purpose 一样。
  • 缺少中间证书。在签名中包含完整的证书链(-certs chain.pem,或在签名主机上安装 CA 的中间证书),否则验证能否通过取决于用户机器上恰好有哪些证书。
  • 指望 EV 让 SmartScreen 不再警告。现在信誉需要随时间积累;每个版本都用同一个身份签名,信誉才能延续下去。
  • 从自己已签名文件的末尾读取数据(安装程序、自解压程序):这一区域不在签名覆盖范围内。

参考资料