Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix NotSupportedError when using NODE-ED25519 #311

Merged
merged 2 commits into from
Aug 19, 2022
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
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"devDependencies": {
"@ava/typescript": "^2.0.0",
"@microsoft/api-extractor": "^7.19.4",
"@types/node": "^17.0.35",
"@types/node": "^18.7.6",
"@types/rimraf": "^3.0.2",
"@types/which": "^2.0.1",
"@typescript-eslint/eslint-plugin": "^5.9.1",
Expand All @@ -53,6 +53,6 @@
"node": ">=16.7"
},
"volta": {
"node": "18.2.0"
"node": "18.7.0"
}
}
125 changes: 109 additions & 16 deletions packages/core/src/standards/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const supportedDigests = ["sha-1", "sha-256", "sha-384", "sha-512", "md5"];
export class DigestStream extends WritableStream<BufferSource> {
readonly digest: Promise<ArrayBuffer>;

constructor(algorithm: AlgorithmIdentifier) {
constructor(algorithm: webcrypto.AlgorithmIdentifier) {
// Check algorithm supported by Cloudflare Workers
let name = typeof algorithm === "string" ? algorithm : algorithm?.name;
if (!(name && supportedDigests.includes(name.toLowerCase()))) {
Expand Down Expand Up @@ -47,11 +47,40 @@ export class DigestStream extends WritableStream<BufferSource> {
}
}

// Workers support non-standard MD5 digests
function digest(
algorithm: AlgorithmIdentifier,
data: BufferSource
): Promise<ArrayBuffer> {
const usesModernEd25519 = (async () => {
try {
// Modern versions of Node.js expect `Ed25519` instead of `NODE-ED25519`.
// This will throw a `DOMException` if `NODE-ED25519` should be used
// instead. See https://github.com/nodejs/node/pull/42507.
await webcrypto.subtle.generateKey(
{ name: "Ed25519", namedCurve: "Ed25519" },
false,
["sign", "verify"]
);
return true;
} catch {
return false;
}
})();

async function ensureValidAlgorithm(
algorithm: webcrypto.AlgorithmIdentifier | webcrypto.EcKeyAlgorithm
): Promise<webcrypto.AlgorithmIdentifier | webcrypto.EcKeyAlgorithm> {
if (
typeof algorithm === "object" &&
algorithm.name === "NODE-ED25519" &&
"namedCurve" in algorithm &&
algorithm.namedCurve === "NODE-ED25519" &&
(await usesModernEd25519)
) {
return { name: "Ed25519", namedCurve: "Ed25519" };
}
return algorithm;
}

// Workers support non-standard MD5 digests, see
// https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms
const digest: typeof webcrypto.subtle.digest = function (algorithm, data) {
const name = typeof algorithm === "string" ? algorithm : algorithm?.name;
if (name?.toLowerCase() == "md5") {
if (data instanceof ArrayBuffer) data = new Uint8Array(data);
Expand All @@ -61,32 +90,96 @@ function digest(

// If the algorithm isn't MD5, defer to the original function
return webcrypto.subtle.digest(algorithm, data);
}
};

export function createCrypto(blockGlobalRandom = false): typeof webcrypto {
const getRandomValues = assertsInRequest(
webcrypto.getRandomValues.bind(webcrypto),
blockGlobalRandom
// Workers support the NODE-ED25519 algorithm, unlike modern Node versions, see
// https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms
const generateKey: typeof webcrypto.subtle.generateKey = async function (
algorithm,
extractable,
keyUsages
) {
algorithm = await ensureValidAlgorithm(algorithm);
// @ts-expect-error TypeScript cannot infer the correct overload here
return webcrypto.subtle.generateKey(algorithm, extractable, keyUsages);
};
const importKey: typeof webcrypto.subtle.importKey = async function (
format,
keyData,
algorithm,
extractable,
keyUsages
) {
// Cloudflare Workers only allow importing *public* raw Ed25519 keys, see
// https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms
const forcePublic =
format === "raw" &&
typeof algorithm === "object" &&
algorithm.name === "NODE-ED25519" &&
"namedCurve" in algorithm &&
algorithm.namedCurve === "NODE-ED25519";

algorithm = await ensureValidAlgorithm(algorithm);

// @ts-expect-error `public` isn't included in the definitions, but required
// for marking `keyData` as public key material
if (forcePublic) algorithm.public = true;

return webcrypto.subtle.importKey(
// @ts-expect-error TypeScript cannot infer the correct overload here
format,
keyData,
algorithm,
extractable,
keyUsages
);
const generateKey = assertsInRequest(
webcrypto.subtle.generateKey.bind(webcrypto.subtle),
};
const sign: typeof webcrypto.subtle.sign = async function (
algorithm,
key,
data
) {
algorithm = await ensureValidAlgorithm(algorithm);
return webcrypto.subtle.sign(algorithm, key, data);
};
const verify: typeof webcrypto.subtle.verify = async function (
algorithm,
key,
signature,
data
) {
algorithm = await ensureValidAlgorithm(algorithm);
return webcrypto.subtle.verify(algorithm, key, signature, data);
};

export type WorkerCrypto = typeof webcrypto & {
DigestStream: typeof DigestStream;
};

export function createCrypto(blockGlobalRandom = false): WorkerCrypto {
const assertingGetRandomValues = assertsInRequest(
webcrypto.getRandomValues.bind(webcrypto),
blockGlobalRandom
);
const assertingGenerateKey = assertsInRequest(generateKey, blockGlobalRandom);

const subtle = new Proxy(webcrypto.subtle, {
get(target, propertyKey, receiver) {
if (propertyKey === "digest") return digest;
if (propertyKey === "generateKey") return generateKey;
if (propertyKey === "generateKey") return assertingGenerateKey;
if (propertyKey === "importKey") return importKey;
mrbbot marked this conversation as resolved.
Show resolved Hide resolved
if (propertyKey === "sign") return sign;
if (propertyKey === "verify") return verify;

let result = Reflect.get(target, propertyKey, receiver);
if (typeof result === "function") result = result.bind(webcrypto.subtle);
return result;
},
});

return new Proxy(webcrypto, {
return new Proxy(webcrypto as WorkerCrypto, {
get(target, propertyKey, receiver) {
if (propertyKey === "getRandomValues") return getRandomValues;
if (propertyKey === "getRandomValues") return assertingGetRandomValues;
if (propertyKey === "subtle") return subtle;
if (propertyKey === "DigestStream") return DigestStream;

Expand Down
101 changes: 90 additions & 11 deletions packages/core/test/standards/crypto.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { webcrypto } from "crypto";
import { TextEncoder } from "util";
import { DOMException, DigestStream, createCrypto } from "@miniflare/core";
import { utf8Encode } from "@miniflare/shared-test";
import test, { Macro } from "ava";

const crypto = createCrypto();

const digestStreamMacro: Macro<[AlgorithmIdentifier]> = async (
const digestStreamMacro: Macro<[webcrypto.AlgorithmIdentifier]> = async (
t,
algorithm
) => {
Expand Down Expand Up @@ -69,6 +70,7 @@ test("crypto: provides DigestStream", (t) => {
t.is(crypto.DigestStream, DigestStream);
});

// Check digest function modified to add MD5 support
const md5Macro: Macro<[BufferSource]> = async (t, data) => {
const digest = await crypto.subtle.digest("md5", data);
t.is(Buffer.from(digest).toString("hex"), "098f6bcd4621d373cade4e832627b4f6");
Expand All @@ -90,6 +92,93 @@ test("crypto: computes other digest", async (t) => {
);
});

// Check generateKey, importKey, sing, verify functions modified to add
// NODE-ED25519 support
test("crypto: generateKey/exportKey: supports NODE-ED25519 algorithm", async (t) => {
const keyPair = await crypto.subtle.generateKey(
{ name: "NODE-ED25519", namedCurve: "NODE-ED25519" },
true,
["sign", "verify"]
);
const exported = await crypto.subtle.exportKey("raw", keyPair.publicKey);
t.is(exported.byteLength, 32);
});
test("crypto: generateKey/exportKey: supports other algorithms", async (t) => {
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
const exported = await crypto.subtle.exportKey("raw", key);
t.is(exported.byteLength, 32);
});

test("crypto: importKey/exportKey: supports NODE-ED25519 public keys", async (t) => {
const keyData =
"953e73cb91a2494a33cd7180f05d5bbe6b5ca43cc66eb93ca38c6fc83cb18f29";
const publicKey = await crypto.subtle.importKey(
"raw",
Buffer.from(keyData, "hex"),
{ name: "NODE-ED25519", namedCurve: "NODE-ED25519" },
true,
["verify"]
);
const exported = await crypto.subtle.exportKey("raw", publicKey);
t.is(Buffer.from(exported).toString("hex"), keyData);
});
test("crypto: importKey: fails for NODE-ED25519 private keys", async (t) => {
const keyData =
"f0d3c325a99ef50181faa238e07224ec9fee292e7ebf6585560bab64654ec6209c6afa31187898a43f7ab18c3552c2cd349e912c16c803a2a6ccbd546896fe8e";
await t.throwsAsync(
crypto.subtle.importKey(
"raw",
Buffer.from(keyData, "hex"),
{ name: "NODE-ED25519", namedCurve: "NODE-ED25519" },
false,
["sign"]
)
);
});
test("crypto: importKey/exportKey: supports other algorithms", async (t) => {
const keyData =
"464d832870721bcf28649192bec41bd1fd5b32702d6168f21b8585fb566a4be7";
const key = await crypto.subtle.importKey(
"raw",
Buffer.from(keyData, "hex"),
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
const exported = await crypto.subtle.exportKey("raw", key);
t.is(Buffer.from(exported).toString("hex"), keyData);
});
test("crypto: sign/verify: supports NODE-ED25519 algorithm", async (t) => {
const algorithm: webcrypto.EcKeyAlgorithm = {
name: "NODE-ED25519",
namedCurve: "NODE-ED25519",
};
const { privateKey, publicKey } = await crypto.subtle.generateKey(
algorithm,
false,
["sign", "verify"]
);
const data = utf8Encode("data");
const signature = await crypto.subtle.sign(algorithm, privateKey, data);
t.is(signature.byteLength, 64);
t.true(await crypto.subtle.verify(algorithm, publicKey, signature, data));
});
test("crypto: sign/verify: supports other algorithm", async (t) => {
const key = await crypto.subtle.generateKey(
{ name: "HMAC", hash: "SHA-256" },
false,
["sign", "verify"]
);
const data = utf8Encode("data");
const signature = await crypto.subtle.sign("HMAC", key, data);
t.is(signature.byteLength, 32);
t.true(await crypto.subtle.verify("HMAC", key, signature, data));
});

// Checking other functions aren't broken by proxy...

test("crypto: gets random values", (t) => {
Expand All @@ -102,13 +191,3 @@ test("crypto: gets random values", (t) => {
test("crypto: generates random UUID", (t) => {
t.is(crypto.randomUUID().length, 36);
});

test("crypto: generates aes key", async (t) => {
const key = await crypto.subtle.generateKey(
{ name: "aes-gcm", length: 256 },
true,
["encrypt", "decrypt"]
);
const exported = await crypto.subtle.exportKey("raw", key);
t.is(exported.byteLength, 32);
});
55 changes: 0 additions & 55 deletions types/crypto.d.ts

This file was deleted.

Loading