Git如何处理一个blob上的SHA-1冲突?

这可能在现实世界中从未发生过,也可能永远不会发生,但让我们考虑一下:假设您有一个git存储库,进行了一次提交,然后非常非常不幸:其中一个blob最终与存储库中已经存在的另一个blob具有相同的SHA-1。问题是,Git将如何处理这个问题?简单的失败?找到一种方法来连接这两个blob,并根据上下文检查哪个是需要的?

与其说这是一个真正的问题,不如说是一个脑筋急转弯,但我觉得这个问题很有趣。

81411 次浏览

根据箴Git:

如果您提交的对象与存储库中的前一个对象散列到相同的SHA-1值,Git将在Git数据库中看到前一个对象,并假定它已经被写入。如果您试图在某个时刻再次检出该对象,则总是会得到第一个对象的数据。

所以它不会失败,但也不会保存你的新对象。
我不知道这在命令行上会是什么样子,但这肯定会令人困惑

再往下看一点,同样的参考文献试图说明这种碰撞的可能性:

这里有一个例子,让您了解如何获得SHA-1碰撞。如果地球上65亿人都在编程,每秒钟每个人都在生成相当于整个Linux内核历史(100万个Git对象)的代码,并将其推入一个巨大的Git存储库,那么需要5年的时间,该存储库才能包含足够的对象,使单个SHA-1对象发生碰撞的概率达到50%。在同一个晚上,你的编程团队的每个成员都有可能在不相关的事件中被狼袭击并杀死。

原始答案(2012年)(见下面shattered.io 2017 SHA1碰撞)

旧的(2006)答案来自莱纳斯可能仍然相关:

没有。如果它具有相同的SHA1,这意味着当我们从另一端接收到对象时,我们将覆盖我们已经拥有的对象。

因此,如果我们看到冲突,任何特定存储库中的“较早”对象最终都会被覆盖。但是请注意,“更早”显然是每个存储库,从某种意义上说,git对象网络生成的DAG并不是完全有序的,因此虽然不同的存储库在直接祖先的情况下会同意什么是“更早”,但如果对象来自独立的且不直接相关的分支,两个不同的repo显然可能以不同的顺序获得两个对象。

然而,从安全的角度来看,“早期将覆盖”正是你想要的:记住,git模型是,你应该主要只信任你的自己的存储库 因此,如果您执行“git pull”,根据定义,新的传入对象比您已经拥有的对象更不可信,因此,允许新对象这样做是错误的

.替换旧的

所以有两种碰撞情况

  • 无意的那种,在这里你非常非常不幸,两个文件最终具有相同的SHA1 在这一点上,发生的事情是当您提交该文件(或执行“git-update-index”将其移动到索引中,但尚未提交)时,将计算新内容的SHA1,但因为它匹配了一个旧对象,所以不会创建一个新对象,提交或索引最终指向old对象.
    您不会立即注意到(因为索引将匹配旧对象SHA1,这意味着像“git diff”这样的东西将使用签出的副本),但如果您曾经执行树级差异(或者您执行克隆或拉出,或强制签出),您会突然注意到该文件已更改为与您预期的完全不同的内容 所以你会很快注意到这种碰撞
    在相关新闻中,问题是如何处理意外碰撞 首先,让我提醒人们,这种无意的碰撞真的非常非常不可能,所以我们很可能永远不会在宇宙的整个历史中看到它 但是如果发生了,这不是世界末日:您最可能要做的就是稍微更改碰撞的文件,并强制使用更改后的内容进行新的提交(添加一个评论说“/* This line added to avoid collision */”),然后教git关于神奇的SHA1,已经被证明是危险的 所以在几百万年的时间里,也许我们将不得不添加一两个“有毒”的SHA1值来git。这不大可能是维护问题;)

  • 攻击者类型的碰撞因为有人破坏了(或暴力强行)SHA1.
    这个库显然更可能是很多,而不是无意中的类型,但根据定义,它始终是一个“远程”存储库。如果攻击者可以访问本地存储库,他就有更容易的方法来搞砸你 所以在这种情况下,这次碰撞完全不是问题:你会得到一个“坏”的存储库,这与攻击者的意图不同,但是因为你永远不会真正使用他的碰撞对象,它是字面上与攻击者根本没有发现碰撞没有什么不同,但只是使用了你已经拥有的对象(即它100%等效于相同文件的“平凡”碰撞,产生相同的SHA1)

使用SHA-256的问题经常被提及,但现在还没有采取行动(2012) 注意:从2018年开始,Git 2.19,代码正在被重构为使用SHA-256

注意(幽默):你可以强制提交一个特定的SHA1 前缀,项目< >强gitbrute < / >强布拉德·菲茨帕特里克(bradfitz)

Gitbrute强制生成一对作者+提交者时间戳,这样得到的git提交就有您想要的前缀。

例如:# EYZ0


丹尼尔Dinnyes指出在评论中7.1 Git工具-版本选择,其中包括:

在同一个晚上,你的编程团队的每个成员都有可能在不相关的事件中被狼袭击并杀死。


