公司动态

工程原则,比工程实现更重要:从一次随机数事件看安全系统的根本边界

📅 2026/8/3 7:15:58
工程原则,比工程实现更重要:从一次随机数事件看安全系统的根本边界
工程实现决定今天能不能运行。工程原则决定十年以后还能不能被信任。一、事故发生后我们往往问错了问题当一个安全系统失败时绝大多数人的第一反应是是哪一行代码出了问题这是一个非常自然的问题。它符合工程师的直觉也符合排障的路径定位提交、复现缺陷、打补丁、发版本、写复盘。整个流程干净利落看起来问题已经解决了。但很多时候这并不是最重要的问题。更值得追问的是另一句为什么这样的代码能够进入系统这两个问题指向完全不同的层次。前者关心的是实现后者关心的是原则。前者能修复一次事故后者才能避免同一类事故在三年后换一个模块、换一个团队、换一种语言重新上演。Coldcard 随机数相关的讨论再一次提醒整个行业真正决定一个系统是否值得信任的往往不是某一种语言、某一块芯片、某一个算法而是系统背后的工程原则。代码缺陷是症状工程原则的缺失才是病因。二、我们熟悉的那些安全技术都没有错在安全工程领域有几条被广泛接受的常识Rust 比 C 更安全因为它在编译期消灭了大量内存问题Secure Element 比普通 MCU 更安全因为密钥可以不出芯片TRNG 比软件伪随机数更安全因为熵源来自物理世界HSM 比软件密钥库更安全因为它有物理防护和访问审计。这些判断本身都是成立的。但它们成立的前提是一个经常被忽略的隐含条件这些机制在运行时真的被使用了。一旦这个前提不成立上面所有结论都会同时失效。而让这个前提悄悄失效的往往不是攻击者而是我们自己写下的那几行健壮性代码。安全技术的价值不在于它被选型而在于它被实际执行。三、静默降级安全系统中最危险的容错来看几段在工程实践中极其常见的逻辑// 常见的健壮写法出问题就退回软件实现 int get_random_bytes(uint8_t *buf, size_t len) { if (trng_read(buf, len) TRNG_OK) { return 0; } ​ // TRNG 不可用退回软件随机数保证功能不中断 LOG_WARN(TRNG unavailable, fallback to PRNG); prng_read(buf, len); return 0; // ← 对调用方而言一切正常 }同样的模式会以各种形式出现在系统的不同角落硬件随机数不可用 → 自动切换到软件随机数安全芯片通信异常 → 自动改用软件密钥硬件单调计数器失败 → 自动改用软件计数器证书链校验超时 → 自动放行本次连接Secure Boot 校验分区损坏 → 自动加载备份镜像。从传统软件工程的角度看这些都是标准的容错设计系统没有崩溃。 业务可以继续运行。 用户甚至不会感觉到任何异常。日志里也许留下了一行WARN但没有人会在凌晨三点因为一条 WARN 被叫醒。问题恰恰在这里。对于一个安全系统来说这可能是最危险的状态——不是失效而是看起来仍然有效。设备照常出货密钥照常生成签名照常产生用户照常信任。唯一改变的是这一切背后的安全假设已经和设计文档上写的不是同一件事了。真正危险的不是系统停止运行而是系统降低了安全等级却仍然继续运行。更糟的是静默降级具有时间放大效应。一次崩溃只影响一个瞬间而一次静默降级会持续污染它之后产生的所有密钥、所有签名、所有身份凭证。等到问题被发现时需要召回的不是一个版本而是一整段历史。崩溃是一个点降级是一条线。四、可用性与可信性两种根本不同的工程目标过去几十年主流软件工程围绕高可用Availability建立了一整套方法论数据库失败要有主备切换缓存失败要有降级读库网络失败要有重试与熔断依赖失败要有兜底默认值。这些原则没有任何问题。因为业务系统的核心目标是持续提供服务——服务中断就是损失可用性就是价值。但安全系统追求的是另一样东西可信Trust。对于承担Root of Trust根信任的模块来说目标发生了根本性的反转维度业务系统安全系统首要目标持续可用持续可信故障期望行为Fail-open继续服务Fail-closed停止服务最坏结果服务中断信任污染降级的意义保住用户体验摧毁安全前提修复成本恢复即结束需要追溯与撤销随机数、安全芯片、硬件计数器、Secure Boot、设备身份——这些都不是普通的业务逻辑。它们决定的是一件事整个系统是否仍然值得信任。一旦这些基础发生变化系统最正确的行为不是继续运行而是明确地停下来。根信任允许失败但绝不允许降级。把上面那段代码按这个原则重写差别其实只有几行int get_random_bytes(uint8_t *buf, size_t len) { if (trng_read(buf, len) TRNG_OK) { return 0; } ​ // 熵源不可用 安全前提不成立禁止任何静默替代 security_fault(FAULT_ENTROPY_UNAVAILABLE); // 记录、上报、锁定 return -EIO; // 明确失败绝不伪装成功 }代码没有变复杂变的是立场它不再试图替调用方做决定而是把安全前提已经不成立这个事实如实地暴露出来。在安全路径上返回一个错误比返回一个可疑的成功要负责得多。五、从相信到证明可验证性才是安全的地基一个成熟的安全系统不应该依赖我相信它用了 TRNG而应该依赖我可以证明它用了 TRNG。需要能够被证明的问题包括这段随机数究竟来自哪个熵源密钥是在哪里生成的是否离开过安全边界安全芯片是否真正参与了本次运算而不只是存在于 BOM 表上运行期间有没有发生过降级有没有进入过备用路径有没有绕过设计时的任何一条安全假设要回答这些问题工程上通常需要三层机制1安全状态必须是显式的一等公民不要让安全等级隐藏在日志里。它应该是一个可读取、可上报、可断言的状态量enum TrustLevel { Attested, // 硬件路径完整可信 Degraded, // 曾发生降级不可用于密钥生成 Compromised, // 安全前提已被破坏永久锁定 }2降级必须是不可逆的、可见的一旦进入Degraded就不能因为下一次调用成功而悄悄回到Attested。安全状态只能单向下降恢复必须经过显式的、有记录的流程。3关键操作必须校验前置状态而不是假设它成立fn generate_master_key(state: SecurityState) - ResultKey, SecError { if state.trust_level() ! TrustLevel::Attested { return Err(SecError::UntrustedEntropyPath); // 拒绝而不是尽力而为 } // ... }这些证明并不是为了应付审计报告而是为了回答一个更本质的问题今天系统所依赖的信任是否仍然是最初设计时的那个信任安全不能建立在相信之上只能建立在可证明之上。无法被观测的安全属性等价于不存在的安全属性。六、现代系统的真实风险藏在模块之间随着软件规模不断膨胀风险的形态也在发生迁移。今天一个中等复杂度的产品往往同时包含C 编写的底层驱动Rust 编写的核心逻辑Python 编写的工具链与测试若干第三方开源库不同厂商提供的 SDK不同厂商提供的安全芯片。没有任何一个工程师能够完整掌握整个系统。每个人只对自己那一层负责而层与层之间靠假设连接。于是越来越多的问题不再来自某一个模块而来自模块之间驱动层认为上层会检查返回值。中间层认为驱动出错时会自己处理。应用层认为底层给我的一定是安全的熵。SDK 文档写着调用方应确保硬件已正确初始化。每一层都认为另一层已经完成了这件事情。最后没有任何一层真正完成。这类问题的特征是代码审查通不出、单元测试测不到、静态分析扫不出。因为每个模块单独看都是正确的错误只存在于它们对彼此的想象里。现代安全系统最大的风险越来越不是某个模块失败而是模块之间的信任关系与真实执行路径发生了偏离。对抗这种风险靠的不是更强的个人能力而是把隐含假设显式化接口契约中明确写出安全前提而不是留在文档角落跨层调用统一失败语义禁止用返回值 0 表示降级成功建立跨模块的安全路径追踪能端到端回答这个密钥的熵从哪来在集成测试中主动注入故障拔掉 TRNG、断开 SE、伪造计数器失败看系统是停下来还是笑着继续跑。没有被故障注入验证过的容错逻辑只是一段从未被执行过的祈祷。七、落地把原则变成可执行的约束原则如果不能落到 CI 和代码评审里就只是海报。给出一份可以直接套用的检查清单设计阶段明确列出系统的 Root of Trust 清单熵源、密钥存储、启动校验、设备身份、防回滚计数器。对清单中的每一项明确标注该项失败时系统的正确行为是什么。默认答案应当是停止而不是继续。把允许降级与禁止降级的边界写进设计文档并作为评审的强制项。实现阶段安全路径上禁止任何隐式 fallback所有替代路径必须显式、必须可见、必须可查询。失败必须沿调用链向上传播禁止在中间层被消化成成功。安全状态单调不可逆恢复需要显式流程。验证阶段对每一个 Root of Trust 组件做故障注入测试断言系统进入锁定态而非降级态。在出厂测试与运行时自检中校验硬件路径确实被使用而不只是硬件存在。把安全状态纳入可观测性体系降级必须触发告警而不是留下一条无人阅读的 WARN。可降级的边界必须在设计时划定而不是在故障发生时临时决定。八、结语真正决定系统寿命的东西回头看那些重大安全事故结论往往令人意外地朴素。它们大多不是因为算法不够先进也不是因为芯片不够昂贵更不是因为团队不够聪明。而是因为系统在某个不起眼的地方违反了最基本的工程原则。技术会不断演进芯片会不断升级语言会不断更替。但真正决定一个系统寿命的从来不是这些而是那些几十年都不会改变的原则。真正优秀的安全系统从来不是因为它永远不会失败。而是因为它清楚地知道哪些地方可以降级哪些地方绝不能降级。工程实现决定今天能不能运行。工程原则决定十年以后还能不能被信任。工程原则是安全系统最后一道看不见的边界。当工程原则被破坏再完美的实现也只是建立在错误基础上的正确代码。如果这篇文章对你有启发欢迎在评论区聊聊你所在的系统里有哪些看起来很合理的降级路径其实正站在安全边界的另一侧