Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions js/polyengine/src/ecdsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
} from "./signature.ts";
import { requireEcdsaJwkAlg, requireEcJwkCurve, requireOnCurveSec1, requireOnCurveSpki } from "./ec.ts";
import { consumeUnwrapInput, type UnwrapInput } from "./wrapping.ts";
import { MINT } from "./internal.ts";
import { unwrappedJwk } from "./util.ts";

const subtle = globalThis.crypto.subtle;
Expand Down Expand Up @@ -73,7 +74,7 @@ export const ecdsaVerify = {
true,
["verify"],
);
return new VerifyingKey(key, entry);
return new VerifyingKey(MINT, key, entry);
},

importVerifyingKeySpki: async (variant: string, spki: Uint8Array): Promise<VerifyingKey> => {
Expand All @@ -88,7 +89,7 @@ export const ecdsaVerify = {
true,
["verify"],
);
return new VerifyingKey(key, entry);
return new VerifyingKey(MINT, key, entry);
},

importVerifyingKeyJwk: async (variant: string, jwkText: string): Promise<VerifyingKey> => {
Expand All @@ -103,7 +104,7 @@ export const ecdsaVerify = {
true,
["verify"],
);
return new VerifyingKey(key, entry);
return new VerifyingKey(MINT, key, entry);
},
};

Expand All @@ -119,7 +120,7 @@ export const ecdsaSign = {
policy.extractable,
["sign", "verify"],
)) as CryptoKeyPair;
return [new SigningKey(pair.privateKey, entry), new VerifyingKey(pair.publicKey, entry)];
return [new SigningKey(MINT, pair.privateKey, entry), new VerifyingKey(MINT, pair.publicKey, entry)];
},

importSigningKeyPkcs8: async (
Expand All @@ -138,7 +139,7 @@ export const ecdsaSign = {
policy.extractable,
["sign"],
);
return new SigningKey(key, entry);
return new SigningKey(MINT, key, entry);
},

importSigningKeyJwk: async (variant: string, jwkText: string, options: SigningKeyOptions): Promise<SigningKey> => {
Expand All @@ -157,7 +158,7 @@ export const ecdsaSign = {
["sign"],
);
if (key.type !== "private") errInvalidKey("EC private JWK must carry `d` (base64url private scalar)");
return new SigningKey(key, entry);
return new SigningKey(MINT, key, entry);
},

