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 trusted

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Pass a 32-byte Ed25519 seed as 64 hex characters with no 'ed25519:' prefix.
  2. Strip the prefix and trim whitespace before calling: key.replace(/^ed25519:/, '').trim().
  3. Share one canonical key-loading helper across signBacktestArtifact and signAttributionArtifact so both receive an identically normalized 32-byte hex string.
  4. 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

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


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