polyengine host: fromCryptoKey/toCryptoKey — the embedder key seams (#391) - #392
Merged
Conversation
…391) Injection and extraction of host-held CryptoKeys, so an embedder can persist NON-EXTRACTABLE keys (structured clone into IndexedDB) instead of being forced into an extractable-material posture. v1 scope is the two consumers on the table: Ed25519 signing pairs (device identity) and HKDF ikm (passkey-PRF roots); every other family refuses with the reason (mint bindings the platform key does not carry). Both directions cross by value, never by reference: fromCryptoKey validates and stores a structured clone (laundering shadowed accessors back to platform-verified slots), toCryptoKey returns a fresh clone per call. The module invariant that buys: the CryptoKey behind a live wrapper is reachable by no code outside the module, and every policy answer is computed from the mint-bound algorithm record or laundered platform slots. Compat breaks, deliberate: SigningKey/VerifyingKey constructors are now token-gated runtime-internal (the algorithm record they bind is the file-header security authority; MINT lives in src/internal.ts, not re-exported, so the deno.json exports gate makes it unforgeable), and VerifyingKey's bare cryptoKey getter is gone in favor of the clone-out method (it had no users). Gates: deno task check + test (27/27, ten new #391 cases including launder/clone-out/provenance and a platform-agreement KAT for injected ikm); conformance-ct::run-polyengine byte-identical to the pre-change baseline (re-run under stash to prove the constructor change moved nothing).
This was referenced Aug 22, 2026
Merged
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.
Closes #391.
Embedder-side injection and extraction of host-held
CryptoKeys in the polyengine host — the seams that let an embedder persist non-extractable keys (WebCrypto §13 structured clone into IndexedDB) instead of holding material in extractable form. No WIT change, no guest-visible change: this is host-implementation API, per the sibling-package rule.Surface (v1 = the two consumers on the table — Ed25519 signing pairs for device identity, HKDF ikm for passkey-PRF roots; other families refuse with the reason: their mint bindings aren't carried by the platform key):
Mechanics
fromCryptoKeyvalidates and stores astructuredClone(laundering — clone carries internal slots only, soObject.definePropertyshadowing ofusages/algorithm/typeon the argument can't talk a key past validation or lie to the policy mirrors);toCryptoKeyreturns a fresh clone per call. Module invariant, stated in signature.ts: theCryptoKeybehind a live wrapper is reachable by no code outside the module.Ikm's derive policy is read off the platform usages — for an injected key the slots are the policy (load is a minting path).invalid-key), wrong family (unsupported, naming the missing-bindings reason), usage-less keys (not-permitted, mirroring the untouched-options mint rule).Compat breaks (deliberate, discussed with @lann)
SigningKey/VerifyingKeyconstructors are token-gated runtime-internal: the algorithm record they bind is the file-header security authority, so construction now goes only through minting interfaces orfromCryptoKey. The token (src/internal.ts) is not re-exported, so thedeno.jsonexports gate makes it unforgeable from outside the package.VerifyingKey's barecryptoKeygetter is removed in favor of the clone-out method (zero users;keyAgreement.PublicKeyuntouched).Gates
deno task checkclean;deno task test27/27 (ten new391:cases: round trip, refusal matrix, launder, clone-out isolation, material identity via subtle-sign/wrapper-verify, constructor provenance, and an injected-ikm derivation KAT againstsubtle.deriveBits).just conformance-ct::run-polyengine: byte-identical pass/fail counts to the pre-change baseline (re-run undergit stash -uto prove the constructor-arity change moved nothing).One platform note recorded in the tests: a conforming WebCrypto refuses to mint usage-less private/secret keys (
SyntaxErrorat bothgenerateKeyandimportKey), so thenot-permittedrefusals are defence in depth — the tests pin the platform's refusal and will trip if an engine ever starts minting such keys.