甚至最近的(2017年2月)<强> # EYZ0 < / >强也证明了伪造SHA1碰撞的可能性 (在我的单独回答 .中看到更多,包括Linus Torvalds的谷歌+帖子)

  • a/仍然需要超过9,223,372,036,854,775,808次SHA1计算。这相当于6500年单cpu计算和110年单gpu计算的处理能力。
  • b/将伪造一个文件(具有相同的SHA1),但附加约束其内容而且大小将产生相同的SHA1(仅对内容的碰撞是不够的):参见“git哈希是如何计算的?”):a blob SHA1是基于内容而且大小计算的。

参见瓦莱丽安妮塔奥罗拉中的“加密哈希函数的生命周期” 在那一页中,她写道:

谷歌花费了6500年的CPU和110年的GPU来说服所有人,我们需要停止在安全关键应用程序中使用SHA-1 也因为它很酷

在我的单独回答下面中看到更多。

我想密码学家们会庆祝的。

引用维基百科关于SHA-1的文章:

2005年2月,王晓云、尹轶群和余洪波被宣布发动攻击。 攻击可以在完整版本的SHA-1中找到冲突,只需要少于2^69次操作。(蛮力搜索需要2^80次操作)

我做了一个实验,以找出Git在这种情况下的确切表现。这是2.7.9~rc0+next.20151210版本(Debian版本)。我基本上只是通过应用以下diff和重建git将哈希大小从160位减少到4位:

--- git-2.7.0~rc0+next.20151210.orig/block-sha1/sha1.c
+++ git-2.7.0~rc0+next.20151210/block-sha1/sha1.c
@@ -246,6 +246,8 @@ void blk_SHA1_Final(unsigned char hashou
blk_SHA1_Update(ctx, padlen, 8);


/* Output hash */
-   for (i = 0; i < 5; i++)
-       put_be32(hashout + i * 4, ctx->H[i]);
+   for (i = 0; i < 1; i++)
+       put_be32(hashout + i * 4, (ctx->H[i] & 0xf000000));
+   for (i = 1; i < 5; i++)
+       put_be32(hashout + i * 4, 0);
}

然后我提交了几次,并注意到以下内容。

  1. 如果已经存在具有相同散列的blob,则根本不会收到任何警告。一切似乎都很好,但当你推送,有人克隆,或你恢复,你将失去最新的版本(与上面解释的一致)。
  2. 如果一个树对象已经存在,并且您使用相同的散列创建了一个blob:一切看起来都很正常,直到您尝试推送或有人克隆您的存储库。然后你会发现回购是腐败的。
  3. 如果一个提交对象已经存在,并且你创建了一个具有相同散列的blob:与#2 - corrupt相同
  4. 如果一个blob已经存在,并且你创建了一个具有相同散列的commit对象,它将在更新“ref”时失败。
  5. 如果一个blob已经存在,并且您使用相同的散列创建了一个树对象。它将在创建提交时失败。
  6. 如果一个树对象已经存在,并且你创建了一个具有相同散列的提交对象,那么在更新“ref”时它将失败。
  7. 如果一个树对象已经存在,并且您使用相同的散列创建了一个树对象,那么一切看起来都没问题。但是当您提交时,所有存储库都会引用错误的树。
  8. 如果一个提交对象已经存在,并且您使用相同的散列创建了一个提交对象,那么一切看起来都是正常的。但是当你提交时,提交将永远不会被创建,HEAD指针将被移动到一个旧的提交。
  9. 如果一个提交对象已经存在,并且您创建了一个具有相同散列的树对象,那么在创建提交时它将失败。

对于#2,当你运行"git push"时,你通常会得到这样的错误:

error: object 0400000000000000000000000000000000000000 is a tree, not a blob
fatal: bad blob object
error: failed to push some refs to origin

或者:

error: unable to read sha1 file of file.txt (0400000000000000000000000000000000000000)

如果你删除文件,然后运行“git checkout file.txt”。

对于#4和#6,你通常会得到这样的错误:

error: Trying to write non-commit object
f000000000000000000000000000000000000000 to branch refs/heads/master
fatal: cannot update HEAD ref

当运行“git commit”时。在这种情况下,你可以再次输入“git commit”,因为这会创建一个新的散列(因为时间戳改变了)

对于#5和#9,你通常会得到这样的错误:

fatal: 1000000000000000000000000000000000000000 is not a valid 'tree' object

当运行git commit时

如果有人试图克隆你损坏的存储库,他们通常会看到如下内容:

git clone (one repo with collided blob,
d000000000000000000000000000000000000000 is commit,
f000000000000000000000000000000000000000 is tree)


Cloning into 'clonedversion'...
done.
error: unable to read sha1 file of s (d000000000000000000000000000000000000000)
error: unable to read sha1 file of tullebukk
(f000000000000000000000000000000000000000)
fatal: unable to checkout working tree
warning: Clone succeeded, but checkout failed.
You can inspect what was checked out with 'git status'
and retry the checkout with 'git checkout -f HEAD'

