Git Commit 与 Tag 签名
一个外部贡献者把 user.name 和 user.email 改成维护者信息,就能创建看起来由维护者编写的 Commit;这两个字段只是对象里的文本。另一个常见误判是平台显示绿色 Verified,团队便把它解释成“代码经过审批”。密码学签名只能证明某个私钥签署了确定的对象,组织仍要回答这把公钥属于谁、当时是否有效、该身份是否有权代表项目签名。
因此,签名链路要连续通过四层判断:
对象完整性:Commit 或附注 Tag 中的签名能否验证,目标对象是否与签署时一致。身份绑定:验证者是否从受控信任源把公钥或证书绑定到自然人、岗位或机器人。平台解释:托管平台是否按账号邮箱、登记密钥、证书链和自身规则认可该签名。
仓库准入:保护规则、Push Rule 或接收钩子是否真的拒绝不合规对象,并怎样处理平台代签、网页提交和紧急例外。
OpenPGP(下文简称 GPG)、SSH 和 X.509/S/MIME 都能为 Git 对象签名,但信任模型与运维成本不同。SSH 登录远端仓库与 SSH 签名也只是复用了密钥类型:前者控制仓库访问,后者验证对象来源;HTTPS/PAT 认证同样不能证明历史 Commit 的作者身份。
先在没有业务数据的临时仓库中确认 Git 和当前身份配置:
git --version
git config --show-origin --get user.name
git config --show-origin --get user.email接着选择一套签名后端,并让签名端与验证端都具备对应工具。GPG 需要 GnuPG 或兼容实现;SSH 需要 Git 与 ssh-keygen;S/MIME 需要组织签发的 X.509 证书和 Git 可调用的签名程序。私钥只能存在于个人密钥库、硬件令牌、系统 agent 或受控签名服务中;示例中的 <signing-key>、<public-key-file> 和 <team-email> 都是占位符。
若目标是托管平台的可信状态,还要登记对应公钥,满足平台的证书信任和邮箱绑定规则。Git 的 git-config 文档定义 openpgp、ssh、x509 三种格式及 SSH 信任和撤销配置,git-commit、git-tag、git-verify-commit 与 git-verify-tag给出创建和验证对象的命令语义。实际能力以本机 git --version、内置帮助和目标托管平台文档为准,不能拿另一台机器或另一家平台的状态规则作结论。
签名不是一个独立 Git 服务,实际链路由 Git、签名程序、私钥和验证者的信任库组成。先选后端,再配置 Git;不要同时启用三套后端后才判断哪套生效。
GPG:兼容广、密钥治理能力完整
通过操作系统或 GnuPG 官方渠道安装后,先确认程序和可用私钥:
gpg --version
gpg --list-secret-keys --keyid-format=long已有组织密钥时按组织流程导入;没有时可交互生成测试密钥:
gpg --full-generate-key生产身份要设置到期时间、保护口令和撤销材料,并由 owner 记录用途。不要把教程临时生成的无口令密钥直接当团队身份。
SSH:开发者上手成本低
SSH 签名通常复用系统已有的 OpenSSH 工具。先确认:
ssh -V可以使用独立签名密钥,也可以按平台允许的方式复用认证密钥。独立密钥能把“允许登录仓库”和“允许代表某身份签名”分开撤销,团队环境更容易审计。生成命令示意如下,实际文件位置由个人密钥目录或硬件代理决定:
ssh-keygen -t ed25519 -C "<team-email>" -f <signing-key-file>S/MIME:适合已有企业 PKI 的组织
S/MIME 不是给个人临时自签证书贴一个平台徽标。它适合组织已经管理证书签发、吊销、续期和私钥托管的场景。先让 PKI 管理员确认代码签名用途、证书链和托管平台的信任边界,再安装组织批准的签名程序,例如 smimesign 或当前 Git 配置支持的 X.509 程序:
smimesign --version没有组织 PKI 时,通常优先选择 SSH 或 GPG,而不是自建一套无人维护的证书体系。
GPG 配置
从 gpg --list-secret-keys --keyid-format=long 找到目标指纹或长 Key ID,然后只在需要签名的仓库先做本地配置:
git config --local gpg.format openpgp
git config --local user.signingkey <gpg-fingerprint>
git config --local commit.gpgsign true
git config --local tag.gpgsign true仓库级配置便于多身份隔离。确认所有仓库都使用同一受控身份后,才考虑改成 --global。验证者还要导入公钥并建立恰当的信任;“能算出签名正确”与“信任该公钥属于某人”不是同一判断。
SSH 配置
签名者配置格式和公钥位置:
git config --local gpg.format ssh
git config --local user.signingkey <public-key-file>
git config --local commit.gpgsign true
git config --local tag.gpgsign true当 user.signingkey 指向公钥时,Git 可让 ssh-agent 找到对应私钥。私钥不在 agent 中时,签名会失败;不要为省事把私钥复制进项目目录。
验证者必须准备 allowed signers 文件。每行由 principal、可选约束和公钥组成,例如:
developer@example.invalid ssh-ed25519 AAAA...example-onlyprincipal 是组织认可的签名身份,不是随手写的注释。OpenSSH 的 ALLOWED SIGNERS格式还允许用 valid-after、valid-before 限制密钥生效窗口,或用 cert-authority 信任签发 SSH 证书的 CA。生命周期时间应由身份系统生成,不能由开发者自行延长:
developer@example.invalid valid-after="<UTC-timestamp>" valid-before="<UTC-timestamp>" ssh-ed25519 AAAA...example-only再告诉 Git 信任库位置:
git config --local gpg.ssh.allowedSignersFile <allowed-signers-file>SSH 本身没有 GPG 那样的信任等级模型。Git 官方文档明确说明:公钥出现在 allowedSignersFile 中时验证可达到 fully;否则通常只能得到未建立身份信任的结果,verify-commit 或 verify-tag 会失败。团队不要让每个人手工维护一份永不更新的名单,应该从身份目录或受审仓库生成。
泄漏或离职密钥还要进入撤销文件。该文件可以是 OpenSSH KRL,也可以是一行一把、不带 principal 的公钥列表;Git 一旦在其中找到签名 key,就把信任级别降为 never:
git config --local gpg.ssh.revocationFile <revoked-keys-file>allowed signers 回答“谁在何时可被信任”,revocation file 回答“哪把 key 必须被拒绝”。只从允许名单删 key 会让新验证失去身份映射,却不能明确表达密钥已泄漏;生产门禁应同时分发新的允许名单和撤销材料。
S/MIME 配置
以 Git 当前支持的 X.509 签名程序为准:
git config --local gpg.format x509
git config --local gpg.x509.program smimesign
git config --local user.signingkey <certificate-identity>
git config --local commit.gpgsign true
git config --local tag.gpgsign true证书身份的写法取决于签名程序。用 git config --show-origin --get-regexp '^(gpg|user\.signingkey|commit\.gpgsign|tag\.gpgsign)' 检查最终来源,避免全局旧配置覆盖仓库策略。
先在空测试目录初始化仓库,并用示例身份创建一个文件。不要在真实项目里试验密钥配置:
git init signing-lab
cd signing-lab
git config user.name "Example Developer"
git config user.email "developer@example.invalid"
printf "signature lab\n" > README.md
git add README.md
git commit -S -m "test: create signed commit"验证刚创建的 Commit:
git log --show-signature -1
git verify-commit HEAD预期是签名程序报告有效签名,且 git verify-commit HEAD 退出码为 0。SSH 后端若只显示签名格式有效却没有可信 principal,应回到 allowedSignersFile,而不是把输出中的 “Good” 当作团队身份已确认。
再创建一个签名的附注 Tag:
git tag -s signing-lab-v1 -m "signing lab checkpoint"
git cat-file -t signing-lab-v1
git verify-tag signing-lab-v1git cat-file -t 应输出 tag,说明它是有独立 Tag 对象的附注 Tag;git verify-tag 应退出 0。轻量 Tag 只是一个引用,没有可承载 Tagger、说明和 Tag 签名的对象。
最后故意使用一个空的 SSH 信任库复核失败路径。SSH 后端可以在仓库外创建空文件,然后临时覆盖配置:
: > ../empty-allowed-signers
git -c gpg.ssh.allowedSignersFile=../empty-allowed-signers verify-commit HEAD
echo $?
rm ../empty-allowed-signers预期 verify-commit 返回非零退出码;若仍返回 0,应检查 gpg.format 和配置来源,确认测试的确经过 SSH 验证器。GPG 或 S/MIME 后端则应在不含目标公钥或证书链的干净验证环境执行同类反例。实验结束后退出目录并删除整个 signing-lab 测试目录;若生成了专用测试密钥,还应按后端流程撤销并清理公私钥,而不是只删仓库。
项目接入应从一条低风险分支开始,而不是直接要求全组织所有历史对象都满足新规则。
在仓库文档中声明采用哪种后端、哪些身份必须签名、哪些对象需要签名,以及平台状态是否只是展示还是合并门禁。给开发者提供仓库级配置检查命令,不分发私钥。公钥登记、邮箱验证、agent 启动和硬件令牌解锁分别写清 owner。在 PR/MR 中同时看本地密码学验证和平台验证详情。平台徽标证明的是该平台按自己的账号与信任规则验证过,不自动等于组织已经完成授权审查。
机器人、网页编辑、合并队列和平台代签会产生不同签名者。先建立机器身份清单,再决定哪些可接受,不能把所有 Verified 都解释为“开发者本人签署”。逐步启用保护规则:先观测未签名比例和失败原因,再对新提交执行门禁,最后演练密钥丢失与紧急绕过。绕过必须有审批、时限和审计记录。
GitHub 受保护分支可以要求进入目标分支的 Commit 已签名并被平台验证,但 vigilant mode 下的 Partially verified 也可能被允许,squash 合并还会改变最终对象和签名者。GitLab Push Rules可以拒绝从外部 Git 客户端推送的未签名 Commit,却明确存在平台内部创建 Commit 的处理差异,而且 Push Rule 只检查 Commit、不检查 Tag。架构评审必须用目标实例和真实合并方式做反向实验,不能从开关名称推断覆盖面。
Commit 被 rebase、commit --amend 或 cherry-pick 后会产生新对象,原签名不会自动转移。执行改写的人签署的是新对象;这并不证明原作者同意改写结果。需要保留作者同意证据时,应依靠评审记录、DCO、合并策略或额外证明,而不是只看新 Commit 的签名。
单次签名与单次禁用默认签名:
git commit -S -m "feat: add example behavior"
git commit --no-gpg-sign -m "test: unsigned negative case"
git tag -s example-v1 -m "example checkpoint"检查对象、签名和配置来源:
git show --show-signature --no-patch HEAD
git log --format='%h %G? %GS %GK %s' -10
git verify-commit HEAD
git verify-tag example-v1
git config --show-origin --get-regexp '^(gpg|user\.signingkey|commit\.gpgsign|tag\.gpgsign)'%G? 是快速筛查入口,不应替代完整验证输出和退出码。批量审计时要明确检查的 Commit 范围,不能只验证分支尖端。
DCO sign-off 使用另一条命令:
git commit -s -m "docs: add example"
git log -1 --format=full它会在消息尾部添加 Signed-off-by trailer,表示提交者作出项目采用的贡献声明。它不是 GPG/SSH/S/MIME 签名,也不证明私钥控制权。一个 Commit 可以同时 -S -s,分别满足身份签名和 DCO 声明。
gpg failed to sign the data
git commit -S 退出,Commit 没有创建。直接运行 gpg --list-secret-keys,再检查 git config --show-origin --get user.signingkey;若终端无法弹出口令提示,还要检查 agent 与 TTY。常见原因:Key ID 选错、私钥不存在、密钥已过期、gpg.program 指向错误程序,或图形终端与 gpg-agent 的口令入口不匹配。选择实际存在且可签名的私钥,修正程序路径,重启受控 agent;不要用取消口令保护来绕过问题。先完成一次独立签名测试,再重新执行 git commit -S 和 git verify-commit HEAD。
SSH 报 Couldn't load public key 或 agent 找不到私钥
配置了 gpg.format=ssh,签名仍失败。检查 user.signingkey 指向的是合法公钥还是私钥,运行 ssh-add -L 查看 agent 中的公钥。常见原因:公钥路径错误、私钥没有加载、硬件令牌未解锁,或 Git 实际读取了另一作用域的配置。修正仓库级公钥位置,并将对应私钥加载到批准的 agent。git config --show-origin --get user.signingkey 应指向预期值,随后创建新的签名 Commit;旧的失败操作不会留下可验证签名。
本地签名有效,平台却显示 Unverified
git verify-commit 成功,平台不显示 Verified。打开平台的验证详情,分别核对签名公钥登记、Committer 邮箱、证书链和签名用途。常见原因:公钥只在本地信任库、上传成了 SSH 认证键而非签名键、邮箱未与账号关联、S/MIME 根证书不被平台信任,或平台实例版本不支持该后端。按目标平台当前官方流程登记签名身份并修正 Committer 信息;不要为了徽标重写已共享的大量历史。创建一个新的测试 Commit 推送,并同时记录本地退出码和平台详情。
平台显示 Verified,本地验证失败
网页有可信状态,克隆后 verify-commit 失败。确认本地是否有对应公钥、SSH allowed signers、证书链和撤销信息。常见原因:平台维护自己的账号映射和验证记录,本地没有同一信任材料;也可能是平台代签的网页 Commit。从受控目录同步公钥与信任策略,并明确平台系统密钥是否被团队接受。在干净验证环境导入团队信任材料后运行 verify-commit,不能把浏览器截图当唯一证据。
密钥轮换后新提交失败,旧提交状态又与预期不同
新签名无法验证,或平台上旧提交仍保持原状态。区分“当前密钥是否仍可签名”“签名时密钥是否有效”“平台是否保存了历史验证记录”。常见原因:新公钥未同步、SSH 有效期或撤销文件错误、GPG 子密钥轮换遗漏,或误以为撤销会改写平台已保存的历史判断。按生效时间发布新信任材料,停止旧私钥签名并登记撤销;对平台持久验证语义按官方说明单独评估。用轮换前后各一个对象,在本地和平台分别记录结果与时间。
本地签名通常不需要访问远端,因此 HTTP 代理不应成为 git commit -S 的依赖。上传公钥、访问企业证书服务、使用网络 HSM 或推送仓库才可能经过代理;把这些环节拆开测试,能避免把“网络失败”误判为“签名失败”。
私钥权限遵循最小暴露原则:
私钥不进入 Git、同步盘、聊天附件、工单或 CI 日志;配置示例只引用公钥或密钥标识。SSH 认证权与签名权尽量分开登记。复用同一密钥虽方便,但任何一次撤销都会同时影响登录与签名。机器人使用独立服务身份、独立密钥和短生命周期授权,不复用离职员工或维护者个人密钥。
allowed signers、公钥环和 CA 根证书也属于安全配置。攻击者若能无审计地修改信任库,就能让自己的签名被接受。CI 只有确实需要生成签名对象时才接触签名能力。优先使用硬件或远程签名、受保护环境和按仓库授权,避免导出长期私钥为普通 Secret。
后端选择没有唯一答案。个人和小团队若已有统一 OpenSSH,SSH 签名最容易推广;开源项目或需要成熟过期、撤销和跨工具生态时,GPG 更常见;已有企业 PKI、智能卡和证书生命周期平台的组织才更适合 S/MIME。
落地时至少维护四份可审计资产:签名者与用途清单、公钥或 CA 信任源、轮换与撤销记录、仓库门禁与例外记录。owner 要能回答“谁能签”“代表谁签”“从何时有效”“离职后怎样失效”“验证失败谁处理”。
建议先规定新 Commit 的签名,不追溯重写历史。重写 Commit 会改变对象 ID、破坏已有引用并制造二次审查成本。对历史可信度的提升应通过保留原对象、归档验证证据、签署里程碑 Tag 或建立可复核的来源证明完成。
平台保护规则应消费签名结果,但不应只认一个绿色徽标。关键仓库还需要 Review、最小权限、状态检查、分支保护和审计日志;签名能证明某个密钥签过对象,不能证明代码正确、变更经过审批,也不能阻止获授权私钥的人员提交恶意内容。
信任源比签名算法更容易失守
签名计算正确只能说明对象与某个私钥对应。真正困难的是把公钥绑定到人、岗位或机器人。若 allowed signers 由每位开发者自行编辑,或者 GPG 公钥只靠聊天传递,团队得到的是分散信任,不是组织证据链。应从身份目录或受保护仓库生成信任材料,并对变更做双人审查。
历史改写会改变签名主体
Rebase、squash、amend 和 cherry-pick 都会创建新 Commit。合并者重新签名后,签名证明的是合并者创建了新对象,不等于每位原作者签署了最终内容。团队应在“保留每个作者签名”和“获得紧凑主线历史”之间明确取舍,并用 PR/MR 审批记录补足作者同意。
撤销不是删除历史
私钥泄漏后的正确动作是停止使用、发布撤销或更新信任源、轮换门禁和调查受影响时间窗。删除平台账号或移除公钥不一定改变已保存的历史验证状态,也不应期待它让恶意 Commit 消失。处置结论必须记录平台语义、签名时间与密钥有效期。
自动签名会放大错误身份
commit.gpgsign=true 很方便,也会让全局配置里的错误身份持续签署多个仓库。个人多账号、客户仓库和开源身份混用时,应使用仓库级配置或条件 include,并在提交前检查 user.email 与 user.signingkey 的来源。
可用性与强门禁需要一起演练
硬件令牌丢失、agent 崩溃、证书过期或签名服务不可用时,强制签名可能阻断紧急修复。不能通过共享备用私钥解决。团队应准备受审批的临时例外、短时应急身份、事后补审和撤销演练,并确保绕过动作进入审计日志。
已明确签名证明的是对象来源,不把它等同于 SSH 登录、HTTPS 认证、代码正确或审批通过。已按 git --version 与官方文档确认后端能力,没有写死未经验证的版本口径。user.name、user.email、user.signingkey 和 gpg.format 的最终来源可解释。
私钥不在仓库、脚本、日志和共享配置中,公钥及信任库有 owner。新 Commit 能通过 git verify-commit,附注 Tag 能通过 git verify-tag。SSH 验证已配置和审查 gpg.ssh.allowedSignersFile,不是只检查算法有效。
本地验证与目标平台验证均已测试,差异有记录,不把一个平台的状态语义套到另一个平台。DCO Signed-off-by 与密码学签名分别定义,门禁不会混淆二者。已覆盖机器人、网页提交、合并队列和平台代签身份。
密钥轮换、过期、撤销、离职回收和紧急绕过均有演练与审计。已评估 rebase、squash、amend、cherry-pick 对原签名和作者同意证据的影响。示例、日志和审计记录不包含真实邮箱、私钥、内网地址、本机用户路径或业务仓库名。
