为什么 Git 提交需要签名
在开源协作和企业内部开发中,Git 仓库的提交记录本质上只是一行文本——任何人都可以通过
1 | git commit --author |
伪装成其他人的身份提交代码。如果你在一个大型项目中看到一个标记为 Linus Torvalds 的提交,你能确定它真的来自 Linus 本人吗?答案是不能,除非该提交带有经过验证的数字签名。
Git 提交签名通过非对称加密技术为每个提交附加一个数字签名,接收方可以用签名者的公钥来验证提交的真实性和完整性。一旦签名验证通过,就可以确认两件事:第一,该提交确实由私钥持有者创建;第二,提交内容在传输过程中未被篡改。这在以下场景中尤为重要:
- 开源项目维护者需要确认贡献者身份,防止恶意代码冒充知名开发者混入主干分支
- 企业合规要求所有生产环境部署的代码必须来自经过验证的来源
- 供应链安全(如 SLSA 框架)要求代码制品具备可追溯的来源证明
- 团队中多名开发者共用同一台构建机器,需要区分谁执行了关键提交
从 Git 2.0 开始支持 GPG 签名,Git 2.34 起原生支持 SSH 签名。两种方式各有优劣,本文将从原理到实战全面讲解如何配置和使用它们。

Git 提交签名的底层原理
理解 Git 签名机制需要先了解 Git 提交对象的内部结构。每个 Git 提交本质上是一个存储在
1 | .git/objects |
目录下的文本对象,其内容包含以下字段:
1
2
3
4
5
6 tree <tree-hash>
parent <parent-commit-hash>
author John Doe <john@example.com> 1694567890 +0800
committer John Doe <john@example.com> 1694567890 +0800
This is the commit message
当你在提交时附加签名,Git 会在提交对象的头部添加一个
1 | gpgsig |
字段,其中嵌入了签名数据:
1
2
3
4
5
6
7
8
9
10 tree <tree-hash>
parent <parent-commit-hash>
author John Doe <john@example.com> 1694567890 +0800
committer John Doe <john@example.com> 1694567890 +0800
gpgsig -----BEGIN PGP SIGNATURE-----
iQIzBAABCAAdFiEE...
-----END PGP SIGNATURE-----
This is the commit message
关键点在于:签名覆盖的是提交对象中从
1 | tree |
行开始到空行之前的所有内容(包括 author、committer 和 message),但不包括签名本身。这意味着签名绑定了提交的作者信息、时间戳、父提交以及提交消息——任何对这些字段的篡改都会导致签名验证失败。
对于 SSH 签名,Git 使用的是同样的机制,只是将 PGP 签名块替换为 SSH 签名块,验证时调用
1 | ssh-keygen |
而非
1 | gpg |
来校验。
使用 GPG 配置提交签名(完整实战)
第一步:生成 GPG 密钥
首先检查系统是否已安装 GPG,推荐使用 GnuPG 2.x 版本:
1
2
3
4
5 # 检查 GPG 版本
gpg --version
# 生成新的密钥对(推荐使用 RSA 4096 位)
gpg --full-generate-key
在交互式提示中,选择以下选项:
- 密钥类型:(1) RSA and RSA(默认即可)
- 密钥长度:4096
- 过期时间:1y(一年后过期,到期前可续期)
- 真实姓名:你的真实姓名
- 邮箱地址:与 Git 提交中使用的邮箱必须一致
生成完成后,列出你的密钥并获取密钥 ID:
1
2
3
4
5
6
7
8
9
10 # 列出所有 GPG 密钥
gpg --list-secret-keys --keyid-format=long
# 输出示例:
# sec rsa4096/3AA5C34371567BD2 2024-01-15 [SC] [expires: 2025-01-14]
# D4F8B1A73AA5C34371567BD2
# uid [ultimate] John Doe <john@example.com>
# ssb rsa4096/4A3B2C1D5E6F7A8B 2024-01-15 [E] [expires: 2025-01-14]
# 上面 3AA5C34371567BD2 就是你的密钥 ID
第二步:将 GPG 密钥与 Git 关联
1
2
3
4
5
6
7
8
9
10
11 # 设置全局签名密钥
git config --global user.signingkey 3AA5C34371567BD2
# 开启所有提交自动签名
git config --global commit.gpgsign true
# 开启所有标签自动签名
git config --global tag.gpgsign true
# 开启推送时自动对合并提交签名
git config --global merge.gpgsign true
如果你在 macOS 上使用 GPG,可能会遇到
1 | gpg failed to sign the data |
错误。这通常是因为 GPG Agent 无法正确获取密码。解决方法是配置
1 | pinentry-mac |
:
1
2
3
4
5
6
7
8
9 # 安装 pinentry-mac
brew install pinentry-mac
# 配置 GPG 使用 pinentry-mac
echo "pinentry-program $(which pinentry-mac)" >> ~/.gnupg/gpg-agent.conf
# 重启 GPG Agent
gpgconf --kill gpg-agent
gpgconf --launch gpg-agent
第三步:将公钥上传到 Git 托管平台
为了让 GitHub、GitLab 等平台验证你的签名,需要将公钥添加到你的账户设置中。首先导出公钥:
1
2
3
4
5
6
7
8 # 以 ASCII 格式导出公钥
gpg --armor --export 3AA5C34371567BD2
# 或者直接复制到剪贴板(macOS)
gpg --armor --export 3AA5C34371567BD2 | pbcopy
# Linux
gpg --armor --export 3AA5C34371567BD2 | xclip -selection clipboard
然后在 GitHub 中:Settings → SSH and GPG keys → New GPG key,粘贴导出的公钥即可。之后,你在该平台上的已签名提交会显示一个 Verified 标记。
第四步:手动签名与验证
1
2
3
4
5
6
7
8
9
10
11
12
13 # 对单个提交签名(即使未开启自动签名)
git commit -S -m "feat: add user authentication module"
# 对已有提交重新签名(如密钥过期后更换密钥)
git rebase --gpg-sign=3AA5C34371567BD2 HEAD~5
# 验证单个提交的签名
git verify-commit HEAD
# 输出验证成功的信息示例:
# gpg: Signature made Mon Jan 15 14:32:01 2024 CST
# gpg: using RSA key 3AA5C34371567BD2
# gpg: Good signature from "John Doe <john@example.com>"

