强化信任

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

首先我们看看自动任务中较简单的---检查并确认每个提交都既签了名,又得到信任站点的)信任。这种实现还满足了方法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的签名进行验证。这个断言   是由函数gGPG)来表示的,如下表达式:∀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系统只是从代码库中下拉代码,然后运行这个脚本,那么攻击者只要修改一下这个脚本就可以完全绕过这样的签名验证。


相关内容