@sproutboat/runtime 0.37.1 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/package.json +3 -2
- package/src/native-fetch-prelude.js +5051 -0
- package/src/subtle/README.md +62 -0
- package/src/subtle/assemble.ts +152 -0
- package/src/subtle/driver.c +33 -0
- package/src/subtle/prune.test.ts +53 -0
- package/src/subtle/subtle.c +1072 -0
- package/src/subtle/subtle.test.ts +458 -0
- package/src/subtle/vendor/MONOCYPHER_LICENCE.md +167 -0
- package/src/subtle/vendor/monocypher-ed25519.c +500 -0
- package/src/subtle/vendor/monocypher-ed25519.h +140 -0
- package/src/subtle/vendor/monocypher.c +2956 -0
- package/src/subtle/vendor/monocypher.h +321 -0
- package/src/subtle-date.ts +6 -0
- package/src/wrap.ts +40 -0
- package/test/bearssl.ts +70 -0
- package/test/native-crypto.ts +15 -12
- package/test/native-jwt.ts +2 -1
- package/test/native-subtle.ts +182 -0
- package/test/native-web-apis.ts +30 -14
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { bearsslEnv } from "./bearssl";
|
|
6
|
+
import { ensurePorffor } from "../../toolchain/src/acquire";
|
|
7
|
+
import { jsonObject, parseJsonValue, type JsonObject, type JsonValue } from "../../wire/src/json";
|
|
8
|
+
import { SUBTLE_DATE } from "../src/subtle-date";
|
|
9
|
+
import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
|
|
10
|
+
|
|
11
|
+
// The date gate and the parts of crypto.subtle that need no native half (the broker transport has
|
|
12
|
+
// none): HMAC, digest, JWK for symmetric keys, and every error's DOMException name, against what
|
|
13
|
+
// workerd answers for the same calls. The RSA/ECDSA/Ed25519/AES paths run in the embedded
|
|
14
|
+
// transport and are covered by src/subtle/subtle.test.ts (C against BoringSSL) and the
|
|
15
|
+
// conformance run in the PR description.
|
|
16
|
+
const handler = `
|
|
17
|
+
const hex = (b) => [...new Uint8Array(b)].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
18
|
+
async function t(f) {
|
|
19
|
+
try { return await f(); } catch (e) { return "ERR " + e.name + (e instanceof DOMException ? " dom" : ""); }
|
|
20
|
+
}
|
|
21
|
+
const raw = new Uint8Array(32).fill(7);
|
|
22
|
+
const hmac = { name: "HMAC", hash: "SHA-256" };
|
|
23
|
+
export default {
|
|
24
|
+
async fetch() {
|
|
25
|
+
const s = crypto.subtle;
|
|
26
|
+
const out = {};
|
|
27
|
+
out.jwkImport = await t(async () => {
|
|
28
|
+
const k = await s.importKey("jwk", { kty: "oct", k: "AAECAwQFBgc", alg: "HS256", ext: true }, hmac, true, ["sign"]);
|
|
29
|
+
return [k.algorithm.length, k.type, k.usages, hex(await s.exportKey("raw", k))];
|
|
30
|
+
});
|
|
31
|
+
out.jwkExport = await t(async () => {
|
|
32
|
+
const k = await s.importKey("raw", raw, hmac, true, ["sign", "verify"]);
|
|
33
|
+
const j = await s.exportKey("jwk", k);
|
|
34
|
+
return [j.kty, j.alg, j.k, j.key_ops, j.ext];
|
|
35
|
+
});
|
|
36
|
+
out.badFormat = await t(() => s.importKey("foo", raw, hmac, true, ["sign"]));
|
|
37
|
+
out.badUsage = await t(() => s.importKey("raw", raw, hmac, true, ["encrypt"]));
|
|
38
|
+
out.empty = await t(() => s.importKey("raw", new Uint8Array(0), hmac, true, ["sign"]));
|
|
39
|
+
out.noHash = await t(() => s.importKey("raw", raw, { name: "HMAC" }, true, ["sign"]));
|
|
40
|
+
out.unknown = await t(() => s.digest("FOO", raw));
|
|
41
|
+
out.nonExtractable = await t(async () => s.exportKey("raw", await s.importKey("raw", raw, hmac, false, ["sign"])));
|
|
42
|
+
out.wrongUsage = await t(async () => s.sign("HMAC", await s.importKey("raw", raw, hmac, true, ["verify"]), raw));
|
|
43
|
+
out.wrongAlgorithm = await t(async () => s.sign("ECDSA", await s.importKey("raw", raw, hmac, true, ["sign"]), raw));
|
|
44
|
+
out.stringData = await t(async () => s.sign("HMAC", await s.importKey("raw", raw, hmac, true, ["sign"]), "abc"));
|
|
45
|
+
out.badJwkAlg = await t(() => s.importKey("jwk", { kty: "oct", k: "AAECAwQFBgc", alg: "HS384" }, hmac, true, ["sign"]));
|
|
46
|
+
out.badJwkKty = await t(() => s.importKey("jwk", { kty: "RSA", k: "AAECAwQFBgc" }, hmac, true, ["sign"]));
|
|
47
|
+
out.badJwkB64 = await t(() => s.importKey("jwk", { kty: "oct", k: "!!!" }, hmac, true, ["sign"]));
|
|
48
|
+
out.extFalse = await t(() => s.importKey("jwk", { kty: "oct", k: "AAECAwQFBgc", ext: false }, hmac, true, ["sign"]));
|
|
49
|
+
out.signVerify = await t(async () => {
|
|
50
|
+
const k = await s.importKey("raw", raw, { name: "HMAC", hash: "SHA-512" }, false, ["sign", "verify"]);
|
|
51
|
+
const sig = await s.sign("HMAC", k, raw);
|
|
52
|
+
return [sig.byteLength, await s.verify("HMAC", k, sig, raw), await s.verify("HMAC", k, sig, new Uint8Array(3))];
|
|
53
|
+
});
|
|
54
|
+
out.digests = await t(async () => [hex(await s.digest("SHA-1", raw)), hex(await s.digest({ name: "sha-256" }, raw)), hex(await s.digest("MD5", raw))]);
|
|
55
|
+
out.tse = await t(() => [s.timingSafeEqual(raw, raw), s.timingSafeEqual(raw, new Uint8Array(32))]);
|
|
56
|
+
out.tseLength = await t(() => s.timingSafeEqual(raw, new Uint8Array(3)));
|
|
57
|
+
out.isKey = await t(async () => { const k = await s.importKey("raw", raw, hmac, true, ["sign"]); return [k instanceof CryptoKey, Object.keys(k)]; });
|
|
58
|
+
// The native half is in the prelude on every transport.
|
|
59
|
+
out.needsNative = await t(async () => { const k = await s.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]); const sig = await s.sign({ name: "ECDSA", hash: "SHA-256" }, k.privateKey, raw); return await s.verify({ name: "ECDSA", hash: "SHA-256" }, k.publicKey, sig, raw); });
|
|
60
|
+
out.unsupportedCurve = await t(() => s.generateKey({ name: "ECDSA", namedCurve: "secp256k1" }, true, ["sign"]));
|
|
61
|
+
return Response.json(out);
|
|
62
|
+
},
|
|
63
|
+
};`;
|
|
64
|
+
|
|
65
|
+
const dom = " dom";
|
|
66
|
+
const expectedNew: JsonObject = {
|
|
67
|
+
jwkImport: [64, "secret", ["sign"], "0001020304050607"],
|
|
68
|
+
jwkExport: ["oct", "HS256", "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc", ["sign", "verify"], true],
|
|
69
|
+
badFormat: "ERR NotSupportedError" + dom,
|
|
70
|
+
badUsage: "ERR SyntaxError" + dom,
|
|
71
|
+
empty: "ERR DataError" + dom,
|
|
72
|
+
noHash: "ERR TypeError",
|
|
73
|
+
unknown: "ERR NotSupportedError" + dom,
|
|
74
|
+
nonExtractable: "ERR InvalidAccessError" + dom,
|
|
75
|
+
wrongUsage: "ERR InvalidAccessError" + dom,
|
|
76
|
+
wrongAlgorithm: "ERR InvalidAccessError" + dom,
|
|
77
|
+
stringData: "ERR TypeError",
|
|
78
|
+
badJwkAlg: "ERR DataError" + dom,
|
|
79
|
+
badJwkKty: "ERR DataError" + dom,
|
|
80
|
+
badJwkB64: "ERR DataError" + dom,
|
|
81
|
+
extFalse: "ERR DataError" + dom,
|
|
82
|
+
signVerify: [64, true, false],
|
|
83
|
+
digests: [],
|
|
84
|
+
tse: [true, false],
|
|
85
|
+
tseLength: "ERR TypeError",
|
|
86
|
+
isKey: [true, ["usages", "algorithm", "extractable", "type"]],
|
|
87
|
+
needsNative: true,
|
|
88
|
+
unsupportedCurve: "ERR NotSupportedError" + dom,
|
|
89
|
+
};
|
|
90
|
+
// The digests are checked against Bun, not typed in.
|
|
91
|
+
const bunHex = async (algo: string, data: Uint8Array) =>
|
|
92
|
+
Buffer.from(await crypto.subtle.digest(algo, data)).toString("hex");
|
|
93
|
+
const rawBytes = new Uint8Array(32).fill(7);
|
|
94
|
+
const digests: JsonValue[] = [
|
|
95
|
+
await bunHex("SHA-1", rawBytes),
|
|
96
|
+
await bunHex("SHA-256", rawBytes),
|
|
97
|
+
Buffer.from(new Bun.CryptoHasher("md5").update(rawBytes).digest()).toString("hex"),
|
|
98
|
+
];
|
|
99
|
+
expectedNew.digests = digests;
|
|
100
|
+
|
|
101
|
+
function freePort(): number {
|
|
102
|
+
const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
|
|
103
|
+
const port = listener.port;
|
|
104
|
+
listener.stop(true);
|
|
105
|
+
return port;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function run(date: string): Promise<JsonObject> {
|
|
109
|
+
const workdir = await mkdtemp(join(tmpdir(), "sb-native-subtle-"));
|
|
110
|
+
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
111
|
+
try {
|
|
112
|
+
const porffor = await ensurePorffor();
|
|
113
|
+
const node = Bun.which("node");
|
|
114
|
+
if (!node) throw new Error("native subtle test needs node on PATH");
|
|
115
|
+
const [core, broker] = await Promise.all([
|
|
116
|
+
readFile(preludePath, "utf8"),
|
|
117
|
+
readFile(transportPath("broker"), "utf8"),
|
|
118
|
+
]);
|
|
119
|
+
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
120
|
+
const generated = join(workdir, "subtle.generated.js");
|
|
121
|
+
const binary = join(workdir, "subtle-native");
|
|
122
|
+
await writeFile(
|
|
123
|
+
generated,
|
|
124
|
+
wrapNativeFetchHandler(handler, core.replace(TRANSPORT_MARKER, broker), undefined, undefined, undefined, date),
|
|
125
|
+
);
|
|
126
|
+
const compile = Bun.spawn(
|
|
127
|
+
[node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
|
|
128
|
+
{
|
|
129
|
+
cwd: workdir,
|
|
130
|
+
env: { ...process.env, ...(await bearsslEnv()) },
|
|
131
|
+
stdout: "pipe",
|
|
132
|
+
stderr: "pipe",
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
136
|
+
compile.exited,
|
|
137
|
+
new Response(compile.stdout).text(),
|
|
138
|
+
new Response(compile.stderr).text(),
|
|
139
|
+
]);
|
|
140
|
+
assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
|
|
141
|
+
const port = freePort();
|
|
142
|
+
server = Bun.spawn([binary], {
|
|
143
|
+
cwd: workdir,
|
|
144
|
+
env: { ...process.env, PORT: String(port) },
|
|
145
|
+
stdout: "ignore",
|
|
146
|
+
stderr: "inherit",
|
|
147
|
+
});
|
|
148
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
149
|
+
try {
|
|
150
|
+
const response = await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
|
|
151
|
+
assert.equal(response.status, 200);
|
|
152
|
+
const body = jsonObject(parseJsonValue(await response.text()));
|
|
153
|
+
assert(body, "the handler returns a JSON object");
|
|
154
|
+
return body;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (error instanceof assert.AssertionError) throw error;
|
|
157
|
+
if (server.exitCode !== null) break;
|
|
158
|
+
await Bun.sleep(100);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
throw new Error(`compiled handler did not start (${date})`);
|
|
162
|
+
} finally {
|
|
163
|
+
if (server) {
|
|
164
|
+
server.kill(9);
|
|
165
|
+
await server.exited;
|
|
166
|
+
}
|
|
167
|
+
await rm(workdir, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const prelude = await readFile(preludePath, "utf8");
|
|
172
|
+
assert(prelude.includes(`const __SB_SUBTLE_DATE = "${SUBTLE_DATE}";`), "SUBTLE_DATE and __SB_SUBTLE_DATE differ");
|
|
173
|
+
const [before, after] = await Promise.all([
|
|
174
|
+
run(new Date(Date.parse(SUBTLE_DATE) - 86_400_000).toISOString().slice(0, 10)),
|
|
175
|
+
run(SUBTLE_DATE),
|
|
176
|
+
]);
|
|
177
|
+
assert.deepEqual(after, expectedNew, "from the new date crypto.subtle answers as workerd does");
|
|
178
|
+
// Before the date nothing changed: the old subset, its plain-object keys and its errors.
|
|
179
|
+
assert.equal(before.jwkImport, "ERR NotSupportedError dom", "before the date, jwk import is not supported");
|
|
180
|
+
assert.equal(before.needsNative, "ERR NotSupportedError dom", "before the date, ECDSA is not supported");
|
|
181
|
+
assert.deepEqual(before.signVerify, [64, true, false]);
|
|
182
|
+
console.log("crypto.subtle: the date gate and the transport-independent surface match workerd");
|
package/test/native-web-apis.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { ensurePorffor } from "../../toolchain/src/acquire";
|
|
6
|
+
import { bearsslEnv } from "./bearssl";
|
|
6
7
|
import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
|
|
7
8
|
|
|
8
9
|
// Web API behaviour the W2 conformance suite found wrong, compared with Bun: Headers
|
|
@@ -25,7 +26,8 @@ const CASES = [
|
|
|
25
26
|
`new URL("http://münchen.test/x").host`,
|
|
26
27
|
`crypto.subtle.digest("SHA-1", new TextEncoder().encode("abc")).then((b) => Array.from(new Uint8Array(b), (x) => x.toString(16).padStart(2, "0")).join(""))`,
|
|
27
28
|
`crypto.subtle.importKey("raw", new TextEncoder().encode("key"), { name: "HMAC", hash: "SHA-1" }, true, ["sign"]).then(async (k) => [k.algorithm.hash.name, k.algorithm.length, Array.from(new Uint8Array(await crypto.subtle.sign("HMAC", k, new TextEncoder().encode("msg"))), (x) => x.toString(16).padStart(2, "0")).join(""), new Uint8Array(await crypto.subtle.exportKey("raw", k)).length].join())`,
|
|
28
|
-
`crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-512" }, true, ["sign"]).then(async (k) => [k.type, k.algorithm.length, (await crypto.subtle.exportKey("raw", k)).byteLength].join())`,
|
|
29
|
+
`crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-512" }, true, ["sign"]).then(async (k) => [k.type, k.algorithm.length, (await crypto.subtle.exportKey("raw", k)).byteLength].join())`,
|
|
30
|
+
`(async () => { const collect = async (rs) => { const r = rs.getReader(); const out = []; for (;;) { const { value, done } = await r.read(); if (done) return out; out.push(value); } }; const bytes = new ReadableStream({ start(c) { c.enqueue(new Uint8Array([0x61, 0xe2])); c.enqueue(new Uint8Array([0x82, 0xac, 0xf0, 0x9f])); c.enqueue(new Uint8Array([0x98, 0x80])); c.close(); } }); const text = new ReadableStream({ start(c) { c.enqueue("a\\uD83D"); c.enqueue("\\uDE00b"); c.close(); } }); return (await collect(bytes.pipeThrough(new TextDecoderStream()))).join("|") + ";" + (await collect(text.pipeThrough(new TextEncoderStream()))).map((u) => Array.from(u).join(",")).join("|"); })()`,
|
|
29
31
|
];
|
|
30
32
|
const handler = `export default {async fetch() {
|
|
31
33
|
const cases = [
|
|
@@ -37,7 +39,15 @@ ${CASES.map((c) => ` () => (${c}),`).join("\n")}
|
|
|
37
39
|
}
|
|
38
40
|
return new Response(JSON.stringify(out));
|
|
39
41
|
}};`;
|
|
40
|
-
const want = await Promise.all(
|
|
42
|
+
const want = await Promise.all(
|
|
43
|
+
CASES.map(async (c) => {
|
|
44
|
+
try {
|
|
45
|
+
return String(await new Function("return " + c)());
|
|
46
|
+
} catch (e) {
|
|
47
|
+
return "THROWS " + (e instanceof Error ? e.message : String(e));
|
|
48
|
+
}
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
41
51
|
|
|
42
52
|
function freePort(): number {
|
|
43
53
|
const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
|
|
@@ -53,10 +63,7 @@ try {
|
|
|
53
63
|
const node = Bun.which("node");
|
|
54
64
|
if (!node) throw new Error("native streams test needs node on PATH");
|
|
55
65
|
|
|
56
|
-
const [core, broker] = await Promise.all([
|
|
57
|
-
readFile(preludePath, "utf8"),
|
|
58
|
-
readFile(transportPath("broker"), "utf8"),
|
|
59
|
-
]);
|
|
66
|
+
const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
|
|
60
67
|
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
61
68
|
const prelude = core.replace(TRANSPORT_MARKER, broker);
|
|
62
69
|
const generated = join(workdir, "web-apis.generated.js");
|
|
@@ -64,12 +71,15 @@ try {
|
|
|
64
71
|
await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
|
|
65
72
|
await rm(binary, { force: true });
|
|
66
73
|
|
|
67
|
-
const compile = Bun.spawn(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
const compile = Bun.spawn(
|
|
75
|
+
[node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
|
|
76
|
+
{
|
|
77
|
+
cwd: workdir,
|
|
78
|
+
env: { ...process.env, ...(await bearsslEnv()) },
|
|
79
|
+
stdout: "pipe",
|
|
80
|
+
stderr: "pipe",
|
|
81
|
+
},
|
|
82
|
+
);
|
|
73
83
|
const [exitCode, stdout, stderr] = await Promise.all([
|
|
74
84
|
compile.exited,
|
|
75
85
|
new Response(compile.stdout).text(),
|
|
@@ -98,8 +108,14 @@ try {
|
|
|
98
108
|
// SAFETY: the handler above is the only producer of this body: a JSON array with one
|
|
99
109
|
// string per case; the comparison below reports any entry that differs.
|
|
100
110
|
const got = JSON.parse(await (await fetch(`http://127.0.0.1:${port}/`)).text()) as string[];
|
|
101
|
-
const diffs = CASES.flatMap((c, i) =>
|
|
102
|
-
|
|
111
|
+
const diffs = CASES.flatMap((c, i) =>
|
|
112
|
+
got[i] === want[i] ? [] : [`${c}\n want ${JSON.stringify(want[i])}\n got ${JSON.stringify(got[i])}`],
|
|
113
|
+
);
|
|
114
|
+
assert.deepEqual(
|
|
115
|
+
diffs,
|
|
116
|
+
[],
|
|
117
|
+
`Web API results differ from Bun in ${diffs.length} of ${CASES.length} cases:\n${diffs.join("\n")}`,
|
|
118
|
+
);
|
|
103
119
|
console.log(`Native Web APIs match Bun in all ${CASES.length} cases`);
|
|
104
120
|
} finally {
|
|
105
121
|
if (server) {
|