Git 使用中的教训:签名提交确保代码完整可信

开发 后端 前端 Linux 系统运维 开源
Git中每一个提交的SHA-1哈希值是根据每一个提交的增量和头信息来生成的。头信息里包含着这一提交的父提交的头信息,父提交的头信息里又包含它的父提交的头信息----以此类推。……

凌晨2点,在安静的房间里,你的孩子已入睡,你的另一半还在沙发上等着你,却已经睡着好一段时间。电视的灯光还闪烁在你的眼角。你的身心已极度疲惫。你为今晚所取得的进展感到欣慰,并提交了代码,它包含过去几个小时的成果:“[master 2e4fd96] 固定安全漏洞 CVE-123”。你把你的更新上传至你的主机服务器,这样其他人就能在天亮后进行的关键版本发布之前,去查看和评论你的更改。挂起(待机)你的电脑,再去把你的另一半摇摇醒,让他/她去床上睡。你再去关掉灯,却因为在卧室里被一个玩具绊倒,然后因为你的孩子听到了他/她最喜欢的玩具发出的声音,你不得不去为他/她泡奶喝。

四小时严重不足的睡眠时间快速过去,你被你设定的手机震动声唤醒。你拍了拍你的脸,想了想你的闹钟,然后,迟钝地从床上爬出来,合上床头柜。(哎!你可能再次把你的孩子吵醒。)你拿起手机,一个疯狂的同事在向你打招呼,“我把我们变更的代码合在一起了,我们需要一个标签打在这个地方”。啊,该死!让你醒来的另一半,叫他/她去照顾一下哭泣的孩子(嗯,进展顺利!),跌跌撞撞地打开你的电脑,困难地键入你的密码,揉揉你的眼睛,把更改的东西给下下来。

你眯着眼瞥了一下,变化像洪水一样涌向你。房间里充斥着孩子尖锐的哭泣声,你的另一半无奈地用微弱的气力去尝试控制局面。git log --pretty=short...每一样东西看起来都不错——你的同事和你的提交,已经在一个分支上被合并。你运行测试包——一切都通过了。看起来你已经准备好了。git tag -s 1.2.3 -m '各种各样的修正, 包含关键的 CVE-123' && git push --tag。 经过努力输入密码到你的私钥,在你键入之后,缓缓地从你的椅子上站起来,跑去帮忙照顾孩子(天啊,他们把这些源代码放哪儿了),你的CI系统将会处理剩下的东西。

快进两个月的时间(两个月之后)。

CVE-123补丁已经被覆盖很久了并且部署地很成功。但是,你收到了同事一个愤怒回应。看起来,你的一个重点用户有一个巨大的安全漏洞。在研究这个问题之后,你的同事找到了这个问题的根源,根据历史日志,漏洞利用了一个你创建的后门! 什么?你从来没想过这个。更糟的是,1.2.3还是你签署的,使用的是你的GPG钥匙(key)——你还确认过当初打这个标签是有效的是有准备的。“3-b-c-4-2-b,混蛋”,你的同事调侃的说,“非常感谢你啊”。

不——那没有意义。你快速地检查历史提交。git log --patch 3bc42b. "为X,Y和Z添加缺失的文档块。"你在困惑的表情中,抬起你的手,轻微地敲了几下键盘空格键,不带任何期待。果然,一些次要的文档块改变了,有一行非常不起眼的代码改变了,导致一个后门被添加进了认证系统。提交信息清楚的显示,没有添加任何红色标记——为什么你不检查一下?此外,提交的作者真是你自己!

你的脑中各种思绪在飞奔!怎么会发生这种事情呢?那次提交是你的名字,但是你没有回忆起那些曾经的变更。而且,你感觉没有改过那行代码;但,这是没有意义的。难道是你的同事在陷害你,让你提交的?还是你的同事的系统被盗用了?还是你的电脑被盗用了?它没有在你的本地存储器上;那次提交是很清楚的合并部分,并没有存在在你的本地存储中,直到你追溯到那个两个月前的早上。

不管发生了什么,有一件事情是极其可怕且清晰的,现在,你是那个被指责的人。

你相信谁?

将你想要的理论化——或许你可能永远不知道是什么导致你的仓库(repository)危害的。上面的故事纯属虚构,但也是完全有可能的。怎么做才能在休息的时候,保证你的仓库对引用(reference)或克隆(clone)它的程序员和可能下载它(例如,从它创建的压缩包)的人都是安全的。

GIT是一款分布式版本控制系统。简而言之,这意味着任何人可以私自拥有一份你的仓库的副本(copy)进行线下工作。他们可能提交版本到自己的仓库,也可以相互之间进行push和pull。中心仓库对分布式版本控制系统来说不是必需的,然而它可以作为一个“官方”中心仓库,其他程序员可以提交工作和克隆。因此,这也意味着某个广为流传的项目X的仓库可能含有恶意代码,就因为其他人交给你一个项目仓库并不代表你就应该实际使用它。

问题不是“我可以相信谁?”;问题是“我相信谁”,或者说你新人你的仓库,甚至你没意识到这一点。对大多数项目来说,包括上面的故事,有许多的个人或组织无意识的把信任放到没有经过充分考虑的决定的分支中:

  • Git 主机

  • Git托管供应商可能是你最容易忽略的受托人,比如Gitorious, GitHub, Bitbucket, SourceForge, Google Code等。每个供应商提供你的仓库的托管和保密。保密措施是通过只允许你和其他的授权的用户使用和账户绑定的SSH密钥去上传文件。使用一个主机作为你的仓库的主要保存地方,这个地方是你托管你整个项目的地方。你说:“是的,我相信我托管的源代是安全的、不会被篡改的”。这是一个危险的想法。 你相信你的主机妥善保护您的帐户信息吗?此外,所有的软件都有bug,这些bug大多出现在一些琐碎的片段中,那么是说,在你的主机系统中没有一个漏洞完全危害你的仓库?

不久以前(2012年3月4号),一个 GitHub的公钥安全漏洞被一个叫 Egor Homakov的俄国人利用了。这个漏洞允许他向GitHub上的 Ruby on Rails框架的主分支上提交代码

  • 朋友和同事/协作者

  • 有一些你信任的某些团体或个人,从“官方”仓库下载或获取补丁,或者允许他们上传代码。假设每个人是真正值得信任的,但这并不能立即表明他们的仓库是可以信任的。他们的安全策略是什么?他们是不是在电脑未锁定状态下离开了?他们是不是有从不安全的网站上下载色情文件的习惯?或者他们运行了一个容易收到0-day攻击的软件。试想,你怎么能够确定是他们本人进行的提交操作?而且,你怎么能够确定那些提交是被他们认可的提交?

假设,他们的网络是安全的、“干净”的。例如,一个愤怒的员工得到了自大的、讨厌的同事的名字/邮箱,并利用名字/邮箱进行了提交。如果你是经理或者项目领导,你该相信谁?你该去怀疑谁?

  • 你自己的仓库

  • Linus Torvalds(Git和Linux内核的创始人)在他的电脑上保存了一个秘密的仓库,来保证有一个他可以完全信任的仓库。大多数开发者只是在他们工作的机器上保存了一个备份,而且并不重视安全性,毕竟,他们的仓库托管在了其它地方。Git是一个分布式的,这是一个很严重的问题。

你使用你的电脑并不仅仅是开发,更多的是,你用它浏览网页、下载软件。并不是每个开发者都有较强的操作系统的安全意识。而且,简单的使用GNU/Linux或其它对*NIX系统并不意味着你对潜在的威胁具有免疫力。

