Skip to content

feat(storage): substrate-level key verification on BlobStore.put (cloister-7e631b, stacked on #85)#86

Closed
jamestexas wants to merge 1 commit into
feat/oci-body-size-capfrom
feat/blobstore-put-verify-7e631b
Closed

feat(storage): substrate-level key verification on BlobStore.put (cloister-7e631b, stacked on #85)#86
jamestexas wants to merge 1 commit into
feat/oci-body-size-capfrom
feat/blobstore-put-verify-7e631b

Conversation

@jamestexas
Copy link
Copy Markdown
Contributor

Stacked on #85. Merge #83#84#85 → this.

Closes cloister-7e631b — defense-in-depth seam flagged by adversarial review of cloister-667ea6 (bundle-isolation-tester + trust-root-adversary).

Context

PR #84 added BlobStore.put(bytes, key?) so the OCI route could honor the build-cache/v1 BLAKE3-in-sha256: wire (storage key is BLAKE3 of body, not SHA-256). All current callers — the three OCI verify sites + bead-create-orchestrator — verify body-against-key BEFORE calling put. But that moved the content-addressed invariant from substrate-enforced to caller-discipline: a future contributor adding a put-with-key call site could skip verification and store bytes under an arbitrary unverified key.

Fix

WorkerdBlobStore.put, when given a key, now re-verifies the body matches it under SHA-256 OR BLAKE3 (same dual-algorithm check the OCI route uses, kept in sync via the shared digestBytes/blake3HexBytes helpers in src/storage/canonical.ts). Mismatch throws — substrate refuses to store under an unverified key.

Cost

One additional hash computation per put-with-key. The OCI route already runs verifyClaimedDigest, so this is duplicated work — by design. The reverse trade is "trust callers forever," which the adversarial review flagged as fragile.

Default callers (put(bytes) with no key) are unchanged: substrate computes SHA-256 as before.

Test plan

  • 4 new cases in test/storage/workerd.test.ts §"caller-provided key verification":
    • put(bytes, sha256-of-bytes) accepts (default content-addressed)
    • put(bytes, blake3-of-bytes) accepts (build-cache/v1)
    • put(bytes, wrong-key) throws — substrate refuses
    • put(bytes) no-key path unchanged
  • task lint → 1145/1145 tests pass (was 1141, +4)

🤖 Generated with Claude Code

…BlobStore.put (cloister-7e631b)

Closes cloister-7e631b — defense-in-depth seam flagged by adversarial
review of cloister-667ea6 (bundle-isolation-tester + trust-root-adversary).

Context: PR #84 added `put(bytes, key?)` so the OCI route could honor
the build-cache/v1 BLAKE3-in-`sha256:` wire (the storage key is BLAKE3
of body, not SHA-256). All current callers — the three OCI verify
sites + bead-create-orchestrator — verify body-against-key BEFORE
calling put. But that moved the content-addressed invariant from
substrate-enforced to caller-discipline: a future contributor adding
a put-with-key call site could skip verification and store bytes
under an arbitrary unverified key.

This PR restores the substrate-side guarantee: `WorkerdBlobStore.put`,
when given a key, now re-verifies the body matches it under SHA-256
OR BLAKE3 (same dual-algorithm check the OCI route uses, kept in
sync via the shared `digestBytes`/`blake3HexBytes` helpers in
storage/canonical.ts). Mismatch throws — substrate refuses to store
under an unverified key.

Cost: one additional hash computation per put-with-key (the same
hash the caller just computed; in practice the OCI route already
ran verifyClaimedDigest, so this is duplicated work). The reverse
trade is the alternative — trust callers forever — which the
adversarial review flagged as fragile. Defense-in-depth wins.

Behavior unchanged for default callers (`put(bytes)` with no key):
the substrate computes SHA-256 as before; the default path is the
content-addressed contract that's existed since the file was
written.

Tests: 4 new cases in storage/workerd.test.ts §"caller-provided key
verification":
  - put(bytes, sha256-of-bytes) accepts
  - put(bytes, blake3-of-bytes) accepts (build-cache/v1 path)
  - put(bytes, wrong-key) throws
  - put(bytes) default path unchanged

Verification: task lint → 1145/1145 tests pass (was 1141, +4).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jamestexas
Copy link
Copy Markdown
Contributor Author

Superseded by #90 (consolidated build-cache/v1 storage-key + hardening). Closing to reduce review load.

@jamestexas jamestexas closed this May 24, 2026
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