在 SSH 和 GPG 提交签名之间做选择,就是选择运行 git commit -S 或 git tag -s 时 Git 使用哪种密钥。使用 OpenPGP(默认选项,通常是 GnuPG)时,Git 把 OpenPGP 签名存放在提交的 gpgsig 头部中。使用 SSH(gpg.format=ssh,Git 2.34 及更高版本)时,Git 调用 ssh-keygen -Y sign,并把 SSHSIG 签名存放在同一位置。无论哪种方式,签名覆盖的都是提交对象,而提交对象写明了树、父提交、作者和提交信息,因此一个签名就能证明截至该提交的整个历史的真实性。
概览
| SSH 签名 | OpenPGP(GPG)签名 | |
|---|---|---|
| Git 支持 | Git 2.34(2021年11月),gpg.format=ssh | git commit -S 自 Git 1.7.9(2012年)起;签名标签早得多 |
| 签名工具 | ssh-keygen -Y sign,OpenSSH 8.1 或更高版本(避免使用 8.7) | gpg 或其他 OpenPGP 实现 |
| 签名格式 | 命名空间为 git 的 SSHSIG,ASCII 装甲格式为 BEGIN SSH SIGNATURE | OpenPGP 签名数据包,ASCII 装甲格式为 BEGIN PGP SIGNATURE |
| 在提交对象中的大小(Ed25519,实测) | 306 字节 | 279 字节 |
| 信任谁 | gpg.ssh.allowedSignersFile 中列出的密钥 | 密钥环中所有者信任度足够的密钥 |
| 过期与轮换 | 在 allowed_signers 中为每个密钥设置 valid-after 和 valid-before(OpenSSH 8.8+) | 密钥内的到期日期,可以延长;子密钥 |
| 吊销 | gpg.ssh.revocationFile(密钥列表或 KRL) | 随密钥发布的吊销证书 |
| 硬件密钥 | FIDO2 密钥(ed25519-sk、ecdsa-sk),由安全隔区支持的代理 | OpenPGP 智能卡,以及带 OpenPGP 应用的安全密钥 |
| 证书颁发机构 | 通过 cert-authority 行使用 SSH 证书 | 无;身份由其他密钥认证 |
| GitHub“Verified”标记 | 自2022年8月23日起 | 自2016年4月起 |
| GitLab“Verified”标记 | 自 15.7(2022年12月)起,15.8 起正式可用 | 支持 |
用两种方式签名和验证
下面的脚本在临时目录中为同一个身份分别生成一个 SSH 密钥和一个 OpenPGP 密钥,用它们各对一个提交签名,然后验证这两个签名(Git 2.54、OpenSSH 10.3、GnuPG 2.5.21)。脚本禁用了全局 Git 配置,因此不会改动你机器上的任何设置。
#!/bin/sh
base=$(pwd)
dir=$(mktemp -d "$base/commitdemo.XXXXXX")
cd "$dir" || exit 1
GNUPGHOME=$(mktemp -d /tmp/gpg.XXXXXX) # short path: gpg-agent sockets need one
export GNUPGHOME GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1
export GIT_AUTHOR_NAME=Alice GIT_AUTHOR_EMAIL=alice@example.org
export GIT_COMMITTER_NAME=Alice GIT_COMMITTER_EMAIL=alice@example.org
# One SSH key with an allowed_signers entry, one OpenPGP key
ssh-keygen -q -t ed25519 -N '' -C alice@example.org -f alice
echo "alice@example.org namespaces=\"git\" $(cat alice.pub)" > allowed_signers
gpg --batch --quiet --passphrase '' --quick-gen-key "Alice <alice@example.org>" ed25519 sign 1y
fpr=$(gpg --list-keys --with-colons alice@example.org 2>/dev/null | awk -F: '/^fpr/ { print $10; exit }')
# Two commits: the first signed with SSH, the second with OpenPGP
git init -q repo && cd repo || exit 1
git config gpg.ssh.allowedSignersFile ../allowed_signers
echo one > file && git add file
git -c gpg.format=ssh -c user.signingkey=../alice.pub commit -q -S -m "signed with SSH"
echo two >> file
git commit -q -a -S"$fpr" -m "signed with OpenPGP" 2>/dev/null
for c in HEAD~1 HEAD; do
echo "== $(git log -1 --format=%s "$c")"
git verify-commit "$c" 2>&1 | grep -E "Good|using"
git cat-file commit "$c" | awk '/^gpgsig/,/END/' > sig.txt
echo "$(head -1 sig.txt | cut -c8-) ($(wc -c < sig.txt | tr -d ' ') bytes in the commit object)"
done
gpgconf --kill gpg-agent
cd "$base" && rm -rf "$dir" "$GNUPGHOME"
== signed with SSH
Good "git" signature for alice@example.org with ED25519 key SHA256:7z1vKkx5UJmp2Qrc5ucSDqImz8+r8pfG21roc32f/2A
-----BEGIN SSH SIGNATURE----- (306 bytes in the commit object)
== signed with OpenPGP
gpg: using EDDSA key 6A4E1CC8308072568C9E240CDDBB1B4BE59BFC00
gpg: Good signature from "Alice <alice@example.org>" [ultimate]
-----BEGIN PGP SIGNATURE----- (279 bytes in the commit object)
SSH 验证会给出 allowed_signers 中的主体(principal)和密钥指纹;OpenPGP 验证会给出用户 ID,并报告密钥环中的信任级别。两者大小相近,因为都只携带一个 Ed25519 签名;SSHSIG 还嵌入了完整的公钥,OpenPGP 只嵌入公钥的指纹。SSH 签名验证工具和 OpenPGP 查看器可以解码 git cat-file commit 输出中的任一签名块。
决定谁的签名算数
只有当验证者知道哪些密钥属于谁时,有效的签名才有意义。在 SSH 中,这种对应关系就是 allowed_signers 文件:每个主体和密钥占一行,还可以限定为 git 命名空间和某个有效期。Git 会对照签名生成的时间检查 valid-after 和 valid-before,因此退役的密钥可以带着结束日期留在文件中,旧提交依然可以验证。带 cert-authority 的一行会信任由组织的 SSH 证书颁发机构认证的所有密钥,适合已经在签发短期 SSH 证书的公司。这个文件可以放在仓库中,因此对它的修改本身也会经过评审和签名。
在 OpenPGP 中,这种对应关系保存在每个验证者的密钥环和信任数据库中,这也是 CI 系统和代码托管平台要显式导入密钥的原因。信任网可以在陌生人之间建立联系,但在历史悠久的社区之外很少有人使用。密钥自带到期日期和吊销证书;借助子密钥,开发者可以把主密钥离线保存,并在不改变身份的情况下轮换签名子密钥。
代码托管平台还会再加一层:当签名密钥注册在提交者已验证电子邮件地址所属的账号下时,GitHub 和 GitLab 会显示“Verified”。这个徽章取决于平台的记录;GitHub 的持久验证自2024年11月起处于预览阶段,它在推送时保存验证结果,因此之后删除密钥也不会改变这个结果。
如何选择
- GitHub 或 GitLab 上的个人开发者:SSH,最好使用安全密钥上的
ed25519-sk密钥,这样每次签名都需要触碰一下设备。 - 团队和公司:SSH,配合仓库中的
allowed_signers文件,或者使用你现有 CA 签发的 SSH 证书。 - 用 OpenPGP 验证上游标签的项目(Linux 内核、许多发行版、公布了密钥的发布经理):OpenPGP。
- 在 CI 中或由机器人创建的提交:两种长期密钥都不适用;使用基于短期证书的无密钥签名(例如 Sigstore 的 gitsign,参见 Sigstore 与 GPG 签名对比),或者代码托管平台自己的机器人签名。
- 使用 X.509 智能卡的受监管环境:
gpg.format=x509配合 S/MIME 签名工具,自 Git 2.19 起支持。
迁移说明
- 配置 SSH 签名:
git config --global gpg.format ssh,把user.signingkey设为你的公钥文件(或用key::加上密钥本身),再设置commit.gpgsign true和tag.gpgsign true。 - 在代码托管平台上把密钥注册为签名密钥;GitHub 和 GitLab 允许同一个密钥同时用于身份验证和签名,但单独的签名密钥更容易轮换。
- 在仓库中发布允许的签名者列表(例如
.gitsigners),在 CI 中把gpg.ssh.allowedSignersFile指向它,并在合并时运行git verify-commit;添加密钥时加上valid-after,退役密钥时加上valid-before。 - 保留旧的 OpenPGP 公钥,让验证者仍能获取,并保留在代码托管平台的账号中,这样切换之前签名的提交仍能通过验证。
- 所有提交者都完成切换后,在受保护分支上强制要求签名提交(GitHub 规则集、GitLab 推送规则)。