JuliusBrussee/caveman · critical

signed checksum manifest is malformed

Error message

signed checksum manifest is malformed

What it means

expectedDigest() in the binary installer parses the signature-verified checksums.txt line by line against a strict '<64 lowercase hex> <safe filename>' format (two spaces). A line that is present but does not match — bad hash casing or length, tab separator, carriage-return residue, odd filename characters — makes the whole manifest untrustworthy, so parsing aborts even though the signature checked out.

Source

Thrown at packages/shared/binary-installer/installer.mjs:121

    const digest = createHash("sha256").update(checksums).digest();
    const bundled = Buffer.from(bundle.messageSignature.messageDigest.digest, "base64");
    if (digest.length !== bundled.length || !digest.equals(bundled)) return false;
    return verify(
      "sha256",
      Buffer.from(checksums),
      createPublicKey(BINARY_SIGNING_PUBKEY),
      Buffer.from(bundle.messageSignature.signature, "base64"),
    );
  } catch {
    return false;
  }
}

function expectedDigest(checksums, artifact) {
  for (const line of checksums.split("\n")) {
    if (!line) continue;
    const match = line.match(/^([a-f0-9]{64})  ([A-Za-z0-9._-]+)$/);
    if (!match) throw new Error("signed checksum manifest is malformed");
    if (match[2] === artifact) return match[1];
  }
  throw new Error(`signed checksum manifest does not contain ${artifact}`);
}

function cleanup(path) {
  try {
    unlinkSync(path);
  } catch (error) {
    if (error.code !== "ENOENT") throw error;
  }
}

async function download(url, part, timeout) {
  const response = await asset(url, timeout);
  if (!response.body) throw new Error("binary download failed: response body missing");
  const hash = createHash("sha256");
  const file = await open(part, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600);

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. If you control the release: regenerate checksums.txt with standard sha256sum output (lowercase hex, two spaces, LF endings)
  2. If consuming: unset CAVE_BINARY_RELEASE_BASE to pull the official manifest, and report the bad mirror or artifact upstream
  3. Never hand-edit checksums.txt to make parsing pass — the format rejection is a tamper signal

Example fix

# before (malformed: uppercase hex, tab separator)
# 0A1B…F	caveman-mcp_linux_amd64

# after
cd artifacts && sha256sum * > ../checksums.txt   # lowercase hex, two spaces
Defensive patterns

Strategy: validation

Validate before calling

// validate a manifest your pipeline produces, before publishing
for (const line of checksums.split("\n")) {
  if (line && !/^([a-f0-9]{64})  ([A-Za-z0-9._-]+)$/.test(line)) {
    throw new Error(`malformed checksum line: ${JSON.stringify(line)}`);
  }
}

Type guard

function isValidChecksumLine(line) {
  return /^([a-f0-9]{64})  ([A-Za-z0-9._-]+)$/.test(line);
}

Try / catch

try { await ensureBinary({ name, envVar }); }
catch (e) {
  if (/signed checksum manifest is malformed/.test(String(e?.message))) {
    delete process.env.CAVE_BINARY_RELEASE_BASE; // retry against the official host
    return ensureBinary({ name, envVar });
  }
  throw e;
}

Prevention

When it happens

Trigger: A republished or repacked release where checksums.txt was regenerated with a different tool (different sha256sum flags, CRLF line endings, uppercase hex); a mirror rewriting files; a genuinely tampered manifest that happens to carry a valid signature over corrupted content.

Common situations: Internal mirrors that normalize line endings; release pipelines switching checksum tools; manual edits to checksums.txt; partial file truncation.

Understand the failure class

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/92a2c96f8a16e30c. Report an issue: GitHub.