这篇文章是在折腾 Git 提交签名和 SSH 推送的时候写的,属于踩坑记录。如果你也遇到过 signed commit 但是 push 被拒的问题,可能对你有帮助。

起因

最近在捣鼓一个项目,想搞一个比较干净的 Git 工作流:writing 分支写东西,然后 merge 到 deploy 分支,再推到远程仓库。

过程中碰到了一个很奇怪的事——我配好了签名提交,commit 确实是 signed 的,但 push 的时候居然被拒绝了。一脸懵逼,明明之前没问题的。

后来才发现:Git 的签名(signing)和推送(push)用的密钥根本不是一回事。

两件事,两条线

简单来说,你在用 Git 的时候,涉及两种完全不同的认证场景:

1. Commit 签名

这玩意儿用来证明”这个 commit 确实是我提交的”。你可以用 GPG 或者 SSH 来签名。

签名只作用于 commit object,不参与网络传输。换句话说,它跟你的远端仓库有没有权限推送,没有任何关系。

关键点:签名配置指向的是公钥文件(.pub)。

2. SSH Push 认证

这才是你真正往远程仓库写东西时用的。SSH key 负责的是”你是谁,我能相信你能操作这个仓库吗”这个问题。

生成方式也很简单:

1
ssh-keygen -t ed25519 -C "your_email@example.com" -f ~/.ssh/id_ed25519

然后把公钥加到你的 GitHub/Gitee 账户里就行了。GitHub 上在 Settings → SSH and GPG keys → New SSH key,把 id_ed25519.pub 的内容贴上去。

关键点:SSH 认证配置指向的是私钥文件(不带 .pub)。

混在一起会怎样?

问题就出在这里。很多人以为配好签名就够了,或者反过来,配了 SSH 推送就觉得 commit 也会自动签名。都不对。

这两个环节完全独立,一个管签名字符串里的 identity,一个管 SSH transport 层的认证。

如果你不小心把它们搞混了,就会遇到以下症状:

  • commit 报 gpg: skipped secret key —— 签名 key 不对或者没导入
  • push 报 Permission denied —— SSH key 没配或者不在 authorized keys 里

这两者看起来都是 push 失败的样子,但根本原因完全不同。

常见配置误区

网上很多教程(包括我最初的写法)都有这几个坑,先排掉:

误区 1:ssh-add 加载公钥

1
2
3
4
5
# ❌ 错:ssh-add 不接受 .pub 公钥
ssh-add ~/.secret/id_sign.pub

# ✅ 对:加载私钥
ssh-add ~/.secret/id_sign

ssh-add 只能加载私钥(且是未加密或已解锁的私钥),加载 .pub 文件会直接报错。

误区 2:IdentityFile 指向公钥

1
2
3
4
5
6
7
# ❌ 错:IdentityFile 必须是私钥
Host github.com
IdentityFile ~/.secret/id_sign.pub

# ✅ 对
Host github.com
IdentityFile ~/.secret/id_sign

SSH 认证时 IdentityFile 必须指向私钥。指向公钥文件,SSH client 根本找不到对应的私钥做签名握手,push 必被拒。

误区 3:配置键写错

1
2
3
4
5
# ❌ 错:gpgformat 少了一个点号,Git 会静默忽略
git config -c gpgformat=ssh ...

# ✅ 对:正确键名是 gpg.format
git config -c gpg.format=ssh ...

误区 4:以为”公钥部分可以复用”

有人会问:”同一个文件的公钥能不能既签名又认证?”答案是不能混用,因为两条线指向的文件类型根本不同:

用途 指向的文件 原因
user.signingkey(签名) 公钥 id_sign.pub SSH 签名格式需要公钥来验证
IdentityFile(推送认证) 私钥 id_auth SSH 握手需要私钥做签名

签名指向公钥、认证指向私钥,这是两个不同性质的密钥文件,不存在”复用”。

怎么正确配置?

我的实际配置长这样,假设有两个独立的 key:

1
2
3
4
~/.secret/id_sign     ← 私钥(签名)
~/.secret/id_sign.pub ← 公钥(签名)
~/.secret/id_auth ← 私钥(推送认证)
~/.secret/id_auth.pub ← 公钥(推送认证)

路径中的 ~ 表示你的用户主目录(Windows 下通常是 C:\Users\你的用户名\)。请把示例路径替换成你自己存放密钥的实际位置。

Commit Signing(仓库内配置,不动全局)

仓库本地配置(--local)的优先级高于全局配置,所以完全可以只在这个仓库里独立设置签名 key,不影响其他仓库,也不怕被全局覆盖。

1
2
3
4
5
6
7
8
9
10
11
cd /path/to/your/repo

# 签名格式设为 SSH
git config --local gpg.format ssh

# 指定签名用的公钥(注意是 .pub)
git config --local user.signingkey ~/.secret/id_sign.pub

# 自动签名
git config --local commit.gpgsign true
git config --local tag.gpgsign true

之后在这个仓库里普通的 git commit 就会自动用 id_sign 签名,验证方式:

1
git log --show-signature -1

SSH Push(私钥走 SSH 层)

推送认证走的是 SSH 层,与 git config 无关。在 ~/.ssh/config 里指定私钥:

1
2
3
4
5
Host github.com
HostName github.com
User git
IdentityFile ~/.secret/id_auth
IdentitiesOnly yes

如果不想改 SSH config,也可以用环境变量临时指定:

1
GIT_SSH_COMMAND='ssh -i ~/.secret/id_auth' git push

验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1. 确认签名配置在仓库内生效
git config --local --list

# 2. 测试签名提交
git commit --allow-empty -S -m "测试:验证签名分离"

# 3. 看签名状态
git log --show-signature -1

# 4. 验证推送用的 SSH key
ssh -T git@github.com
# 看 Hi 后面的用户名是不是 id_auth 对应的账号

# 5. 推送
git push

总结

一句话:Git 的 commit 签名和网络推送是两件独立的事情,各自有自己的 key 体系。

  • Commit 签名 = 证明这条记录是谁写的,用的是 GPG 或 SSH 公钥,在仓库内用 git config --local 配置
  • Push 认证 = 证明你有权限操作这个仓库,用的是 SSH 私钥,在 ~/.ssh/config 里配置

别把它们混为一谈,排查问题的时候效率能高不少。

下次再遇到 push 失败,先搞清楚到底是签名出了问题还是 SSH 认证出了问题。看错误信息就能大致判断——有 gpg 字眼的肯定是签名问题,纯 Permission denied 的话就去检查 SSH key 配置。