unwrapSigningKeyPkcs8: (variant: string, input: UnwrapInput, options: SigningKeyOptions): Promise<SigningKey> => {
Expand Down
87 changes: 85 additions & 2 deletions js/polyengine/src/hkdf.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// `polymorph:webcrypto/hkdf` + `hkdf-sha2` + `hkdf-sha1` — wit/hkdf.wit.

import { errOther, errUnsupported, notPermitted, platformCall } from "./errors.ts";
import { errInvalidKey, errNotPermitted, errOther, errUnsupported, notPermitted, platformCall } from "./errors.ts";
import {
DeriveInput,
type DerivePolicy,
Expand All @@ -17,14 +17,97 @@ const subtle = globalThis.crypto.subtle;

const ikmState = new WeakMap<Ikm, { key: CryptoKey; policy: DerivePolicy }>();

/** `hkdf.ikm`: input keying material, consumable only by `prepare`. */
/**
* `hkdf.ikm`: input keying material, consumable only by `prepare`.
*
* The constructor is effectively internal already — all state lives in the
* module-private `ikmState` WeakMap, so a bare `new Ikm()` yields an object
* every method refuses. The supported external construction path is
* {@link Ikm.fromCryptoKey} (polymorph-webcrypto#391).
*/
export class Ikm {
canDeriveBits(): boolean {
return ikmState.get(this)!.policy.deriveBits;
}
canDeriveKey(): boolean {
return ikmState.get(this)!.policy.deriveKey;
}

/**
* Adopt an embedder-held HKDF `CryptoKey` — the injection half of the
* persistence seam (polymorph-webcrypto#391): an embedder that keeps its
* input keying material as a NON-EXTRACTABLE `CryptoKey` in IndexedDB gets
* it back as an `ikm` here, instead of having to hold the raw bytes.
*
* Synchronous, and validating: a platform `CryptoKey`, of type `secret`,
* with `algorithm.name === "HKDF"`.
*
* The derive policy is READ OFF THE PLATFORM USAGES rather than taken from a
* `derive-options` — for an injected key the platform's `deriveBits` /
* `deriveKey` slots ARE the policy, since loading is itself a minting path
* and the platform will refuse anything the slots do not cover regardless of
* what this wrapper claimed. A key with neither derive usage is a degenerate
* injection and is refused (`not-permitted`), the same rule as an options
* resource granting nothing (derivation.ts:50-52).
*
* Validation and storage both use a LAUNDERED clone (see
* signature.ts's `launderCryptoKey` for the reasoning): `usages` and
* `algorithm` are shadowable own-property accessors on the caller's object,
* and structured clone carries only the internal slots, so `canDeriveBits()`
* answers platform truth and no caller retains a handle to the key this
* `ikm` derives with.
*/
static fromCryptoKey(key: CryptoKey): Ikm {
const what = "ikm injection";
if (!(key instanceof CryptoKey)) errInvalidKey(`${what} takes a platform CryptoKey`);
let clone: CryptoKey;
try {
clone = structuredClone(key);
} catch {
errUnsupported(
`${what}: this host does not serialize CryptoKey (structured clone), which key injection requires`,
);
}
if (clone.type !== "secret") {
errInvalidKey(`${what} takes a secret key, got a ${clone.type} key`);
}
if (clone.algorithm.name !== "HKDF") {
errInvalidKey(`${what} takes an HKDF key, got ${clone.algorithm.name}`);
}
const policy: DerivePolicy = {
deriveBits: clone.usages.includes("deriveBits"),
deriveKey: clone.usages.includes("deriveKey"),
};
if (!policy.deriveBits && !policy.deriveKey) {
errNotPermitted("an ikm permitting neither derive-bits nor derive-key cannot be injected");
}
return mintIkm(clone, policy);
}

/**
* Hand back the platform key — the extraction half of the persistence seam
* (polymorph-webcrypto#391). The returned `CryptoKey` is structured-clonable
* into IndexedDB with its non-extractability preserved, which is how keying
* material is meant to outlive a session.
*
* Security framing, as on `signing-key`: material confidentiality belongs to
* the `extractable` bit and stays platform-enforced in both directions —
* `hkdf.import-ikm` mints non-extractable and this hands back a key, not
* bytes. What the wrapper scopes is the USE capability in durable,
* parameter-free form: a raw HKDF `CryptoKey` derives under any salt/info/
* hash its holder picks, whereas an `ikm` is consumable only through
* `prepare` under the policy above. Returning a FRESH CLONE per call keeps
* the wrapper's own key unreachable, so that scoping is total.
*
* Inverse of {@link Ikm.fromCryptoKey}: the returned key satisfies that
* validation by construction (the derive policy round-trips through the
* platform usages).
*/
toCryptoKey(): CryptoKey {
const state = ikmState.get(this);
if (state === undefined) errOther("ikm minted by another provider");
return structuredClone(state.key);
}
}

function mintIkm(key: CryptoKey, policy: DerivePolicy): Ikm {
Expand Down
15 changes: 15 additions & 0 deletions js/polyengine/src/internal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Module-private construction token for the resource classes whose
// constructors are runtime-internal (polymorph-webcrypto#391).
//
// This module is deliberately NOT re-exported from mod.ts: `deno.json`'s
// single `exports` entry (`./src/mod.ts`) is what a consumer can reach, so
// keeping `MINT` out of mod.ts makes the token unforgeable from outside the
// package rather than merely undocumented.
//
// `Symbol()` and not `Symbol.for()`: a registry symbol is reachable by name
// from any realm-sharing code, which would hand the token to exactly the
// callers the constructor guard exists to refuse. Module-private IDENTITY is
// the whole mechanism.

/** The witness that a `signing-key`/`verifying-key` came out of a minting interface in this package. */
export const MINT: unique symbol = Symbol("polymorph:webcrypto mint");
11 changes: 6 additions & 5 deletions js/polyengine/src/rsaSignature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
VerifyingKey,
} from "./signature.ts";
import { consumeUnwrapInput, type UnwrapInput } from "./wrapping.ts";
import { MINT } from "./internal.ts";
import { unwrappedJwk } from "./util.ts";

const subtle = globalThis.crypto.subtle;
Expand Down Expand Up @@ -137,7 +138,7 @@ async function importRsaVerifyingKeySpki(
requireRsaEncryptionSpki(spki);
const key = await importPlatformKey(`${name} spki`, "spki", spki, { name, hash }, true, ["verify"]);
const modulusLength = rsaAdmittedModulusLength(key, `${name} spki`);
return new VerifyingKey(key, rsaAlgorithm(name, hash, modulusLength, saltLength));
return new VerifyingKey(MINT, key, rsaAlgorithm(name, hash, modulusLength, saltLength));
}

/**
Expand Down Expand Up @@ -176,7 +177,7 @@ async function importRsaVerifyingKeyJwk(
requireStrictBase64url(jwk.e);
const key = await importPlatformKeyJwk(`${name} public JWK`, jwk, { name, hash }, true, ["verify"]);
const modulusLength = rsaAdmittedModulusLength(key, `${name} public JWK`);
return new VerifyingKey(key, rsaAlgorithm(name, hash, modulusLength, saltLength));
return new VerifyingKey(MINT, key, rsaAlgorithm(name, hash, modulusLength, saltLength));
}

/** The `polymorph:webcrypto/rsassa-pkcs1-v15-verify@0.1.0` interface. */
Expand Down Expand Up @@ -213,7 +214,7 @@ async function generateRsaSigningKey(
["sign", "verify"],
)) as CryptoKeyPair;
const algorithm = rsaSigningAlgorithm(name, entry, modulusLength);
return [new SigningKey(pair.privateKey, algorithm), new VerifyingKey(pair.publicKey, algorithm)];
return [new SigningKey(MINT, pair.privateKey, algorithm), new VerifyingKey(MINT, pair.publicKey, algorithm)];
}

async function importRsaSigningKeyPkcs8(
Expand All @@ -235,7 +236,7 @@ async function importRsaSigningKeyPkcs8(
["sign"],
);
const modulusLength = rsaAdmittedModulusLength(key, `${name} pkcs8`, RSA_SIGNING_MIN_BITS, RSA_SIGNING_MAX_BITS);
return new SigningKey(key, rsaSigningAlgorithm(name, entry, modulusLength));
return new SigningKey(MINT, key, rsaSigningAlgorithm(name, entry, modulusLength));
}

async function importRsaSigningKeyJwk(
Expand Down Expand Up @@ -267,7 +268,7 @@ async function importRsaSigningKeyJwk(
RSA_SIGNING_MIN_BITS,
RSA_SIGNING_MAX_BITS,
);
return new SigningKey(key, rsaSigningAlgorithm(name, entry, modulusLength));
return new SigningKey(MINT, key, rsaSigningAlgorithm(name, entry, modulusLength));
}

/** The minting-object shape returned by `rsaSigningInterface` for one RSA signing scheme. */
Expand Down
Loading
Loading