From be87f7c3e29ae0e6189fa3f285fcaea9dec8d9ef Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Sat, 1 Aug 2026 14:21:15 -0400 Subject: [PATCH 1/2] Cover the SHA-1 collision policy through the shim; bump componentize-js past its record-field reversal The componentize-demo guest gains a sha1-collision-policy check: honest input digests to standard SHA-1 in both postures, attacked input (the SHAttered five-block prefix, shared with the Rust conformance probe) yields the deterministic sha1dc safe hash under the default mitigating posture and throws OperationError under setSha1CollisionPolicy("reject"), with the ("lann:webcrypto", "collision-detected") pair asserted on the DOMException's cause; an invalid policy name is a TypeError. This is the first JS-side exercise of mapWitError's extension branch and its known-pair table. The cause-pair assertion exposed a field permutation in the pinned componentize-js: its record lift/lower traversed the value stack without the reversal the tuple paths apply, so origin and message arrived swapped and the known-pair lookup always fell through to the unknown-pair fallback. Upstream fixed it one commit past the pin (dicej/componentize-js bdd7c3d); the rev bump takes exactly that two-line fix. Toolchain digests are refreshed once the toolchain workflow publishes the new build. Fixes #184 --- examples/componentize-demo/app.js | 71 ++++++++++++++++++++++++++++- js/componentize/componentize-js.rev | 2 +- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/examples/componentize-demo/app.js b/examples/componentize-demo/app.js index b5f418aa..98a377d8 100644 --- a/examples/componentize-demo/app.js +++ b/examples/componentize-demo/app.js @@ -8,7 +8,13 @@ // which the justfile recipe sets to the repository root — hence the // root-relative library path below. -import { crypto, subtle, CryptoKey, DOMException } from "./js/componentize/webcrypto.js"; +import { + crypto, + subtle, + CryptoKey, + DOMException, + setSha1CollisionPolicy, +} from "./js/componentize/webcrypto.js"; const encoder = new TextEncoder(); @@ -31,6 +37,24 @@ const GCM_CIPHERTEXT = "8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662"; const GCM_TAG = "76fc6ece0f4e1768cddf8853bb2d551b"; +// The first SHAttered message's five-block prefix: the shortest input on +// which sha1dc's collision detection fires (shared with the Rust +// conformance probe, which pins the same answers cross-target). +const SHATTERED_PREFIX = + "255044462d312e330a25e2e3cfd30a0a0a312030206f626a0a3c3c2f57696474" + + "682032203020522f4865696768742033203020522f547970652034203020522f" + + "537562747970652035203020522f46696c7465722036203020522f436f6c6f72" + + "53706163652037203020522f4c656e6774682038203020522f42697473506572" + + "436f6d706f6e656e7420383e3e0a73747265616d0affd8fffe00245348412d31" + + "20697320646561642121212121852fec092339759c39b1a1c63c4c97e1fffe01" + + "7346dc9166b67e118f029ab621b2560ff9ca67cca8c7f85ba84c79030c2b3de2" + + "18f86db3a90901d5df45c14f26fedfb3dc38e96ac22fe7bd728f0e45bce046d2" + + "3c570feb141398bb552ef5a0a82be331fea48037b8b5d71f0e332edf93ac3500" + + "eb4ddc0decc1a864790c782c76215660dd309791d06bd0af3f98cda4bc4629b1"; +// Its deterministic sha1dc safe hash, and the FIPS 180-1 SHA-1 of "abc". +const SHATTERED_SAFE_HASH = "7117b3cb9225aaf0d8ef1a40e493957b0bf8693d"; +const ABC_SHA1 = "a9993e364706816aba3e25717850c26c9cd0d89d"; + // --- small helpers ------------------------------------------------------------ function hex(bytes) { @@ -367,6 +391,50 @@ async function getRandomValuesCheck() { ); } +/** + * `digest("SHA-1")` and the collision policy, end to end: honest input + * digests to standard SHA-1 in both postures; input carrying a collision + * attack yields the deterministic sha1dc safe hash under the default + * mitigating posture, and under `setSha1CollisionPolicy("reject")` throws + * `OperationError` — the shim's mapping of the package's + * `("lann:webcrypto", "collision-detected")` extension condition, whose + * pair rides in the `DOMException`'s `cause`. + */ +async function sha1CollisionPolicy() { + const attacked = unhex(SHATTERED_PREFIX); + try { + // The default posture: mitigate. + expectEq(hex(await subtle.digest("SHA-1", encoder.encode("abc"))), ABC_SHA1, "mitigate: abc"); + expectEq(hex(await subtle.digest("SHA-1", attacked)), SHATTERED_SAFE_HASH, "mitigate: attacked input safe hash"); + + setSha1CollisionPolicy("reject"); + expectEq(hex(await subtle.digest("SHA-1", encoder.encode("abc"))), ABC_SHA1, "reject: abc"); + let thrown; + try { + await subtle.digest("SHA-1", attacked); + } catch (e) { + thrown = e; + } + if (!(thrown instanceof DOMException) || thrown.name !== "OperationError") { + throw new Error(`reject: attacked input: expected OperationError, got ${thrown ?? "success"}`); + } + const ext = thrown.cause?.val; + expectEq(ext?.origin, "lann:webcrypto", "reject: cause origin"); + expectEq(ext?.name, "collision-detected", "reject: cause condition name"); + } finally { + setSha1CollisionPolicy("mitigate"); + } + let badPolicy; + try { + setSha1CollisionPolicy("plain"); + } catch (e) { + badPolicy = e; + } + if (!(badPolicy instanceof TypeError)) { + throw new Error(`invalid policy: expected TypeError, got ${badPolicy ?? "success"}`); + } +} + const CHECKS = [ ["hmac-known-answer", hmacKnownAnswer], ["hmac-verify", hmacVerify], @@ -381,6 +449,7 @@ const CHECKS = [ ["jwk-rejects-malformed", jwkRejectsMalformed], ["ed25519-sign-verify", ed25519SignVerify], ["get-random-values", getRandomValuesCheck], + ["sha1-collision-policy", sha1CollisionPolicy], ]; // The `demo:webcrypto-demo/demo@0.1.0` export. `run` returns the ok summary diff --git a/js/componentize/componentize-js.rev b/js/componentize/componentize-js.rev index 70d93ef7..fcd1f886 100644 --- a/js/componentize/componentize-js.rev +++ b/js/componentize/componentize-js.rev @@ -1 +1 @@ -eed6d46b4741ad523592c4e3e044dab855386e91 +bdd7c3d50e064d9c820218dbeb98be8bcae96875 From bb8e413dc310d3e062e3c73ffff36b6db398900e Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Sat, 1 Aug 2026 14:39:38 -0400 Subject: [PATCH 2/2] Trust the bdd7c3d toolchain builds Digests recorded by update-toolchain-digest for both platforms, with build provenance verified against the componentize-js-toolchain workflow run for this branch. --- js/componentize/componentize-js.sha256 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/componentize/componentize-js.sha256 b/js/componentize/componentize-js.sha256 index a2ac6436..32c9a93b 100644 --- a/js/componentize/componentize-js.sha256 +++ b/js/componentize/componentize-js.sha256 @@ -15,5 +15,5 @@ # Regenerate with `just update-toolchain-digest` when the revision changes: # it verifies the attestation before recording, so adding a line here is the # deliberate, reviewable act of trusting a new binary. -linux-x86_64 9954f2a66d5165ca86c425c297d6b7b98f8760c5e97cf910b9e0fc3afd23121d bbf7be7397518def75f030821d759f15765bd13cdb20416e009c1e68bef64dc7 -linux-aarch64 8ce76bd96feaf68fb2439f7b49d631adba7cdfc930442baf2eea77142d6d74c8 fe8b00ff3ca78ea99563bfdc1a7371fc7b839ede3070a7c9437d2b3858989ae1 +linux-aarch64 043b3695a82fef4114454fa3e7195d8bfcaf704ac8e3880ba53e27526d2620ee e64592d1009bf7eed52d2d3efe33420c5c4c8d323bd9c3062db4fffe75cb713c +linux-x86_64 9a38c0a6b82f0bab9ac63cfc65d3a4218f65c96c68fe43998e0053762d1697cd 69b9b79f422abf26b0836a5ddd653ce751c433adf944d465b71f89cca31ff927