Git 使用中的教训:签名提交确保代码完整可信(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 TorvaldsGit和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以后支持双重签名)。不过,你可以考虑对合并后的代码进行签名,我们将在下一节来讨论这个问题。


相关内容