探讨地更深入一点,让我们考虑一下世界上最大的开源软件项目——Linux内核——它的原创者Linus Torvalds是如何处理信任问题的。他2007年在谷歌的一次演讲中,描述了其建立他和其他人(他指的是他的“助手们”)之间可信赖联系的事。Linus他本人不可能管理那些数量庞大的代码,因此有其他人帮助他处理内核的一部分代码。这些“助手们”处理了大部分需求,之后提交给Linus,他来合并进他的分支。在这样的情况下,他已经信赖这些助手们的工作,他们会仔细地查看每一个补丁,实际上Linus的很多补丁都来源于他们。

我不了解补丁从助手们到Linus是如何进行交流的。当然,一种可能的方式就是这些高水平经确认的补丁在他的“助手们”手中是通过E-Mail发给Linus,然后他们再各自使用自己的GPG/PGP钥匙去确认签署。这说明,信任网络的执行是通过签名确认的。Linus来确保他私人的资源库(他尽他最大的可能确保安全性,就前面提及的),这些库里包含的只有他私人信赖的数据。他的资源库是安全的,就他所知,他可以自信地使用它们。

在这一点上,假设Linus的关系网络信任是正确验证的,他怎么样才能将这些信任上的变化充满信心地传递给其他人呢?他肯定知道他自己的提交,但是其他人怎么知道这个提交是“Linus Torvalds”这个人实际签署的呢?证明这个假设的场景在这篇文章的开头,任何人都可以宣称自己就是Linus。如果攻击者获取对存储库的任何克隆权限的使用,并以Linus之名提交,就不会有人知道差异。幸运的是,一个人能绕过签署的标签,需要提供他/她私有的GPG钥匙(git tag -s)。一个标签指向一个特别的提交并且那个提交依赖整个历史记录的引入。这意味着那个签名的提交是用SHA-1哈希的,假设SHA-1没有安全上的缺陷,那么给定历史提交的状态将永远是指向那个信任的标签。

好吧,这是有帮助的,但是那不能帮助我们去验证所有的提交及其后面打的标记(直到下一个包含根的新标签的标签被打上)。这也不一定能保证所有过去的提交是正确的——他只是表明,对于Linus来说这是最好的,这颗信任树是可信的。注意:在我们假设的故事中,我们一直使用他/她的私钥去签署标签。不幸地是,他/她成了牺牲品,这太常见了——人为错误。他/她信任他/她的“信任”的同事,实际上能完全可信吗!即使我们从这个等式中移除人为的错误,这就很完美了吗?

信任保障

如果我们有一种方法去确认提交是通过某个被叫做“Mike Gerwitz”的人提交的,而实际上是我用我自己的e-mail地址提交的,就像实际上是我使用我的私钥打的标签, 我们就可以断言一个标签的签署吗?是的,我们试图去证明我们是谁?如果你仅仅提供你的标识给一个项目的作者/维护者,然后你要用任何合理的方式去标识你自己。举个例子:如果你的工作中有同样的内部网络,那么你可以从内部IP中确认是安全的。如果发送是通过e-mail,你可以用你的GPG钥匙来签署补丁。不幸地是,这仅仅是对作者/维护者这一层信任级的扩展,没有其他用户!如果我去克隆你的库并且查看历史记录,我怎么做才能知道提交是来自于“Foo Bar”而且还是来自Foo Bar的可信的提交,尤其是如果库频繁地接收来自用户的补丁和合并这些请求,如何确保是可信的?

以前,仅仅只是在打标签时使用GPG。幸运地是,Git v1.7.9 据说支持对单个的提交将可以使用GPG签署——这个特性我已经期待很久了。考虑到可能发生文章开头那样的故事,如果你对每一个提交进行签署,类似这样:

  1. $ git commit -S -m 'Fixed security vulnerability CVE-123'#             ^ GPG-sign commit 

注意上面例子中的「-S」,它告诉Git用你的GPG公钥来提交变更的代码(请注意「-s」和「-S」的区别)。如果你毫无例外的每一次都用你的GPG公钥来提交代码,那么你(或者其他人)就可以相当肯定变更是否真的是你自己提交的。在上面的故事中,你就可以保护自己,指出后门代码不是你提交的,因为你的代码都有GPG公钥签名。(当然,别人也可能说你是想好这个借口而故意不签名的。后面将会稍稍讨论一下这个问题。)
为了设置你的GPG公钥签名,你首先要用gpg --list-secret-keys命令得到你的GPG公钥:

  1. $ gpg --list-secret-keys | grep ^sec  
  2. sec   4096R/8EE30EAB 2011-06-16 [expires: 2014-04-18]#           ^^^^^^^^ 

你感兴趣的就是上面的输出中斜杠后面的16进制值。(你的输出可能跟上面相差很大,即使没有上面的4096R你也不用担心)。如果你有多个公钥,选择其中一个作为你的签名。这个16进制值需要设置到Git的环境变量user.signingkey中:

  1. # remove --global to use this key only on the current repository  
  2. $ git config --global user.signingkey 8EE30EAB#                                        ^ replace with your key id 

接下来,就让我们来试一下带签名的提交。首先要建立一个测试用的代码仓库,后面的文章会在这个代码仓库上做实验。

  1. $ mkdir tmp && cd tmp  
  2. $ git init .$ echo foo > foo  
  3. $ git add foo  
  4. $ git commit -S -m 'Test commit of foo'You need a passphrase to unlock the secret key foruser: "Mike Gerwitz (Free Software Developer) <mike@mikegerwitz.com>"4096-bit RSA key, ID 8EE30EAB, created 2011-06-16[master (root-commit) cf43808] Test commit of foo 1 file changed, 1 insertion(+)  
  5.  create mode 100644 foo 

此时的提交与未签名的提交唯一不同的地方是增加了-S选项,这个选项表明我们要求的是具有GPG签名的提交。如果一切设置正确,那么就应该会提示你输入密钥对应的密码(如果你运行了gpg代理,那么就不会有任何提示),然后提交就像你所期望的那样继续运行,最终的结果就像上面输出所示(你的GPG详细信息和SHA-1哈希值会有所不同)。

默认情况下(至少在Git 1.7.9版本),git log不会列出或者验证签名。要显示提交所对应的签名,我们可以使用--show-signature 选项,如下:

  1. $ git log --show-signature  
  2. commit cf43808e85399467885c444d2a37e609b7d9e99d  
  3. gpg: Signature made Fri 20 Apr 2012 11:59:01 PM EDT using RSA key ID 8EE30EAB 
  4. gpg: Good signature from "Mike Gerwitz (Free Software Developer) <mike@mikegerwitz.com>" 
  5. Author: Mike Gerwitz <mike@mikegerwitz.com>  
  6. Date:   Fri Apr 20 23:59:01 2012 -0400 
  7.    
  8.     Test commit of foo 

