跳到主要内容
ecdsa.com

SSH vs GPG:Git 提交签名该用哪种密钥

两者都把签名写入提交对象,供 Git 和代码托管平台验证。SSH 签名复用开发者手头已有的密钥;OpenPGP 则自带过期、吊销机制和一套独立的信任模型。

在 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=sshgit commit -S 自 Git 1.7.9(2012年)起;签名标签早得多
签名工具ssh-keygen -Y sign,OpenSSH 8.1 或更高版本(避免使用 8.7)gpg 或其他 OpenPGP 实现
签名格式命名空间为 git 的 SSHSIG,ASCII 装甲格式为 BEGIN SSH SIGNATUREOpenPGP 签名数据包,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 配置,因此不会改动你机器上的任何设置。

一个仓库,一个 SSH 签名的提交和一个 OpenPGP 签名的提交
#!/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 起支持。

迁移说明

  1. 配置 SSH 签名:git config --global gpg.format ssh,把 user.signingkey 设为你的公钥文件(或用 key:: 加上密钥本身),再设置 commit.gpgsign true 和 tag.gpgsign true。
  2. 在代码托管平台上把密钥注册为签名密钥;GitHub 和 GitLab 允许同一个密钥同时用于身份验证和签名,但单独的签名密钥更容易轮换。
  3. 在仓库中发布允许的签名者列表(例如 .gitsigners),在 CI 中把 gpg.ssh.allowedSignersFile 指向它,并在合并时运行 git verify-commit;添加密钥时加上 valid-after,退役密钥时加上 valid-before。
  4. 保留旧的 OpenPGP 公钥,让验证者仍能获取,并保留在代码托管平台的账号中,这样切换之前签名的提交仍能通过验证。
  5. 所有提交者都完成切换后,在受保护分支上强制要求签名提交(GitHub 规则集、GitLab 推送规则)。

参考资料

全部对比