The device seal as a component (polymorph:webcrypto) - #144
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.
…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.
|
Follow-up commit: the |
|
Obviated by the reboot (#149 onward). The seal now lives in the kernel crate ( |
Pull request was closed
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.