Skip to content

feat(sec): cosign keyless signing + verify-install + unsafe-unverified contract (genie-supply-chain-signing)#1363

Merged
namastex888 merged 1 commit into
devfrom
supply-chain-signing
Apr 23, 2026
Merged

feat(sec): cosign keyless signing + verify-install + unsafe-unverified contract (genie-supply-chain-signing)#1363
namastex888 merged 1 commit into
devfrom
supply-chain-signing

Conversation

@namastex888
Copy link
Copy Markdown
Contributor

Summary

Lands the supply-chain signing stack from the canisterworm umbrella (#1360). Two groups:

G1 — Release signing CI pipeline

  • .github/workflows/release.yml — cosign KEYLESS signing (OIDC via GitHub Actions) + SLSA Level 3 provenance. Signs before the GitHub Release is created, so a broken signature or failed self-verify guarantees no release assets are published. In-band tamper-detection self-test gates every release.
  • .github/cosign.pub — explicit NO-PINNED-KEY sentinel (not a PEM). There is no long-lived fallback key anywhere (no repo secret, no HSM, no ceremony). Any tool loading this expecting a PEM must fail closed.
  • .github/ISSUE_TEMPLATE/signing-key-fingerprint.md — redirects operator pinned-key questions toward the real verification contract: certificate-identity-regexp + certificate-oidc-issuer.
  • scripts/verify-release.sh (+ bun run verify:release alias) — operator-facing script that pins cert-identity-regexp, cert-oidc-issuer, and provenance source-uri. Never reads a key fingerprint.

G2 — verify-install subcommand + --unsafe-unverified contract

  • src/sec/unsafe-verify.ts — single source of truth for the --unsafe-unverified <INCIDENT_ID> escape hatch: INCIDENT_ID_REGEX, TYPED_ACK_PREFIX, LEGITIMATE_CONTEXTS, validateUnsafeUnverified. Council-mandated (M2 → HIGH) to prevent divergent implementations eroding friction.
  • src/term-commands/sec.tsgenie sec verify-install subcommand: runs cosign verify-blob + slsa-verifier verify-artifact, pins signer identity regexp + OIDC issuer, supports --offline, --json, --tarball, --bundle-dir. Public exit-code contract: VERIFIED(0) / SIGNATURE_INVALID(2) / SIGNER_IDENTITY_MISMATCH(3) / PROVENANCE_INVALID(4) / NO_SIGNATURE_MATERIAL(5) / MISSING_BINARY(127). Treats the cosign.pub sentinel as exit 5 (no signature material) — no false positives.
  • docs/security/key-rotation.md — operator runbook making clear there is no key to rotate: only cert-identity or OIDC-issuer changes matter.

Tests

  • bun test src/sec/unsafe-verify.test.ts35 pass / 0 fail
  • bun test src/term-commands/sec.test.ts18 pass / 0 fail (includes sentinel → exit-5 guard)
  • bun run typecheck — clean
  • bun run lint — 670 files, no fixes needed

Review dossier: .genie/wishes/genie-supply-chain-signing/REVIEW.md.

Integration follow-up (deliberately out of scope)

sec-remediate landed in #1361 with a stub validator for its --unsafe-unverified flag. A follow-up integration PR will wire remediate/restore/rollback to validateUnsafeUnverified from src/sec/unsafe-verify.ts. That's why scripts/sec-scan.cjs and scripts/sec-remediate.cjs are intentionally untouched in this PR — single-abstraction focus.

This PR unblocks the last wish in the umbrella: sec-incident-runbook.

Test plan

  • bun test src/sec/unsafe-verify.test.ts passes on CI
  • bun test src/term-commands/sec.test.ts passes on CI
  • bun run typecheck + bun run lint clean on CI
  • CodeRabbit / Gemini reviews surface no security regressions
  • Once merged + released, confirm release.yml produces signed tarball + provenance and genie sec verify-install returns exit 0

🤖 Generated with Claude Code

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9080a038-29e2-42a7-9b88-09fae01ae8ed

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch supply-chain-signing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 124fb4d165

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/term-commands/sec.ts
Comment on lines +365 to +367
if (options.bundleDir) return options.bundleDir;
if (options.tarball) return dirname(resolve(options.tarball));
return resolve(genieRoot);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Verify the tarball path passed to --tarball

The --tarball option is documented as targeting a specific artifact, but this code discards the filename (dirname(resolve(options.tarball))) and then discoverSignatureBundle verifies the first *.tgz it finds in that directory. If a directory contains multiple release bundles, genie sec verify-install --tarball <path> can return success for a different tarball than the one the operator asked to check, which creates a false verification result in exactly the scenario this command is meant to secure.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements the genie sec verify-install command and establishes a supply chain signing contract using Cosign keyless signing and SLSA provenance. It introduces centralized validation logic for the --unsafe-unverified flag, a local verification script, and a detailed key rotation runbook. Feedback focuses on resolving non-deterministic file discovery in directories with multiple artifacts, ensuring shell script portability across different operating systems (specifically macOS), and tightening the regex for incident identifiers.

Comment thread src/term-commands/sec.ts
Comment on lines +203 to +212
for (const tarballName of candidates) {
const tarball = join(bundleDir, tarballName);
const signature = `${tarball}.sig`;
const certificate = `${tarball}.cert`;
if (!deps.existsSync(signature)) continue;
if (!deps.existsSync(certificate)) continue;
const provenancePath = join(bundleDir, 'provenance.intoto.jsonl');
const provenance = deps.existsSync(provenancePath) ? provenancePath : null;
return { tarball, signature, certificate, provenance };
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The discoverSignatureBundle function returns the first .tgz file it finds that has matching .sig and .cert siblings. If a directory contains multiple release tarballs (e.g., from multiple downloads), this behavior is non-deterministic and might verify a different version than the one intended by the operator. Consider throwing an error or requiring an explicit selection if multiple candidates are found.

Comment thread scripts/verify-release.sh
;;
--local)
[ -n "${2:-}" ] || usage
tarball="$(readlink -f "$2")"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The readlink -f command is not POSIX-compliant and is unavailable on macOS by default (unless coreutils is installed). Since this script is intended for local operator use, it should use a more portable method to resolve absolute paths.

Suggested change
tarball="$(readlink -f "$2")"
tarball="$(cd "$(dirname "$2")" && pwd)/$(basename "$2")"

Comment thread scripts/verify-release.sh
--pattern 'provenance.intoto.jsonl' \
|| { echo "error: release assets missing — exit 5" >&2; exit 5; }
)
tarball="$(ls "${workdir}"/*.tgz 2>/dev/null | head -1)"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Parsing the output of ls is generally discouraged as it can fail with filenames containing special characters (like newlines). Additionally, using head -1 makes the selection non-deterministic if multiple .tgz files are present in the temporary directory. It is safer to use a glob expansion into an array.