使用 SSH 配置提交签名(Git 2.34+)
SSH 签名是 Git 2.34 引入的功能,它使用你已有的 SSH 密钥对提交进行签名,无需安装和维护额外的 GPG 工具链。对于已经在使用 SSH 密钥进行仓库认证的开发者来说,这大大降低了配置门槛。
配置 SSH 签名
1
2
3
4
5
6
7
8
9 # 告诉 Git 使用 SSH 进行签名
git config --global gpg.format ssh
# 指定用于签名的 SSH 密钥
git config --global user.signingkey ~/.ssh/id_ed25519.pub
# 开启自动签名
git config --global commit.gpgsign true
git config --global tag.gpgsign true
如果你有多个 SSH 密钥,可以创建一个专用的签名密钥,与推送密钥分离管理:
1
2
3
4
5
6 # 生成专用的签名密钥(不含密码短语以简化 CI 流程,
# 但本地使用建议设置密码)
ssh-keygen -t ed25519 -C "signing@example.com" -f ~/.ssh/signing_key
# 配置 Git 使用专用签名密钥
git config --global user.signingkey ~/.ssh/signing_key.pub
在 SSH config 中指定签名密钥
当使用 SSH 签名时,
1 | ssh-keygen |
需要找到正确的密钥。如果你的签名密钥不在默认路径下,需要在
1 | ~/.ssh/config |
中指定:
1
2
3
4
5
6
7 # ~/.ssh/config
Host *
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
# Git 签名使用的密钥
git config --global gpg.ssh.program "$(which ssh-keygen)"
将 SSH 公钥添加到 GitHub 用于验证
GitHub 支持将 SSH 公钥作为签名密钥添加。在 GitHub 中:Settings → SSH and GPG keys → New SSH key,在 Key type 下拉菜单中选择 Signing Key(而非 Authentication Key),然后粘贴你的公钥。
验证 SSH 签名
1
2
3
4
5 # 验证提交的 SSH 签名
git verify-commit HEAD
# 在日志中显示签名状态
git log --show-signature -1
要在
1 | git log |
中快速查看哪些提交有签名、哪些没有,可以使用以下自定义格式:
1
2
3
4
5
6
7
8
9
10
11
12 # 显示最近 20 个提交的签名状态
git log --pretty=format:"%H %G? %an %s" -20
# %G? 的含义:
# G - 签名验证通过
# B - 签名验证失败
# U - 签名使用未知密钥(需要导入公钥)
# X - 签名正确但过期
# Y - 签名正确但密钥过期
# R - 签名正确但撤销
# E - 签名无法验证
# N - 没有签名
GPG 签名与 SSH 签名的对比与选择
两种签名方式各有优劣,下表对比了它们的主要差异:
| 特性 | GPG 签名 | SSH 签名 |
|---|---|---|
| 最低 Git 版本 | 2.0+ | 2.34+ |
| 依赖工具 | GnuPG(独立的工具链) | OpenSSH(通常已安装) |
| 密钥管理 | 密钥服务器、信任网络(Web of Trust) | SSH 密钥对,可复用现有密钥 |
| 密钥过期与轮换 | 支持子密钥轮换,主密钥长期有效 | 需手动管理过期,不如 GPG 灵活 |
| 多方验证 | 支持密钥签名链,信任网络成熟 | 较简单,主要依赖平台验证 |
| CI/CD 集成 | 需要额外配置 GPG Agent | SSH 密钥更易于在 CI 中使用 |
| 平台支持 | GitHub、GitLab 均原生支持 | GitHub 原生支持,GitLab 15.0+ 支持 |
实际选择建议:
- 个人开发者:优先使用 SSH 签名,配置简单,无需维护额外的密钥体系
- 开源项目维护者:推荐使用 GPG 签名,利用信任网络和密钥服务器实现更广泛的身份验证
- 企业团队:可根据内部安全策略选择,SSH 签名更易于在 CI/CD 流水线中集成
- 安全要求极高的场景:建议使用 GPG 的硬件令牌(如 YubiKey)存储密钥,实现物理隔离
标签签名与验证
除了提交签名外,Git 标签也可以签名。这在发布版本时尤为重要——签名标签可以让用户验证软件包确实来自发布者本人,而非被篡改的版本。
1
2
3
4
5
6
7
8 # 创建签名标签
git tag -s v1.0.0 -m "Release version 1.0.0"
# 验证标签签名
git tag -v v1.0.0
# 验证远程标签时,需要先获取对应的公钥
git verify-tag v1.0.0
标签签名和提交签名使用相同的密钥配置,无需额外设置。对于 SSH 签名标签,Git 会使用
1 | gpg.format ssh |
的配置自动处理。
在 CI/CD 中实现自动签名提交
在自动化流水线中实现提交签名需要特殊处理,因为密钥不能以明文形式存储。以下是使用 GPG 在 CI 中配置自动签名的完整方案:
1
2
3
4
5
6
7
8 # 导出 GPG 密钥用于 CI(在本地执行)
# 注意:这会导出私钥,请妥善保管
gpg --armor --export-secret-keys 3AA5C34371567BD2 | base64 > gpg_private_key.b64
gpg --armor --export 3AA5C34371567BD2 > gpg_public_key.asc
# 将 base64 编码的私钥内容设置为 CI 的 Secret 变量
# 变量名:GPG_PRIVATE_KEY
# 同时设置密钥密码变量:GPG_PASSPHRASE
在 CI 流水线脚本中导入密钥并配置签名:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 # .github/workflows/release.yml 示例片段
steps:
- name: Import GPG key
run: |
echo "$GPG_PRIVATE_KEY" | base64 --decode | gpg --batch --import
echo "default-cache-ttl 7200" >> ~/.gnupg/gpg-agent.conf
echo "passphrase $GPG_PASSPHRASE" | gpg --batch --pinentry-mode loopback --passphrase-fd 0 --command-fd 0 --edit-key 3AA5C34371567BD2 trust
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
- name: Configure Git signing
run: |
git config --global user.signingkey 3AA5C34371567BD2
git config --global commit.gpgsign true
git config --global gpg.program gpg
对于 SSH 签名,CI 配置更加简单——只需将签名密钥作为 Secret 存储,然后在流水线中写入文件即可:
1
2
3
4
5
6
7
8
9
10
11 steps:
- name: Configure SSH signing
run: |
mkdir -p ~/.ssh
echo "$SSH_SIGNING_KEY" > ~/.ssh/signing_key
chmod 600 ~/.ssh/signing_key
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/signing_key.pub
git config --global commit.gpgsign true
env:
SSH_SIGNING_KEY: ${{ secrets.SSH_SIGNING_KEY }}

