ruvnet/ruflo · error · Error
signBacktestArtifact: privateKey must be 32 bytes (got ${pri
Error message
signBacktestArtifact: privateKey must be 32 bytes (got ${privateKey.length}) What it means
Thrown by signBacktestArtifact() after hex-decoding the supplied private key: Ed25519 private keys must be exactly 32 bytes, and the decoder result is length-checked before any signing happens. The key is expected as raw hex with no 'ed25519:' prefix; 64 hex characters decode to 32 bytes. Anything else (wrong length, embedded prefix, base64, seed phrase) is rejected up front.
Source
Thrown at plugins/ruflo-neural-trader/src/signed-artifact.ts:85
* `SignedBacktestArtifact` envelope.
*
* The signature covers the artifact body WITHOUT `witnessSignature` and
* WITHOUT `witnessPublicKey` (CWE-347 pattern). This means an attacker who
* swaps the served `witnessPublicKey` field cannot bypass verification when
* the verifier pins to a trusted key (which is the only safe verifier).
*
* @param body — the artifact body (everything except signature fields + schema)
* @param privateKeyHex — 32-byte Ed25519 private key as hex string (no 'ed25519:' prefix)
* @returns — the signed artifact ready to be stored
*/
export async function signBacktestArtifact(
body: SignedBacktestArtifactBody,
privateKeyHex: string,
): Promise<SignedBacktestArtifact> {
const ed = await import('@noble/ed25519');
const privateKey = hexToBytes(privateKeyHex);
if (privateKey.length !== 32) {
throw new Error(
`signBacktestArtifact: privateKey must be 32 bytes (got ${privateKey.length})`,
);
}
// Canonical body = the artifact WITHOUT signature fields, plain JSON.stringify.
// Matches scripts/smoke-plugin-registry-signature.mjs:193-200.
const canonical = canonicalBytes(body);
const signatureBytes = await ed.signAsync(canonical, privateKey);
const publicKeyBytes = await ed.getPublicKeyAsync(privateKey);
return {
schema: 'ruflo-neural-trader-backtest/v1',
...body,
witnessPublicKey: `ed25519:${bytesToHex(publicKeyBytes)}`,
witnessSignature: bytesToHex(signatureBytes),
};
}
View on GitHub (pinned to 6b01dc5a68)
Solutions
- Provide a 32-byte Ed25519 seed as 64 hex characters with no prefix: strip any leading 'ed25519:' before calling.
- Generate a valid key with a known tool, e.g. node -e "const c=require('crypto');console.log(c.randomBytes(32).toString('hex'))".
- Trim whitespace/newlines from the env-sourced key before passing it.
- If your source key is 64 bytes (expanded form), derive/extract the 32-byte seed instead of passing the full secret key.
Example fix
// before await signBacktestArtifact(body, process.env.SIGNING_KEY); // 'ed25519:abcd...' // after const raw = process.env.SIGNING_KEY!.replace(/^ed25519:/, '').trim(); await signBacktestArtifact(body, raw);
Defensive patterns
Strategy: validation
Validate before calling
function isValidEd25519Hex(key: string): boolean {
const clean = key.replace(/^ed25519:/, '').trim();
return /^[0-9a-fA-F]{64}$/.test(clean);
}
if (!isValidEd25519Hex(privateKeyHex)) throw new Error('expected 64 hex chars (32-byte Ed25519 seed)');
await signBacktestArtifact(body, privateKeyHex.replace(/^ed25519:/, '').trim()); Type guard
function isEd25519SeedHex(key: string): key is string {
return /^[0-9a-fA-F]{64}$/.test(key.replace(/^ed25519:/, '').trim());
} Try / catch
try { await signBacktestArtifact(body, key); } catch (e) { if (e instanceof Error && /privateKey must be 32 bytes/.test(e.message)) { throw new Error('Signing key misconfigured: pass a 64-hex-char Ed25519 seed without prefix', { cause: e }); } throw e; } Prevention
- Strip 'ed25519:' and trim before passing the key.
- Store only the 32-byte seed (64 hex chars) in secrets managers.
- Share one key-normalization helper across all signing calls.
When it happens
Trigger: Passing privateKeyHex that decodes to a byte length other than 32: a 16-byte key (32 hex chars), a 64-byte key (128 hex chars), a key with the literal 'ed25519:' prefix still attached, a base64-encoded key, or a non-hex string that hexToBytes truncates/parses oddly.
Common situations: Copying a key from a config that stores it as 'ed25519:<hex>' and forgetting to strip the prefix; using a full 64-byte expanded Ed25519 secret key instead of the 32-byte seed; passing an HF token or API key by mistake; trailing whitespace/newline in the env var inflating the decoded length.
Related errors
- signAttributionArtifact: privateKey must be 32 bytes (got ${
- 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/0869a65ebbedd549.
Report an issue: GitHub.