-
Notifications
You must be signed in to change notification settings - Fork 0
Wave-21 C04: commit signing policy and check #180
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| # L34 commit signing evidence: verify main tip signature + soft branch-protection checklist. | ||
| # Branch protection itself is not enforceable from OSS CI without admin API scope. | ||
|
|
||
| name: Commit signing | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - ".github/workflows/commit-signing.yml" | ||
| - "scripts/commit-signing-check.ps1" | ||
| - "docs/ops/commit-signing.md" | ||
| - "docs/adr/0004-commit-signing-policy.md" | ||
| - "SECURITY.md" | ||
| - "CONTRIBUTING.md" | ||
| push: | ||
| branches: [main] | ||
| paths: | ||
| - ".github/workflows/commit-signing.yml" | ||
| - "scripts/commit-signing-check.ps1" | ||
| - "docs/ops/commit-signing.md" | ||
| - "docs/adr/0004-commit-signing-policy.md" | ||
| - "SECURITY.md" | ||
| - "CONTRIBUTING.md" | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| commit-signing: | ||
| name: commit signing policy | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: ensure origin/main is available on PR checkouts | ||
| run: git fetch --no-tags origin main | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WARNING: For cross-repo (fork) PRs, Reply with |
||
|
|
||
| - name: verify main tip and recent signed commits | ||
| shell: pwsh | ||
| run: ./scripts/commit-signing-check.ps1 -Ref main -Count 30 | ||
|
|
||
| - name: branch protection checklist (soft / docs-only) | ||
| shell: pwsh | ||
| run: ./scripts/commit-signing-check.ps1 -BranchProtectionChecklist | ||
| continue-on-error: true | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| # ADR 0004: Git commit signing policy (GPG / SSH) | ||
|
|
||
| - Status: Accepted | ||
| - Date: 2026-07-13 | ||
| - Decision owners: SessionLedger maintainers | ||
| - Related: `docs/ops/commit-signing.md`, `CONTRIBUTING.md`, `SECURITY.md`, ADR 0003 | ||
|
|
||
| ## Context | ||
|
|
||
| SessionLedger is an open-source repository on GitHub. Supply-chain controls already | ||
| cover dependency advisories, secret scanning, SBOM emission, and signed Releases | ||
| (see ADR 0003). **Git commit signing** is a separate pillar: it binds each commit | ||
| object to a cryptographic identity so reviewers can detect tampering after push. | ||
|
|
||
| Today: | ||
|
|
||
| | Signal | Status | | ||
| |--------|--------| | ||
| | DCO `Signed-off-by:` trailers | Required in `CONTRIBUTING.md` (legal attestation) | | ||
| | GitHub merge-commit PGP signatures | Present on squash/merge commits via `noreply@github.com` | | ||
| | Contributor GPG/SSH signatures on every commit | **Not enforced in-repo** | | ||
| | Branch protection "Require signed commits" | **Not machine-verifiable without admin API access** | | ||
|
|
||
| Maintainer **2FA** is an org/account control and is intentionally **out of scope** | ||
| for this ADR (see `SECURITY.md` maintainer hygiene, recorded separately). | ||
|
|
||
| OSS repositories cannot prove GitHub branch-protection settings from a checkout | ||
| alone. CI therefore combines: | ||
|
|
||
| 1. **Machine checks** — inspect recent `main` history for GPG/SSH signature blocks | ||
| and validate the tip commit is signed. | ||
| 2. **Documented enforcement** — maintainers enable "Require signed commits" on | ||
| `main` in GitHub Settings → Branches. | ||
| 3. **Soft checklist** — `scripts/commit-signing-check.ps1 -BranchProtectionChecklist` | ||
| prints required settings and queries the API only when `gh` + admin scope exist; | ||
| otherwise it exits 0 with documentation pointers (fail-soft / docs-only). | ||
|
|
||
| ## Decision | ||
|
|
||
| **All commits landing on `main` must carry a verifiable GPG or SSH signature.** | ||
| Enforcement is layered: | ||
|
|
||
| 1. **Contributors** configure `git config commit.gpgsign true` (GPG) or | ||
| `git config gpg.format ssh` + `user.signingkey` (SSH) before pushing. | ||
| 2. **Maintainers** enable GitHub branch protection rule on `main`: | ||
| - Require signed commits | ||
| - (Recommended) Require pull request reviews before merging | ||
| 3. **CI** runs `scripts/commit-signing-check.ps1` on PRs and `main` pushes: | ||
| - **Blocking:** `main` tip commit must include a `gpgsig` block (GPG or SSH). | ||
| - **Advisory:** recent-history coverage report; branch-protection checklist is | ||
| soft-fail when the API is unreachable or lacks admin scope. | ||
|
|
||
| DCO sign-off (`git commit -s`) remains required and is **complementary** to | ||
| cryptographic signing — DCO is not a substitute for GPG/SSH. | ||
|
|
||
| ### Accepted signature kinds | ||
|
|
||
| | Kind | Detection | Notes | | ||
| |------|-----------|-------| | ||
| | GPG | `gpgsig` block containing `BEGIN PGP SIGNATURE` | GitHub merge bot uses PGP | | ||
| | SSH | `gpgsig` block containing `BEGIN SSH SIGNATURE` | Git ≥ 2.34; no local gpg needed | | ||
|
|
||
| ### Out of scope | ||
|
|
||
| - Maintainer 2FA / hardware-key attestation (org policy; not provable from git tree) | ||
| - Platform Authenticode / Apple notarization (ADR 0003) | ||
| - Re-signing historical unsigned commits (forward-only policy) | ||
|
|
||
| ## Consequences | ||
|
|
||
| - `CONTRIBUTING.md` documents how contributors enable GPG or SSH signing. | ||
| - `SECURITY.md` cross-links commit-signing expectations for reporters and auditors. | ||
| - `docs/ops/commit-signing.md` is the operator runbook (setup, verification, checklist). | ||
| - CI provides auditable evidence without pretending branch protection is enforced | ||
| when GitHub admin APIs are unavailable. | ||
|
|
||
| ## Reconsider when | ||
|
|
||
| Revisit this policy if: | ||
|
|
||
| 1. The project moves off GitHub or disables merge-queue signing, **or** | ||
| 2. A verified incident shows unsigned commits reached `main` despite protection, **or** | ||
| 3. GitHub ships a tokenless, read-only API to assert branch-protection flags for | ||
| public repos (then tighten CI from soft checklist to hard gate). | ||
|
|
||
| Until then, treat merge-commit GitHub signatures plus documented branch protection | ||
| as the production baseline for L34 (signed commits). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| # Commit signing operations | ||
|
|
||
| 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](../adr/0004-commit-signing-policy.md) for rationale and scope | ||
| (maintainer 2FA is explicitly out of scope here). | ||
|
|
||
| ## Policy summary | ||
|
|
||
| | 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 | | ||
|
|
||
| ## Contributor setup | ||
|
|
||
| ### GPG (traditional) | ||
|
|
||
| ```bash | ||
| gpg --full-generate-key | ||
| gpg --list-secret-keys --keyid-format=long | ||
| git config --global user.signingkey <KEY_ID> | ||
| git config --global commit.gpgsign true | ||
| ``` | ||
|
|
||
| Export the public key to GitHub → Settings → SSH and GPG keys → New GPG key. | ||
|
|
||
| ### SSH (Git ≥ 2.34) | ||
|
|
||
| ```bash | ||
| 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 true | ||
| ``` | ||
|
|
||
| Add the **public** key as a **Signing key** on GitHub (not only as an auth key). | ||
|
|
||
| ### Verify before push | ||
|
|
||
| ```bash | ||
| git log -1 --show-signature | ||
| # or | ||
| git verify-commit HEAD | ||
| ``` | ||
|
|
||
| ## Maintainer: GitHub branch protection | ||
|
|
||
| Branch 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; the repository | ||
| ships a machine checklist (below) that queries GitHub when `gh` has admin scope. | ||
|
|
||
| ## Machine verification | ||
|
|
||
| Run locally or in CI: | ||
|
|
||
| ```powershell | ||
| # Blocking tip check + recent-history report (default CI mode) | ||
| pwsh -NoProfile -File scripts/commit-signing-check.ps1 | ||
|
|
||
| # 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 50 | ||
| ``` | ||
|
|
||
| ### What the script checks | ||
|
|
||
| 1. **`main` tip** — commit object contains a `gpgsig` block (GPG or SSH). | ||
| 2. **Recent history** — for each of the last *N* commits (default 30), classify | ||
| as `gpg`, `ssh`, or `unsigned`. | ||
| 3. **When signatures are present** — run `git verify-commit` when a verifier is | ||
| available; malformed `gpgsig` blocks fail even in soft mode. | ||
| 4. **Branch protection** (optional `-BranchProtectionChecklist`) — if `gh api` | ||
| succeeds with admin scope, assert `required_signatures` on `main`; otherwise | ||
| print the checklist above and exit 0 (OSS fail-soft). | ||
|
|
||
| ### CI workflow | ||
|
|
||
| [`.github/workflows/commit-signing.yml`](../../.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. | ||
|
|
||
| ## Interpreting GitHub merge signatures | ||
|
|
||
| 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. | ||
|
|
||
| ## Related documents | ||
|
|
||
| - [CONTRIBUTING.md](../../CONTRIBUTING.md) — DCO + signing setup for contributors | ||
| - [SECURITY.md](../../SECURITY.md) — supply-chain controls index | ||
| - [ADR 0004](../adr/0004-commit-signing-policy.md) — policy decision record |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SUGGESTION:
pull_requesttrigger has apathsfilter that limits this workflow to run only when policy files changeThe ADR and runbook state the check runs on every PR and
mainpush, but thepathsfilter means most PRs will skip this workflow entirely. If the intent is to verify commit signing on every PR, remove thepathsfilter or add a separate lightweight workflow without it.Reply with
@kilocode-bot fix itto have Kilo Code address this issue.