{"record":{"id":"92a2c96f8a16e30c","repo":"JuliusBrussee/caveman","slug":"signed-checksum-manifest-is-malformed","errorCode":null,"errorMessage":"signed checksum manifest is malformed","messagePattern":"signed checksum manifest is malformed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/shared/binary-installer/installer.mjs","lineNumber":121,"sourceCode":"    const digest = createHash(\"sha256\").update(checksums).digest();\n    const bundled = Buffer.from(bundle.messageSignature.messageDigest.digest, \"base64\");\n    if (digest.length !== bundled.length || !digest.equals(bundled)) return false;\n    return verify(\n      \"sha256\",\n      Buffer.from(checksums),\n      createPublicKey(BINARY_SIGNING_PUBKEY),\n      Buffer.from(bundle.messageSignature.signature, \"base64\"),\n    );\n  } catch {\n    return false;\n  }\n}\n\nfunction expectedDigest(checksums, artifact) {\n  for (const line of checksums.split(\"\\n\")) {\n    if (!line) continue;\n    const match = line.match(/^([a-f0-9]{64})  ([A-Za-z0-9._-]+)$/);\n    if (!match) throw new Error(\"signed checksum manifest is malformed\");\n    if (match[2] === artifact) return match[1];\n  }\n  throw new Error(`signed checksum manifest does not contain ${artifact}`);\n}\n\nfunction cleanup(path) {\n  try {\n    unlinkSync(path);\n  } catch (error) {\n    if (error.code !== \"ENOENT\") throw error;\n  }\n}\n\nasync function download(url, part, timeout) {\n  const response = await asset(url, timeout);\n  if (!response.body) throw new Error(\"binary download failed: response body missing\");\n  const hash = createHash(\"sha256\");\n  const file = await open(part, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600);","sourceCodeStart":103,"sourceCodeEnd":139,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/shared/binary-installer/installer.mjs#L103-L139","documentation":"`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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["curl the checksums.txt URL and inspect it: confirm every non-empty line is `<64 hex chars>  <filename>`.","Check for CRLF line endings (`file` or `cat -A`); regenerate or normalize the manifest to LF.","Verify you are not getting an HTML error/login page through a proxy — bypass or authenticate the proxy.","If artifact names legitimately contain characters like `+` outside `[A-Za-z0-9._-]`, rename the artifact or extend the regex in expectedDigest.","Fall back to installing the binary manually and pointing the envVar at it."],"exampleFix":"// before: manifest line with one space\n\"abc123...  artifact.tgz\" -> \"abc123... artifact.tgz\"  (malformed)\n// after: regenerate with correct two-space format (sha256sum style)\n\"abc123...  artifact.tgz\"","handlingStrategy":"validation","validationCode":"function manifestLooksValid(text) {\n  return text.split(\"\\n\").filter(Boolean).every(l => /^[a-f0-9]{64}  [A-Za-z0-9._-]+$/.test(l));\n}\n// fetch checksums.txt yourself and assert manifestLooksValid before ensureBinary","typeGuard":null,"tryCatchPattern":"try {\n  await ensureBinary({ name, envVar });\n} catch (e) {\n  if (String(e.message).includes(\"manifest is malformed\")) {\n    const raw = await fetch(`${release}/checksums.txt`).then(r => r.text());\n    console.error(\"Manifest preview:\", raw.slice(0, 500));\n  }\n  throw e;\n}","preventionTips":["Check for proxies/captive portals that substitute HTML for plain-text files.","Normalize line endings to LF when regenerating manifests.","Keep artifact names within [A-Za-z0-9._-] to match the parser.","Diff the fetched manifest against the published one on a good network."],"tags":["supply-chain","checksum","validation"],"backgroundTag":"invalid-argument-format","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}