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

  1. Provide a 32-byte Ed25519 seed as 64 hex characters with no prefix: strip any leading 'ed25519:' before calling.
  2. Generate a valid key with a known tool, e.g. node -e "const c=require('crypto');console.log(c.randomBytes(32).toString('hex'))".
  3. Trim whitespace/newlines from the env-sourced key before passing it.
  4. 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

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


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/0869a65ebbedd549. Report an issue: GitHub.