Skip to content

feat(registry): verify delegated releases independently - #2746

Open
ascorbic wants to merge 12 commits into
feat/drs-review-03-approvalsfrom
feat/drs-review-04-verification
Open

feat(registry): verify delegated releases independently#2746
ascorbic wants to merge 12 commits into
feat/drs-review-03-approvalsfrom
feat/drs-review-04-verification

Conversation

@ascorbic

Copy link
Copy Markdown
Collaborator

What does this PR do?

Adds the isolated verifier Worker, shared artifact/manifest/provenance verification, authoritative direct-PDS record reads, release Workflow verification steps, and independent EmDash installer enforcement. Moderation labels remain metadata-only visibility inputs bound to exact signed profile or release CIDs; they never attest to plugin code.

This is PR 4 of 7. It retains the main verification commit and the artifact-proxy checksum compatibility follow-up separately. The stack merges as one unit and this branch is not deployable by itself.

Discussion: #1590

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
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible admin strings are wrapped for translation; no messages.po files are included
  • I have added and reviewed the user-facing changesets for core, admin, registry client, and registry verification
  • New features link to an approved Discussion: RFC: Attested Automated Publishing #1590

AI-generated code disclosure

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

Screenshots / test output

Core direct-PDS install conformance, admin, registry-client, registry-verification Node/workerd, release-service verification, and verifier Worker suites pass.

@changeset-bot

changeset-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0dc935c

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

This PR includes changesets to release 22 packages
Name Type
@emdash-cms/registry-verification Minor
emdash Minor
@emdash-cms/admin Minor
@emdash-cms/registry-client Minor
@emdash-cms/labeler Patch
@emdash-cms/release-service Patch
@emdash-cms/release-verifier Patch
@emdash-cms/cloudflare Minor
@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/plugin-cli Patch
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@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

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 22,401 lines across 71 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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 27, 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 547e7f5 Aug 28 2026, 11:12 PM

@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 PR adds the isolated release-verifier Worker, direct-PDS record reads, and independent install/update verification, which is the right next step for the delegated-release stack. The architecture keeps moderation labels as metadata-only visibility inputs bound to exact signed CIDs, while the artifact, manifest, and provenance are verified independently — matching the design from the approved Discussion.

Static review found no security regressions, no SSRF bypasses in the new paths (PDS and provenance fetches both validate hosts), and no broken contracts in the consent flow. The admin strings are Lingui-wrapped, RTL-safe wrappers are used for CID/DID display, and the changed routes are authenticated (plugins:manage / plugins:read). The test coverage is extensive across core, admin, registry-verification, and release-service.

I do have one needs_fixing finding on the changeset: it publicly states that nonconforming bare-hexadecimal checksums are rejected, but the registry artifact proxy still accepts them via verifyRegistryArtifactChecksum. Two suggestions round out the review: a numbered module comment that violates the repo's comment discipline, and a latent footgun where DirectPdsClient caches a rejected publisher-resolution promise.

The test checklist in the PR description is taken at face value; I did not run the suite.


