设为首页收藏本站

LUPA开源社区

 找回密码
 注册
文章 帖子 博客
LUPA开源社区 首页 业界资讯 技术文摘 查看内容

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

2014-9-23 10:53| 发布者: joejoe0332| 查看: 7801| 评论: 0|原作者: 几点人, 无若, petert, Sophietyl, f4f, sg90, 请叫我益达张|来自: oschina

摘要: GIT是一款分布式版本控制系统。简而言之,这意味着任何人可以私自拥有一份你的仓库的副本(copy)进行线下工作。他们可能提交版本到自己的仓库,也可以相互之间进行push和pull。中心仓库对分布式版本控制系统来说不是 ...


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

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

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/sh
#
# Validate signatures on each and every commit within the given range ## 
 
# if a ref is provided, append range spec to include all children
chkafter="${1+$1..}" 
 
# note: bash users may instead use $'\t'; the echo statement below is a more
# portable option 
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}" \
  | 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 ]
  
上面就是全部脚本代码;Git已经做了大部分工作!如果传入参数,那么这个参数将被转换为 范围格式,也就是在其后增加".."(例如,a1b2c3就会转换为a1b2c3..),如果没有参数传入,我们将会以不带范围格式的HEAD结束,它只是简单地罗列出每个提交(空串将使Git抛出错误,因此我们必须对该字符串两边加上引号,这样用户就可以执行类似于获取"master@{5 days ago"}这样的任务了)。我们给git log加上--pretty选项,这样就会输出带有%G?的GPG签名,以及其他可用的信息,通过这些信息我们就可以看到哪些提交没有通过验证。接下来,我们对所有用密钥签名的提交进行过滤,删除所有以"G"结尾的行----依据%G?得到的输出说明这样的提交通过了签名验证。


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

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

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

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

$ ./signchk f7292
$ echo $?
0

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


可信任签名验证

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


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

$ git log --show-signature \
  | grep 'key ID' \
  | grep -o '[A-Z0-9]\+$' \
  | sort \
  | uniq \
  | 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的输出( 如上所示)。把现在的输出和 不带可信任验证的脚本结合起来,我们就会得到以下输出,这才是我们要解析的:

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

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

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

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

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


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

$ git log --pretty="format:^%H$t%aN$t%s$t%G?" --show-signature \
> | grep '^\^\|gpg: .*not certified' \
> | awk '>   /^gpg:/ {
>     getline;
>     printf "%s U\n", $0;
>     next;>   }
>   { print; }
> ' \
> | sed 's/^\^//'
f72924356896ab95a542c495b796555d016cbddd        Mike Gerwitz    Yet another foo
afb1e7373ae5e7dae3caab2c64cbb18db3d96fba        Mike Gerwitz    Modified bar    G U
f227c90b116cc1d6770988a6ca359a8c92a83ce2        Mike Gerwitz    Added bar       G U
652f9aed906a646650c1e24914c94043ae99a407        John Doe        Signed off      G U
16ddd46b0c191b0e130d0d7d34c7fc7af03f2d3e        John Doe        Added feature X G U
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选项强制创建合并提交,以避免给每个所涉及到的提交进行签名。


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

$ git checkout -b diverge
$ echo foo > diverged
$ git add diverged
$ git commit -m 'Added content to diverged'[diverge cfe7389] Added content to diverged 1 file changed, 1 insertion(+)
 create mode 100644 diverged
$ echo foo2 >> diverged
$ git commit -am 'Added additional content to diverged'[diverge 996cf32] Added additional content to diverged 1 file changed, 1 insertion(+)$ git checkout master
Switched to branch 'master'$ echo foo >> foo
$ 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

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.
 diverged |    2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 diverged

上述操作,确保master和分支都有相应的合并提交以避免快进 (也可以通过用 --no-ff 选项实现).



酷毙

雷人

鲜花

鸡蛋

漂亮
  • 快毕业了,没工作经验,
    找份工作好难啊?
    赶紧去人才芯片公司磨练吧!!

最新评论

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号   

返回顶部