-
Notifications
You must be signed in to change notification settings - Fork 6
feat(security): enforce verified commit signing #466
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
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
0d64ce2
feat(security): enforce verified commit signing
qnbs 9775493
fix(security): forward pre-push hook arguments
qnbs 2f1567d
fix(security): parse complete pre-push updates
qnbs 5b77930
fix(security): close signing review gaps
qnbs c9eb3ad
fix(security): complete signing declarations
qnbs File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| # Verified signing policy | ||
|
|
||
| WorldScript Studio requires signed commit objects at the local hook boundary and requires | ||
| GitHub's `commit.verification.verified == true` result at the CI boundary. These are related | ||
| but different checks: | ||
|
|
||
| - `git verify-commit` proves that the local Git installation can validate the signature object | ||
| against its configured trust/key policy. It does not prove that GitHub will associate the | ||
| commit with a verified account. | ||
| - GitHub's `verification.verified` result is the release and merge gate. It covers GitHub's | ||
| signature parser, key association, and account identity rules. | ||
| - Annotated release tags have two objects to verify: the tag object and its target commit. | ||
| Lightweight release tags are rejected because they have no independently verifiable tag | ||
| object; a verified target commit alone is insufficient for a release tag. | ||
|
|
||
| ## Local setup and recovery | ||
|
|
||
| Run `pnpm run signing:doctor` after configuring a signing key. The doctor performs a | ||
| plumbing-level `git commit-tree -S` probe in an isolated temporary repository and validates the | ||
| result with Git-native verification. It never invokes normal repository hooks, reads private key | ||
| material, or prints signatures. `pnpm run hooks:install` installs the fail-closed pre-commit and | ||
| pre-push wrappers. | ||
|
|
||
| If a hook rejects a commit or push: | ||
|
|
||
| 1. Run the doctor and correct the reported effective configuration, identity, key availability, | ||
| or trust/allowed-signers configuration. | ||
| 2. Re-run the exact failed operation. Never use `--no-gpg-sign`, `--no-verify`, an unsigned | ||
| temporary commit, or an unsigned tag as a recovery path. | ||
| 3. Use `pnpm run signing:check-range -- before..after` to inspect an exact local range. | ||
| 4. For a pull request, use `pnpm run signing:verify-remote -- <owner>/<repo> <number>` when a | ||
| GitHub token is available; the CI gate remains authoritative. | ||
|
|
||
| The hook reads effective Git configuration, including repository and worktree configuration. | ||
| Repository-local and worktree-local values override global values, and environment-provided Git | ||
| configuration can override all of them. The doctor reports unsafe configuration overrides rather | ||
| than silently accepting them. Configure only a public signing-key reference where possible; | ||
| never copy, print, commit, or place private key material in the repository. | ||
|
|
||
| ## History and squash semantics | ||
|
|
||
| The signing cutover applies to every new commit, push, release tag, and GitHub merge result. It | ||
| does not rewrite legacy history. A squash merge creates a new signed commit whose tree contains | ||
| the reviewed change; it does not make the unsigned source commits in the old branch signed or | ||
| erase their historical verification state. A historical audit is evidence for migration planning, | ||
| not a release waiver for new unsigned objects. | ||
|
|
||
| CI verifies the complete `before..after` range for branch pushes and the complete paginated PR | ||
| commit list for pull requests. Public-fork pull requests use read-only GitHub API access. API | ||
| errors, missing pages, missing verification data, invalid signatures, unsigned commits, and | ||
| unverified tag objects fail closed. | ||
|
|
||
| ## Historical audit snapshot | ||
|
|
||
| The audit below is generated against `main` with GitHub's verification result, not merely local | ||
| Git trust. Dates are UTC and the interval is inclusive of commits reachable from `main` whose | ||
| committer timestamp is within the stated trailing window. Keep the command output or API response | ||
| with the release evidence when refreshing these figures. | ||
|
|
||
| | Window ending 2026-08-23 UTC | Verified | Unverified | Total | | ||
| | --- | ---: | ---: | ---: | | ||
| | 7 days | 89 | 0 | 89 | | ||
| | 14 days | 124 | 0 | 124 | | ||
| | 30 days | 178 | 0 | 178 | | ||
|
|
||
| This snapshot intentionally distinguishes the signed squash result from the source history that | ||
| preceded it. Open and merged PR source histories should be audited separately when investigating | ||
| legacy unsigned commits. The counts were collected on 2026-08-23 with GitHub's REST commits API, | ||
| `sha=main`, at collection time `2026-08-23T00:15:24Z`, with cutoff | ||
| `2026-08-23T00:15:24Z` and trailing UTC windows, using the `commit.verification.verified` | ||
| boolean; they are not inferred from local Git trust. | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import process from 'node:process'; | ||
| import { ensureDependencyState, runLocalBinary } from './shared.mjs'; | ||
| import { ensureDependencyState, runLocalBinary, runNodeScript } from './shared.mjs'; | ||
|
|
||
| if (runNodeScript('scripts/signing/doctor.mjs', ['--hook']) !== 0) process.exit(1); | ||
| if (!ensureDependencyState()) process.exit(1); | ||
| process.exit(runLocalBinary('lint-staged')); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,6 @@ | ||
| import process from 'node:process'; | ||
| import { runNodeScript } from './shared.mjs'; | ||
|
|
||
| if (runNodeScript('scripts/signing/verify-outgoing.mjs', process.argv.slice(2)) !== 0) | ||
| process.exit(1); | ||
| process.exit(runNodeScript('scripts/ci-prepush-lowend.mjs')); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| #!/usr/bin/env node | ||
| import { verifyCommitRange } from './signing-core.mjs'; | ||
|
|
||
| const range = process.argv[2]; | ||
| if (!range) { | ||
| console.error('usage: pnpm run signing:check-range -- <before..after>'); | ||
| process.exit(2); | ||
| } | ||
| try { | ||
| const reports = verifyCommitRange(range); | ||
| for (const report of reports) { | ||
| console.log( | ||
| `${report.sha.slice(0, 12)} ${report.verification.ok ? 'verified' : 'REJECTED'} ${report.verification.reason} ${report.subject}`, | ||
| ); | ||
| } | ||
| process.exit(reports.every((report) => report.verification.ok) ? 0 : 1); | ||
| } catch (error) { | ||
| console.error( | ||
| `signing range check failed: ${error instanceof Error ? error.message : 'invalid Git range'}`, | ||
| ); | ||
| process.exit(1); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| #!/usr/bin/env node | ||
| import { | ||
| getIdentity, | ||
| getSigningConfig, | ||
| getUnsafeOverrides, | ||
| isGitHubCompatibleEmail, | ||
| isSigningEnabled, | ||
| runSigningProbe, | ||
| safeConfigSummary, | ||
| } from './signing-core.mjs'; | ||
|
|
||
| const jsonMode = process.argv.includes('--json'); | ||
| const cwd = process.cwd(); | ||
| const signing = getSigningConfig(cwd); | ||
| const identity = getIdentity(cwd); | ||
| const unsafeOverrides = getUnsafeOverrides(); | ||
| const probe = runSigningProbe(cwd); | ||
|
qnbs marked this conversation as resolved.
|
||
| const summary = { | ||
| ...safeConfigSummary(cwd), | ||
| probe: { ok: probe.ok, reason: probe.reason ?? 'signed probe verified' }, | ||
| }; | ||
|
|
||
| if (jsonMode) { | ||
| process.stdout.write(`${JSON.stringify(summary)}\n`); | ||
| } else { | ||
| console.log(`signing format: ${signing.format}`); | ||
| console.log(`commit.gpgsign: ${isSigningEnabled(signing.config) ? 'enabled' : 'disabled'}`); | ||
| console.log(`signing key: ${signing.keyConfigured ? 'configured' : 'missing'}`); | ||
| console.log( | ||
| `identity: ${identity.name && identity.email ? 'configured' : 'missing'} (${isGitHubCompatibleEmail(identity.email) ? 'GitHub noreply-compatible' : 'GitHub identity requires account verification'})`, | ||
| ); | ||
| console.log( | ||
| `hooks: ${summary.hooks.pathConfigured ? 'custom path configured' : 'default path'} (${summary.hooks.hooksInstalled ? 'available' : 'not installed'})`, | ||
| ); | ||
| console.log( | ||
| `unsafe config overrides: ${unsafeOverrides.length ? unsafeOverrides.join(', ') : 'none detected'}`, | ||
| ); | ||
| console.log(`isolated signing probe: ${probe.ok ? 'passed' : `failed — ${probe.reason}`}`); | ||
| } | ||
|
|
||
| const hookFailure = | ||
| !signing.enabled || | ||
| !signing.keyConfigured || | ||
| !identity.name || | ||
| !identity.email || | ||
| unsafeOverrides.length > 0 || | ||
| !probe.ok; | ||
| process.exit(hookFailure ? 1 : 0); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.