ruvnet/ruflo · error

Footer hash must be bytes, got

Error message

Footer hash must be ${SHA256_SIZE} bytes, got ${footerHash.length}

What it means

RvfaSigner.signSections(footerHash) requires exactly the 32-byte SHA256 footer hash from an RVFA image — it signs that digest with Ed25519. Passing anything of a different length (a 64-byte hex string, a raw file buffer, a base64 blob, a SHA-512 digest) is rejected before signing because the detached signature contract is specifically over the 32-byte footer hash.

Solutions

  1. Pass the raw binary digest: createHash('sha256').update(data).digest() with no 'hex' argument — exactly 32 bytes
  2. If you have a hex string, convert first: Buffer.from(hex, 'hex')
  3. Confirm you're extracting the actual footer: buf.subarray(buf.length - 32), not a hash of the header or a recomputed value
  4. Keep the digest computation and signSections adjacent so encodings can't drift apart

Example fix

// before — hex digest passed as utf8 bytes (64B) &/or wrong hash
const hash = createHash('sha256').update(data).digest('hex');
const sig = await signer.signSections(Buffer.from(hash));

// after — raw 32-byte digest of the footer region
const footer = buf.subarray(buf.length - 32);
const sig = await signer.signSections(footer); // already the 32B SHA256
Defensive patterns

Strategy: validation

Validate before calling

import { createHash } from 'node:crypto';

const footerHash = buf.subarray(buf.length - 32); // raw 32B SHA256 region
if (footerHash.length !== 32) throw new Error('expected 32-byte footer');
// equivalently: createHash('sha256').update(data).digest() (no encoding arg)

Type guard

function isSha256Footer(b: Buffer): b is Buffer { return b.length === 32; }

Try / catch

try { const sig = await signer.signSections(footerHash); }
catch (e) {
  if (/Footer hash must be 32 bytes/.test(String((e as Error).message))) {
    // you passed hex/base64/whole-file: convert to the raw 32-byte digest and retry
  }
  throw e;
}

Prevention

When it happens

Trigger: signSections(hexString) instead of the raw digest; signSections(createHash('sha512').digest()) (64 bytes); signSections(entireFile); or passing a hash re-encoded as UTF-8 (a 64-char hex string becomes 64 bytes).

Common situations: Hash plumbing confusion: upstream code produced hex while the API wants raw bytes; switching hash algorithms (sha512/blake3) without updating the signing call; copy-paste from a codebase that signed whole files rather than footer hashes.

Related errors


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

Appendix: source

Thrown at v3/@claude-flow/cli/src/appliance/rvfa-signing.ts:327

    };

    // Embed signature in header and rebuild
    header.signature = metadata;
    const rebuilt = rebuildRvfa(buf, header, sectionData, footer);
    await writeFile(rvfaPath, rebuilt);

    return metadata;
  }

  /**
   * Sign a section footer hash (detached signature).
   *
   * @param footerHash  The 32-byte SHA256 footer hash from an RVFA file.
   * @returns Hex-encoded Ed25519 signature.
   */
  async signSections(footerHash: Buffer): Promise<string> {
    if (footerHash.length !== SHA256_SIZE) {
      throw new Error(
        `Footer hash must be ${SHA256_SIZE} bytes, got ${footerHash.length}`,
      );
    }
    const sig = sign(null, footerHash, this.keyObj);
    return sig.toString('hex');
  }

  /**
   * Sign an RVFP patch file (detached signature).
   *
   * @param patchData  The raw patch binary data.
   * @returns Hex-encoded Ed25519 signature.
   */
  async signPatch(patchData: Buffer): Promise<string> {
    const digest = createHash('sha256').update(patchData).digest();
    const sig = sign(null, digest, this.keyObj);
    return sig.toString('hex');
  }

View on GitHub (pinned to fa13ee4ad6)