Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit adb8171

Browse files
yusefnaporamrbbot
andauthored
fix NotSupportedError when using NODE-ED25519 (#311)
* fix NotSupportedError when using NODE-ED25519 * Fixes for `NODE-ED25519` support and additional tests - Require `namedCurve` to be `NODE-ED25519` - Gate `Ed25519` usage behind feature detect - Use `Ed25519` algorithm object instead of string - Override `sign`, `verify` methods too - Do not require `importKey` to be within `RequestContext` - Automatically add `public: true` when importing `NODE-ED25519` keys - Upgrade `@types/node` for improved `webcrypto` types - Add tests for other algorithms using modified methods Co-authored-by: bcoll <[email protected]>
1 parent e99db4d commit adb8171

File tree

6 files changed

+210
-91
lines changed

6 files changed

+210
-91
lines changed

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"devDependencies": {
3232
"@ava/typescript": "^2.0.0",
3333
"@microsoft/api-extractor": "^7.19.4",
34-
"@types/node": "^17.0.35",
34+
"@types/node": "^18.7.6",
3535
"@types/rimraf": "^3.0.2",
3636
"@types/which": "^2.0.1",
3737
"@typescript-eslint/eslint-plugin": "^5.9.1",
@@ -53,6 +53,6 @@
5353
"node": ">=16.13"
5454
},
5555
"volta": {
56-
"node": "18.2.0"
56+
"node": "18.7.0"
5757
}
5858
}

packages/core/src/standards/crypto.ts

Lines changed: 109 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const supportedDigests = ["sha-1", "sha-256", "sha-384", "sha-512", "md5"];
1515
export class DigestStream extends WritableStream<BufferSource> {
1616
readonly digest: Promise<ArrayBuffer>;
1717

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

50-
// Workers support non-standard MD5 digests
51-
function digest(
52-
algorithm: AlgorithmIdentifier,
53-
data: BufferSource
54-
): Promise<ArrayBuffer> {
50+
const usesModernEd25519 = (async () => {
51+
try {
52+
// Modern versions of Node.js expect `Ed25519` instead of `NODE-ED25519`.
53+
// This will throw a `DOMException` if `NODE-ED25519` should be used
54+
// instead. See https://github.com/nodejs/node/pull/42507.
55+
await webcrypto.subtle.generateKey(
56+
{ name: "Ed25519", namedCurve: "Ed25519" },
57+
false,
58+
["sign", "verify"]
59+
);
60+
return true;
61+
} catch {
62+
return false;
63+
}
64+
})();
65+
66+
async function ensureValidAlgorithm(
67+
algorithm: webcrypto.AlgorithmIdentifier | webcrypto.EcKeyAlgorithm
68+
): Promise<webcrypto.AlgorithmIdentifier | webcrypto.EcKeyAlgorithm> {
69+
if (
70+
typeof algorithm === "object" &&
71+
algorithm.name === "NODE-ED25519" &&
72+
"namedCurve" in algorithm &&
73+
algorithm.namedCurve === "NODE-ED25519" &&
74+
(await usesModernEd25519)
75+
) {
76+
return { name: "Ed25519", namedCurve: "Ed25519" };
77+
}
78+
return algorithm;
79+
}
80+
81+
// Workers support non-standard MD5 digests, see
82+
// https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms
83+
const digest: typeof webcrypto.subtle.digest = function (algorithm, data) {
5584
const name = typeof algorithm === "string" ? algorithm : algorithm?.name;
5685
if (name?.toLowerCase() == "md5") {
5786
if (data instanceof ArrayBuffer) data = new Uint8Array(data);
@@ -61,32 +90,96 @@ function digest(
6190

6291
// If the algorithm isn't MD5, defer to the original function
6392
return webcrypto.subtle.digest(algorithm, data);
64-
}
93+
};
6594

66-
export function createCrypto(blockGlobalRandom = false): typeof webcrypto {
67-
const getRandomValues = assertsInRequest(
68-
webcrypto.getRandomValues.bind(webcrypto),
69-
blockGlobalRandom
95+
// Workers support the NODE-ED25519 algorithm, unlike modern Node versions, see
96+
// https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms
97+
const generateKey: typeof webcrypto.subtle.generateKey = async function (
98+
algorithm,
99+
extractable,
100+
keyUsages
101+
) {
102+
algorithm = await ensureValidAlgorithm(algorithm);
103+
// @ts-expect-error TypeScript cannot infer the correct overload here
104+
return webcrypto.subtle.generateKey(algorithm, extractable, keyUsages);
105+
};
106+
const importKey: typeof webcrypto.subtle.importKey = async function (
107+
format,
108+
keyData,
109+
algorithm,
110+
extractable,
111+
keyUsages
112+
) {
113+
// Cloudflare Workers only allow importing *public* raw Ed25519 keys, see
114+
// https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms
115+
const forcePublic =
116+
format === "raw" &&
117+
typeof algorithm === "object" &&
118+
algorithm.name === "NODE-ED25519" &&
119+
"namedCurve" in algorithm &&
120+
algorithm.namedCurve === "NODE-ED25519";
121+
122+
algorithm = await ensureValidAlgorithm(algorithm);
123+
124+
// @ts-expect-error `public` isn't included in the definitions, but required
125+
// for marking `keyData` as public key material
126+
if (forcePublic) algorithm.public = true;
127+
128+
return webcrypto.subtle.importKey(
129+
// @ts-expect-error TypeScript cannot infer the correct overload here
130+
format,
131+
keyData,
132+
algorithm,
133+
extractable,
134+
keyUsages
70135
);
71-
const generateKey = assertsInRequest(
72-
webcrypto.subtle.generateKey.bind(webcrypto.subtle),
136+
};
137+
const sign: typeof webcrypto.subtle.sign = async function (
138+
algorithm,
139+
key,
140+
data
141+
) {
142+
algorithm = await ensureValidAlgorithm(algorithm);
143+
return webcrypto.subtle.sign(algorithm, key, data);
144+
};
145+
const verify: typeof webcrypto.subtle.verify = async function (
146+
algorithm,
147+
key,
148+
signature,
149+
data
150+
) {
151+
algorithm = await ensureValidAlgorithm(algorithm);
152+
return webcrypto.subtle.verify(algorithm, key, signature, data);
153+
};
154+
155+
export type WorkerCrypto = typeof webcrypto & {
156+
DigestStream: typeof DigestStream;
157+
};
158+
159+
export function createCrypto(blockGlobalRandom = false): WorkerCrypto {
160+
const assertingGetRandomValues = assertsInRequest(
161+
webcrypto.getRandomValues.bind(webcrypto),
73162
blockGlobalRandom
74163
);
164+
const assertingGenerateKey = assertsInRequest(generateKey, blockGlobalRandom);
75165

76166
const subtle = new Proxy(webcrypto.subtle, {
77167
get(target, propertyKey, receiver) {
78168
if (propertyKey === "digest") return digest;
79-
if (propertyKey === "generateKey") return generateKey;
169+
if (propertyKey === "generateKey") return assertingGenerateKey;
170+
if (propertyKey === "importKey") return importKey;
171+
if (propertyKey === "sign") return sign;
172+
if (propertyKey === "verify") return verify;
80173

81174
let result = Reflect.get(target, propertyKey, receiver);
82175
if (typeof result === "function") result = result.bind(webcrypto.subtle);
83176
return result;
84177
},
85178
});
86179

87-
return new Proxy(webcrypto, {
180+
return new Proxy(webcrypto as WorkerCrypto, {
88181
get(target, propertyKey, receiver) {
89-
if (propertyKey === "getRandomValues") return getRandomValues;
182+
if (propertyKey === "getRandomValues") return assertingGetRandomValues;
90183
if (propertyKey === "subtle") return subtle;
91184
if (propertyKey === "DigestStream") return DigestStream;
92185

packages/core/test/standards/crypto.spec.ts

Lines changed: 90 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import { webcrypto } from "crypto";
12
import { TextEncoder } from "util";
23
import { DOMException, DigestStream, createCrypto } from "@miniflare/core";
34
import { utf8Encode } from "@miniflare/shared-test";
45
import test, { Macro } from "ava";
56

67
const crypto = createCrypto();
78

8-
const digestStreamMacro: Macro<[AlgorithmIdentifier]> = async (
9+
const digestStreamMacro: Macro<[webcrypto.AlgorithmIdentifier]> = async (
910
t,
1011
algorithm
1112
) => {
@@ -69,6 +70,7 @@ test("crypto: provides DigestStream", (t) => {
6970
t.is(crypto.DigestStream, DigestStream);
7071
});
7172

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

95+
// Check generateKey, importKey, sing, verify functions modified to add
96+
// NODE-ED25519 support
97+
test("crypto: generateKey/exportKey: supports NODE-ED25519 algorithm", async (t) => {
98+
const keyPair = await crypto.subtle.generateKey(
99+
{ name: "NODE-ED25519", namedCurve: "NODE-ED25519" },
100+
true,
101+
["sign", "verify"]
102+
);
103+
const exported = await crypto.subtle.exportKey("raw", keyPair.publicKey);
104+
t.is(exported.byteLength, 32);
105+
});
106+
test("crypto: generateKey/exportKey: supports other algorithms", async (t) => {
107+
const key = await crypto.subtle.generateKey(
108+
{ name: "AES-GCM", length: 256 },
109+
true,
110+
["encrypt", "decrypt"]
111+
);
112+
const exported = await crypto.subtle.exportKey("raw", key);
113+
t.is(exported.byteLength, 32);
114+
});
115+
116+
test("crypto: importKey/exportKey: supports NODE-ED25519 public keys", async (t) => {
117+
const keyData =
118+
"953e73cb91a2494a33cd7180f05d5bbe6b5ca43cc66eb93ca38c6fc83cb18f29";
119+
const publicKey = await crypto.subtle.importKey(
120+
"raw",
121+
Buffer.from(keyData, "hex"),
122+
{ name: "NODE-ED25519", namedCurve: "NODE-ED25519" },
123+
true,
124+
["verify"]
125+
);
126+
const exported = await crypto.subtle.exportKey("raw", publicKey);
127+
t.is(Buffer.from(exported).toString("hex"), keyData);
128+
});
129+
test("crypto: importKey: fails for NODE-ED25519 private keys", async (t) => {
130+
const keyData =
131+
"f0d3c325a99ef50181faa238e07224ec9fee292e7ebf6585560bab64654ec6209c6afa31187898a43f7ab18c3552c2cd349e912c16c803a2a6ccbd546896fe8e";
132+
await t.throwsAsync(
133+
crypto.subtle.importKey(
134+
"raw",
135+
Buffer.from(keyData, "hex"),
136+
{ name: "NODE-ED25519", namedCurve: "NODE-ED25519" },
137+
false,
138+
["sign"]
139+
)
140+
);
141+
});
142+
test("crypto: importKey/exportKey: supports other algorithms", async (t) => {
143+
const keyData =
144+
"464d832870721bcf28649192bec41bd1fd5b32702d6168f21b8585fb566a4be7";
145+
const key = await crypto.subtle.importKey(
146+
"raw",
147+
Buffer.from(keyData, "hex"),
148+
{ name: "AES-GCM", length: 256 },
149+
true,
150+
["encrypt", "decrypt"]
151+
);
152+
const exported = await crypto.subtle.exportKey("raw", key);
153+
t.is(Buffer.from(exported).toString("hex"), keyData);
154+
});
155+
test("crypto: sign/verify: supports NODE-ED25519 algorithm", async (t) => {
156+
const algorithm: webcrypto.EcKeyAlgorithm = {
157+
name: "NODE-ED25519",
158+
namedCurve: "NODE-ED25519",
159+
};
160+
const { privateKey, publicKey } = await crypto.subtle.generateKey(
161+
algorithm,
162+
false,
163+
["sign", "verify"]
164+
);
165+
const data = utf8Encode("data");
166+
const signature = await crypto.subtle.sign(algorithm, privateKey, data);
167+
t.is(signature.byteLength, 64);
168+
t.true(await crypto.subtle.verify(algorithm, publicKey, signature, data));
169+
});
170+
test("crypto: sign/verify: supports other algorithm", async (t) => {
171+
const key = await crypto.subtle.generateKey(
172+
{ name: "HMAC", hash: "SHA-256" },
173+
false,
174+
["sign", "verify"]
175+
);
176+
const data = utf8Encode("data");
177+
const signature = await crypto.subtle.sign("HMAC", key, data);
178+
t.is(signature.byteLength, 32);
179+
t.true(await crypto.subtle.verify("HMAC", key, signature, data));
180+
});
181+
93182
// Checking other functions aren't broken by proxy...
94183

95184
test("crypto: gets random values", (t) => {
@@ -102,13 +191,3 @@ test("crypto: gets random values", (t) => {
102191
test("crypto: generates random UUID", (t) => {
103192
t.is(crypto.randomUUID().length, 36);
104193
});
105-
106-
test("crypto: generates aes key", async (t) => {
107-
const key = await crypto.subtle.generateKey(
108-
{ name: "aes-gcm", length: 256 },
109-
true,
110-
["encrypt", "decrypt"]
111-
);
112-
const exported = await crypto.subtle.exportKey("raw", key);
113-
t.is(exported.byteLength, 32);
114-
});

types/crypto.d.ts

Lines changed: 0 additions & 55 deletions
This file was deleted.

0 commit comments

Comments
 (0)