为什么Git使用GPG密钥进行签名而不是使用SSH密钥?

28

SSH和GPG非对称密钥有何不同?为什么Git支持使用GPG签名而不是使用SSH代理?


请参见 https://security.stackexchange.com/q/120706。 - torek
那篇帖子并没有完全回答我的主要问题。我知道SSH用于认证,但为什么不能使用(可能是不同的)由SSH代理管理的密钥来签署提交,而不是GPG呢? - Dan Kowalczyk
确实,但如果你仔细阅读它(以及它的参考文献),你会发现这在理论上是可能的,只是不太方便。此外,PGP具有额外的功能(虽然Git本身并没有直接使用它们——Git本身只是调用一些外部软件——但像密钥吊销这样的功能在这些情况下非常有用)。 - torek
谢谢,这补充了这里的最佳答案。 - Dan Kowalczyk
注意:2022年8月,GitHub支持SSH提交验证 - VonC
4个回答

33

2022年9月更新:1Password支持生成和存储用于Git提交签名的SSH密钥,被GitHub认可。


更新2021:

OpenSSH 8.2+已经可用(例如打包在Git For Windows 2.33.1中),并且“现在可以使用您的SSH密钥对任意数据进行签名”(Andrew Ayer),包括Git中的提交。

Andrew指向{{link4:git/git PR 1041“ssh signing:Add commit & tag signing/verification via SSH keys using ssh-keygen”}}, 现在使用Git 2.34(2021年11月)

{{link6:gpg.format}}将有一个新值“ssh

