{"record":{"id":"0869a65ebbedd549","repo":"ruvnet/ruflo","slug":"signbacktestartifact-privatekey-must-be-32-bytes","errorCode":null,"errorMessage":"signBacktestArtifact: privateKey must be 32 bytes (got ${privateKey.length})","messagePattern":"signBacktestArtifact: privateKey must be 32 bytes \\(got (.+?)\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/ruflo-neural-trader/src/signed-artifact.ts","lineNumber":85,"sourceCode":" * `SignedBacktestArtifact` envelope.\n *\n * The signature covers the artifact body WITHOUT `witnessSignature` and\n * WITHOUT `witnessPublicKey` (CWE-347 pattern). This means an attacker who\n * swaps the served `witnessPublicKey` field cannot bypass verification when\n * the verifier pins to a trusted key (which is the only safe verifier).\n *\n * @param body                 — the artifact body (everything except signature fields + schema)\n * @param privateKeyHex        — 32-byte Ed25519 private key as hex string (no 'ed25519:' prefix)\n * @returns                      — the signed artifact ready to be stored\n */\nexport async function signBacktestArtifact(\n  body: SignedBacktestArtifactBody,\n  privateKeyHex: string,\n): Promise<SignedBacktestArtifact> {\n  const ed = await import('@noble/ed25519');\n  const privateKey = hexToBytes(privateKeyHex);\n  if (privateKey.length !== 32) {\n    throw new Error(\n      `signBacktestArtifact: privateKey must be 32 bytes (got ${privateKey.length})`,\n    );\n  }\n\n  // Canonical body = the artifact WITHOUT signature fields, plain JSON.stringify.\n  // Matches scripts/smoke-plugin-registry-signature.mjs:193-200.\n  const canonical = canonicalBytes(body);\n  const signatureBytes = await ed.signAsync(canonical, privateKey);\n  const publicKeyBytes = await ed.getPublicKeyAsync(privateKey);\n\n  return {\n    schema: 'ruflo-neural-trader-backtest/v1',\n    ...body,\n    witnessPublicKey: `ed25519:${bytesToHex(publicKeyBytes)}`,\n    witnessSignature: bytesToHex(signatureBytes),\n  };\n}\n","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/plugins/ruflo-neural-trader/src/signed-artifact.ts#L67-L103","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait signBacktestArtifact(body, process.env.SIGNING_KEY); // 'ed25519:abcd...'\n\n// after\nconst raw = process.env.SIGNING_KEY!.replace(/^ed25519:/, '').trim();\nawait signBacktestArtifact(body, raw);","handlingStrategy":"validation","validationCode":"function isValidEd25519Hex(key: string): boolean {\n  const clean = key.replace(/^ed25519:/, '').trim();\n  return /^[0-9a-fA-F]{64}$/.test(clean);\n}\nif (!isValidEd25519Hex(privateKeyHex)) throw new Error('expected 64 hex chars (32-byte Ed25519 seed)');\nawait signBacktestArtifact(body, privateKeyHex.replace(/^ed25519:/, '').trim());","typeGuard":"function isEd25519SeedHex(key: string): key is string {\n  return /^[0-9a-fA-F]{64}$/.test(key.replace(/^ed25519:/, '').trim());\n}","tryCatchPattern":"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; }","preventionTips":["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."],"tags":["crypto","ed25519","signing","neural-trader"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}