用 SSH 密钥给提交签名,比 GPG 轻便得多。这篇看签名在 commit 对象里长什么样、怎样在本地验证、怎样配置“信任谁”,以及篡改、陌生人签名、密钥过期时各自会怎么显示。
$ git config --get-regexp "gpg|signing" | sed -E "s#/[^ ]*/_ssh#<ssh目录>#" gpg.format ssh user.signingkey <ssh目录>/alice.pub | tree <sha40> | author Alice <alice@example.com> 1767261600 +0000 | committer Alice <alice@example.com> 1767261600 +0000 | gpgsig -----BEGIN SSH SIGNATURE----- | U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgJrryP71ohXfy3ddKQtwME+9SS0 | Amw+2hmInLH/kW7lEAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 | AAAAQDBULC1lJV0+/lOKCuYx1W2nk5GX7rc7QdBoumEzwaaRyMCVGzivgKR2qOAggy7FiX | AvzzKlCo8NT5ieI26NIww= | -----END SSH SIGNATURE----- | | 签名的提交 (gpgsig 头:从 -----BEGIN SSH SIGNATURE----- 到 -----END SSH SIGNATURE-----,后续行以空格开头)
设置 gpg.format=ssh 和 user.signingkey(公钥文件),git commit -S 之后,commit 对象里多一个 gpgsig 头,里面是 SSH 签名的文本形式。签名是提交内容(去掉 gpgsig 头)的签名,命名空间是 git。
$ git log -1 --show-signature --format=%s 2>&1 | head -4 | sed -E "s/SHA256:[A-Za-z0-9+\/]+/SHA256:<指纹>/" error: gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification No signature 签名的提交 配置 gpg.ssh.allowedSignersFile 之后(文件内容:一行“主体 公钥类型 公钥”): $ git log -1 --show-signature --format=%s 2>&1 | head -4 | sed -E "s/SHA256:[A-Za-z0-9+\/]+/SHA256:<指纹>/" Good "git" signature for alice@example.com with ED25519 key SHA256:<指纹> 签名的提交 签名里的指纹与 ssh-keygen -l 给出的 alice 公钥指纹一致: True
git 需要 gpg.ssh.allowedSignersFile:每行“主体 [选项] 公钥”。没有它就无法验证(No signature)。公钥在白名单里才算 Good for 某主体;不在就是 No principal matched;白名单条目可带 valid-before 等选项。
ssh-keygen -Y verify: Good "git" signature for alice@example.com with ED25519 key SHA256:<指纹> | 退出码 0 改一个字之后再验证: Could not verify signature. Signature verification failed: incorrect signature | 退出码 255 命名空间不是 git(-n other): Could not verify signature. Couldn't verify signature: namespace does not match | 退出码 255
去掉 gpgsig 头的 commit 内容用 ssh-keygen -Y verify -n git 验证,结果与 git 一致;改一个字符、换命名空间都会失败。
$ git verify-commit <篡改后的提交>
Could not verify signature.
Signature verification failed: incorrect signature
退出码 1签名覆盖提交的全部内容,保留原来的 gpgsig 头而改动说明得到的新对象,git verify-commit 报 incorrect signature。
$ git log -1 --show-signature --format=%s 2>&1 | head -4 | sed -E "s/SHA256:[A-Za-z0-9+\/]+/SHA256:<指纹>/" Good "git" signature with ED25519 key SHA256:<指纹> No principal matched. 陌生人签名的提交 $ git log -1 --show-signature --format=%s 2>&1 | head -4 | sed -E "s/SHA256:[A-Za-z0-9+\/]+/SHA256:<指纹>/" Good "git" signature with ED25519 key SHA256:<指纹> /work/_ssh/allowed_signers_expired:1: key has expired: verify time 2026-01-01T18:02:00 > valid-before 2020-01-01T00:00:00 No principal matched. alice 再签一次
公钥不在白名单里:签名本身有效,git 显示 Good "git" signature with ED25519 key … 但接着 No principal matched;白名单条目带 valid-before 且已过期时同样不受信,并说明 key has expired。
$ git log --format='%G? %GS %s' | sed -E 's/^(.) /\1 /' N 没签名的提交 G alice@example.com alice 再签一次 U 陌生人签名的提交 G alice@example.com 签名的提交 $ git tag -v v1 2>&1 | head -3 | sed -E "s/SHA256:[A-Za-z0-9+\/]+/SHA256:<指纹>/"; git cat-file -p v1 | grep -c "BEGIN SSH SIGNATURE" | sed "s/^ */标签对象里的签名块个数: /" Good "git" signature for alice@example.com with ED25519 key SHA256:<指纹> object d0edab54455df698c71810dacfe3631435b083d0 type commit 标签对象里的签名块个数: 1
%G?:G 好签名且受信,U 好签名但不受信,B 坏签名,N 没有签名,E 无法检查。标签用 git tag -s 签名,git tag -v 验证。