让我“担心”的是,在两种情况(2,3)中,存储库会在没有任何警告的情况下损坏,在三种情况(1,7,8)中,一切看起来都很好,但存储库内容与您期望的不同。人们克隆或复制的内容将与你所拥有的内容不同。情况4、5、6和9是可以的,因为它将停止与一个错误。至少在所有情况下,我认为如果它失败并出现错误会更好。

要添加到我之前的答案是2012年的,现在(5年后的2017年2月),有一个实际的SHA-1与< >强shattered.io < / >强碰撞的例子,在那里你可以制作两个碰撞的PDF文件:即在第一个PDF文件上获得SHA-1数字签名,这也可以被滥用为第二个PDF文件的有效签名 参见“多年来,广泛使用的SHA1函数已经濒临死亡”和“这幅图”。

2月26日更新:莱纳斯确认了以下几点在谷歌+的帖子里:

首先,天没有塌下来。对于安全签名之类的事情使用加密散列,与为内容可寻址的系统(如git)生成“内容标识符”使用加密散列之间存在很大区别。

(2)其次,这种特殊的SHA1攻击的性质意味着它实际上很容易缓解,并且已经发布了两组补丁来缓解。

(3)最后,实际上有一个相当简单的过渡到其他一些哈希,不会破坏世界-甚至旧的git存储库。

关于转换,请参阅添加表示哈希算法的结构的2018年Q1 Git 2.16。这一过渡的实施已经开始。

启动Git 2.19(2018年第三季度), Git已经选择了SHA-256为NewHash,并且正在将其集成到代码中(这意味着SHA1仍然是默认的(2019年第二季度,Git 2.21),但SHA2将是继任者)


原始答案(2月25日) 但是:< / p >

乔伊赫斯尝试Git回购他发现中的pdf:

包含两个具有相同SHA和大小的文件 不同的blobs多亏了git将头文件前置到 内容。< / p >

joey@darkstar:~/tmp/supercollider>sha1sum  bad.pdf good.pdf
d00bbe65d80f6d53d5c15da7c6b4f0a655c5a86a  bad.pdf
d00bbe65d80f6d53d5c15da7c6b4f0a655c5a86a  good.pdf
joey@darkstar:~/tmp/supercollider>git ls-tree HEAD
100644 blob ca44e9913faf08d625346205e228e2265dd12b65    bad.pdf
100644 blob 5f90b67523865ad5b1391cb4a1c010d541c816c1    good.pdf

当向这些碰撞文件追加相同的数据时,确实会生成

.

.

所以主要攻击向量(伪造提交)将是:

  • 生成一个常规提交对象;
  • 使用整个提交对象+ NUL作为所选的前缀,并且
  • 使用相同前缀的碰撞攻击来生成碰撞好的/坏的对象。
  • ... 这是无用的,因为好的和坏的提交对象仍然指向同一个树!

此外,您已经可以使用<强> # EYZ0 < / >强检测每个文件中针对SHA-1的密码分析碰撞攻击

在Git本身会有一些计算成本中添加类似的检查。

在改变哈希值时,Linux的评论:

哈希的大小和哈希算法的选择是独立的问题 你可能会做的是切换到256位哈希,使用它 在内部和本地git数据库中,然后仅在默认情况下 显示将散列作为40个字符的十六进制字符串(有点像我们如何 在很多情况下已经缩写了) 这样,git周围的工具甚至看不到更改,除非传入 一些特殊的“--full-hash”参数(或“--abbrev=64”或其他参数) 默认是缩写为40)。

仍然是一个转换计划(从SHA1到另一个哈希函数)仍然是复杂的,但积极学习 convert-to-object_id运动在进行中:


3月20日更新:GitHub详细介绍了可能的攻击及其保护:

可以通过各种机制为SHA-1名称分配信任。例如,Git允许您对提交或标记进行加密签名。这样做只对提交或标记对象本身进行签名,而该对象又通过使用它们的SHA-1名称指向包含实际文件数据的其他对象。这些对象中的碰撞可能产生一个看似有效的签名,但它指向的数据与签名者预期的数据不同。在这样的攻击中,签名者只看到了碰撞的一半,而受害者看到了另一半。

保护:

最近的攻击使用特殊技术来利用SHA-1算法中的弱点,在更短的时间内发现碰撞。这些技术在字节中留下了一种模式,当计算碰撞对的任意一半的SHA-1时,可以检测到这种模式。

GitHub.com现在对它计算的每个SHA-1执行这种检测,如果有证据表明该对象是碰撞对的一半,则会中止该操作。这可以防止攻击者使用GitHub来说服一个项目接受他们碰撞的“无辜”一半,以及防止他们托管恶意的一半。

参见< >强马克·史蒂文斯< / >强的“<强> # EYZ0 < / >强


同样,2018年Q1 Git 2.16添加了一个表示哈希算法的结构,向新哈希转换的实现已经开始 如上所述,新支持的哈希值将是sha - 256.