diff --git a/services/edge-agent/dist/agent.mjs b/services/edge-agent/dist/agent.mjs index 86ecf401..fa729747 100644 --- a/services/edge-agent/dist/agent.mjs +++ b/services/edge-agent/dist/agent.mjs @@ -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`); } diff --git a/services/edge-agent/test/agent.test.mjs b/services/edge-agent/test/agent.test.mjs index 96665a88..c5817af1 100644 --- a/services/edge-agent/test/agent.test.mjs +++ b/services/edge-agent/test/agent.test.mjs @@ -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-"));