Skip to content

chore(hash): the synchronous SHA-256 is @noble/hashes, not hand-written code of unrecorded origin - #317

Merged
MerciHanrim merged 1 commit into
mainfrom
chore/sha256-noble
Oct 4, 2026
Merged

MerciHanrim merged 1 commit into
mainfrom
chore/sha256-noble

Conversation

@MerciHanrim

Copy link
Copy Markdown
Owner

Part of #301 — the second step, after decision 1 (#316).

Why

The pure-JS SHA-256 in src/model/workspace.ts (sha256Js) cites only FIPS 180-4, and the repository's record does not say where its text came from, so it could not go into the notice list. It is not only a fallback: the synchronous revision and proposal digest (digestOfCanonical, 17 product call sites) always uses it, whether or not the page has Web Crypto. Removing it would mean making the revision pipeline asynchronous, so it is replaced instead by @noble/hashes (MIT, no dependencies), which gives the same bytes.

What changes

  • package.json: "@noble/hashes": "2.4.0", exact. The lockfile gains that one package and nothing else.
  • src/model/workspace.ts: sha256Js keeps its name and its synchronous signature; its body becomes toHex(sha256(bytes)), with sha256 imported from @noble/hashes/sha2.js. sha256Hex still uses Web Crypto first and falls back to it. fullContentDigest is left unchanged.
  • check:share-crypto keeps "no cryptography library" with one narrow exception (below).
  • docs/specs/SEMANTICS-W.md and SEMANTICS-R.md require a bundled pure-JS SHA-256 fallback; @noble/hashes is one, so they are unchanged.
  • Declared internal: the version stays 0.17.1 and there is no release note. Nothing a person sees or stores changes.

The digests are byte-identical

src/model/sha256Baseline.fixture.ts was recorded with the replaced code on main at 44af9a4, before the change, and src/model/sha256Baseline.test.ts checks all of it against the new code:

  • 205 hash vectors (every length from 0 to 200 bytes, then 1,000, 4,096, 65,536 and 1,048,576) and the 4 published vectors;
  • the semantic, content and full-content digests of the 12 example diagrams;
  • what the import layer's reader makes of the 8 example revision and proposal files: 7 verify against their stored digest, and the one built to fail still fails at the same stage.

The same values are checked through Web Crypto, and with Web Crypto removed. A deliberately wrong hash fails 22 of these 25 tests.

The exception in check:share-crypto

Held in scripts/share-crypto-noble.mjs and tested on 38 made-up cases in scripts/share-crypto-noble.test.mjs:

  • package.json may name exactly @noble/hashes 2.4.0, in dependencies; any other version, range or field fails, and every other cryptography-looking package still fails.
  • The lockfile has exactly one @noble/* package, @noble/hashes at 2.4.0, with no dependencies.
  • Exactly one source imports it: src/model/workspace.ts, as import { sha256 } from '@noble/hashes/sha2.js'. Another path, another or an additional name, a namespace or default import, a re-export or a dynamic import fails, and so does an import from any other product file, unit test, end-to-end spec or script.
  • src/model/shareProtected.ts may not reach @noble/hashes, src/model/workspace.ts or src/model/revision.ts, directly or through another module, and may not name their hash functions. The existing Web-Crypto-only checks on protected links are unchanged.

Run against a deliberate violation (shareProtected.ts importing the workspace wrapper), the real check fails on all three counts.

Measured

  • Bundle: +3,731 bytes of JavaScript, +1,444 gzipped, in the web and PWA builds; +3,847 bytes in the portable file. The modules of the package that are bundled are sha2.js, _md.js, _u64.js and utils.js.
  • Node: 24.14.1 locally and 22.23.3 on the CI runners; the package needs 20.19 or later. The Cloudflare build's Node version is not visible from here; it builds with Vite 8, which needs the same.
  • On a non-local plain-http origin (http://192.0.2.10/, where the browser exposes no crypto.subtle), with the dev build in Chromium: a revision file imported through the toolbar opens with its stored digest verified, a proposal applies, and both graph digests equal the committed verification oracle; in the page, 20 hash vectors and the 12 example diagrams match the baseline.

Verification (local)

  • npx tsc -b, oxlint (39 warnings, the existing baseline), 3,028 unit tests and all check scripts pass; the web, PWA and portable builds succeed.
  • End-to-end: the revision, revision-fixture, workspace, portable-file and protected-link specs passed 69 of 69; the production-bundle suite 15 of 15 and the PWA suite 18 of 18; none failed or retried.
  • The full five-shard suite is left to this pull request's CI.

Not in this pull request

…en code of unrecorded origin

Part of #301, the second step. The pure-JS SHA-256 in src/model/workspace.ts
(sha256Js) cites only FIPS 180-4, and the repository's record does not say
where its text came from, so it could not be listed with a notice. It is not
just a fallback: the synchronous revision and proposal digest
(digestOfCanonical, 17 product call sites) always uses it. It is replaced by
@noble/hashes 2.4.0 (MIT, no dependencies), which gives the same bytes.

- package.json: "@noble/hashes": "2.4.0", exact. The lockfile gains that one
  package and nothing else.
- src/model/workspace.ts: sha256Js keeps its name and synchronous signature;
  its body is toHex(sha256(bytes)) with sha256 imported from
  '@noble/hashes/sha2.js'. sha256Hex still uses Web Crypto first.
  fullContentDigest is left as it is.
- The digests are byte-identical. src/model/sha256Baseline.fixture.ts was
  recorded with the replaced code on main 44af9a4, before the change: 205
  hash vectors (every length 0 to 200, and 1,000 to 1 MiB) and the 4
  published vectors, the semantic, content and full-content digests of the
  12 example diagrams, and what the import layer's reader makes of the 8
  example revision and proposal files. sha256Baseline.test.ts checks all of
  it against the new code, and against Web Crypto, and with Web Crypto
  removed.
- check:share-crypto keeps "no cryptography library" with one narrow
  exception, held in scripts/share-crypto-noble.mjs and tested on 38 made-up
  cases: exactly @noble/hashes 2.4.0 in dependencies, one copy in the
  lockfile with no dependencies, one importer (src/model/workspace.ts), one
  path ('@noble/hashes/sha2.js'), one name (sha256); no other product file,
  unit test, end-to-end spec or script may import it; and
  src/model/shareProtected.ts may not reach it, the workspace or revision
  hashes, directly or through another module, nor name those functions. The
  Web-Crypto-only checks on protected links are unchanged.
- The specs' requirement of a bundled pure-JS SHA-256 is still met, so they
  are unchanged.
- Declared internal; the version stays 0.17.1 and there is no release note.

Bundle: +3,731 bytes of JavaScript (+1,444 gzipped) in the web and PWA
builds, +3,847 bytes in the portable file. Bundled modules of the package:
sha2.js, _md.js, _u64.js and utils.js.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying cozy-loop-studio with  Cloudflare Pages  Cloudflare Pages

Latest commit: c463c62
Status: ✅  Deploy successful!
Preview URL: https://d6d5613a.cozy-loop-studio.pages.dev
Branch Preview URL: https://chore-sha256-noble.cozy-loop-studio.pages.dev

View logs

@MerciHanrim
MerciHanrim merged commit 1ec21ee into main Oct 4, 2026
10 checks passed
@MerciHanrim
MerciHanrim deleted the chore/sha256-noble branch October 4, 2026 13:08
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