Skip to content

fix: bind Sigstore provenance to admitted workload - #2847

Merged
ascorbic merged 7 commits into
mainfrom
fix/sigstore-workload-identity-binding
Sep 2, 2026
Merged

fix: bind Sigstore provenance to admitted workload#2847
ascorbic merged 7 commits into
mainfrom
fix/sigstore-workload-identity-binding

Conversation

@ascorbic

@ascorbic ascorbic commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Binds each verified Sigstore attestation to the GitHub OpenID Connect identity that admitted the release intent. The provenance verifier now reports the verified repository ID, workflow ref, commit, and invocation identity. The release service canonically parses and rehashes its stored workload identity, checks it against the active workload policy and attestation during initial and final verification, then repeats that check immediately before entering the create phase.

Adds regression coverage for substituted repository IDs, workflow files, refs, commits, run and invocation identities, malformed or non-canonical stored identities, digest mismatches, incomplete verifier reports, and a policy change between final verification and create.

Closes: N/A — independently validated delegated-release security finding with no public issue.

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes — not run repo-wide; all three affected package typechecks pass
  • pnpm lint passes — full type-aware lint reports zero diagnostics
  • pnpm test passes (or targeted tests for my change) — complete affected package suites pass
  • pnpm format has been run — all changed files were formatted and checked directly to avoid touching unrelated untracked worktree files
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main. (N/A: no admin UI strings)
  • I have added and reviewed the user-facing changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/... (N/A: security bug fix)

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: GPT-5 Codex

Screenshots / test output

No visual changes.

  • pnpm lint:json | jq '.diagnostics | length'0
  • Affected package typechecks — pass
  • @emdash-cms/registry-verification Node, packed-output, and Workerd suites — 152 tests pass
  • @emdash-cms/release-verifier suite — 14 tests pass
  • @emdash-cms/release-service core suite, run serially to avoid Workerd contention — 438 tests pass
  • Release-service encryption-v2 and UI suites — 14 tests pass
  • Release-service and release-verifier production builds — pass
  • publint — pass

attw --pack 0.18.2 crashes locally on Node 24.17.0 with Cannot read properties of undefined (reading 'filename'); npm pack, the repository's packed-output test, package typecheck, and publint all complete successfully.

@changeset-bot

changeset-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8e85dda

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 21 packages
Name Type
@emdash-cms/registry-verification Patch
emdash Patch
@emdash-cms/labeler Patch
@emdash-cms/release-action Patch
@emdash-cms/release-service Patch
@emdash-cms/release-verifier Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
create-emdash Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added the review/needs-review No maintainer or bot review yet label Sep 2, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 2, 2026

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://fix-sigstore-workload-identity-binding.try.emdashcms.com, https://fix-sigstore-workload-identity-binding-emdash-playground.emdash-cms.workers.dev (commit 8e85dda)

This URL reflects your latest Preview deployment

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://d1e85c82.try.emdashcms.com, https://d1e85c82-emdash-playground.emdash-cms.workers.dev 8e85dda 2026-09-02T11:40:50.139Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://dc463d58.try.emdashcms.com, https://dc463d58-emdash-playground.emdash-cms.workers.dev f7a9390 2026-09-02T11:26:46.069Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://15eec1e5.try.emdashcms.com, https://15eec1e5-emdash-playground.emdash-cms.workers.dev 4d0faa4 2026-09-02T11:00:18.928Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://538f8233.try.emdashcms.com, https://538f8233-emdash-playground.emdash-cms.workers.dev 05140e9 2026-09-02T10:08:23.998Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://1170dd1a.try.emdashcms.com, https://1170dd1a-emdash-playground.emdash-cms.workers.dev c2125e1 2026-09-02T09:52:06.369Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://3a68be8e.try.emdashcms.com, https://3a68be8e-emdash-playground.emdash-cms.workers.dev cabc840 2026-09-02T09:17:57.466Z Visit the dashboard ↗

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 624 lines across 14 files. Large PRs are harder to review and more likely to be closed without review.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

@pkg-pr-new

pkg-pr-new Bot commented Sep 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@2847

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@2847

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@2847

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@2847

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@2847

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@2847

emdash

npm i https://pkg.pr.new/emdash@2847

create-emdash

npm i https://pkg.pr.new/create-emdash@2847

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@2847

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@2847

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@2847

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@2847

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@2847

@emdash-cms/registry-moderation

