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
21 changes: 21 additions & 0 deletions .github/renovate.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
$schema: "https://docs.renovatebot.com/renovate-schema.json",
enabledManagers: ["github-actions"],
// PR titles: `deps(<action>): ...`
semanticCommits: "enabled",
semanticCommitType: "deps",
packageRules: [
{
matchManagers: ["github-actions"],
schedule: ["on monday"],
// skip fresh releases: may be unstable or compromised
minimumReleaseAge: "14 days",
// semver-coerced so short tags (v3, v1.7) still parse; pin to full SHA, tag as trailing comment
versioning: "semver-coerced",
pinDigests: true,
separateMajorMinor: false,
semanticCommitScope: "{{depName}}",
commitMessageTopic: "{{depName}}",
},
],
}
13 changes: 13 additions & 0 deletions .github/workflows/self-verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: self-verify 🪞

on:
pull_request:

permissions: {}

jobs:
verify:
permissions:
contents: read
pull-requests: read
uses: ./.github/workflows/verify.yml
29 changes: 29 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: verify commit signatures 🕵🏻‍♂️

on:
workflow_call: {}

permissions: {}

jobs:
verify-ssh-signatures:
timeout-minutes: 10
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
steps:
# pins script + allowed_signers to the workflow revision the caller pinned
- name: 📥 Check out commit-verifier
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: ${{ job.workflow_repository }}
ref: ${{ job.workflow_sha }}
persist-credentials: false

- name: 🔑 Verify SSH commit signatures
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
run: ./verify_commits.sh "$PR"
74 changes: 74 additions & 0 deletions .github/workflows/zizmor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: zizmor

on:
pull_request:
paths:
- ".github/**"
push:
branches: [main]
paths:
- ".github/**"

permissions: {}

jobs:
zizmor:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
pull-requests: write # sticky summary comment
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

# fails on findings; annotations cap at 10, the PR comment carries the full list
# advanced-security off: no GHAS on this repo, SARIF upload would 403
- name: Run zizmor
uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1
with:
version: "1.28.0"
advanced-security: false
annotations: true

# re-run via docker for JSON output; the action only emits SARIF/annotations
- name: Generate summary
if: always() && github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ github.token }} # online audits, same as the action run
run: |
rc=0
docker run --rm -e GH_TOKEN -v "$PWD:/src" ghcr.io/zizmorcore/zizmor:1.28.0@sha256:8e6b3e4fb74d1aa5d23e83ea369f386c66eced0d1fb944d32cd8b2aac100b00d \
--format=json /src > findings.json || rc=$?
# exit codes 10-14 = findings; anything else is a real error
if [ "$rc" -ne 0 ] && { [ "$rc" -lt 10 ] || [ "$rc" -gt 14 ]; }; then exit "$rc"; fi

cat > summary.jq <<'JQ'
def rank: {"High": 0, "Medium": 1, "Low": 2, "Informational": 3, "Unknown": 4}[.] // 5;
def loc: .locations[0] as $l
| (.locations[0].symbolic.key.Local.given_path // "?" | ltrimstr("/src/"))
+ ":" + (($l.concrete.location.start_point.row + 1) | tostring);
if length == 0 then
"### 🌈 zizmor\n\n✅ No findings in GitHub Actions workflows."
else
( group_by(.determinations.severity) | map({(.[0].determinations.severity): length}) | add ) as $counts
| "### 🌈 zizmor: \(length) finding\(if length == 1 then "" else "s" end)\n\n"
+ ( ["High", "Medium", "Low", "Informational", "Unknown"]
| map(select($counts[.] != null) | "**\($counts[.])** \(.)") | join(" · ") ) + "\n\n"
+ "<details><summary>Full results</summary>\n\n"
+ "| Severity | Confidence | Rule | Location | Description |\n|---|---|---|---|---|\n"
+ ( sort_by([(.determinations.severity | rank), loc])
| map("| \(.determinations.severity) | \(.determinations.confidence) | [\(.ident)](\(.url)) | `\(loc)` | \(.desc) |")
| join("\n") )
+ "\n\n</details>"
end
JQ
jq -r -f summary.jq findings.json > comment.md

- name: Post PR comment
if: always() && github.event_name == 'pull_request'
uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
with:
header: zizmor
path: comment.md
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @cowprotocol/devops
71 changes: 70 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,70 @@
# commit-verifier
# commit-verifier

Canonical home of the `allowed_signers` registry and the reusable workflow that
checks every human-authored commit in a PR was SSH-signed by a hardware-backed
(`sk-`, FIDO2) key registered to the author's GitHub account and enrolled in
`allowed_signers`.

## Using it from a repo

```yaml
jobs:
verify-commit-signatures:
uses: cowprotocol/commit-verifier/.github/workflows/verify.yml@<full-commit-sha>
permissions:
contents: read
pull-requests: read
```

Pin to a full commit SHA, not a branch or tag. The workflow checks out this
repo at that same SHA, so the script and `allowed_signers` are pinned too.
Never pass `secrets: inherit`; the workflow only needs the automatic
`GITHUB_TOKEN`.

## Enrollment (once per person)

1. **Create a hardware key** (FIDO2 authenticator, e.g. YubiKey; confirm it's
genuine first: <https://www.yubico.com/genuine/>). It requires a touch to
sign, which the check enforces.

```bash
ssh-keygen -t ed25519-sk -C "$(whoami) commit signing" -f ~/.ssh/id_ed25519_sk_git
```

2. **Register it on GitHub** → _Settings → SSH and GPG keys → New SSH key_,
type **Signing Key**. Paste the contents of `~/.ssh/id_ed25519_sk_git.pub`.

3. **Tell git to sign with it:**

```bash
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519_sk_git.pub
git config --global commit.gpgsign true
```

4. **Add your key to `allowed_signers`** here: one line, `<your-git-email>
<contents of the .pub>`, and open a PR. A reviewer merging it enrolls you.

The email you commit with must also be on your GitHub account; the check
resolves your account from it.

## How the check works

Runs on `pull_request`. Each commit must pass, in order:

1. the author's GitHub account has an `sk-` signing key;
2. the signature is valid and made by one of those keys;
3. that key is enrolled in `allowed_signers`;
4. the signature carries the user-presence flag: the key was **touched**.

Failing any step is a hard CI failure (aka red X check failure), unless the
commit is from an allow-listed automated account (they have no hardware key);
those warn instead. An automated account is allowed through only when the
**signature** proves.

## Run locally

```bash
GH_TOKEN=$(gh auth token) GITHUB_REPOSITORY=owner/repo \
./verify_commits.sh <PR-number>
```
5 changes: 5 additions & 0 deletions allowed_signers
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Enrolled hardware (sk-) commit-signing keys — the trusted registry.
assem@cow.fi sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIPq6XpBwnJEBmey+fnfxR6Z1RXR084NfIy48v+J1DXk+AAAABHNzaDo=

# cow-protocol CI bot — non-sk software key
gh_111@cow.fi ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFHJQvdfBMMNNDCsq8LJS2/yCxYKbFiw6Jkcjym8CvZw
Loading