Findings

  • [needs fixing] .changeset/verify-registry-bundles.md:26

    The changeset states: "Release records must contain a lowercase base32 multibase sha2-256 multihash. Existing releases produced by the EmDash plugin CLI already use this format; nonconforming bare hexadecimal checksums are rejected."

    That is only true for the install/update path. The registry artifact proxy (packages/core/src/astro/routes/api/admin/plugins/registry/artifact.ts) still resolves artifact checksums through verifyRegistryArtifactChecksum, which explicitly accepts a 64-character bare hex SHA-256 digest in addition to multibase multihashes. A release record whose image artifact uses bare hex will still verify and be served, so the blanket rejection claim is inaccurate.

    Either update the changeset to say that bare hex is rejected only during install/update/verification, or update the artifact proxy to require multibase checksums as well.

    Release records must contain a lowercase base32 multibase `sha2-256` multihash for install, update, and verification. Existing releases produced by the EmDash plugin CLI already use this format; nonconforming bare hexadecimal checksums are rejected for plugin installs and updates.
    
  • [suggestion] packages/core/src/api/handlers/registry.ts:7-26

    This module comment enumerates the install flow with explicit numbers (1., 2., …, 11.). AGENTS.md says numbered comments are always wrong because they are brittle narrative that quickly goes stale when steps are reordered. The list was also re-numbered in this diff, which makes the fragility visible.

    Rewrite the header as prose paragraphs that describe the trust boundaries and sources of truth, without an ordered list. The numbered sequence belongs in the implementation, not the comment.

  • [suggestion] packages/registry-client/src/direct-pds/index.ts:214

    this.#resolvedPublisher ??= this.#resolvePublisher() will store a rejected promise if the first DID-document resolution fails. Because a rejected promise is truthy, every subsequent read on the same client instance will await that same rejection rather than retrying resolution. The clients created by readAuthoritativePackageRelease are short-lived today, but this is still a latent footgun if the client is ever reused or if a transient PDS/TCP timeout makes the instance permanently unusable.

    Clear the cached promise on rejection so later reads can retry:

    this.#resolvedPublisher ??= this.#resolvePublisher();
    try {
    	return await this.#resolvedPublisher;
    } catch (error) {
    	this.#resolvedPublisher = undefined;
    	throw error;
    }

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 27, 2026

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://feat-drs-review-04-verification.try.emdashcms.com, https://feat-drs-review-04-verification-emdash-playground.emdash-cms.workers.dev (commit 0dc935c)

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://04390cc4.try.emdashcms.com, https://04390cc4-emdash-playground.emdash-cms.workers.dev 0dc935c 2026-08-29T07:15:52.489Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://13db4095.try.emdashcms.com, https://13db4095-emdash-playground.emdash-cms.workers.dev 547e7f5 2026-08-28T23:16:55.926Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://61ff945c.try.emdashcms.com, https://61ff945c-emdash-playground.emdash-cms.workers.dev 4d1c6cf 2026-08-28T23:01:48.742Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://ed48fd7c.try.emdashcms.com, https://ed48fd7c-emdash-playground.emdash-cms.workers.dev 0b3fe34 2026-08-28T22:23:29.781Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://edf3e06f.try.emdashcms.com, https://edf3e06f-emdash-playground.emdash-cms.workers.dev 29a2bdc 2026-08-28T22:43:10.397Z Visit the dashboard ↗
  • Build: In progress 🔵

View logs ↗
9091ddb 2026-08-28T21:16:00.635Z View logs ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://4dda0623.try.emdashcms.com, https://4dda0623-emdash-playground.emdash-cms.workers.dev adfb630 2026-08-28T15:16:57.117Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://3a5984d6.try.emdashcms.com, https://3a5984d6-emdash-playground.emdash-cms.workers.dev 99343df 2026-08-28T13:35:34.294Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://cf9dcb89.try.emdashcms.com, https://cf9dcb89-emdash-playground.emdash-cms.workers.dev 266afc6 2026-08-28T11:46:50.708Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://35e35fd8.try.emdashcms.com, https://35e35fd8-emdash-playground.emdash-cms.workers.dev 2310136 2026-08-28T11:20:51.119Z Visit the dashboard ↗

View all previews: View all previews ↗

@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@ascorbic
ascorbic force-pushed the feat/drs-review-04-verification branch from 22cf022 to b38f41c Compare August 28, 2026 10:15
@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 Aug 28, 2026
@pkg-pr-new

pkg-pr-new Bot commented Aug 28, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/registry-moderation

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

@emdash-cms/registry-verification

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 0dc935c

@github-actions github-actions Bot added the query-count changed PR diff modifies query-count snapshot files label Aug 28, 2026
@ascorbic
ascorbic force-pushed the feat/drs-review-04-verification branch from 97d3e4e to 2310136 Compare August 28, 2026 11:15
@github-actions github-actions Bot added query-count changed PR diff modifies query-count snapshot files and removed query-count changed PR diff modifies query-count snapshot files labels Aug 28, 2026
@github-actions github-actions Bot removed the query-count changed PR diff modifies query-count snapshot files label Aug 28, 2026
@ascorbic

Copy link
Copy Markdown
Collaborator Author

🔍 Adversarial review — stack #2766, layer 5 of 8

Automated deep review of this layer's diff (feat/drs-review-03-approvals...feat/drs-review-04-verification). Findings ranked most-severe first; confidence is CONFIRMED (full code path read) or PLAUSIBLE (strong suspicion, path partially read). Nothing was auto-fixed.

What the PR actually does + verdict

