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
- Pass the raw binary digest: createHash('sha256').update(data).digest() with no 'hex' argument — exactly 32 bytes
- If you have a hex string, convert first: Buffer.from(hex, 'hex')
- Confirm you're extracting the actual footer: buf.subarray(buf.length - 32), not a hash of the header or a recomputed value
- 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
- Keep digest() without an encoding argument — raw bytes are the signing interface
- If converting from hex, use Buffer.from(hex, 'hex'), never Buffer.from(hex)
- Extract the footer as subarray(buf.length - 32) rather than recomputing it
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
- Buffer too small to be a valid RVFA file
- Failed to parse RVFA header JSON
- Header length extends beyond buffer
- Invalid RVFA magic: expected "RVFA", got
- Buffer too small to contain declared header
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)