ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Kubesphere 依赖链中的 sha1cd:一个具备碰撞检测能力的 SHA-1 实现

Kubesphere 依赖链中的 sha1cd:一个具备碰撞检测能力的 SHA-1 实现 Kubesphere 依赖链中的 sha1cd一个具备碰撞检测能力的 SHA-1 实现【免费下载链接】kubesphereThe container platform tailored for Kubernetes multi-cloud, datacenter, and edge management ⎈ ☁️项目地址: https://gitcode.com/GitHub_Trending/ku/kubesphere本文以 Kubesphere 仓库中 vendor 进来的vendor/github.com/pjbgf/sha1cd/README.md为主线完整讲解 sha1cd 这个带反密码分析counter-cryptanalysis碰撞检测能力的 SHA-1Go 库如何在代码中把它当作crypto/sha1的替代品来调用、CollisionResistantSum返回的碰撞标志位意味着什么并结合仓库内 v0.3.2 的实际源码剖析其 80 步压缩、3 个预步压缩状态、Disturbance Vector 校验表以及检测到碰撞后扩展为 240 步的完整实现机制。1. sha1cd 是什么以及它在 Kubesphere 仓库中的位置sha1cd 的 README 对库本身的定位只有一句话一个 Go 实现的 SHA-1具备碰撞攻击检测能力。原文同时交代了两条实现脉络README 中提到的cgo/lib部分是对原始 C 实现的carbon copy原样复制其算法基础是 Marc Stevens 获奖的 counter-cryptanalysis 白皮书纯 Go 部分则largely based off Gos generic sha1即基于 Go 标准库crypto/sha1的纯 Go 通用实现改写并且 README 明确说明当前没有实现 SIMD 优化。在 Kubesphere 仓库中这个库并不是被业务代码直接调用的。从 go.mod 可以看到它是以间接依赖的形式声明的github.com/pjbgf/sha1cd v0.3.2 // indirectmodules.txt 中登记了三个包路径也就是 vendor 目录实际保留的全部包github.com/pjbgf/sha1cd根包、github.com/pjbgf/sha1cd/internal和github.com/pjbgf/sha1cd/ubc。从源码结构看结合 go.mod 中同时声明了github.com/go-git/go-git/v5 v5.16.0可以推断 sha1cd 是经由 go-git 这条依赖链进入 Kubesphere 的依赖图的——go-git 系列库将 sha1cd 用作其内容寻址哈希的后端以在对象哈希边界上防御 SHA-1 碰撞攻击。对 Kubesphere 本身而言这份 vendor 代码的价值在于它把哈希后端如何抵抗已被攻破的 SHA-1 碰撞攻击这一机制完整地固化在了仓库内可以直接阅读。为什么 git 工具链要关心 SHA-1 碰撞因为 SHA-1 的密码学碰撞在 2017 年的 Shattered 演示后已是工程上可构造的。git 生态用 SHA-1 作为 blob/commit 的内容指纹若攻击者能构造出两个哈希相同但内容不同的对象仓库完整性与供应链安全就会受到威胁。sha1cd 的思路不是替换算法而是在计算哈希的同时在线检测输入是否正在遭受碰撞攻击并在检测到后改变输出行为。2. API 用法README 给出的两种调用方式2.1 基础用法SumREADME 给出的最简示例是把 sha1cd 当作crypto/sha1的 drop-in replacementimport github.com/pjbgf/sha1cd func test(){ data : []byte(data to be sha1 hashed) h : sha1cd.Sum(data) fmt.Printf(hash: %q\n, hex.EncodeToString(h)) }需要注意的一个版本差异README 的这段示例对应较早版本在当前仓库 vendor 的 v0.3.2 源码中Sum的签名已经返回了两个值——哈希本身加一个碰撞标志位见 sha1cd.go// Sum returns the SHA-1 checksum of the data. func Sum(data []byte) ([Size]byte, bool) { d : New().(*digest) d.Write(data) return d.checkSum(), d.col }因此用 v0.3.2 调用时应写作h, col : sha1cd.Sum(data)。2.2 显式询问碰撞CollisionResistantSumREADME 的第二个示例演示了如何获取是否检测到碰撞的信息import github.com/pjbgf/sha1cd func test(){ data : []byte(data to be sha1 hashed) h, col : sha1cd.CollisionResistantSum(data) if col { fmt.Println(collision found!) } fmt.Printf(hash: %q, hex.EncodeToString(h)) }CollisionResistantSum是接口层面的约定。detection.go 定义了CollisionResistantHash接口type CollisionResistantHash interface { // CollisionResistantSum extends on Sum by returning an additional boolean // which indicates whether a collision was found during the hashing process. CollisionResistantSum(b []byte) ([]byte, bool) hash.Hash }它的实现位于 sha1cd.go先复制一份 digestd0 : *d保证调用者可以继续 Write执行checkSum()完成 padding 与定长处理最后返回追加了摘要的字节切片和d0.col标志。2.3 流式用法New 与 NewGeneric除了一次性Sumv0.3.2 还提供两个构造函数sha1cd.go// New returns a new hash.Hash computing the SHA1 checksum. The Hash also // implements encoding.BinaryMarshaler and encoding.BinaryUnmarshaler to // marshal and unmarshal the internal state of the hash. func New() hash.Hash // NewGeneric is equivalent to New but uses the Go generic implementation, // avoiding any processor-specific optimizations. func NewGeneric() hash.Hash两者返回的hash.Hash都是*digest区别仅在于内部blockFunc指向哪个块处理函数New走默认路径在 amd64 gc 且未禁用汇编时为汇编实现NewGeneric强制走纯 Go 的blockGeneric。与标准库crypto/sha1一致Size()返回 20BlockSize()返回 64这两个常量在 internal/const.go 中定义为Size 20、Chunk 64。另一个值得注意的能力digest实现了encoding.BinaryMarshaler/encoding.BinaryUnmarshalersha1cd.go可以把哈希中间状态序列化到字节流再恢复。序列化格式以魔数shacd\x01开头总长度MarshaledSize为 6 5×4 64 8 98 字节internal/const.go反序列化时会校验魔数与长度不匹配则报错。3. 碰撞是怎么被检测出来的三个预步压缩状态这一节回答 README 中最关键的一句行为描述背后的原理。先回顾 SHA-1 的压缩结构输入按 64 字节Chunk分块每块经过 80 步Rounds 80每 20 步一轮分别使用K0~K3与不同的 f 函数压缩到 5 个 32 位字h0..h4上。sha1cd 在压缩循环中额外做了两件事全部实现在 sha1cdblock_generic.go 的blockGeneric里记录每步的消息字m1 [80]uint32保存该块 80 步各自使用的消息字记录三个预步压缩状态// cs stores the pre-step compression state for only the steps required for the // collision detection, which are 0, 58 and 65. cs : [shared.PreStepState][shared.WordBuffers]uint32{}对应源码中的三处采样点步骤 0块开始L46-L47cs[0] {a,b,c,d,e}步骤 58L84-L87步骤 65L100-L103。这正是 internal/const.go 中PreStepState 3的由来Currently there are 3 pre-step compression states required: 0, 58, 65.。3.1 检测逻辑DV 表、掩码与反向重放80 步走完后L122-L134hi 1即该块的第一次处理时调用checkCollision(m1, cs, state)state是块压缩完成后的h0..h4。其判定分三步L142-L179计算 DV 掩码ubc.CalculateDvMask(m1)根据该块 80 个消息字算出一个位掩码标识出该块在哪些 Disturbance VectorDV扰动向量意义下逼近碰撞条件查表匹配遍历ubc.SHA1_dvs()返回的 DV 表定义在 ubc/const.go对每条 DV 检查其MaskB对应的掩码位是否命中。表中每条DvInfo携带DvType、DvK目标字编号如 43~56、DvB字内 bit、TestT测试步本表中为 58 或 650 表示块首、以及一个 80 字的扰动消息增量Dm重放验证命中的 DV 交由hasCollidedL181-L268做真正的数学验证——从TestT处保存的中间状态出发先反向执行 SHA-1 的逆步用b RotateLeft32(b, -30)撤销左旋、按轮次的 f 函数与 K 常数做减法还原出中间哈希值IHV再以m2 m1 XOR Dm即攻击者构造的第二条消息正向重新压缩剩余步数最后把重建的 IHV 与块压缩的真实输出h逐字异或比较全零即判定碰撞成立L262-L265。用白皮书的术语概括碰撞攻击要求消息在某个中间步同时满足一组不可避免位条件unavoidable bit conditions。sha1cd 把这一条件转化为一次确定性的重放检验——如果攻击者的第二条消息 m2 在TestT步之后压缩结果必须与第一条消息 m1 完全一致才能成功碰撞那么真实h与 m2 重放结果相等就是攻击正在发生的数学指纹。ubc包文件头注释也点明了包职责ubc.go provides ways for SHA1 blocks to be checked for Unavoidable Bit Conditions that arise from crypto analysis attacks。4. 检测到碰撞后从 80 步扩展到 240 步README 用一小段话描述了检测结果对输出的影响原文要点是算法在检测到碰撞尝试时会自动避免碰撞——把 SHA-1 从 80 步扩展到 240 步因此包含不可避免位条件的输入其sha1cd哈希将与crypto/sha1的结果不同而合法非攻击输入的两者输出完全一致。源码中这一自动避免的落地方式非常直白命中碰撞后把dig.col true记下然后把同一个块再完整压缩两遍合计三遍 80 步即 240 步。纯 Go 路径用循环计数器与goto实现sha1cdblock_generic.go 的注释解释了动机// Collision attacks are thwarted by hashing a detected near-collision block 3 times. // Think of it as extending SHA-1 from 80-steps to 240-steps for such blocks: // The best collision attacks against SHA-1 have complexity about 2^60, // thus for 240-steps an immediate lower-bound for the best cryptanalytic attacks would be 2^180. // An attacker would be better off using a generic birthday search of complexity 2^80. rehash:hi从 1 递增到 2 时goto rehash第二次仍检测到碰撞则hi到 3 结束循环从而保证同一块最多压缩三次。amd64 汇编路径语义相同sha1cdblock_amd64.goblockAMD64处理一个 64 字节块并填好m1/cscheckCollision命中后把dig.col置真并再调用两次blockAMD64。注释中给出的安全论证值得完整引用SHA-1 上最好的碰撞攻击复杂度约为 2^60把被攻击的块强制重压到 240 步后对240 步 SHA-1构造同类攻击的下界被抬高到 2^180远超 2^80 的通用生日搜索成本——理性攻击者反而只能退回暴力搜索。换句话说重压不是为了产出更安全的哈希函数而是让特定攻击向量在该条数据流上失效。与之配套的是 internal/const.go 中的InitTmp0..InitTmp4五组常量注释写明它们是SHA recompression step重压缩步骤中临时变量ihvtmp0..ihvtmp4的初始值供汇编路径的快速重放使用。5. 源码结构与构建细节把 vendor 目录里的文件组织起来看整个库的分工是文件职责sha1cd.godigest状态机、New/NewGeneric/Sum/CollisionResistantSum、状态序列化detection.goCollisionResistantHash接口定义sha1cdblock_generic.go纯 Go 块压缩 checkCollision/hasCollided检测核心sha1cdblock_amd64.go、sha1cdblock_amd64.samd64 汇编块压缩构建标签!noasm gc amd64sha1cdblock_noasm.go禁用汇编时退回纯 Go 实现ubc/const.go、ubc/*.go/.sDV 位掩码计算与sha1_dvs扰动向量表internal/const.goSHA-1 公共常数K0..K3、Init0..4、Size、Rounds、Chunk、PreStepState、序列化魔数几点从源码结构可以直接确认的细节碰撞状态是逐 digest 的digest结构体中专门的col bool字段sha1cd.go只在检测到碰撞时置位Reset()会将其清零L115-L125因此同一digest复用前必须 Reset否则碰撞标志会跨段残留。对 Go 注册表的行为init()中执行了crypto.RegisterHash(crypto.SHA1, New)sha1cd.go。从源码结构看只要某个二进制 import 了 sha1cdGocrypto包注册表中 SHA1 的构造器就会指向sha1cd.New后续经crypto.New(crypto.SHA1)取哈希的调用方拿到的就是这个带检测的实现——这也是它能无缝替换标准库 SHA-1 的机制所在。无 SIMD 的准确性README 说At present no SIMD optimisations have been implemented与 v0.3.2 源码一致——amd64 汇编路径是标量汇编优化NewGeneric则给出与汇编无关的纯 Go 结果两者可用于交叉验证同一输入的普通哈希值。性能代价的边界checkCollision对每个 64 字节块都会做一次CalculateDvMask与查表只有在掩码命中后才可能进入hasCollided重放。对不触位条件的普通数据额外开销是每块的掩码计算重压240 步仅对攻击块发生。这一结论可由blockGeneric/block的控制流直接读出。6. 小结与延伸阅读sha1cd 在 README 中承诺的行为可以浓缩为三条且都能在本仓库 vendor 的 v0.3.2 源码中找到对应实现作为crypto/sha1的 drop-in replacementSize/BlockSize与标准库一致20/64API 形状相同检测到碰撞攻击输入时通过 3 次重压80→240 步使输出偏离标准 SHA-1并可通过Sum的第二个返回值或CollisionResistantSum显式读到col标志合法输入的哈希结果与crypto/sha1完全一致。对 Kubesphere 使用者而言理解这份库的实际意义在于当你使用基于 go-git 的 Git 操作功能时其对象哈希边界上已经内建了这套 SHA-1 碰撞攻击在线检测机制其完整算法资料counter-cryptanalysis 白皮书、原始 C 实现、NIST SHAVS 测试向量可在 README 的 References 一节中按名称追溯仓库内的入口文件为 vendor/github.com/pjbgf/sha1cd/README.md。【免费下载链接】kubesphereThe container platform tailored for Kubernetes multi-cloud, datacenter, and edge management ⎈ ☁️项目地址: https://gitcode.com/GitHub_Trending/ku/kubesphere创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进