Merge pull request #249 from copenhagentruckwash/fix-edge-agent-vulnerability-for-unsigned-artifacts
Require checksums for edge agent updates
This commit is contained in:
Vendored
+10
-1
@@ -759,6 +759,15 @@ async function fetchArtifactBuffer(url, expectedSha256, label, fetchImpl = fetch
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!expectedSha256) {
|
||||
throw new Error(`${label} checksum is required`);
|
||||
}
|
||||
|
||||
const normalizedExpectedSha256 = String(expectedSha256).toLowerCase();
|
||||
if (!/^[a-f0-9]{64}$/.test(normalizedExpectedSha256)) {
|
||||
throw new Error(`${label} checksum must be a valid sha256 hex digest`);
|
||||
}
|
||||
|
||||
const response = await fetchImpl(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`${label} download failed: HTTP ${response.status}`);
|
||||
@@ -766,7 +775,7 @@ async function fetchArtifactBuffer(url, expectedSha256, label, fetchImpl = fetch
|
||||
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
const sha256 = createHash("sha256").update(buffer).digest("hex");
|
||||
if (expectedSha256 && String(expectedSha256).toLowerCase() !== sha256.toLowerCase()) {
|
||||
if (normalizedExpectedSha256 !== sha256.toLowerCase()) {
|
||||
throw new Error(`${label} checksum mismatch`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile as execFileCallback } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
@@ -40,6 +41,10 @@ function makeFetchResponse(body) {
|
||||
};
|
||||
}
|
||||
|
||||
function sha256Hex(body) {
|
||||
return createHash("sha256").update(body).digest("hex");
|
||||
}
|
||||
|
||||
async function waitFor(predicate, { timeoutMs = 1000, intervalMs = 10, description = "condition" } = {}) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
|
||||
@@ -320,19 +325,23 @@ test("runUpdate stages a pending verification restart after installing new artif
|
||||
execCalls.push({ command, args, options });
|
||||
return { stdout: "{}" };
|
||||
};
|
||||
const agentBody = "// new agent\n";
|
||||
const packageBody = JSON.stringify({ name: "new-edge-agent" }, null, 2);
|
||||
const fakeFetch = async (url) => {
|
||||
if (String(url).endsWith("/agent.mjs")) {
|
||||
return makeFetchResponse("// new agent\n");
|
||||
return makeFetchResponse(agentBody);
|
||||
}
|
||||
if (String(url).endsWith("/package.json")) {
|
||||
return makeFetchResponse(JSON.stringify({ name: "new-edge-agent" }, null, 2));
|
||||
return makeFetchResponse(packageBody);
|
||||
}
|
||||
throw new Error(`Unexpected URL: ${url}`);
|
||||
};
|
||||
|
||||
const result = await runUpdate({
|
||||
artifactUrl: "https://api.example.test/edge-agent/artifacts/agent.mjs",
|
||||
sha256: sha256Hex(agentBody),
|
||||
packageUrl: "https://api.example.test/edge-agent/artifacts/package.json",
|
||||
packageSha256: sha256Hex(packageBody),
|
||||
targetVersion: "1.1.0",
|
||||
releaseChannel: "stable",
|
||||
restartMode: "spawn",
|
||||
@@ -360,6 +369,45 @@ test("runUpdate stages a pending verification restart after installing new artif
|
||||
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
test("runUpdate rejects artifacts without required checksums", async () => {
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), "edge-agent-update-checksum-"));
|
||||
const configPath = path.join(tempDir, "config.json");
|
||||
const liveConfig = {
|
||||
apiUrl: "https://api.example.test",
|
||||
gatewayId: 42,
|
||||
agentToken: "agent-token",
|
||||
installDir: tempDir,
|
||||
restartMode: "spawn",
|
||||
installedVersion: "1.0.0",
|
||||
targetVersion: "1.0.0",
|
||||
};
|
||||
|
||||
await writeFile(configPath, JSON.stringify(liveConfig, null, 2));
|
||||
await writeFile(path.join(tempDir, "agent.mjs"), "// old agent\n");
|
||||
|
||||
let fetchCalled = false;
|
||||
await assert.rejects(
|
||||
runUpdate({
|
||||
artifactUrl: "https://api.example.test/edge-agent/artifacts/agent.mjs",
|
||||
targetVersion: "1.1.0",
|
||||
}, async () => {
|
||||
fetchCalled = true;
|
||||
return makeFetchResponse("// new agent\n");
|
||||
}, {
|
||||
configPath,
|
||||
config: liveConfig,
|
||||
liveConfig,
|
||||
execFileImpl: async () => ({ stdout: "{}" }),
|
||||
}),
|
||||
/Agent artifact checksum is required/
|
||||
);
|
||||
|
||||
assert.equal(fetchCalled, false);
|
||||
assert.equal(await readFile(path.join(tempDir, "agent.mjs"), "utf8"), "// old agent\n");
|
||||
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
|
||||
test("handleAgentCommand returns an uninstall follow-up envelope for gateway removal", async () => {
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), "edge-agent-uninstall-envelope-"));
|
||||
|
||||
Reference in New Issue
Block a user