npm i https://pkg.pr.new/@emdash-cms/registry-moderation@2847

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@2847

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@2847

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@2847

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@2847

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@2847

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@2847

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@2847

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@2847

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@2847

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@2847

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@2847

commit: 8e85dda

@emdashbot emdashbot Bot left a comment

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.

This is the right change for the stated security gap. It binds Sigstore provenance to the admitted workload by threading verified repository, workflow, commit, and invocation metadata from the registry verifier through the release verifier into the release service, where it is checked against the canonicalized stored workload identity both during initial verification and again immediately before create (TOCTOU protection on policy changes). The architecture fits EmDash’s existing policy/evaluation split, and the regression coverage is broad.

I checked the changed files, the workload identity parser, the release-action/e2e fixtures, and the workload-policy semantics. The implementation is mostly solid, but one logic bug in the ref binding check conflates two distinct GitHub refs and would reject legitimate releases where the workflow file ref and the triggering run ref differ—a configuration the rest of the codebase explicitly supports (e.g. workflowRef on refs/heads/main and ref on refs/tags/v1.2.3). There are also two smaller robustness/formatter items.

Headline: merge blocked on fixing the workflow/run-ref comparison in evaluateWorkloadAttestation; the other two items are defensive cleanups.

return { ok: false, reasonCode: "ATTESTED_WORKFLOW_MISMATCH" };
}
const workflowRef = identity.workflow.ref.slice(identity.workflow.ref.lastIndexOf("@") + 1);
if (provenance.workflowRef !== workflowRef || provenance.workflowRef !== identity.run.ref) {

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.

[needs fixing] The ref check conflates the workflow file ref with the run ref. provenance.workflowRef is the verified workflow file ref (externalParameters.workflow.ref in SLSA), while identity.run.ref is the ref that triggered the run (github.ref). The codebase supports them being different: connection/request fixtures use workflowRef on refs/heads/main and ref on refs/tags/v1.2.3, and evaluateWorkloadPolicy checks them independently. Since the attestation does not carry the triggering ref, the binding should only verify that the attested workflow file ref matches the ref portion of identity.workflow.ref.

Suggested change
if (provenance.workflowRef !== workflowRef || provenance.workflowRef !== identity.run.ref) {
if (provenance.workflowRef !== workflowRef) {
return { ok: false, reasonCode: "ATTESTED_REF_MISMATCH" };
}

Remove || provenance.workflowRef !== identity.run.ref.

}
const workflowMarker = "/.github/workflows/";
const markerIndex = identity.workflow.ref.toLowerCase().indexOf(workflowMarker);
const expectedBuilderId = `${provenance.sourceRepository}${identity.workflow.ref.slice(markerIndex)}`;

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.

[suggestion] Use the normalized sourceRepository already computed on line 376 instead of raw provenance.sourceRepository. The repository check is case-insensitive, so if a verifier returned a non-canonical-cased source repository, expectedBuilderId would be built with that mixed casing while a canonicalized builderId would be lowercase, causing a false ATTESTED_WORKFLOW_MISMATCH even though the repository matches.

Suggested change
const expectedBuilderId = `${provenance.sourceRepository}${identity.workflow.ref.slice(markerIndex)}`;
const expectedBuilderId = `${sourceRepository}${identity.workflow.ref.slice(markerIndex)}`;

if (provenance.commitSha !== identity.run.commitSha) {
return { ok: false, reasonCode: "ATTESTED_COMMIT_MISMATCH" };
}
const invocationId = `${provenance.sourceRepository}/actions/runs/${identity.run.id}/attempts/${identity.run.attempt}`;

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.

[suggestion] Same canonicalization point as the builderId line: construct invocationId from the normalized sourceRepository so the comparison stays consistent with the repository binding established above.

Suggested change
const invocationId = `${provenance.sourceRepository}/actions/runs/${identity.run.id}/attempts/${identity.run.attempt}`;
const invocationId = `${sourceRepository}/actions/runs/${identity.run.id}/attempts/${identity.run.attempt}`;

Comment thread apps/release-service/vitest.config.ts Outdated
Comment on lines +46 to +53
predicateType: input.provenance.predicateType,
sourceRepository: input.provenance.sourceRepository,
builderId: input.provenance.builderId,
repositoryId: "123456789",
workflowRef: "refs/heads/main",
commitSha: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
invocationId: "https://github.com/example/gallery/actions/runs/100/attempts/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.

[suggestion] The embedded Worker script added lines 46–52 at a different indentation level than the surrounding object keys (8 tabs vs. 14 tabs). It is syntactically valid inside the template literal, but the misalignment makes the structure easy to misread and edit. Please re-indent the new provenance fields to match the surrounding keys before merging.

@github-actions github-actions Bot added cla: signed review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Sep 2, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 2, 2026
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Sep 2, 2026

@emdashbot emdashbot Bot left a comment

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.

This is the right change for the stated security gap. The prior blocking bug—the conflation of the workflow-file ref with the triggering run ref in evaluateWorkloadAttestation—is fixed: the attested ref is now extracted from identity.workflow.ref (the workflow file ref), while the policy's allowedRefs still governs identity.run.ref (the trigger ref). The rest of the implementation is sound: the registry provenance verifier binds repository, workflow, commit, and invocation identity; the release verifier guards against missing fields; the release service parses, re-digests, and canonicalizes the stored workload identity; and the publication workflow now re-checks the active workload policy immediately before entering the create phase, closing the TOCTOU window.

I checked the changed files, the new stored-identity parser, the attestation binding logic, the registry provenance contract test, the release-verifier field guard, and the integration test covering a policy change between final verification and create. The regression coverage matches the PR description.

The remaining items are tiny test-clarity cleanups, not blockers. The changeset is correct to list only @emdash-cms/registry-verification; the two affected apps are private and do not need entries.

sourceRepository: PROVENANCE.sourceRepository,
builderId: PROVENANCE.builderId,
repositoryId: WORKLOAD_IDENTITY.repository.id,
workflowRef: WORKLOAD_IDENTITY.run.ref,

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.

[suggestion] The verifierReport() helper uses WORKLOAD_IDENTITY.run.ref as the attested workflow ref. This happens to equal the workflow file ref (refs/heads/main) in the default fixture, but the field semantically represents the workflow file ref extracted from identity.workflow.ref. Derive it from there so the fixture stays correct if a future test changes the triggering run ref independently.

Suggested change
workflowRef: WORKLOAD_IDENTITY.run.ref,
workflowRef: WORKLOAD_IDENTITY.workflow.ref.slice(
WORKLOAD_IDENTITY.workflow.ref.lastIndexOf("@") + 1,
),

).resolves.toMatchObject({ success: true });
});