将 `gpg.format = ssh` 和 `user.signingkey` 设置为 SSH 公钥字符串(如来自 `authorized_keys` 文件的字符串),就可以使用来自您的 ssh-agent 的私钥对提交/标签进行签名。
Andrew 增加了:
始终注意不要将加密密钥重新用于不同的协议。如果没有仔细执行此操作,可能存在跨协议攻击的风险。例如,如果 Git 签署的消息结构类似于 SSH 协议消息的结构,则攻击者可能通过误用 SSH 记录中的签名来伪造 Git 人工制品。幸运的是,SSH 协议消息的结构与由 ssh-keygen 签名的消息的结构有足够大的差异,因此不会存在混淆的风险。
这来自于:
Git 2.34 (Q4 2021): 用于对象和推送证书签名的 SSH 公共加密。
参见 commit 1bfb57f, commit f265f2d, commit 3326a78, commit facca53, commit 4838f62, commit fd9e226, commit 29b3157, commit 64625c7, commit b5726a5(2021年9月10日),作者为Fabian Stelzer(FStelzer
(由Junio C Hamano(gitstercommit 18c6653中合并,日期为2021年10月25日)

ssh签名:使用ssh-keygen验证签名

签署者:Fabian Stelzer

为了验证ssh签名,我们首先调用ssh-keygen -Y find-principal来通过公钥从allowedSignersFile中查找签名主体。
如果找到该密钥,则进行验证。
否则,我们只验证签名,但无法验证签名者的身份。
验证使用gpg.ssh.allowedSignersFile(参见ssh-keygen(1)“ALLOWED SIGNERS”),其中包含有效的公钥和主体(通常为user@domain)。
根据环境,此文件可以由各个开发人员管理,也可以例如由具有推送访问权限的已知ssh密钥的中央存储库服务器生成。
该文件通常存储在存储库之外,但如果存储库仅允许签名提交/推送,则用户可能选择将其存储在存储库中。
要撤销密钥,请将不带主体前缀的公钥放入gpg.ssh.revocationKeyring或生成KRL(参见ssh-keygen(1)“KEY REVOCATION LISTS”)。
allowedSignersFile一样,关于信任谁进行验证的考虑也适用于此。
还可以使用SSH CA密钥与这些文件。
在主体和密钥之间添加“cert-authority”作为密钥选项,以将其标记为CA,并将由其签名的所有密钥标记为对此CA有效。
请参见ssh-keygen(1)中的“CERTIFICATES”。

git config现在在其手册页面中包含了:

gpg.ssh.allowedSignersFile

一个包含你愿意信任的 ssh 公钥的文件。 该文件由一个或多个主体和一个 ssh 公钥组成。
例如:user1@example.com,user2@example.com ssh-rsa AAAAX1...
详见 ssh-keygen(1) 中的 "ALLOWED SIGNERS"。
主体仅用于标识密钥,在验证签名时可用。

SSH 没有像 gpg 那样的信任级别概念。为了能够区分有效签名和受信任的签名, 当公钥存在于 allowedSignersFile 中时,签名验证的信任级别设置为 fully
否则,信任级别为 undefined,git verify-commit/tag 将失败。

此文件可以设置到存储库之外的位置,每个开发人员都维护自己的信任存储。 中央存储库服务器可以从具有推送访问权限的 ssh 密钥自动生成此文件以验证代码。
在企业环境中,此文件可能是在全局位置生成的,使用已处理开发人员 ssh 密钥的自动化。

只允许签名提交的存储库可以将该文件存储在存储库本身中,使用相对于工作树顶部的路径。 这样,只有已经具有有效密钥的提交者才能添加或更改密钥。

使用带有 cert-authority 选项的 SSH CA 密钥 (详见 ssh-keygen(1) 中的 "CERTIFICATES")也是有效的。

gpg.ssh.revocationFile

SSH KRL 或吊销的公钥列表(不包括主体前缀)。
详见 ssh-keygen(1)
如果在此文件中找到公钥,则始终将其视为信任级别为 "never",签名将显示为无效。


通过 Git 2.35(2022年第一季度),使用SSH密钥扩展对象签名,并学会在验证时注意密钥有效期时间范围。

请查看 提交 50992f9, 提交 122842f, 提交 dd3aa41, 提交 4bbf378, 提交 6393c95, 提交 30770aa, 提交 0276943, 提交 cafd345, 提交 5a2c1c0 (2021年12月09日) 由 Fabian Stelzer (FStelzer) 提交。
(由 Junio C Hamano -- gitster -- 合并于 提交 d2f0b72, 2021年12月21日)

ssh签名:使verify-commit考虑密钥生命周期

签署者:Fabian Stelzer

如果在allowedSigners文件中为此签名密钥配置了有效日期,则验证应检查密钥在提交时是否有效。
这允许优雅地进行密钥翻转并撤销密钥,而不会使所有先前的提交无效。
此功能需要openssh > 8.8。
旧版本的ssh-keygen将简单地忽略此标志并使用当前时间。
严格来说,此功能在8.7中可用,但由于8.7存在一个错误,使其在另一个所需的调用中无法使用,因此我们需要8.8。
大多数check_signature调用中都存在时间戳信息。
但是签名者标识符不存在。
我们将需要签名者电子邮件/名称以便能够稍后实现“第一次使用信任”功能。
由于负载包含所有必要的信息,因此我们可以从那里解析它。
调用者只需要通过在signature_check结构中设置payload_type来向我们提供有关负载的一些信息即可。
  • 添加payload_type字段和枚举以及payload_timestamp到结构体`signature_check`
  • 如果我们知道负载类型,则在尚未设置时间戳时填充时间戳
  • 在用户时区中传递-Overify-time={payload_timestamp}以进行所有ssh-keygen验证调用
  • 验证提交时设置负载类型
  • 添加过期、尚未有效以及具有超出密钥有效性的提交日期和在内部的密钥的测试

git config现在在其手册页面中包括以下内容:

自OpensSSH 8.8以来,该文件允许使用valid-after和valid-before选项指定密钥生命周期。

如果签名密钥在签名创建时是有效的,则Git将标记签名为有效。

这使用户可以更改签名密钥而不会使所有先前生成的签名无效。


而且,仍然使用Git 2.35(2022年第一季度),使用ssh密钥进行加密签名的可以通过使用“key ::”前缀机制指定文字密钥以供不以“ssh-”前缀开头的键类型使用(例如,“ key :: ecdsa-sha2-nistp256 ”)。
查看 提交 3b4b5a7提交 350a251(2021年11月19日)由Fabian Stelzer (FStelzer)
(由Junio C Hamano -- gitster --合并于提交 ee1dc49,2021年12月21日)

ssh签名:支持非 ssh-* 密钥类型

签署者:Fabian Stelzer

user.signingKey 配置用于 ssh 签名,支持包含密钥的文件路径或为方便起见的 ssh 公钥字面字符串。

为了区分这两种情况,我们检查前几个字符是否包含 "ssh-",这不太可能是路径的开头。
ssh 支持其他未以 "ssh-" 为前缀的密钥类型,目前将被视为文件路径并因此无法加载。
为了解决这个问题,我们将前缀检查移动到自己的函数中,并引入前缀 key:: 用于字面 ssh 密钥。
这样,当新的密钥类型可用时,我们就不需要添加新的密钥类型了。
现有的 ssh- 前缀保留了与当前用户配置的兼容性,但从官方文档中删除以防止使用。

git config现在在其man页面中包含以下内容:

如果gpg.format设置为ssh,则可以包含路径到私有SSH密钥或者使用ssh-agent时的公钥。 或者它可以直接包含以key::为前缀的公钥(例如:"key::ssh-rsa XXXXXX identifier")。

私钥需要通过ssh-agent可用。
如果未设置,则git将调用gpg.ssh.defaultKeyCommand(例如:"ssh-add -L")并尝试使用第一个可用的密钥。

为了向后兼容,以"ssh-"开始的原始密钥,比如"ssh-rsa XXXXXX identifier",被视为"key::ssh-rsa XXXXXX identifier",但是这种形式已经过时了; 请改用key::形式。


"git merge $signed_tag"(man) 从 Git 2.35 (Q1 2022) 开始,会默认包含标签信息在合并信息中,这是之前由于意外而被删除的。

查看 提交 c39fc06 (2022年1月10日),作者为Taylor Blau (ttaylorr)
(由Junio C Hamano -- gitster --合并于提交 cde28af,2022年1月12日)

fmt-merge-msg:防止使用已签名标签后释放

报告者:Linus Torvalds
签署者:Taylor Blau

When merging a signed tag, fmt_merge_msg_sigs() is responsible for populating the body of the merge message with the names of the signed tags, their signatures, and the validity of those signatures.

In 0276943 ("ssh signing: use sigc struct to pass payload", 2021-12-09, Git v2.35.0-rc0 -- merge listed in batch #4), check_signature() was taught to pass the object payload via the sigc struct instead of passing the payload buffer separately.

In effect, 0276943 causes buf, and sigc.payload to point at the same region in memory.
This causes a problem for fmt_tag_signature(), which wants to read from this location, since it is freed beforehand by signature_check_clear() (which frees it via sigc's payload member).

That makes the subsequent use in fmt_tag_signature() a use-after-free.

As a result, merge messages did not contain the body of any signed tags.
Luckily, they tend not to contain garbage, either, since the result of strstr()-ing the object buffer in fmt_tag_signature() is guarded:

const char *tag_body = strstr(buf, "\n\n");
if (tag_body) {
  tag_body += 2;
  strbuf_add(tagbuf, tag_body, buf + len - tag_body);
}

Resolve this by waiting to call signature_check_clear() until after its contents can be safely discarded.
Harden ourselves against any future regressions in this area by making sure we can find signed tag messages in the output of fmt-merge-msg, too.


原始答案(2017年):Git 中有关签署 任何 东西的最初概念是在提交 ec4465a,Git v0.99,2005 年 4 月 中引用的(几乎是从一开始)

/**
 * A signature file has a very simple fixed format: three lines
 * of "object <sha1>" + "type <typename>" + "tag <tagname>",
 * followed by some free-form signature that git itself doesn't
 * care about, but that can be verified with gpg or similar.
 **/

所以你的问题有其可行性。

第一个带签名的提交使用了 GPG,但也可以使用其他方式(commit 65f0d0e):

#!/bin/sh
object=${2:-$(cat .git/HEAD)}
type=$(cat-file -t $object) || exit 1
( echo -e "object $object\ntype $type\ntag $1\n"; cat ) > .tmp-tag
rm -f .tmp-tag.asc
gpg -bsa .tmp-tag && cat .tmp-tag.asc >> .tmp-tag
git-mktag < .tmp-tag
#rm .tmp-tag .tmp-tag.sig

从技术上讲,您可以使用gpg代替ssh。但我很少看到反过来的情况。
但是您可以使用ssh密钥对与PGP/GPG一起使用
这意味着第一个验证脚本仍然可能有效(提交f336e71)...除非它期望有一个PGP注释:

#!/bin/sh
GIT_DIR=${GIT_DIR:-.git}

tag=$1
[ -f "$GIT_DIR/refs/tags/$tag" ] && tag=$(cat "$GIT_DIR/refs/tags/$tag")

git-cat-file tag $tag > .tmp-vtag || exit 1
cat .tmp-vtag | sed '/-----BEGIN PGP/Q' | gpg --verify .tmp-vtag -
rm -f .tmp-vtag

那么,“为什么git使用GPG密钥进行签名而不是使用SSH密钥?”:这就是GPG的作用,与SSH相反,后者无法仅使用openssh实现(它需要openssl)

正如 torek {{commented:所述}}, 理论上使用SSH是可能的,只是不方便。

此外,PGP具有额外的功能(尽管Git本身仅调用一些外部软件,但像密钥吊销之类的功能在这些情况下非常有用)。


在 Git 2.37 (Q3 2022) 中,解释了 ssh.defaultKeyCommand

请参见 commit ce18a30(由 Fabian Stelzer (FStelzer) 于 2022 年 6 月 8 日提交)
(由 Junio C Hamano -- gitster -- 合并于 commit 686790f,2022 年 6 月 15 日)

gpg文档:更好地解释ssh.defaultKeyCommand的使用方法

签名作者:Fabian Stelzer

对于gpg.ssh.defaultKeyCommand,使用ssh-add -L并不是一个好的建议。
它可能会根据已知密钥的顺序切换密钥,并且仅支持ssh-*而不支持ecdsa或其他密钥。

明确表示我们期望一个以key::为前缀的文字密钥,给出有效的示例用例,并将user.signingKey作为首选选项。

git config现在在其手册页面中包括:

请求签名。 成功退出后,预期在其输出的第一行带有前缀 key :: 的有效ssh公钥。 这允许脚本在不可能静态配置user.signingKey时动态查找正确的公共密钥。 例如,当密钥或SSH证书经常轮换或选择正确的密钥取决于对git未知的外部因素时。

同时,由Fabian Stelzer(FStelzer2022年6月8日提交的ce18a30与之结合。
(由Junio C Hamano -- gitster --于2022年6月15日在commit 686790f中合并)

686790f6c1:合并分支'fs/ssh-default-key-command-doc'

文档更新:fs/ssh-default-key-command-doc:gpg文档:更好地解释了ssh.defaultKeyCommand的使用方法。

gpg.ssh.defaultKeyCommand

当未设置user.signingkey并请求ssh签名时,将运行此命令。
成功退出后,预期在其输出的第一行中有一个带有key::前缀的有效ssh公钥。
这允许脚本在无法静态配置user.signingKey时动态查找正确的公钥。

例如,当密钥或SSH证书经常轮换或选择正确的密钥取决于git不知道的外部因素时。


谢谢,这个答案真的为我提供了理解所需的背景。 - Dan Kowalczyk

6

为什么不应该使用ssh来签署提交的原因是密码学的一个常规规则:您不应该将相同的密钥用于不同的应用程序/用例。

在SSH中,您使用密钥进行身份验证,但这与签署提交不同。对于这个问题,GPG更适合,因为它已经广泛用于签署电子邮件、文件等。


您可以使用不同的 ssh 密钥来签署您的提交。 - Marcos Tapajós

3
可能的一个原因是并非所有使用git的人都在使用ssh。 你可以创建一个git仓库并且从未将其离开你的本地磁盘。你可以使用git协议,或http,或https,或网络文件系统... 这些都不涉及ssh,但你仍然可以签署提交,因为这与任何网络传输或推送/拉取分享提交无关。

3
这是个不错的观点,但我认为SSH及其工具集更普遍,为什么不利用它呢?我开始签署提交并不得不下载GPG工具集,这引发了我的疑问。最佳答案对此进行了解释。 - Dan Kowalczyk

2

好的观点。我相信这在Git 2.34中是可用的。我已经相应地更新了我的答案 - VonC
感谢您的更新。您关于企业使用情况的说明是一个重要的旁注,因为它暗示了确定谁是权威并对所涉及身份做出最终决定的问题。GPG 的设计是分散的,通过鼓励分布式的“信任网络”(https://www.gnupg.org/gph/en/manual.html#AEN554)作为权威,而不是单个人或机构。`git` 使用 GPG 是有道理的,因为它共享分散式设计原则。 - Dan Kowalczyk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接