It adds an isolated apps/release-verifier Worker (no bindings, no secrets; RPC-only) that fetches an artifact + Sigstore provenance under DoH-based SSRF guards and verifies checksum, bundle shape, and SLSA subject digests (now multi-algorithm via artifactDigests); a Cloudflare Workflow (ReleaseIntentWorkflow) that reads the publisher's PDS three times, persists six idempotent verification steps in the publisher DO (digest-bound, conflict-on-divergence), evaluates policy/access-diff, and drives the intent state machine (whose transitionIntent correctly replays same-digest transitions); a DirectPdsClient in registry-client that fetches records as CAR proofs via com.atproto.sync.getRecord and genuinely verifies MST inclusion + commit signature against the DID document key; and a rewritten core install/update path that reads authoritative records from the publisher PDS (readAuthoritativePackageRelease over ssrfSafeFetch httpsOnly), rejects aggregator URI/CID drift (AGGREGATOR_RECORD_MISMATCH), verifies artifact multihash + bundle + canonical declaredAccess equality + provenance (real GitHubProvenanceVerifier in production), binds consent to exact profile/release CIDs (RECORD_CONSENT_REQUIRED/RECORD_VERIFICATION_DRIFT), and adds a verifyOnly preview endpoint with proper plugins:manage authorization. Moderation labels are CID-bound and block-only. Overall quality is high — checks are ordered fail-closed, the cross-implementation conformance fixture is real (exercised against both the verifier Worker and the core installer with a fake-PDS repo signing real proofs, including key-substitution and tampered-CAR cases), and no verification bypass was found. The real defects are fail-closed operational breaks and two service-vs-installer divergences.

Findings

  • [high] apps/release-service/src/verification/pds.ts:228,257listPackageReleases filters by rkeyStart/rkeyEnd, parameters that no longer exist in com.atproto.repo.listRecords (verified against the current lexicon: only limit, cursor, reverse); real PDSes ignore unknown params and return the whole collection, and releaseVersion() throws RELEASE_LIST_INVALID for any record outside the {slug}: prefix instead of filtering. Failure scenario: any publisher with a second package (or any non-semver release rkey) can never complete delegated-release verification — every intent dies in the authoritative-records step. Fails closed, so no bypass, but it breaks the feature's core flow on real PDSes; the test (verification-pds.test.ts) asserts the dead params are echoed and returns only in-range records, encoding the wrong assumption. CONFIRMED.
  • [medium] apps/release-service/src/verification/evaluate.ts:339,299 + apps/release-verifier/src/verify.ts:167,246 — the verifier reports the post-redirect final URL (resource.value.url), while evaluateVerifiedRelease requires it to equal the signed record's URL exactly (ARTIFACT_RECORD_MISMATCH), and reportBackedVerifier does the same for provenance (PROVENANCE_UNVERIFIABLE). fetchVerifiedResource follows up to 3 redirects, so the redirect-following is dead weight: any artifact or provenance URL that redirects — e.g. GitHub release-asset URLs, the natural home for GitHub-provenance artifacts — invalidates the intent. Fails closed. CONFIRMED behavior (possibly intended strictness, but then redirects should be disabled at fetch, not converted into a confusing mismatch).
  • [medium] packages/registry-verification/src/records.ts:287 — the installer path calls verifier.verify() without artifactDigests, so core install/update only matches sha256 SLSA subjects, while the release-verifier Worker supplies sha256/384/512 candidates. A release whose attestation subject carries only a sha384/512 digest passes delegated service verification but fails independent installer verification on every EmDash instance ("Artifact digest mismatch") — a publish/install split-brain the shared conformance fixture doesn't cover (its subject is sha256). CONFIRMED divergence.
  • [medium] packages/core/src/astro/routes/api/admin/plugins/registry/artifact.ts:305 (commit 5185cc8) — the follow-up commit dropped the deliberate !descriptor.blobCid skip and now verifies the raw-blob multihash against the bytes returned by the record-scoped cache's /img/{preset} URL, which the aggregator lexicon itself describes as "image representations" (i.e. transformed renditions). If the external Cumulus cache resizes/re-encodes (as avatar/banner/thumbnail presets exist to do), every blob-backed icon/banner/screenshot 502s with ARTIFACT_CHECKSUM_MISMATCH. The proxy test mocks the cache returning byte-identical originals, so the suite cannot detect this. PLAUSIBLE (depends on external cache behavior; the removed guard came from the layer below, suggesting the transform case was previously accounted for). On the masquerade question: the retained bare-hex path here is image-proxy-only; the install/update path strictly rejects hex (INVALID_MULTIHASH), and hex is still SHA-256, so no weaker binding is reachable for installable code.
  • [low] packages/admin/src/components/RegistryPluginDetail.tsx:148 + packages/admin/src/lib/api/registry.ts (resolveDidToHandle) — publisher handle re-verification blocks only on status === "invalid"; network/DID-method failures return "missing", which installs fine, and the server never verifies handle↔DID correspondence at all. The changeset's impersonation defense is advisory client-side UI; an aggregator that maps a coveted handle to an attacker DID is only caught if bidirectional resolution provably fails rather than merely erroring. CONFIRMED behavior (arguably deliberate fail-open on "couldn't determine", but worth an explicit decision).
  • [low] apps/release-service/src/workflows/release-intent.ts:1446-1463 (isolated-verifier step) and step-conflict paths — NonRetryableError on invalid verifier input or VERIFICATION_STEP_CONFLICT (PDS content changed between step retries) terminates the workflow without transitioning the intent, leaving it in verifying until expiry rather than invalid with a reason code. Operational only. CONFIRMED.
  • [low] packages/core/src/api/handlers/registry.ts (registryArtifactError default branch) — artifact fetch failures (network/host errors from fetchReleaseArtifact) now map to INVALID_BUNDLE, mislabeling transient network problems as bundle corruption for the admin's code→message mapping; previously they surfaced as a generic install error. CONFIRMED, cosmetic/error-semantics.