此时最大的不同是:提交者和本次提交对应的签名可能指的是不同的两个人。换句话说:提交的签名在理念上与-s选项相似,它给提交增加了签名---这么做确保你对提交进行了签名,但并不能说明你提交的就是你所修改的。为了说明这个问题,想想我们接收并希望应用来自“John Doe"的补丁。代码仓库的策略是每次提交都必须由可信赖的个人进行签名;而所有其他提交都会被项目管理者拒绝。为了说明不费周折就能采用真正的补丁,我们只要按照以下步骤去做:

  1. $ echo patch from John Doe >> foo  
  2. $ git commit -S --author="John Doe <john@doe.name>" -am 'Added feature X' 
  3. You need a passphrase to unlock the secret key for 
  4. user: "Mike Gerwitz (Free Software Developer) <mike@mikegerwitz.com>" 
  5. 4096-bit RSA key, ID 8EE30EAB, created 2011-06-16 
  6.    
  7. [master 16ddd46] Added feature X  
  8.  Author: John Doe <john@doe.name>  
  9.  1 file changed, 1 insertion(+)  
  10. $ git log --show-signature  
  11. commit 16ddd46b0c191b0e130d0d7d34c7fc7af03f2d3e 
  12. gpg: Signature made Sat 21 Apr 2012 12:14:38 AM EDT using RSA key ID 8EE30EAB 
  13. gpg: Good signature from "Mike Gerwitz (Free Software Developer) <mike@mikegerwitz.com>" 
  14. Author: John Doe <john@doe.name>  
  15. Date:   Sat Apr 21 00:14:38 2012 -0400 
  16.    
  17.     Added feature X# [...] 

这就会产生问题---对于那些用自己的GPG密钥签名提交的人来说,我们该怎么做呢?要从两个角度来分析这个问题。首先,从项目管理者角度考虑这个问题--我们是否需要仔细确认第三方贡献者的身份,还是只接收其提供的代码? 这要看具体情况。另外,从法律的角度来看,我们可能需要确认身份,但并不是每个用户都有GPG密钥。想想这种情况:某个人只是为了签名几次提交而不需要对其身份进行验证而创建了密钥,后来就丢弃了这个密钥(或者说忘记了这个密钥),那么他就不会为验证身份而提供更多信息。(事实上,PGP的整体理念是创建一个可信任的站点,一边能够验证使用自己密钥签名的人能够真实地说出自己是谁,因此使用的场景重要,而目的不重要。)因此,对贡献补丁的每个人采用严格的签名策略可能是失败的。Linux 和Git在提交中采用“签名”满足了法律方面的要求,这就意味着创建者同意了采用“ 原始开发者证书”;这实际上也说明创建者对提交里包含的代码有法律上的拥有权。什么时候开始接受来自可信任站点之外的第三方的补丁是接下来将要做的事。

对补丁采用这种方针,需要作者们做下面的事,而不需要他们用GPG来签名:

  1. $ git commit -asm 'Signed off'#              ^ -s flag adds Signed-off-by line$ git log  
  2. commit ca05f0c2e79c5cd712050df6a343a5b707e764a9  
  3. Author: Mike Gerwitz <mike@mikegerwitz.com>Date:   Sat Apr 21 15:46:05 2012 -0400 
  4.    
  5.     Signed off  
  6.    
  7.     Signed-off-by: Mike Gerwitz <mike@mikegerwitz.com># [...] 

当你收到这样的补丁,你可以用「-S」(大写S)来进行GPG签名并提交;这样同时会保留作者的 Signed-off-by签名行。对于一个pull请求,你可以通过修改来签名提交(git commit -S --amend)。注意,这样做会改变提交的SHA-1哈希值。

万一你想保留提出pull请求的人的签名行呢?你不能修改提交,因为这样会使对方的签名无效,所以双重签名不可行(即使Git以后支持双重签名)。不过,你可以考虑对合并后的代码进行签名,我们将在下一节来讨论这个问题。

#p#

大量合并的管理

到此刻为止,我们讨论的都是应用补丁或者合并单个提交。接下来,假设我们接收到一个对有300个提交的(我向你保证这么做很正常)某个功能或者漏洞修补的pull请求,我们该怎么做的?这时我们可以选择下面其中的一种方法:

  1. 要求用户把所有的提交压缩为单个提交,这么做可以避免使用前面讨论方法时所出现的问题。不过我个人由于以下原由不愿意采用这一方法:

    • 我们不能再追踪到功能或者漏洞修补的历史,从而不能够了解到开发如何进行或者说不能够看到开始采用后来替代的其他处理方法。

    • 这么做使得git bisect无法使用。如果我们要查找由压缩300个提交为单个补丁而引起的软件漏洞,我们将需要亲自进行代码阅读和调试,而不是可能让Git给我们指出问题所在。

  2. 增加这样的安全策略:要求只给合并提交进行签名(如果需要的话,可以通过使用--no-ff强制进行合并提交)。

    • 这当然是最快的解决方案,它允许代码审核人员在对全部代码的存在差异的部分审核后对合并进行签名。

    • 不过这也让个人随时可以进行提交。例如,某个提交引入了负荷,但这个负荷要在未来的某个提交中删除,因此为了防止提交引起与整体代码出现差异,并且会引入极坏的结果,那么就应当对这样的提交进行单独审核(比如,通过git bitsect)。压缩所有的提交(方法1),对每个提交分别进行签名(方法3),或者在执行(不必对每个提交进行签名的)合并前只要对每个提交进行审核就可以避免这个问题的发生。

    • 这一方法还不能完全阻止在这篇文章开始时虚构的故事中出现的哪种情形 --- 其他人仍然可以以你作者的名义进行提交,而且这样的提交不需要签名。

    • 给每个单独的提交保存一份SHA-1哈希值。

  3. 对合并所涉及的每个提交都进行签名

    • 这一处理方法中的繁琐枯燥的工作可以通过使用gpg-agent而大大地减少。

    • 要确保对每个提交都进行仔细的审核,而不是对整体上有差异的地方进行审核,这样才能确保不会有任何恶意提交偷偷的出现在提交历史里(参考方法2中的各项说明)。如果你不想对每个差异之处进行审核,而只想通过脚本对每个提交进行签名,那么你可以考虑一下方法2

    • 如果你要挑选出最好的单个提交,那么这种方法就非常适合,因为这种方法结果就是所有的提交都进行了签名。

    • 你可能辩论说这种方法存在不必须的多余的处理,认为你只要对没有签名的单个提交进行审核,然后再对合并提交进行签名就可以表明所有的提交都经过了审核(方法2)。这儿要提到的最重要的一点是:这一方法为所有审核了的提交提供了证明(除非自动进行提交,那是就不会出现审核)。

    • 这一方法为每个提交创建了一份新的SHA-1哈希值(这个哈希值不会保存)。

你选择三种方法中的哪一个取决于具体项目中哪个因素最重要且最可行。特别是:

  • 如果历史信息对你来说不怎么重要,那么你只要要求压缩所有提交(方法1)就可以避免许多麻烦。

  • 如果历史信息对你很重要,而且你没有足够的时间审核各个提交:

    • 如果你明了方法2的风险,那么就可以采用方法2。

    • 否则,采用方法3,但是不要自动进行签名处理,这样你才能查看到每个提交。如果你希望保存历史信息,那么请这么做。

上面罗列的方法1可以很容易地解决前一节的所讨论的问题。

(方法2)

方法2就像给git merge传递-S参数那样简单。如果合并运行的非常快(也就是说,所有的提交都只是对HEAD进行修改,而没有进行任何合并),那么你就必须使用--no-ff选项,强制进行合并提交。

  1. # set up another branch to merge   
  2. $ git checkout -b bar  
  3. $ echo bar > bar  
  4. $ git add bar  
  5. $ git commit -m 'Added bar'   
  6. $ echo bar2 >> bar  
  7. $ git commit -am 'Modified bar'   
  8. $ git checkout master    
  9.  
  10. # perform the actual merge (will be a fast-forward, so --no-ff is needed)   
  11. $ git merge -S --no-ff bar  
  12.  # ^ GPG-sign merge commit You need a passphrase to unlock the secret key for   
  13. user: "Mike Gerwitz (Free Software Developer) <mike@mikegerwitz.com>"   
  14. 4096-bit RSA key, ID 8EE30EAB, created 2011-06-16   
  15.  
  16. Merge made by the 'recursive' strategy.  
  17.   bar | 2 ++   
  18.  1 file changed, 2 insertions(+)  
  19.  create mode 100644 bar 

查看日志,你会看到以下内容:

  1. $ git log --show-signature  
  2. commit ebadba134bde7ae3d39b173bf8947a69be089cf6  
  3. gpg: Signature made Sun 22 Apr 2012 11:36:17 AM EDT using RSA key ID 8EE30EAB 
  4. gpg: Good signature from "Mike Gerwitz (Free Software Developer) <mike@mikegerwitz.com>" 
  5. Merge: 652f9ae 031f6ee 
  6. Author: Mike Gerwitz <mike@mikegerwitz.com>  
  7. Date: Sun Apr 22 11:36:15 2012 -0400 
  8.  
  9.   Merge branch 'bar'   
  10.  
  11. commit 031f6ee20c1fe601d2e808bfb265787d56732974 
  12. Author: Mike Gerwitz <mike@mikegerwitz.com>   
  13. Date: Sat Apr 21 17:35:27 2012 -0400 
  14.  
  15.   Modified bar  
  16.  
  17. commit ce77088d85dee3d687f1b87d21c7dce29ec2cff1  
  18. Author: Mike Gerwitz <mike@mikegerwitz.com>   
  19. Date: Sat Apr 21 17:35:20 2012 -0400 
  20.  
  21.   Added bar  
  22. # [...]  

注意合并提交包含了签名,而合并所涉及的两个提交(031f6ee和ce77088)却不包含签名。这就是问题所在---如果提交031f6ee中含有文章一开始所讲述的故事中提到的后门,该怎么做呢?这个提交可以认为是你授权的,然而由于没有签名,所以这样的提交可以由任何人授权。再者,如果提交ce77088里含有已经在031f6ee提交里删除的恶意代码,那么在对两个代码分支进行差异化比较时就不会把恶意代码呈现出来。不过,这是另一个需要安全策略定位的问题。你是否要审核每个提交呢? 如果这么做,那么对提交审核时就会发现任何潜在的问题,同时不需要单独对每个提交进行签名。合并本身就表示"是的,我已经对每个提交单独进行了审核,并没有发现这些修改带了了任何问题。"

如果单独对每个提交进行审核承担的责任太大,那么请考虑使用 方法1

(方法3)

上面所列的方法3很明确也很显然要多每个提交进行审核;而方法2只要求你懒洋洋的瞥一下提交或者根本连瞥一下都不需要。也就是说,你可以在方法3里通过自动对每个提交进行签名做同方法2一样的事情,不过这样就可能认为完全不需要方法3。使用哪一方法,你自己评判。

要使的方法3远端可执行,尤其是在含有大量提交的情况下仍然能够远端可执行方法3的唯一方法是要做到这一点:针对每一次提交,我们都不需要重新输入密钥所对应的密码。要做到这一点,我们需要使用 gpg-agent,它会安全地把密码存储在内存中,以便下一次请求时使用。通过使用gpg-agent,我 们就可以只提示一次输入密码。依据gpg-agent的不同启动方式,你一定要确保在完成工作后以不同的方式杀死该进程!

可以采用许多种方法对每个提交进行签名。总的来说,由于对一个提交进行签名就意味着有一个全新的提交,那么选择哪种签名方法就显得有那么一点重要了。例如,如果你愿意,那么你可以选择每个提交,然后给每个提交加上-S --amedn选项,不过不会把这些提交看作一次合并,而且(除非合并运行的非常快,否则)在查看分支的历史信息的时候会非常迷惑。因此,(再次强调一下,除非合并提交运行的非常快,否则)我们将确定一个能够生成合并提交的方法。实现这个的一种方法就是交互式地对每个提交进行基线更新,这样你就可以非常容易地查看到代码差异,给提交签名,继续在该提交的基础上进行下一个提交。

  1. # create a new audit branch off of bar   
  2. $ git checkout -b bar-audit bar  
  3. $ git rebase -i master   
  4. # | ^ the branch that we will be merging into   
  5. # ^ interactive rebase (alternatively: long option --interactive)  

首先,我们根据bar代码分支创建一个新的代码分支---bar-audit----可以对其进行基线更新(参考说明 方法2中创建的bar代码分支)。然后,为了能够逐个执行合并到master中的每个提交,我们需要执行一个把master当作上级分支的基线更新。这将会显示哪些bar-audit(实际上是bar)里有的而master里没有的所有提交,接着用你所选的编辑器打开:

  1. e ce77088 Added bar  
  2. 031f6ee Modified bar  
  3.  
  4. # Rebase 652f9ae..031f6ee onto 652f9ae  
  5. #  
  6. # Commands:  
  7. #  p, pick = use commit  
  8. #  r, reword = use commit, but edit the commit message  
  9. #  e, edit = use commit, but stop for amending  
  10. #  s, squash = use commit, but meld into previous commit  
  11. #  f, fixup = like "squash", but discard this commit's log message  
  12. #  x, exec = run command (the rest of the line) using shell  
  13. #  
  14. # If you remove a line here THAT COMMIT WILL BE LOST.  
  15. # However, if you remove everything, the rebase will be aborted.  
  16. #  

修改这些提交,用e(或者edit)来替代所有pick,如上所示。(如果你使用的是vim,那么你还可以使用ex命令:%s/^pick/e/;根据所用编辑器,调整正则表达式格式)。保存,然后关闭。这样就会给呈现首次的(也就是最早的)提交:

  1. Stopped at ce77088... Added bar  
  2. You can amend the commit now, with  
  3.  
  4.         git commit --amend  
  5.  
  6. Once you are satisfied with your changes, run  
  7.  
  8.         git rebase --continue    
  9.  
  10. # first, review the diff (alternatively, use tig/gitk)   
  11. $ git diff HEAD^    
  12. # if everything looks good, sign it   
  13. $ git commit -S --amend    
  14. # GPG-sign ^ ^ amend commit, preserving author, etc   
  15.  
  16. You need a passphrase to unlock the secret key for   
  17. user: "Mike Gerwitz (Free Software Developer) <mike@mikegerwitz.com>"    
  18. 4096-bit RSA key, ID 8EE30EAB, created 2011-06-16    
  19.  
  20. [detached HEAD 5cd2d91] Added bar   
  21.  1 file changed, 1 insertion(+)  
  22.  create mode 100644 bar   
  23.  
  24. # continue with next commit   
  25. $ git rebase --continue    
  26.  
  27. # repeat.   
  28. $ ...   
  29. Successfully rebased and updated refs/heads/bar-audit. 

浏览一下日志,我们就会看到已经重写了提交,并且包含了签名(不过,SHA-1哈希值却不匹配):

  1. $ git log --show-signature HEAD~2..  
  2. commit afb1e7373ae5e7dae3caab2c64cbb18db3d96fba  
  3. gpg: Signature made Sun 22 Apr 2012 01:37:26 PM EDT using RSA key ID 8EE30EAB 
  4. gpg: Good signature from "Mike Gerwitz (Free Software Developer) <mike@mikegerwitz.com>" 
  5. Author: Mike Gerwitz <mike@mikegerwitz.com>  
  6. Date:   Sat Apr 21 17:35:27 2012 -0400 
  7.  
  8.     Modified bar  
  9.  
  10. commit f227c90b116cc1d6770988a6ca359a8c92a83ce2  
  11. gpg: Signature made Sun 22 Apr 2012 01:36:44 PM EDT using RSA key ID 8EE30EAB 
  12. gpg: Good signature from "Mike Gerwitz (Free Software Developer) <mike@mikegerwitz.com>" 
  13. Author: Mike Gerwitz <mike@mikegerwitz.com>  
  14. Date:   Sat Apr 21 17:35:20 2012 -0400 
  15.  
  16.     Added bar  

接下来,我们就可以像往常一样继续把代码合并到master里。下面要考虑的是是否要像方法2那样对合并提交进行签名的问题。在我们所举的例子里,合并是快速运行的,因此就没有必要进行合并提交了(由于要进行合并的提交都已经签名了,我们就不需要纯粹为了对其进行签名而使用--no-ff选项创建合并提交了)。不过,想想你亲自执行了检查而其他人进行真正的合并这种情况;也许项目是这样运行的:项目管理者必须对代码进行审核,然后对其签名,而其他开发者则负责合并和管理冲突。在这种情况下,你可能就需要对谁合并了修改有一个清新的记录。

#p#

强化信任

既然你已经确定了适合具体项目/仓库的安全策略(至少假设是这样),那么接下来就需要有某种方式来加强签名策略。手工强化是可行的,不过可能容易出现人为错误,而且需要(“只是让其通过”)同行评审,甚至还不必要的浪费时间。非常幸运,其中有一种方法就是:你编写脚本,然后坐下来休息并乐呵呵地运行这个脚本。

首先我们看看自动任务中较简单的---检查并确认每个提交都既签了名,又得到(信任站点的)信任。这种实现还满足了方法3里合并方面的要求。然而,也许并不是所有的提交都考虑进来。不过,如果你有一个具有相当可观数量提交的代码库,那么你就可以做到所有的提交都既签了名又得到信任。如果你想进行回溯,并对所有这些提交进行签名,那么你就彻底地更改了整个代码库中的历史信息,这会让其他用户头痛不已。相反,你可以考虑在某个提交之后开始进行安全检查。

简说提交的历史信息

Git中每一个提交的SHA-1哈希值是根据每一个提交的增量和头信息来生成的。头信息里包含着这一提交的父提交的头信息,父提交的头信息里又包含它的父提交的头信息----以此类推。另外,Git根据代码库里的整个历史信息来生成请求修改的提交信息。这也就意味着历史信息在没有通知某个人的情况下不得更改(实际上,不全是这样的;我们稍后讨论这个问题)。例如,看看下面代码分支:

  1. Pre-attack:  
  2.  
  3. ---o---o---A---B---o---o---H  
  4.     a1b2c3d^  

如上,H表示的是当前的头信息,标识为A的提交是B提交的父提交。为了讨论方便,我们假设提交A是由SHA-1哈希值a1b2c3来确认的。我们再假设攻击者决定用另一个提交替换A提交。要进行替换的话,这个提交的SHA-1哈希值就会发生变更,以匹配头中新的增量和内容信息。新提交标识为X:

  1. Post-attack:  
  2.  
  3. ---o---o---X---B---o---o---H  
  4.     d4e5f6a^   ^!expects parent a1b2c3d  

现在我们会遇到问题;当对提交B运行git时(记住:Git一定会用产生H的所有历史信息来构造H的。),Git将会检查SHA-1哈希值,然后就会注意到这个哈希值已经与其父提交的哈希值不一致了。攻击者是无法更改提交B里的哈希值的,因为用来生成某个提交SHA-1的头信息是针对某一提交的,这也就意味着B已经有一个完全不同的SHA-1哈希值了(技术上来说,这个提交已经不在是B提交了---它已经是另一个完全不同的提交了;为了方便说明,我们忍让使用B标识)。这将会使得任何B的子提交无效,以此类推。因此要对某个提交的历史信息重写,那么位于这个提交后的所有提交都必须进行重写(通过git rebase来完成)。要想这么做的话,H的SHA-1哈希值也必须得到更改。否则,H的历史信息将是无效的,而且在在你试图对代码进行检出的时候,Git会立即抛出错误信息。

这里有一个非常重要的结论——对于任何的提交。我们可以放心,如果它在本地存储器上存在,Git总是会重构,使之提交的能与被创建(包括所有之前的历史创建及提交)时一致,除非不这样做。的确,Linus提及了在Google的一次展示, 他只 需要记住SHA-1散列上的一个提交,放心吧,它会把它发送到其他的存储器上,倘若我们的东西丢失,之前的提交会发送一份完全一致的提交到其他人的存储器上。这对我们意味着什么?是的,这意味着我们不需要强制重写历史记录到每个单一的提交上,因为我们其他的历史提交是被保证的。唯一的缺点是,提交历史本身可能已经被利用起来,类似于我们开头讲的故事,但是许多过去的提交记录是被自动签名的,这样对于一个给定的作者将不能抓住类似的事情。

这就是说,明白存储的完整性保障是重要的,尽管哈希碰撞不会发生——就是说,如果攻击者能对不同的数据创建一样的SHA-1哈希,那么子提交将仍然是有效的,而存储库就已经成功地被破解了。从2005年开始,可用的哈希计算速度快得超过了强力破解,这样,在SHA-1上的缺陷就变得众所周知了,尽管利用这一点并不廉价。基于这样一个事实,为了你的储存库的安全,将来的某个时候,SHA-1将会瘫痪,就像现在的MD5一样。在那个时间点上,Git可能会提供一个安全的迁移方案类似SHA-256算法或者更好的算法。的确,SHA-1哈希不能保证Git的密码安全

正是如此,大部分人可能会不再去看他/她的历史记录,我们将会在这个假设下实现我们的操作,这提供了能去忽略所有之前确认提交的能力。如果某人希望去验证所有的提交,只是参考提交可能就会遗漏。

自动进行签名验证

验证某些提交是可信任的想法非常简单:

假定要用到的提交是r(可为空),C为所有提交的集合,此时C=r..Head(范围说明),同时K是给定GPG密钥链中所有公钥   的集合。我们断言:对C中的每个提交c,密钥链K中一定存在一个密钥k可信任,同时可用来对c的签名进行验证。这个断言   是由函数g(GPG)来表示的,如下表达式:∀c∈Cg(c)。

很幸运,就像我们在前一节在git log上使用--show-signature选项后看到的,Git帮助我们验证了签名;这样就把我们的验证签名实现简化为一个简单的shell脚本。不过我们得到的输出不是很适合于解析。如果我们可以让每个提交的提交和签名信息出现在单行上就很适合解析了。这可以通过--pretty选项完成,不过还有这样一个问题--在编写(Git 1.7.10)文档的时候,GPG --pretty选项没有写入文档中。

format_commit_one() in pretty.c 有三个不同的格式:

  • %GG---GPG 输出(我们在git记录里看到的--show-signature)

  • %G?--好的签名输出“G",差的签名输出”B";否则输出空字符窜(见mapping in signature_check struct)

  • %GS---签名者的名字。

