SHA-256:从原理到构建、发布与运行时完整性
俊宁 · 日期未详文章目录
区分内容摘要、数字签名与平台签名,梳理 hash 对象、编码、可信摘要来源、集合闭包与失败恢复的设计检查。
本文讨论通用机制和设计判断;示意不对应任何具体产品的已交付状态。源码阅读坐标沿用原稿,本次整理不构成新的运行时验收。

(此版本省略该配图。)
一句话总结
| 职责 | 回答的问题 | 典型对象 |
|---|---|---|
| 输入完整性 | 下载或安装到本地的是不是约定输入 | Sparkle archive、Bun runtime、native CLI、sigclient |
| 制品内容身份 | 这个 Runtime、CRX、DMG、Installer 或 manifest 到底是哪份字节 | per-file hash、crxSha256、manifestSha256、DMG SHA-256 |
| 发布回读 | CDN/对象存储实际提供的内容是否等于刚上传的内容 | Sparkle artifact/appcast、Windows installer/CRX/descriptor |
| 运行时校验与协议 | 启动、安装或协议交互时是否拿到契约指定内容 | Runtime materialization、managed plugin、CUP、PKCE、CRX3 |
| 可复现证据 | 两次评测、缓存或生成任务是否属于同一输入范围 | performance scope、artifact baseline、obfuscation seed、Toolbox content address |
同一个算法可以出现在安全边界、缓存键和实验标识中。判断一处 SHA-256 是否真的构成安全控制,关键不在算法名字,而在“期望摘要从哪里来、谁能修改它、校验失败会不会阻断主流程”。
SHA-256 到底是什么
SHA-256 属于 SHA-2 家族,是一种 cryptographic hash function。它接收任意长度的消息,输出固定 256 bit,也就是 32 byte 的 message digest。常见十六进制编码每个字符表示 4 bit,因此完整摘要正好是 64 个 hex 字符。
以 UTF-8 字符串 hello 的字节为输入(不含结尾换行),输出为:
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
Node.js 中对应的最小写法是:
import { createHash } from 'node:crypto';
const digest = createHash('sha256')
.update(Buffer.from('hello'))
.digest('hex');
它有四个对工程系统很实用的性质:
- 同一串字节始终产生同一摘要。
- 输入只要发生微小变化,输出通常就会完全不同。
- 从摘要反推原始输入在计算上不可行。
- 找到两份不同输入却产生同一摘要,在现实工程规模下不可行。
NIST 在 FIPS 180-4 中把这类 digest 的用途概括为检测消息在摘要生成后是否发生变化。注意,这句话隐含了一个前提:比较使用的 expected digest 本身必须可信。
SHA-256 不是什么
它不是加密
加密的目标是“有密钥就能恢复明文”;hash 没有解密操作。对同一输入重复计算,只会再次得到相同摘要。
它不是数字签名
裸 SHA-256 没有私钥。任何人都能对任意文件计算摘要。如果攻击者可以同时替换文件和旁边记录的 SHA-256,那么比较仍会通过。
它不是 code signing、notarization 或 Gatekeeper
macOS App 的 Developer ID、nested code、entitlements、notarization 与 Gatekeeper 是平台信任链。DMG 的 SHA-256 相同,只能说明 DMG 字节相同,不能自动证明 App 签名有效、公证完成或 updater 能执行。
先建立一个正确的工程心智模型
一个完整的 hash contract 至少包含五个部分:
最常见的错误是只记录“用了 SHA-256”,却没有说明 hash 的是哪一层。例如下面四个对象逻辑上都可以叫“Runtime 1.0.0”,但它们不是同一串字节:
| 对象 | SHA-256 语义 |
|---|---|
artifact-metadata.json | metadata 文件的原始字节 |
runtime-manifest.json | 最终 manifest 文件的原始字节 |
manifest 中的 file.sha256 | package 内源文件字节 |
manifest 中的 file.outputSha256 | Brotli 解压后的运行时文件字节 |
所以,看到某个摘要时,第一句不应问“hash 对不对”,而应问“它绑定的是哪一层字节”。
设计一个新的 SHA-256 contract
第一步:先写清楚被 hash 的对象
不要只命名 hash。优先使用能表达层级的字段,例如 archiveSha256、manifestSha256、outputSha256、unsignedSha256。在 contract 注释中写清:
- 输入是原始 bytes 还是 UTF-8 string。
- JSON 是否 canonicalize,采用什么排序规则。
- 目录如何排序 path,是否加入 file type、executable bit、symlink target。
- 压缩前还是解压后。
- 签名前还是签名后。
第二步:固定编码与 schema
结构化总摘要应把 schemaVersion 放进被 hash 内容,避免未来更换 canonicalization 或目录遍历规则后,新旧算法产生无法解释的相同字段名。
第三步:给 expected digest 建立可信来源
安全校验的关键不是 createHash(),而是 expected digest 的控制面:
| expected digest 来源 | 可信度判断 |
|---|---|
与文件同目录、同权限、同时下载的 .sha256 | 只能发现偶发损坏,不能抵抗同源篡改 |
| Git review 后进入 Kernel manifest/lock | 能绑定受审查源码状态 |
| 被 private key 签名的 descriptor | 同时提供内容与发布者身份 |
| 用户手工复制的聊天文本 | 容易抄错、过期,缺少自动审计 |
第四步:同时校验 size、path 与集合闭包
- 路径必须相对且不能逃逸 owner root。
- 只允许 regular file,按场景拒绝 symlink。
- size 必须一致。
- manifest 中不能有重复 path。
- 实际集合不能缺文件或多文件。
- executable bit 在需要时必须一致。
这样可以在计算大文件摘要前快速失败,也防止“hash 正确但文件落到了错误位置”等结构问题。
第五步:定义失败语义与恢复动作
安全、完整性和签名门禁失败必须阻断后续动作;cache/hash 只用于优化时,则可以删除 cache 并重建。不要把二者混成 false 后静默继续。
一条好的错误至少要包含对象、expected、actual 和恢复动作,但避免泄露 private key、token、用户路径或敏感 URL query。
第六步:测试正反两条路径
至少覆盖:正确 bytes、单 byte 变化、size 变化、非法编码、uppercase/lowercase 约束、缺失/多余文件、symlink/路径逃逸、压缩前后不一致、expected digest 被替换,以及平台不支持优化时的安全降级。
常见误区
| 误区 | 正确判断 |
|---|---|
| “SHA-256 一样,所以文件可信” | 只能说明与某个 expected digest 一致;先问 expected digest 是否可信 |
| “文件名和版本号一样,可以复用 hash” | hash 绑定 bytes,不绑定逻辑名称 |
| “签名后 hash 不应该变” | code signing、notarization、staple、封装都会改变 bytes |
| “JSON 内容看起来一样,hash 就应该一样” | raw byte hash 受空格、换行和 key 顺序影响;只有明确 canonicalization 才是语义 hash |
| “目录里每个文件都 hash 了,目录身份就稳定” | 还要固定 path 排序、分隔符、metadata、symlink 与集合闭包 |
| “比较前 8/16 位已经够了” | 只适合目录名/显示;安全门禁比较完整 digest |
| “有 SHA-256,就不需要数字签名” | 裸 hash 没有发布者身份,不能替代 Ed25519/RSA/ECDSA 或平台 code signing |
| “hash 通过即可宣布上线” | source、package、signing、upload、channel 与客户端验收是独立状态 |
最后记住三句话
第一,SHA-256 是内容摘要,不是加密,也不是发布者身份。
第三,排障时永远同时问:hash 的是哪串字节、expected digest 从哪里来、失败后当前流程是否真的停止。
参考资料
- NIST FIPS 180-4:Secure Hash Standard:SHA-2 家族与 message digest 的正式定义。
- RFC 7636:Proof Key for Code Exchange by OAuth Public Clients:PKCE
S256的计算和安全边界。 - Chromium Cookie DB v24 change:domain SHA-256 prefix 的上游格式背景。