强制签名策略与团队管理
在团队协作中,仅靠个人自觉开启签名是不够的。可以通过以下机制在团队层面强制执行签名策略:
使用保护分支规则
GitHub 和 GitLab 都支持在保护分支上要求所有合并提交必须通过签名验证。在 GitHub 的仓库设置中:Settings → Branches → Branch protection rules → Require signed commits。开启后,未签名的提交将被拒绝合并到受保护分支。
使用服务端钩子检查
如果你运行自托管的 Git 服务器(如 Gitea、Forgejo),可以通过 pre-receive 钩子强制要求所有推送的提交都带有有效签名:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 #!/bin/bash
# pre-receive hook: 强制要求签名提交
while read oldrev newrev refname; do
# 跳过分支删除
if [ "$newrev" = "0000000000000000000000000000000000000000" ]; then
continue
fi
# 检查每个提交是否有签名
for commit in $(git rev-list $oldrev..$newrev); do
signature_status=$(git verify-commit $commit 2>&1)
if ! echo "$signature_status" | grep -q "Good signature"; then
echo "ERROR: Commit $commit lacks a valid GPG signature!"
echo "All commits must be signed before pushing."
exit 1
fi
done
done
使用 git-trust 模型验证
Git 本身支持通过
1 | gpg.program |
配置自定义验证程序。你可以编写一个包装脚本,只信任特定密钥的签名,实现白名单式的信任管理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 #!/bin/bash
# ~/bin/git-gpg-wrapper.sh
# 只信任指定的密钥 ID
TRUSTED_KEYS="3AA5C34371567BD2 A1B2C3D4E5F6G7H8"
# 调用真正的 gpg 进行验证
RESULT=$(/usr/bin/gpg "$@")
EXIT_CODE=$?
# 如果是验证操作,检查密钥是否在白名单中
if echo "$1" | grep -q "verify"; then
for key in $TRUSTED_KEYS; do
if echo "$RESULT" | grep -q "$key"; then
echo "$RESULT"
exit 0
fi
done
echo "WARNING: Signature key not in trusted list!"
exit 1
fi
echo "$RESULT"
exit $EXIT_CODE
然后配置 Git 使用这个包装器:
1 git config --global gpg.program ~/bin/git-gpg-wrapper.sh
常见问题与故障排查
提交签名失败:gpg failed to sign the data
这是最常见的签名错误,通常由以下原因导致:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 # 1. 检查 GPG Agent 是否运行
gpg-agent --daemon
# 2. 测试 GPG 是否能正常签名
echo "test" | gpg --clearsign
# 3. 如果提示 "Inappropriate ioctl for device",
# 需要设置 pinentry 为循环模式
echo "allow-loopback-pinentry" >> ~/.gnupg/gpg-agent.conf
gpgconf --kill gpg-agent
gpgconf --launch gpg-agent
# 4. 配置 Git 使用 loopback pinentry
git config --global gpg.pinentry-mode loopback
SSH 签名失败:No keys available
1
2
3
4
5
6
7
8 # 确保 user.signingkey 指向公钥文件路径
git config --global user.signingkey ~/.ssh/id_ed25519.pub
# 验证密钥文件存在且可读
ls -la ~/.ssh/id_ed25519.pub
# 测试 SSH 签名
echo "test" | ssh-keygen -Y sign -f ~/.ssh/id_ed25519 -n file
GitHub 显示 “Unverified” 但签名验证通过
这通常是因为 GitHub 上配置的公钥与本地签名密钥不匹配。检查以下几项:
- 确认提交使用的邮箱与 GitHub 账户的邮箱一致(包括大小写)
- 确认上传到 GitHub 的公钥与本地使用的密钥 ID 对应
- 如果使用子密钥签名,确认上传的是子密钥而非主密钥的公钥
- 检查密钥是否已过期或被撤销
密钥过期后如何续期
1
2
3
4
5
6
7
8
9
10
11
12 # 延长 GPG 密钥的过期时间
gpg --edit-key 3AA5C34371567BD2
gpg> expire
# 输入新的过期时间(如 2y 表示两年)
gpg> key 1 # 选择子密钥
gpg> expire # 为子密钥也设置过期时间
gpg> save
# 将更新后的公钥重新上传到 GitHub 和密钥服务器
gpg --armor --export 3AA5C34371567BD2 | curl -T - https://keys.openpgp.org
gpg --keyserver keyserver.ubuntu.com --send-keys 3AA5C34371567BD2
总结
Git 提交签名是保障代码供应链安全的重要手段。通过 GPG 或 SSH 签名,你可以在技术上确保提交的真实性和完整性,防止身份伪造和内容篡改。在配置选择上,SSH 签名凭借更低的配置门槛成为个人开发者的首选,而 GPG 签名则凭借成熟的信任网络和灵活的密钥管理体系更适合开源项目维护者。
无论选择哪种方式,核心原则是一致的:私钥安全是整个信任链的基础。妥善保管私钥、定期轮换密钥、在 CI/CD 中使用最小权限原则,这些安全实践与签名配置本身同等重要。结合平台级别的保护分支规则和服务端钩子,你可以构建一套从提交到合并全链路可验证的代码安全体系。
汤不热吧