SessionLedger requires cryptographic Git commit signatures (GPG or SSH) on
every commit that reaches main, in addition to DCO Signed-off-by: trailers.
See ADR 0004 for rationale and scope
(maintainer 2FA is explicitly out of scope here).
| Layer | Requirement | Enforcement |
|---|---|---|
| DCO | Signed-off-by: on each commit |
PR template + review |
| GPG / SSH | Signature block on each commit object | GitHub branch protection + CI tip check |
| Merge commits | GitHub signs merges as noreply@github.com |
Automatic on squash/merge |
gpg --full-generate-key
gpg --list-secret-keys --keyid-format=long
git config --global user.signingkey <KEY_ID>
git config --global commit.gpgsign trueExport the public key to GitHub → Settings → SSH and GPG keys → New GPG key.
ssh-keygen -t ed25519 -C "signing@example.com" -f ~/.ssh/id_ed25519_sign
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519_sign.pub
git config --global commit.gpgsign trueAdd the public key as a Signing key on GitHub (not only as an auth key).
git log -1 --show-signature
# or
git verify-commit HEADBranch protection cannot be asserted from a bare git clone. Maintainers must
configure the following in Settings → Branches → Branch protection rules → main:
- Require signed commits
- Require a pull request before merging (recommended)
- Do not allow bypassing the above settings (recommended for admins)
Record the date protection was enabled in an internal ops note. Dedicated
machine-verify (best-effort gh api, skip without token/admin scope):
branch-protection.md and
scripts/branch-protection-check.ps1. The commit-signing checklist below remains
a soft companion when gh has admin scope.
Run locally or in CI:
# Blocking tip check + recent-history report (default CI mode)
pwsh -NoProfile -File scripts/commit-signing-check.ps1
# Hermetic bounded header scanner + fixtures (no signed main required)
pwsh -NoProfile -File scripts/commit-signing-check.ps1 -SelfCheck
# Branch-protection checklist (soft-fail / docs-only without admin API)
pwsh -NoProfile -File scripts/commit-signing-check.ps1 -BranchProtectionChecklist
# Strict: fail if any commit in the window lacks a signature block
pwsh -NoProfile -File scripts/commit-signing-check.ps1 -Strict -Count 50maintip — commit object contains agpgsigblock (GPG or SSH).- Recent history — for each of the last N commits (default 30), classify
as
gpg,ssh, orunsigned. - When signatures are present — run
git verify-commitwhen a verifier is available; malformedgpgsigblocks fail even in soft mode. - Branch protection (optional
-BranchProtectionChecklist) — ifgh apisucceeds with admin scope, assertrequired_signaturesonmain; otherwise print the checklist above and exit 0 (OSS fail-soft).
.github/workflows/commit-signing.yml
checks out full history (fetch-depth: 0), runs the script on PRs and main
pushes, and uploads the text report as a job summary.
Squash and merge commits are signed by GitHub's bot key. Individual commits inside
a PR may be unsigned until branch protection rejects them; only the merge commit
on main must be signed for the tip check to pass today.
Long term, Require signed commits ensures every commit in a PR is signed before merge, not only the merge commit.
- CONTRIBUTING.md — DCO + signing setup for contributors
- SECURITY.md — supply-chain controls index
- ADR 0004 — policy decision record