我们感兴趣的是使用最精确和最小限度的表达---¥G?。因为这个占位符只是匹配GPG输出的内容,字符窜“gpg: Can’t check signature: public key not found”不能对应 insignature_check, 不能识别的字符讲会输出空字符窜,不是"B".这点并不明显,所以我不确信是否这个在以后的版本会改变。幸运的是,我们只是对”G"感兴趣,所以这个细节对于我们的实施来说不关键。记住这点,我们能够做提交一次输出某个有用的一行。下面是基于演示上面的merge option #3 的输出的结果:

  1. $ git log --pretty="format:%H %aN  %s  %G?"afb1e7373ae5e7dae3caab2c64cbb18db3d96fba Mike Gerwitz  Modified bar  G  
  2. f227c90b116cc1d6770988a6ca359a8c92a83ce2 Mike Gerwitz  Added bar  G  
  3. 652f9aed906a646650c1e24914c94043ae99a407 John Doe  Signed off  G  
  4. 16ddd46b0c191b0e130d0d7d34c7fc7af03f2d3e John Doe  Added feature X  G  
  5. cf43808e85399467885c444d2a37e609b7d9e99d Mike Gerwitz  Test commit of foo  G  

注意每一行的后缀"G",它表明签名有效(这可以理解,因为是我们自己的签名)。再增添一个提交,我们看看进行未签名提交时会出现什么情况:

  1. $ echo foo >> foo  
  2. $ git commit -am 'Yet another foo' 
  3. $ git log --pretty="format:%H %aN  %s  %G?" HEAD^..  
  4. f72924356896ab95a542c495b796555d016cbddd Mike Gerwitz  Yet another foo 

注意:就像前面提到那样,在进行未签名提交时,%G?被替换为空字符串。那签了名但不可信任(即不在站点的信任内)的提交会出现什么情况?

  1. $ gpg --edit-key 8EE30EAB 
  2. [...]  
  3. gpg> trust  
  4. [...]  
  5. Please decide how far you trust this user to correctly verify other users' keys  
  6. (by looking at passports, checking fingerprints from different sources, etc.)  
  7.  
  8.   1 = I don't know or won't say  
  9.   2 = I do NOT trust  
  10.   3 = I trust marginally  
  11.   4 = I trust fully  
  12.   5 = I trust ultimately  
  13.   m = back to the main menu  
  14.  
  15. Your decision? 2 
  16. [...]  
  17.  
  18. gpg> save  
  19. Key not changed so no update needed.  
  20. $ git log --pretty="format:%H %aN  %s  %G?" HEAD~2..  
  21. f72924356896ab95a542c495b796555d016cbddd Mike Gerwitz  Yet another foo  
  22. afb1e7373ae5e7dae3caab2c64cbb18db3d96fba Mike Gerwitz  Modified bar  G 

哦,哦,Git似乎没有核查签名是否可信。我们看一看完整的GPG输出:

  1. $ git log --show-signature HEAD~2..HEAD^   
  2. commit afb1e7373ae5e7dae3caab2c64cbb18db3d96fba  
  3. gpg: Signature made Sun 22 Apr 2012 01:37:26 PM EDT using RSA key ID 8EE30EAB 
  4. gpg: Good signature from "Mike Gerwitz (Free Software Developer) <mike@mikegerwitz.com>"   
  5. gpg: WARNING: This key is not certified with a trusted signature!   
  6. gpg: There is no indication that the signature belongs to the owner.   
  7. Primary key fingerprint: 2217 5B02 E626 BC98 D7C0  C2E5 F22B B815 8EE3 0EAB 
  8. Author: Mike Gerwitz <mike@mikegerwitz.com>   
  9. Date: Sat Apr 21 17:35:27 2012 -0400 
  10.  
  11.   Modified bar 

我们可以看到GPG给出了明确的警告信息。不幸的是 pretty.c中的parse_signature_lines()引用了struct signature_check结构里的一个简单映射,并忘乎所以地忽略了警告信息,只匹配了"Good signatrue from",生成了"G"。为不信任密钥提供单独的符号,这样的补丁程序很简单,但目前我们暂时使用的是两个不同的实现方法---一个方法是对忽略是否可信任的单行输出进行解析,另一个是上面提到的对GPG输出进行解析的非简洁化实现方法。[假若采纳了这个补丁,那么这篇文档就会立即更新,使用新的符号。]

没有可信任验证的签名验证脚本

上面已经提到过,由于目前%G?实现的限制,我们无法从单行输出确定所提供的签名是可信任的。这不一定是问题所在。考虑一下运行这个脚本的一般情形---由持续集成(CI)系统运行。要让CI系统明确什么样的签名才是可信任的,你可能要为知名的提交者提供密钥,这样就不需要站点的信任了(把公钥放在服务器上就标兵你信任这些密钥)。因此,如果可识别到提交的签名而且正确,那么这次提交就值得信任。

另外一个要考虑的是不对提交的所有祖先进行签名,旧的代码库就是这么做的,其中旧的提交都是未签名的(关于为什么不需要对旧的提交进行签名的信息可参考提交信息简说一节,而且对旧提交进行签名是非常糟糕的事情)。因此,这个脚本将接收参数,而且只对该参数的子提交进行签名验证。

这个脚本假定每个提交都签了名,同时它会输出未签名或者错误提交的SHA-1哈希值,除此之外还显示其他可用的信息,信息之间以制表符间隔。

  1. #!/bin/sh   
  2.  
  3. # Validate signatures on each and every commit within the given range ##    
  4.    
  5. # if a ref is provided, append range spec to include all children  
  6. chkafter="${1+$1..}"   
  7.    
  8. # note: bash users may instead use $'\t'; the echo statement below is a more   
  9. # portable option    
  10. t=$( echo '\t' )    
  11.    
  12. # Check every commit after chkafter (or all commits if chkafter was not    
  13. # provided) for a trusted signature, listing invalid commits. %G? will output    
  14. # "G" if the signature is trusted.   
  15. git log --pretty="format:%H$t%aN$t%s$t%G?" "${chkafter:-HEAD}" \  
  16.   | grep -v "${t}G$"   
  17.    
  18. # grep will exit with a non-zero status if no matches are found, which we    
  19. # consider a success, so invert it   
  20. [ $? -gt 0 ]  
  21.    

上面就是全部脚本代码;Git已经做了大部分工作!如果传入参数,那么这个参数将被转换为 范围格式,也就是在其后增加".."(例如,a1b2c3就会转换为a1b2c3..),如果没有参数传入,我们将会以不带范围格式的HEAD结束,它只是简单地罗列出每个提交(空串将使Git抛出错误,因此我们必须对该字符串两边加上引号,这样用户就可以执行类似于获取"master@{5 days ago"}这样的任务了)。我们给git log加上--pretty选项,这样就会输出带有%G?的GPG签名,以及其他可用的信息,通过这些信息我们就可以看到哪些提交没有通过验证。接下来,我们对所有用密钥签名的提交进行过滤,删除所有以"G"结尾的行----依据%G?得到的输出说明这样的提交通过了签名验证。

我们看一看实际中脚本运行情况(假设脚本存储为文件signchk):

  1. $ chmod +x signchk  
  2. $ ./signchk  
  3. f72924356896ab95a542c495b796555d016cbddd        Mike Gerwitz    Yet another foo  
  4. $ echo $?  
  5. 1 

如果没有参数传入,那么这个脚本就会对整个代码库里的每个提交进行检查,查找一个没有签名的提交。此时,我们要么通过查看脚本的自己的输出,要么查看脚本退出时的状态来确定是否失败。如果脚本是由CI系统来运行的,那么此时最好是退出构建过程,同时立刻通知项目管理者潜在的安全入口所在(或者更可能是某个人只是忘记对自己的提交签名)。

如果在失败之后我们检查提交,此时假设子提交都已经签了名,那么我们就会看到下面结果:

  1. $ ./signchk f7292  
  2. $ echo $?  
  3. 0 

从代码库里直接运行脚本的时候要特别小心,尤其是通过CI系统运行的时候要格外小心----你一定要做到:要么把脚本拷贝到代码库之外,要么从历史提交中一个可信任的提交处运行。举个例子,如果你所使用的CI系统只是从代码库中下拉代码,然后运行这个脚本,那么攻击者只要修改一下这个脚本就可以完全绕过这样的签名验证。

#p#

可信任签名验证

信任网络可用在具有许多贡献者的情况;此时,CI系统在需要密钥的时候就会试图从预先配置好的密钥服务器中下载公钥(如果需要可信任签名,就必须更新密钥)。依据由CI系统直接信任的公钥组建的信任网络,你就可自动确定提交是否可信任,即便提交所对应的公钥没有存放在密钥服务器上也可以确定是否可信任。

为了完成这样的工作,我们把脚本分割成两个部分---获取或者更新给定范围内的所有密钥,接着是真正的签名验证部分。我们先看看密钥收集部分,这个工作实际上微不足道:

  1. $ git log --show-signature \  
  2.   | grep 'key ID' \  
  3.   | grep -o '[A-Z0-9]\+$' \  
  4.   | sort \  
  5.   | uniq \  
  6.   | xargs gpg --keyserver key.server.org --recv-keys $keys 

上面的命令字符串只是通过grep命令从git log的输出(即使用--show-signature选项后生成的GPG输出)中提交密钥ID,然后向给定的密钥服务器只请求不重复的密钥。通篇文章里我们使用的代码库只有一个签名---即我自己的签名。而针对大型的代码库,所有不重复的密钥都会被罗列出来。注意:上面的例子没有指定提交的范围;你可以按照自己的意愿把它嵌入到signchk脚本,这样就可以使用同样的范围了,不过严格的来说,并不需要这么做(这么做也许在性能上有些许提高,而且这种性能上的提高还取决于你所忽略的提交的数量)。

有了这些更新的密钥,我们就可以根据信任网络对提交进行验证了。某个密钥是否可信任取决于你个人的设置。理念是信任你所配置的信任网络里用户的哪些用户(例如Linus的“助理们”)就是可信任的,即便你个人不信任他们也如此。同样的理念也适用于CI服务器,此时你可以用CI服务器的密钥链替换你自己的密钥链(这样,你就不需要运行CI服务器,可以自己运行这个脚本)。

很不幸,由于受到目前Git的%G?实现限制, 我们不能够通过检查单行输出给出结果。相反,我们必须解析每个关联提交的--show-signature的输出( 如上所示)。把现在的输出和 不带可信任验证的脚本结合起来,我们就会得到以下输出,这才是我们要解析的:

  1. $ git log --pretty="format:%H$t%aN$t%s$t%G?" --show-signature  
  2. f72924356896ab95a542c495b796555d016cbddd       Mike Gerwitz    Yet another foo  
  3. gpg: Signature made Sun 22 Apr 2012 01:37:26 PM EDT using RSA key ID 8EE30EAB 
  4. gpg: Good signature from "Mike Gerwitz (Free Software Developer) <mike@mikegerwitz.com>" 
  5. gpg: WARNING: This key is not certified with a trusted signature!  
  6. gpg:          There is no indication that the signature belongs to the owner.  
  7. Primary key fingerprint: 2217 5B02 E626 BC98 D7C0  C2E5 F22B B815 8EE3 0EAB 
  8. afb1e7373ae5e7dae3caab2c64cbb18db3d96fba       Mike Gerwitz    Modified bar    G  
  9. [...]  

看看上面部分运行结果,你应当注意到第一个提交(f7292)是未签名的,而第二个(afb1e)是签了名的。因此,GPG输出应在提交行之前。现在看看我们的目标:

  1. 罗列出所有未签名的提交,或者未知签名或者无效的签名提交。

  2. 罗列出所有使用已知签名签了名的,但不可信任的签名提交。

前面的脚本很好地执行了第1个目标,因此我们只需要对这个脚本进行代码提交,使得它能够实现第2个目标。实际上---如果提交行前面的GPG输出表明这个签名是不可信任的,那么我们希望转换其中以"G"结尾的行为其他别的东西。

达到第2个目标有许多方法,我们选择的是在前面脚本上增家几个非常简洁的命令的方法。为了不使输出过滤掉以"G"结尾的行(这样的提交都是不可信任的),我们给这样的不信任行后增加了"U"。看-看下面输出:

  1. $ git log --pretty="format:^%H$t%aN$t%s$t%G?" --show-signature \  
  2. > | grep '^\^\|gpg: .*not certified' \  
  3. > | awk '>   /^gpg:/ {  
  4. >     getline;  
  5. >     printf "%s U\n", $0;  
  6. >     next;>   }  
  7. >   { print; }  
  8. > ' \  
  9. > | sed 's/^\^//' 
  10. f72924356896ab95a542c495b796555d016cbddd        Mike Gerwitz    Yet another foo  
  11. afb1e7373ae5e7dae3caab2c64cbb18db3d96fba        Mike Gerwitz    Modified bar    G U  
  12. f227c90b116cc1d6770988a6ca359a8c92a83ce2        Mike Gerwitz    Added bar       G U  
  13. 652f9aed906a646650c1e24914c94043ae99a407        John Doe        Signed off      G U  
  14. 16ddd46b0c191b0e130d0d7d34c7fc7af03f2d3e        John Doe        Added feature X G U  
  15. cf43808e85399467885c444d2a37e609b7d9e99d        Mike Gerwitz    Test commit of foo      G U  

在这儿,我们发现如果我们过滤前天提及的以"G"结尾的行,我们将得到的就像%G?所表示的那样:是不信任的提交,以及错误提交("B")或者未签名提交(结尾是空白)。要做到这些,我们首先使用--show-signature选项,给日志输出中增加GPG输出,为了更容易地进行过滤,我们给所有的提交行前加上控制符(^),后面我们会删除这个符号。然后我们过滤所有以控制符开头的行或者包含有"not certified"字符串的行(GPG输出中存在这样的行)。如果提交时不可信任的,那么这个就会在提交行之前出现一个"gpg:"行。 接着我们把得到的结果传递给awk命令,它将删除所有以"gpg:"作为前缀的行,然后给下一行(也就是提交行)添加上"U"。最后,我们将删除在处理开始时添加的前导控制符(^),得到最终的输出结果。

请注意,通常使用的PGP/GPG(我声明我知道这个人就是他们宣称的那个样子“)信托和信任某人提交代码之间有巨大差别。同样地,可能你最大的兴趣是,维护一个完整的独立的信誉网页,给你的CI服务商或者使用的任一个用户进行签名验证。

自动合并签名验证

如果你希望检验每个提交的有效与否,前文提到的脚本非常棒,但是并非每个人都希望做那么多努力。反之,维护的人可能更喜欢只需要标记合并的提交(上面提到的选项2),而不是每次合并引入的提交。我们来考虑下对这种情况我们要采用的方法。

