JuliusBrussee/caveman · error · Error

signed checksum manifest is malformed

Error message

signed checksum manifest is malformed

What it means

`expectedDigest(checksums, artifact)` parses the signed checksums.txt manifest line by line, expecting each non-empty line to match `^([a-f0-9]{64}) ([A-Za-z0-9._-]+)$` (sha256 hex digest, two spaces, filename). If any line deviates from that strict shape, the manifest cannot be trusted, so the installer throws before attempting any digest comparison. This is a supply-chain guard: a malformed manifest is treated as hostile or corrupt.

Solutions

  1. curl the checksums.txt URL and inspect it: confirm every non-empty line is `<64 hex chars> <filename>`.
  2. Check for CRLF line endings (`file` or `cat -A`); regenerate or normalize the manifest to LF.
  3. Verify you are not getting an HTML error/login page through a proxy — bypass or authenticate the proxy.
  4. If artifact names legitimately contain characters like `+` outside `[A-Za-z0-9._-]`, rename the artifact or extend the regex in expectedDigest.
  5. Fall back to installing the binary manually and pointing the envVar at it.

Example fix

// before: manifest line with one space
"abc123...  artifact.tgz" -> "abc123... artifact.tgz"  (malformed)
// after: regenerate with correct two-space format (sha256sum style)
"abc123...  artifact.tgz"
Defensive patterns

Strategy: validation

Validate before calling

function manifestLooksValid(text) {
  return text.split("\n").filter(Boolean).every(l => /^[a-f0-9]{64}  [A-Za-z0-9._-]+$/.test(l));
}
// fetch checksums.txt yourself and assert manifestLooksValid before ensureBinary

Try / catch

try {
  await ensureBinary({ name, envVar });
} catch (e) {
  if (String(e.message).includes("manifest is malformed")) {
    const raw = await fetch(`${release}/checksums.txt`).then(r => r.text());
    console.error("Manifest preview:", raw.slice(0, 500));
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling `ensureBinary()` whose `signedDigest`/`expectedDigest` path receives a checksums.txt that contains any line failing the regex — e.g. MD5-length digests, blank-but-whitespace lines, filenames with spaces or `+`/`~` characters, CRLF line endings leaving a trailing `\r`, or HTML error text saved instead of the manifest.

Common situations: Proxy or captive portal returning an HTML page for checksums.txt; release tooling changed manifest format (different hash size, single space separator); Windows CRLF artifacts when the manifest was regenerated; artifact names with `+` (common in versions) that the regex rejects.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/92a2c96f8a16e30c. Report an issue: GitHub.

Appendix: 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 3ee70a1026)