Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/commit-signing.yml
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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: pull_request trigger has a paths filter that limits this workflow to run only when policy files change

The ADR and runbook state the check runs on every PR and main push, but the paths filter means most PRs will skip this workflow entirely. If the intent is to verify commit signing on every PR, remove the paths filter or add a separate lightweight workflow without it.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

- ".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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: git fetch --no-tags origin main fetches from origin, which for PRs from forks is the fork's main branch, not the upstream main

For cross-repo (fork) PRs, origin points to the fork. The script should fetch the upstream main explicitly so the tip check validates the correct branch.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


- 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
33 changes: 33 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,38 @@ By signing off, you certify that you have the right to submit the work under thi
repository's dual MIT OR Apache-2.0 license. PRs should confirm the DCO checkbox
in the pull request template.

## Cryptographic commit signing (GPG / SSH)

DCO sign-off is **not** a substitute for Git commit signatures. Configure signing
before your first commit on a feature branch:

**GPG**

```bash
git config --global user.signingkey <KEY_ID>
git config --global commit.gpgsign true
git commit -S -s -m "your message"
```

**SSH** (Git 2.34+)

```bash
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519_sign.pub
git config --global commit.gpgsign true
git commit -S -s -m "your message"
```

Publish your public key to GitHub (GPG key or SSH **signing** key). Maintainers
enable **Require signed commits** on `main`; see
[`docs/ops/commit-signing.md`](docs/ops/commit-signing.md) and
[ADR 0004](docs/adr/0004-commit-signing-policy.md).

Verify locally:

```powershell
pwsh -NoProfile -File scripts/commit-signing-check.ps1 -Ref HEAD -Count 5
```

## Governance
This repository follows governance guidelines defined in ~/.claude/CLAUDE.md at a high level.
14 changes: 14 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ We follow coordinated disclosure. If a fix cannot ship within 90 days, we will d

Critical supply-chain or remote-code issues may be accelerated at maintainer discretion.

## Commit signing

- Every commit on `main` must carry a **GPG or SSH** signature in addition to DCO
sign-off (see [`CONTRIBUTING.md`](CONTRIBUTING.md)).
- Policy and operator steps: [`docs/ops/commit-signing.md`](docs/ops/commit-signing.md);
decision record: [`docs/adr/0004-commit-signing-policy.md`](docs/adr/0004-commit-signing-policy.md).
- CI runs [`scripts/commit-signing-check.ps1`](scripts/commit-signing-check.ps1) via
[`.github/workflows/commit-signing.yml`](.github/workflows/commit-signing.yml) to
verify the `main` tip is signed and to emit a branch-protection checklist.
GitHub **Require signed commits** on `main` is the enforcement control; the
checklist step is intentionally soft when admin API scope is unavailable in OSS.
- Maintainer **2FA** is recommended org hygiene but is not attestable from this
repository (out of scope for commit-signing evidence).

## Supply Chain & SBOM

- Dependency policy is enforced by [`deny.toml`](deny.toml) via `cargo deny check` (see [`.github/workflows/security.yml`](.github/workflows/security.yml)).
Expand Down
87 changes: 87 additions & 0 deletions docs/adr/0004-commit-signing-policy.md
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).
105 changes: 105 additions & 0 deletions docs/ops/commit-signing.md
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
Loading
Loading