PR description vs code

  • "Isolated verifier Worker" is accurate in the no-shared-state sense (zero bindings/secrets, RPC only), but its inputs (URL, checksum, repository) are computed by the release service from its own PDS read — the verifier is an isolated executor that cannot detect a compromised release service feeding it consistent-but-wrong inputs; the genuine independent backstop is the EmDash installer path, which does re-verify everything from authoritative sources (claim holds there).
  • "Authoritative direct-PDS record reads": the core installer verifies MST proofs and commit signatures (verifyRecord over CARs); the release service reads plain com.atproto.repo.getRecord JSON and trusts the PDS-asserted CID with no proof or CID recomputation (apps/release-service/src/verification/pds.ts parseRecord). Self-consistent under a PDS-as-authority model, but weaker than the stack's proof language; upper layers must not treat those CIDs as proof-verified.
  • Moderation-label claims check out: hasCurrentRecordLabel binds uri+cid exactly, respects neg, blocks only (metadata-only). One nit: a malformed exp makes a yank label not current, i.e. fails open for a blocking label.

Test-coverage gaps

  • verification-pds.test.ts is tautological about rkeyStart/rkeyEnd — no test with a PDS that ignores them or a second package in the collection.
  • The artifact-proxy blob test mocks the image cache returning original bytes, hiding the transform-vs-checksum contradiction.
  • No test that a redirecting artifact/provenance URL survives service-side verification (it doesn't).
  • No installer-side test for a provenance subject using sha384/512 only (the divergence above).
  • The workflow "ready" path uses an echo-stub verifier in miniflare, so it proves state-machine wiring only — acceptable because the verifier Worker and the core installer both run the same shared conformance fixture against a fake PDS with real signed proofs, which is the opposite of tautological and the strongest test asset in the PR.

~ 🤖 Fable

@ascorbic
ascorbic force-pushed the feat/drs-review-04-verification branch from 99343df to adfb630 Compare August 28, 2026 15:11
@ascorbic
ascorbic force-pushed the feat/drs-review-04-verification branch from adfb630 to 9091ddb Compare August 28, 2026 21:15
@ascorbic
ascorbic force-pushed the feat/drs-review-04-verification branch from 9091ddb to 29a2bdc Compare August 28, 2026 21:19
@github-actions github-actions Bot added the query-count changed PR diff modifies query-count snapshot files label Aug 28, 2026
@ascorbic
ascorbic force-pushed the feat/drs-review-04-verification branch from 0b3fe34 to 29a2bdc Compare August 28, 2026 22:38
@github-actions github-actions Bot removed the query-count changed PR diff modifies query-count snapshot files label Aug 28, 2026
@ascorbic
ascorbic force-pushed the feat/drs-review-04-verification branch from 4d1c6cf to 547e7f5 Compare August 28, 2026 23:07
@github-actions github-actions Bot added query-count changed PR diff modifies query-count snapshot files and removed query-count changed PR diff modifies query-count snapshot files labels Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant