The device seal as a component (polymorph:webcrypto) - #144
Open
lannbot wants to merge 2 commits into
Open
Conversation
…ver polymorph:webcrypto
WHAT MOVED. runtime/device-store/seal.ts (837), identity-keys.ts (234)
and the AES-GCM half of sealed-fs.ts are gone; their rules live in
runtime/device-seal/, a wasm32-wasip2 component (polyvisor:device-seal
@0.1.0, contract wit/world.wit) that reaches WebCrypto through the
polymorph:webcrypto guest bindings and the same @polymorph/webcrypto
host module the engine already instantiates with. The worker holds a
component and asks it to seal and open bytes; seal-component.ts is the
adapter, seal-records.ts the record shapes plus the one keyless reader
(getPrfEnrollment), and the namespace import is a field-mapping codec
with no decisions in it.
WHAT IT BUYS. The unsealed DEK exists nowhere in JavaScript — a
resource in the component, not a CryptoKey variable a grep guards. The
component's reach is its imports: five record kinds and four key slots
of ONE device namespace, closed over at instantiation. The ladder's
rules ("absent origin means generated", the PMSEALv1 layout, every
refusal's code and sentence) are 31 native cargo tests instead of only
browser rows. Persisted handles cross through polymorph-webcrypto#391's
fromCryptoKey/toCryptoKey seam, so non-extractability is still the
platform's flag.
WHAT DID NOT CHANGE. The on-disk format, by requirement:
tests/devstore/fixtures/legacy-seal-v1.json is a device sealed by the
pre-component seal.ts, and matrix row 3b opens it through the component
every run. The PRF derivation stays on the page (PERSISTENCE.md's
ruling); the KEK enters as a non-extractable kw-key. Validate-on-load
for identity stays host-side — the add-if-absent transaction must apply
it — and the component re-checks extractable() so a codec bug fails
loud (row 5b plants an extractable pair).
FOUND ON THE WAY. My first WIT said fromCryptoKey's refusals were
validate-on-load; they do not check extractable (row 5b caught it).
The AAD is the 8-byte magic, not the header (sealed-fs.ts:127). And the
refusal SENTENCE is copy the visor renders: seal-error is now
{code, message} with seal.ts's eleven sentences ported word for word,
because solo-persistence asserts one of them — the runtime matrix could
not see that, the demo e2e could.
Gates: runtime matrix 74 PASS / 0 FAIL; cargo test 31; demo e2e 35/36,
the one failure (firefox-smoke) reproducing identically on main's own
latest CI run; check-invariants clean; worker bundle +2.9 KB; artifact
282,520 raw / 80,739 gzipped.
lannbot
enabled auto-merge
September 5, 2026 16:50
…ync export firefox-smoke caught it and a minimal page probe isolated it: with polyengine's auto-detected suspension mode the seal component's first `async func` export kills the Firefox content process; the identical sequence passes with `jspi: false`. The component never needs a suspended frame — every import it calls is an `async func` on the component-model async ABI — but wit-bindgen's sync-form cancel built-ins for the drop-a-pending-future path classify as block-capable and tip auto-detection into jspi, which promising-wraps every export. Under Gecko's pref-gated JSPI that is fatal. Plain mode is what the component actually is; a wrongly-sync import would be refused loudly (NeedsJspi), never degraded. Gates: runtime matrix 74/0; firefox-smoke + cross-engine-pairing green; full demo e2e 35/36 with store-outage-recovery flaking once and passing alone (the known timing-sensitive one). I had earlier read the identical firefox-smoke failure on main's own CI as pre-existing; a clean local main passes it, so that read was wrong and this commit is the fix.
Collaborator
Author
|
Follow-up commit: the |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The per-device seal — the DEK, the KEK ladder (passphrase / until-reseal / passkey rungs), PMSEALv1 per-file sealing, and the device's Ed25519 signing handles — moves out of TypeScript into a wasm component,
runtime/device-seal/(polyvisor:device-seal@0.1.0), reaching WebCrypto throughpolymorph:webcrypto.seal.tsandidentity-keys.tsare deleted;sealed-fs.tskeeps only the OPFS proxy.What the boundary buys
localStorage.cargo test, 31) rather than only browser-observable.fromCryptoKey/toCryptoKey, so non-extractability remains the platform's own flag.What did not change
tests/devstore/fixtures/legacy-seal-v1.jsonis a device sealed by the pre-component code; row3b legacyopens it through the component every run.kw-key.seal-erroris{code, message}, seal.ts's wording ported verbatim).Gates
runtime$ just test: 74 PASS / 4 INFO / 0 FAIL (the whole matrix, plus the legacy row and a planted-extractable-identity assertion).cargo test: 31; clippy clean both targets;wasm-tools validateok.demo$ just e2e: 35/36 —firefox-smokefails with the identical symptom onmain's latest CI run (33978040646), so it is pre-existing, not this change.check-invariants.sh: clean. Worker bundle +2.9 KB. Artifact 282,520 raw / 80,739 gzipped.Two contract defects were found by the gates and fixed in the same branch: my WIT claimed
fromCryptoKeycovered validate-on-load (it doesn't checkextractable— row 5b), and the AAD is the 8-byte magic rather than the header (sealed-fs.ts:127).demo/justfileand both workflows' cargo caches now carry the new crate.Automerge is armed.