it("uses canonical repository casing for builder and invocation identity", async () => {

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.

[suggestion] The test title claims it checks canonical casing for the builder and invocation identity, but the body only mutates provenance.sourceRepository and relies on the repository casing tolerance in evaluateWorkloadAttestation. Rename it to describe what it actually verifies.

Suggested change
it("uses canonical repository casing for builder and invocation identity", async () => {
it("uses canonical repository casing for the attested source repository", async () => {

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 2, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 2, 2026

@emdashbot emdashbot Bot left a comment

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.

This re-review confirms the fix remains sound. The prior blocking bug—the conflation of the attested workflow-file ref with the triggering run ref in evaluateWorkloadAttestation—is still correctly resolved: provenance.workflowRef is matched against the @ref suffix of identity.workflow.ref (the workflow file ref), while evaluateWorkloadPolicy independently governs identity.run.ref (the trigger ref) against allowedRefs.

I re-read the diff and the changed files in full, traced the new parseStoredWorkloadIdentity parser against the github-oidc normalizer that produces the stored JSON, and checked the re-check path in the publication workflow. Nothing new jumped out:

  • The registry provenance verifier now binds and verifies repositoryId, workflowRef, commitSha, and invocationId from the Sigstore certificate OIDs / predicate, returning the narrower VerifiedGitHubProvenance type compatibly.
  • The release verifier guards against missing new fields before propagating them.
  • parseStoredWorkloadIdentity re-parses, validates, canonicalizes, and re-digests the stored workload identity; the canonical check matches the JSON produced by the existing OIDC normalizer.
  • evaluateWorkloadAttestation binds the verified provenance identity back to the parsed stored identity and re-evaluates the active workload policy.
  • publishVerifiedIntent repeats that binding/policy check immediately before advancing to the creating phase, closing the TOCTOU window.
  • Test coverage matches the PR description: substituted IDs/refs/commits/identities, malformed/non-canonical stored identities, digest mismatches, incomplete verifier reports, and a policy change between final verification and create are all exercised.

No new logic bugs, regressions, or convention violations. Approving again.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 2, 2026
@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview Author pushed changes since the last review labels Sep 2, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 2, 2026

@emdashbot emdashbot Bot left a comment

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.

This is the right change for the reported security gap: binding the Sigstore attestation to the stored workload identity and re-evaluating the policy immediately before the create phase closes a real TOCTOU window. The structure—verifier returns a richer provenance identity, the service canonicalizes and re-digests the stored identity, and both initial evaluation and the pre-create check use the same binding function—is sound.

I re-read the diff and traced the flow through the github-oidc normalizer, both stored-identity parsers, the workload policy evaluator, and the publication workflow. The regression coverage matches the PR description, the changeset correctly covers the only published package (@emdash-cms/registry-verification), and no AGENTS.md conventions for this code area are violated.

New finding not caught in the prior review: evaluateWorkloadAttestation lowercases the repository prefix when reconstructing expectedBuilderId and invocationId (it takes the repository name from identity.repository.name, which github-oidc.ts lowercases). GitHub's Fulcio builder ID and invocation URI preserve the original repository casing from workflow_ref, so mixed-case repositories will pass admission-time policy matching but fail attestation with ATTESTED_WORKFLOW_MISMATCH or ATTESTED_INVOCATION_MISMATCH. The fix is to derive the repo prefix from identity.workflow.ref (which the parser already validates starts with the repository) and compare source repositories case-insensitively. A regression test with a mixed-case provenance builderId/invocationId should be added alongside the fix.

Comment on lines +375 to +382
if (!policyDecision.ok) return { ok: false, reasonCode: policyDecision.code };
const sourceRepository = `https://github.com/${identity.repository.name}`;
if (
provenance.repositoryId !== identity.repository.id ||
provenance.sourceRepository.toLowerCase() !== sourceRepository
) {
return { ok: false, reasonCode: "ATTESTED_REPOSITORY_MISMATCH" };
}

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.

[needs fixing] sourceRepository is built from identity.repository.name, which github-oidc.ts lowercases when normalizing the OIDC token. The Sigstore-attested builderId and OID 21 invocationId, however, preserve the original casing from the GitHub workflow_ref claim. Repositories with uppercase letters therefore pass admission-time policy matching (which normalizes case) but fail here with ATTESTED_WORKFLOW_MISMATCH or ATTESTED_INVOCATION_MISMATCH because expectedBuilderId and invocationId use a lowercased prefix.

Derive the repo prefix from identity.workflow.ref — the parser already validates that the ref starts with the repository — and compare source repositories case-insensitively. Add a regression test that upper-cases the provenance builderId/invocationId (or the stored workflow ref).

Suggested change
if (!policyDecision.ok) return { ok: false, reasonCode: policyDecision.code };
const sourceRepository = `https://github.com/${identity.repository.name}`;
if (
provenance.repositoryId !== identity.repository.id ||
provenance.sourceRepository.toLowerCase() !== sourceRepository
) {
return { ok: false, reasonCode: "ATTESTED_REPOSITORY_MISMATCH" };
}
const workflowMarker = "/.github/workflows/";
const markerIndex = identity.workflow.ref.toLowerCase().indexOf(workflowMarker);
if (markerIndex < 1) {
return { ok: false, reasonCode: "ATTESTED_WORKFLOW_MISMATCH" };
}
const sourceRepository = `https://github.com/${identity.workflow.ref.slice(0, markerIndex)}`;
if (
provenance.repositoryId !== identity.repository.id ||
provenance.sourceRepository.toLowerCase() !== sourceRepository.toLowerCase()
) {
return { ok: false, reasonCode: "ATTESTED_REPOSITORY_MISMATCH" };
}
const expectedBuilderId = `${sourceRepository}${identity.workflow.ref.slice(markerIndex)}`;
if (provenance.builderId !== expectedBuilderId) {
return { ok: false, reasonCode: "ATTESTED_WORKFLOW_MISMATCH" };
}

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 2, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 2, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 2, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
docs f7a9390 Sep 02 2026, 11:24 AM

@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/approved Approved; no new commits since labels Sep 2, 2026

@emdashbot emdashbot Bot left a comment

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.

This is the right change for the reported delegated-release TOCTOU gap. Binding the Sigstore attestation to the stored workload identity through repository ID, workflow ref, commit SHA, and invocation identity, and re-evaluating that binding immediately before the create phase, closes a real window where policy or identity could drift after final verification. The richer VerifiedGitHubProvenance contract in @emdash-cms/registry-verification, the canonical digest-aware parser in workload/stored-identity.ts, and the dual workload checks in evaluateVerifiedRelease and the pre-create evaluateWorkloadAttestation call form a coherent defense.

I re-read the full diff and traced the flow through GitHubProvenanceVerifier, the release-verifier guard, normalizeVerifierReport/parseNormalizedVerifierReport, evaluateWorkloadAttestation, both evaluateVerifiedRelease callers, and publication-operation.ts#advancePhase. The prior review's mixed-case repository finding is fixed: evaluateWorkloadAttestation now derives the repository prefix from identity.workflow.ref (which preserves GitHub casework) and compares sourceRepository case-insensitively, while the builderId and invocationId comparisons stay case-sensitive. The regression coverage matches the PR description, the changeset correctly covers the only published package (@emdash-cms/registry-verification), and no AGENTS.md conventions are violated.

The code is essentially ready. One consolidation suggestion remains: a publication-critical path in publisher-do.ts still uses the older, non-digest-aware parseStoredWorkloadIdentity from workload/types.js even though this PR introduces a stronger one in workload/stored-identity.ts. Downstream checks in advancePhase currently catch any mismatch, but centralizing on the new parser removes the duplicate name and contract.


Findings

  • [suggestion] apps/release-service/src/publisher-do/publisher-do.ts:1111

    This call site still uses parseStoredWorkloadIdentity from ../workload/types.js, which only validates canonical JSON and does not verify the stored digest against intent.workloadIdentityDigest. The PR introduces a digest-aware parseStoredWorkloadIdentity in ../workload/stored-identity.js and relies on it for all security-critical verification paths; leaving this publication-critical advancePublicationOperationPhase authorization construction on the weaker parser splits canonical parsing and digest verification across two functions with the same name. Downstream advancePhase currently compares authorization.identityDigest !== intent.workload_identity_digest, so the current flow is safe, but it would be cleaner and less error-prone to use the new parser here too.

    		const identity = intent
    			? await parseStoredWorkloadIdentity(
    					intent.workloadIdentityJson,
    					intent.workloadIdentityDigest,
    				)
    			: null;
    

    Update the import to ../workload/stored-identity.js and drop the now-redundant parseStoredWorkloadIdentity export from types.ts once this migration is complete.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 2, 2026
@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview Author pushed changes since the last review labels Sep 2, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 2, 2026
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/approved Approved; no new commits since labels Sep 2, 2026

@emdashbot emdashbot Bot left a comment

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.

This is the right change for the delegated-release TOCTOU gap and the follow-up to the prior review. The PR now consistently uses the digest-aware, canonical parseStoredWorkloadIdentity from workload/stored-identity.ts everywhere security-critical workload identity is parsed, including the previously-flagged publisher-do.ts#advancePublicationOperationPhase path.

I re-read the full set of changes:

  • stored-identity.ts replaces the old types.ts parser with canonical JSON + digest verification, preserving the existing lowercase normalization for repository owner/name while allowing the workflow ref to retain GitHub casing.
  • evaluateWorkloadAttestation binds the Sigstore attestation to the stored identity by checking repository ID (case-insensitive repository URL), workflow file (case-sensitive builder ID), ref, commit SHA, and invocation identity against both the active policy and the parsed stored identity.
  • evaluateVerifiedRelease and the pre-create path in publishing/workflow.ts both re-evaluate the workload against the current policy, closing the window where policy or attestation could drift between final verification and record creation.
  • release-verifier propagates the enriched VerifiedGitHubProvenance fields and defensively rejects any non-GitHub verifier that omits them.
  • Tests cover the regression cases described in the PR, including mixed-casing handling, digest/canonical failures, substituted attestation fields, incomplete reports, and a policy change between final verification and create.
  • Only the published package @emdash-cms/registry-verification is changed; the changeset accurately reflects that.

No AGENTS.md conventions are violated: all SQL uses parameterized queries, no UI strings are introduced, and the documentation/changeset claims match the implementation. The code is ready to merge.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 2, 2026
@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview Author pushed changes since the last review labels Sep 2, 2026
@ascorbic
ascorbic merged commit 529b28b into main Sep 2, 2026
51 checks passed
@ascorbic
ascorbic deleted the fix/sigstore-workload-identity-binding branch September 2, 2026 12:44
@emdashbot emdashbot Bot mentioned this pull request Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla: signed review/approved Approved; no new commits since size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant