ruvnet/ruflo · error · Error
signAttributionArtifact: privateKey must be 32 bytes (got ${
Error message
signAttributionArtifact: privateKey must be 32 bytes (got ${privateKey.length}) What it means
The attribution-artifact counterpart to signBacktestArtifact: signAttributionArtifact() hex-decodes the private key and requires exactly 32 bytes before signing. Same Ed25519 contract — 64 hex chars, no 'ed25519:' prefix — applied to the attribution schema (ruflo-neural-trader-attribution/v1).
Source
Thrown at plugins/ruflo-neural-trader/src/signed-attribution.ts:101
* Sign the body of an attribution artifact and return the fully-formed
* `SignedAttributionArtifact` envelope.
*
* The signature covers the artifact body WITHOUT `witnessSignature` and
* WITHOUT `witnessPublicKey` (CWE-347 pattern, same as Phase 4). The
* verifier MUST pin to a trusted key for the pin to be a real defense.
*
* @param body — artifact body (everything except signature fields + schema)
* @param privateKeyHex — 32-byte Ed25519 private key as hex (no 'ed25519:' prefix)
* @returns the signed artifact ready to be stored
*/
export async function signAttributionArtifact(
body: SignedAttributionArtifactBody,
privateKeyHex: string,
): Promise<SignedAttributionArtifact> {
const ed = await import('@noble/ed25519');
const privateKey = hexToBytes(privateKeyHex);
if (privateKey.length !== 32) {
throw new Error(
`signAttributionArtifact: privateKey must be 32 bytes (got ${privateKey.length})`,
);
}
const canonical = canonicalBytes(body);
const signatureBytes = await ed.signAsync(canonical, privateKey);
const publicKeyBytes = await ed.getPublicKeyAsync(privateKey);
return {
schema: 'ruflo-neural-trader-attribution/v1',
...body,
witnessPublicKey: `ed25519:${bytesToHex(publicKeyBytes)}`,
witnessSignature: bytesToHex(signatureBytes),
};
}
/**
* Verify a signed attribution artifact against a caller-supplied trustedView on GitHub (pinned to 6b01dc5a68)
Solutions
- Pass a 32-byte Ed25519 seed as 64 hex characters with no 'ed25519:' prefix.
- Strip the prefix and trim whitespace before calling: key.replace(/^ed25519:/, '').trim().
- Share one canonical key-loading helper across signBacktestArtifact and signAttributionArtifact so both receive an identically normalized 32-byte hex string.
- Generate keys with crypto.randomBytes(32).toString('hex').
Example fix
// before await signAttributionArtifact(body, signingKeyHex); // 128 hex chars (64 bytes) // after const seedHex = signingKeyHex.slice(0, 64); // take the 32-byte seed portion await signAttributionArtifact(body, seedHex);
Defensive patterns
Strategy: validation
Validate before calling
const cleanKey = privateKeyHex.replace(/^ed25519:/, '').trim();
if (!/^[0-9a-fA-F]{64}$/.test(cleanKey)) throw new Error('attribution signing key must be 64 hex chars');
await signAttributionArtifact(body, cleanKey); Type guard
function isEd25519SeedHex(key: string): boolean { return /^[0-9a-fA-F]{64}$/.test(key.replace(/^ed25519:/, '').trim()); } Try / catch
try { await signAttributionArtifact(body, key); } catch (e) { if (e instanceof Error && /privateKey must be 32 bytes/.test(e.message)) throw new Error('Attribution signing key invalid', { cause: e }); throw e; } Prevention
- Reuse the same key-loading helper as signBacktestArtifact.
- Validate key length once at startup, not per call.
- Document the 'no prefix, 64 hex chars' contract wherever the key is configured.
When it happens
Trigger: Passing privateKeyHex whose decoded byte length is not 32: prefixed keys, expanded 64-byte secret keys, base64-encoded keys, truncated/oversized hex, or non-hex input.
Common situations: Reusing the same signing key variable across both artifact types but with a prefix left on for one of them; env var populated from a secrets manager that base64-encodes by default; key generated by a library that emits the 64-byte expanded form.
Related errors
- signBacktestArtifact: privateKey must be 32 bytes (got ${pri
- both Ed25519 private and public PEM keys are required
- Encrypted buffer too short: need >= ${minLen}B, got ${buf.le
- Footer hash must be ${SHA256_SIZE} bytes, got ${footerHash.l
AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12).
Data as JSON: /api/errors/b2d8b480bcc7e01f.
Report an issue: GitHub.