Skip to content

polyengine host: fromCryptoKey/toCryptoKey — the embedder key seams (#391) - #392

Merged
lann merged 1 commit into
mainfrom
embedder-key-seams
Aug 22, 2026
Merged

polyengine host: fromCryptoKey/toCryptoKey — the embedder key seams (#391)#392
lann merged 1 commit into
mainfrom
embedder-key-seams

Conversation

@lannbot

@lannbot lannbot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

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):

SigningKey.fromCryptoKey(key)    /  signingKey.toCryptoKey()
VerifyingKey.fromCryptoKey(key)  /  verifyingKey.toCryptoKey()
Ikm.fromCryptoKey(key)           /  ikm.toCryptoKey()

Mechanics

  • Both directions cross by value: fromCryptoKey validates and stores a structuredClone (laundering — clone carries internal slots only, so Object.defineProperty shadowing of usages/algorithm/type on the argument can't talk a key past validation or lie to the policy mirrors); toCryptoKey returns a fresh clone per call. Module invariant, stated in signature.ts: the CryptoKey behind 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).
  • Degenerate injections refuse loudly: wrong half (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/VerifyingKey constructors are token-gated runtime-internal: the algorithm record they bind is the file-header security authority, so construction now goes only through minting interfaces or fromCryptoKey. The token (src/internal.ts) is not re-exported, so the deno.json exports gate makes it unforgeable from outside the package.
  • VerifyingKey's bare cryptoKey getter is removed in favor of the clone-out method (zero users; keyAgreement.PublicKey untouched).

Gates

  • deno task check clean; deno task test 27/27 (ten new 391: cases: round trip, refusal matrix, launder, clone-out isolation, material identity via subtle-sign/wrapper-verify, constructor provenance, and an injected-ikm derivation KAT against subtle.deriveBits).
  • just conformance-ct::run-polyengine: byte-identical pass/fail counts to the pre-change baseline (re-run under git stash -u to 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 (SyntaxError at both generateKey and importKey), so the not-permitted refusals are defence in depth — the tests pin the platform's refusal and will trip if an engine ever starts minting such keys.

…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).
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.

polyengine host: embedder API to inject host-held CryptoKeys as typed key handles

2 participants