假定要用到的提交时r(可为空),C'为所有第一级父提交的集合,此时C'=r..Head(范围说明),同时K是给定GPG密钥链中所有公钥   的集合。我们断言:对C'中的每个提交c,密钥链K中一定存在一个密钥k可信任,同时可用来对c的签名进行验证。这个断言   是由函数g(GPG)来表示的,如下表达式:∀c∈C' g(c)。

这个脚本与只对单个提交进行签名验证的脚本的唯一不同是 这个脚本只对特定代码分支(比如master分支)下的提交进行验证。这一点非常重要---如果我们直接在master上提交,那么我们需要确保这个提交是签了名的(因为,不会存在合并提交)。如果我们需要合并到master分支上,那么就会创建合并提交,这时我们可以
 

对合并提交签名,同时不对合并的所涉及的提交进行签名。如果合并运行的非常快,我们就使用--no-ff选项强制创建合并提交,以避免给每个所涉及到的提交进行签名。

为了模拟能验证此种提交的脚本,咱们先做一些修改来触发合并功能:

  1. $ git checkout -b diverge  
  2. $ echo foo > diverged  
  3. $ git add diverged  
  4. $ git commit -m 'Added content to diverged'[diverge cfe7389] Added content to diverged 1 file changed, 1 insertion(+)  
  5.  create mode 100644 diverged  
  6. $ echo foo2 >> diverged  
  7. $ git commit -am 'Added additional content to diverged'[diverge 996cf32] Added additional content to diverged 1 file changed, 1 insertion(+)$ git checkout master  
  8. Switched to branch 'master'$ echo foo >> foo  
  9. $ git commit -S -am 'Added data to master'You need a passphrase to unlock the secret key foruser: "Mike Gerwitz (Free Software Developer) <mike@mikegerwitz.com>"4096-bit RSA key, ID 8EE30EAB, created 2011-06-16[master 3cbc6d2] Added data to master 1 file changed, 1 insertion(+)$ git merge -S diverge  
  10.  
  11. You need a passphrase to unlock the secret key foruser: "Mike Gerwitz (Free Software Developer) <mike@mikegerwitz.com>"4096-bit RSA key, ID 8EE30EAB, created 2011-06-16Merge made by the 'recursive' strategy.  
  12.  diverged |    2 ++  
  13.  1 file changed, 2 insertions(+)  
  14.  create mode 100644 diverged  

上述操作,确保master和分支都有相应的合并提交以避免快进 (也可以通过用 --no-ff 选项实现). 结果如下 (你本机的哈希结果可能不同):

  1. $ git log --oneline --graph  
  2. *   9307dc5 Merge branch 'diverge' 
  3. |\  
  4. | * 996cf32 Added additional content to diverged  
  5. | * cfe7389 Added content to diverged  
  6. * | 3cbc6d2 Added data to master  
  7. |/  
  8. * f729243 Yet another foo  
  9. * afb1e73 Modified bar  
  10. * f227c90 Added bar  
  11. 652f9ae Signed off  
  12. 16ddd46 Added feature X  
  13. * cf43808 Test commit of foo  

从上图中可以看出,只有两处需要签名: 3cbc6d2, 在master直接创建, 9307dc5---合并提交后生成.  另外两处提交 (996cf32 和 cfe7389) 不需要签名,在合并时就确保了其有效性 (假设提交者是谨慎的).  但怎么忽略这些提交呢?

  1. $ git log --oneline --graph --first-parent  
  2. 9307dc5 Merge branch 'diverge' 
  3. 3cbc6d2 Added data to master  
  4. * f729243 Yet another foo  
  5. * afb1e73 Modified bar  
  6. * f227c90 Added bar  
  7. 652f9ae Signed off  
  8. 16ddd46 Added feature X  
  9. * cf43808 Test commit of foo 

上述例子简单的添加了 --first-parent选项, 这样在遇到有合并提交时只会显示最初的提交记录. 重点就是, 这就只剩下master上的提交记录 (或是你需要参照的分支).这些是需要验证的.

现在的验证工作仅仅需要微调原来的脚本即可:

  1. #!/bin/sh## Validate signatures on only direct commits and merge commits for a particular# branch (current branch)### if a ref is provided, append range spec to include all childrenchkafter="${1+$1..}"# note: bash users may instead use $'\t'; the echo statement below is a more# portable option (-e is unsupported with /bin/sh)t=$( echo '\t' )# Check every commit after chkafter (or all commits if chkafter was not# provided) for a trusted signature, listing invalid commits. %G? will output# "G" if the signature is trusted.git log --pretty="format:%H$t%aN$t%s$t%G?" "${chkafter:-HEAD}" --first-parent \  
  2.   | grep -v "${t}G$"# grep will exit with a non-zero status if no matches are found, which we# consider a success, so invert it[ $? -gt 0 ]  

如果你在刚建好的分支上运行上述脚本, 你会发现分支中不会包含相应的历史提交记录.由于合并提交的自带标记,结果中也不会显示相应的记录(剩下的就是那些未做标记的提交记录).要展示未标记的合并提交, 可以使用以下命令 (忽略 -S选项):

  1. $ git commit --amend[master 9ee66e9] Merge branch 'diverge'$ ./signchk  
  2. 9ee66e900265d82f5389e403a894e8d06830e463        Mike Gerwitz    Merge branch 'diverge'f72924356896ab95a542c495b796555d016cbddd        Mike Gerwitz    Yet another foo  
  3. $ echo $?1 

合并提交将被列出来,需要签名验证.  

[如果需要验证签名的有效性,可以参照以下文章 the section on verifying commits within a web of trust.]

总结

  • 注意安全性. PC机上的资源库是否安全? 哪些用户可以信赖?

    • 主机并没有想象的那么安全。谨慎使用远端资源库.

  • 用GPG 签名你的提交 可以确保身份认证,从而远离危害.

  • 对于大规模的代码合并,最好验证出合适的规则. 具体地说,可以验证每次提交 , 只对合并功能提供签名, 或者在 一次提交完成后 再签名相应的提交.

  • 如果有现成的资源库, 没必要再重写原有的提交记录.

  • 在确定了你的安全策略后,最好能 自动化签名验证以确保提交的安全性.

译文出自:http://www.oschina.net/translate/git-horror-story?lang=chs&page=1#

责任编辑:林师授 来源: 开源中国社区 编译
相关推荐

2021-01-22 05:55:12

GitAngularJStype

2022-04-28 09:02:55

Gitcommitlint配置

2021-01-04 13:25:10

Git开源工具

2017-09-07 16:00:20

2022-12-02 10:18:55

Gitbisect

2021-03-30 08:43:29

黑客PHP团队Git

2011-02-18 02:51:59

RSA可信云计算

2009-09-25 11:03:35

PCI DSS数据完整数据安全

2024-04-08 08:37:41

代码githook

2016-10-19 20:34:46

2020-11-02 08:21:50

Git办法代码

2024-01-08 21:01:36

NFSGit文件目录

2012-03-01 15:26:25

移动技术厂商RSA

2016-08-12 10:13:03

2023-03-13 14:23:56

企业数据大数据

2022-03-14 22:55:54

人工智能数据机器学习

2015-07-29 10:39:33

代码审查经验教训

2018-07-10 11:05:18

开发者技能命令

2022-05-26 08:38:10

Docker镜像运维

2023-05-22 11:26:45

点赞
收藏

51CTO技术栈公众号