我们正在努力将ZendFramework迁移到Git。我们试图解决的一个问题是强制提交来自CLA签名者。
向我们提出的一种可能性是利用GPG签名提交消息的可能性。不幸的是,我在网上几乎找不到关于如何做到这一点的信息,所以我开始尝试一些解决方案。
我选择的方法利用了githooks,特别是commit-msg钩子客户端,以及pre-receive钩子服务器端。
客户端提交消息挂钩
commit-msg钩子接收一个参数,即包含提交消息的临时文件的路径。这允许您在完成提交之前检查它或修改它。与所有git挂钩一样,非零退出状态将中止提交。
我的commit-msg挂钩如下所示:
#!/bin/sh
echo -n "GPG Signing message... ";
PASSPHRASE=$(git config --get hooks.gpg.passphrase)
if [ "" = "$PASSPHRASE" ];then
echo "no passphrase found! Set it with git config --add hooks.gpg.passphrase <passphrase>"
exit 1
fi
gpg --clearsign --yes --passphrase $PASSPHRASE -o $1.asc $1
mv $1.asc $1
echo "[DONE]"
此挂钩要求您首先将GPG密钥的密码添加到您的localgit配置中,这可以按如下方式完成:
$ git config --add hooks.gpg.passphrase "mySecret"
一旦这个钩子就位,所有的提交消息都会被明确签名,导致提交日志如下所示:
commit f921f0defb18f8a5218d5c3346693dbb4179920e
Author: Matthew Weier O'Phinney <somebody@example.com>
Date: Tue Mar 23 17:18:35 2010 -0400
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
how now, brown cow
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (GNU/Linux)
iEYEARECAAYFAkupMCsACgkQtUV5aSPtKdqERQCeN5taRATpB4/XJZiP9Vs5FVNY
PcoAn0OZbIIcn7nC01yxp9tY7HbxVVFu
=C/Ju
-----END PGP SIGNATURE-----
服务器端预接收钩子
pre-receive钩子就没那么直接了。此挂钩通过STDIN接收输入。每行由三个项目组成,由一个空格分隔:
[previous commit's sha1] [new commit's sha1] [refspec]
通常,只有新的sha1对我们有用。在内部,git实际上在跟踪新的提交,即使它在技术上还没有被存储库接受。这允许我们使用诸如gitshow之类的工具来获取有关提交的信息并根据该信息采取行动。
我需要做的是检查GPG签名消息的提交消息;如果没有找到,则直接拒绝提交,但如果存在,则根据我的密钥环对其进行验证,如果签名消息无效则中止。
我最初使用gitshow--pretty="format:%b"[sha1]但是,我发现git做了一些……奇怪的事情……来提交消息。前50个左右的字符被认为是提交的“主题”——在主题中发现的任何换行符都被默默地删除。这意味着,出于我的目的,我得到了一条永远无法验证的截断消息(因为GPG签名头被剥离);即使在格式中包含主题也不起作用,因为其中的换行符丢失了。我发现获取完整提交消息的唯一方法是使用gitshow--pretty=raw[sha1]。然而,这也为我提供了提交标头和差异—这意味着我必须解析响应。
接下来是我做的一个PHP实现,它就是这样做的:抓取完整的消息并将其重定向到一个临时文件,解析该文件以获取提交消息,然后对其执行操作。
#!/usr/bin/php
<?php
echo "Checking for GPG signature... ";
$fh = fopen('php://stdin', 'r');
$tmpdir = sys_get_temp_dir();
while (!feof($fh)) {
$line = fgets($fh);
list($old, $new, $ref) = explode(' ', $line);
// Create a tmp file with the commit log
$logTmp = tempnam($tmpdir, 'LOG_');
$body = shell_exec('git show --pretty=raw ' . $new . ' > ' . $logTmp);
$msgTmp = tempnam($tmpdir, 'MESSAGE_');
// Scan the commit log for a commit message
$log = fopen($logTmp, 'r');
$msg = fopen($msgTmp, 'a');
$signatureDetected = false;
while (!feof($log)) {
$line = fgets($log);
if (preg_match('/^(commit(ter)?|tree|parent|author)\s/', $line)) {
// Skip the commit log headers
continue;
}
if (preg_match('/^diff\s/', $line)) {
// Stop scanning when we reach the diff
break;
}
if (preg_match('/^\s+-+BEGIN [A-Z]+ SIGNED MESSAGE/', $line)) {
// We have a signed message, so start appending it
// to a separate tmp file
$signatureDetected = true;
$line = preg_replace('/^\s+/', '', $line);
fwrite($msg, $line);
continue;
}
if ($signatureDetected) {
// If we have detected a signed message, continue appending lines to
// it. Commit message lines are indented, so strip indentation.
$line = preg_replace('/^\s+/', '', $line);
if ('' === $line) {
$line = "\n";"
}
fwrite($msg, $line);
}
}
fclose($log);
fclose($msg);
if (!signatureDetected) {
// No signed message detected; report and abort
unlink($logTmp);
unlink($msgTmp);
echo "no GPG signature detected; commit aborted\n";
exit(1);
}
$verification = shell_exec('gpg --verify ' . $msgTmp . ' 2>&1');
if (!preg_match('/Good signature/s', $verification)) {
// Failed to verify signed message; report and abort
unlink($logTmp);
unlink($msgTmp);
echo "invalid GPG signature; commit aborted\n";
exit(1);
}
unlink($logTmp);
unlink($msgTmp);
}
echo "verified!\n";
exit(0);
可能有更优雅的方法来实现这一点,包括其他语言的解决方案。但是,它工作得很好。
结论
Git钩子非常强大,深入研究它们让我有信心在我们准备好向公众开放时,我可以为ZFgit存储库创建一些不错的自动化。
也就是说,我不知道我们是否真的会使用这样的提交签名,因为它有一些缺点:
- 提交签名并不是真正的跨平台。这可能是可以补救的,但需要使用不同操作系统和使用不同工具(例如EGit、TortoiseGit等)的人为客户端开发和提供签名机制。
- 它为那些引入了复杂性开发补丁。如果开发人员在没有安装
commit-msg挂钩的情况下开始,那么他们必须创建一个新分支并随后进行压缩提交,以确保最终补丁可以进入规范存储库。 - 上述两个原因有点违背了转向分布式VCS的初衷——即简化开发并使其更加民主。
不管我们是否决定使用这种技术,在研究这个问题时,我看到很多人发帖希望实施提交签名,但不确定如何实现。也许这篇文章将成为许多人的起点。