Suggested change
tarball="$(ls "${workdir}"/*.tgz 2>/dev/null | head -1)"
local tgz_files=("${workdir}"/*.tgz)
tarball="${tgz_files[0]}"

Comment thread src/sec/unsafe-verify.ts
* faithful form: UPPER-snake prefix (at least one letter, trailing underscore
* before the date), YYYY_MM_DD, and an optional alphanumeric `_extra` tail.
*/
export const INCIDENT_ID_REGEX = /^[A-Z][A-Z0-9_]*_[0-9]{4}_[0-9]{2}_[0-9]{2}(_[A-Za-z0-9_]+)?$/;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The regex [A-Z][A-Z0-9_]* allows for multiple consecutive underscores or a trailing underscore before the date, which might deviate from the strict UPPER_SNAKE requirement mentioned in the documentation. While the current regex is functional, a stricter pattern like [A-Z]+(_[A-Z0-9]+)* would better enforce the snake_case convention.

Suggested change
export const INCIDENT_ID_REGEX = /^[A-Z][A-Z0-9_]*_[0-9]{4}_[0-9]{2}_[0-9]{2}(_[A-Za-z0-9_]+)?$/;
export const INCIDENT_ID_REGEX = /^[A-Z]+(_[A-Z0-9]+)*_[0-9]{4}_[0-9]{2}_[0-9]{2}(_[A-Za-z0-9_]+)?$/;


```bash
# 1. Stand up a disposable fixture directory under /tmp. No production repos.
export DRILL=$(mktemp -d -t genie-signing-drill-XXXXXX)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The mktemp -t flag behavior varies between GNU and BSD (macOS) implementations. On macOS, -t requires a prefix argument, whereas on Linux it is often optional or behaves differently. For a runbook intended to be executed by operators on various platforms, using a more compatible mktemp invocation is recommended.

Suggested change
export DRILL=$(mktemp -d -t genie-signing-drill-XXXXXX)
export DRILL=$(mktemp -d 2>/dev/null || mktemp -d -t 'genie-signing-drill')

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant