ruvnet/ruflo · critical · ReleaseVerificationError

sha256 mismatch for : expected …, got …

Error message

sha256 mismatch for ${input.assetFilename}: expected ${expected.slice(0, 12)}…, got ${actual.slice(0, 12)}…

What it means

The final gate of verifyRelease(): the sha256 of the downloaded archive must equal the signature-verified SHA256SUMS entry for that asset. A mismatch means the bytes received are not the bytes that were signed — truncated transfer, corrupted cache, or deliberate replacement. Only the first 12 hex characters of each hash are shown for comparison; the install aborts.

Solutions

  1. Re-run `ruflo proxy install` after clearing caches — truncation/corruption in transit is the most common cause and a fresh download usually verifies
  2. Manually cross-check: download the archive and SHA256SUMS yourself and run sha256sum to see the full hashes
  3. If the mismatch reproduces from clean networks, stop and report it — a persistent mismatch against a validly-signed manifest signals real tampering and must be escalated, not retried around
Defensive patterns

Strategy: retry

Validate before calling

import { createHash } from 'node:crypto';
// Independent pre-check: hash a manual download, compare with the signed manifest
const expected = parseSha256Sums(sumsText)[assetName];
const actual = createHash('sha256').update(archiveBytes).digest('hex');
if (actual !== expected) throw new Error('bytes differ from signed manifest — redownload');

Type guard

const isReleaseVerification = (e: unknown): e is Error & { name: 'ReleaseVerificationError' } =>
  e instanceof Error && e.name === 'ReleaseVerificationError';

Try / catch

let lastErr: unknown;
for (let i = 0; i < 2; i++) {
  try {
    return verifyRelease(input);
  } catch (e) {
    lastErr = e;
    if (!(isReleaseVerification(e) && /sha256 mismatch/.test(e.message))) throw e;
    input = await refetchAssets(); // corruption in transit → exactly one redownload
  }
}
throw lastErr; // persistent mismatch = possible tampering: stop and report

Prevention

When it happens

Trigger: Truncated downloads (dropped connections, partial proxy responses); a caching layer serving stale or corrupt bytes for the asset URL; genuine tampering where the archive differs from the signed manifest.

Common situations: Flaky networks behind TLS-terminating proxies; CI caches keyed on URL but not content; mirrors serving different bytes; disk issues corrupting temp files before hashing.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/159535c0b25eebc4. Report an issue: GitHub.

Appendix: source

Thrown at v3/@claude-flow/cli/src/proxy/verify.ts:90

 * Full verification: signature over SHA256SUMS, then the asset's own hash
 * against the matching line. Throws `ReleaseVerificationError` on ANY
 * failure — there is no partial-trust outcome, matching ADR-307's "refuses
 * on any mismatch" requirement.
 */
export function verifyRelease(input: VerifyReleaseInput): VerifyReleaseResult {
  if (!verifySha256SumsSignature(input.sumsBytes, input.sigBase64, input.pubkeyPem)) {
    throw new ReleaseVerificationError('SHA256SUMS.sig failed Ed25519 verification — refusing to install');
  }

  const sums = parseSha256Sums(input.sumsBytes.toString('utf-8'));
  const expected = sums[input.assetFilename];
  if (!expected) {
    throw new ReleaseVerificationError(`SHA256SUMS has no entry for ${input.assetFilename}`);
  }

  const actual = sha256Hex(input.assetBytes);
  if (actual !== expected) {
    throw new ReleaseVerificationError(
      `sha256 mismatch for ${input.assetFilename}: expected ${expected.slice(0, 12)}…, got ${actual.slice(0, 12)}…`,
    );
  }

  return { sha256: actual };
}

View on GitHub (pinned to fa13ee4ad6)