ACF
acfstandard.io
Developer docs
FR
Signatures

Verify in Node.js

Verify the Ed25519 signature of an acf-mcp output using only Node’s standard crypto module — zero external dependencies.

iNote
The node:crypto module supports Ed25519 natively since Node 12. No npm dependency is required. The public key is encoded in SPKI base64 (the standard format for Ed25519 public keys).

Quick version

If you already have the acf-mcp package in your project, import the embedded verification helper:

verify-quick.tstypescript
import { readFileSync } from "node:fs";
import { verifyDoctrineSignature } from "acf-mcp/lib/doctrine-signature";

const signed = JSON.parse(readFileSync("./tool-output.json", "utf8"));
const ok = verifyDoctrineSignature({
  contentHash: signed.doctrine_hash,
  signature: signed.doctrine_signature,
  publicKey: signed.doctrine_public_key,
});
console.log(ok ? "✓ signature valid" : "✗ signature INVALID");

Standalone (zero dependency)

If you want to embed verification in an audit pipeline that does not depend on the acf-mcp package (e.g. a CI job auditing archived outputs), the snippet below is enough. It uses only node:crypto.

verify-doctrine.tstypescript
import { createPublicKey, verify } from "node:crypto";
import { readFileSync } from "node:fs";

// 1. Load the public key embedded in the acf-mcp release.
//    (For Node.js apps consuming the npm package, this lives at
//    node_modules/acf-mcp/dist/archive/doctrine-v1.0.json under
//    .meta.doctrine_public_key — the SPKI base64 form.)
const PUBLIC_KEY_SPKI_B64 =
  "MCowBQYDK2VwAyEAojtKfh20SGGV63LMETjZBXRWo2tY0viAYziG/y3/L0s=";

const publicKey = createPublicKey({
  key: Buffer.from(PUBLIC_KEY_SPKI_B64, "base64"),
  format: "der",
  type: "spki",
});

// 2. Read the signed tool output (anything returned by an acf.* tool).
const signed = JSON.parse(readFileSync("./tool-output.json", "utf8"));

// 3. Recompute what was signed: the doctrine_hash field as raw UTF-8.
const message = Buffer.from(signed.doctrine_hash, "utf8");

// 4. Strip the "ed25519:" prefix and decode the signature from base64.
const sigB64 = signed.doctrine_signature.replace(/^ed25519:/, "");
const signature = Buffer.from(sigB64, "base64");

// 5. Verify.
const ok = verify(null, message, publicKey, signature);
console.log(ok ? "✓ signature valid" : "✗ signature INVALID");

What is signed

The signature is taken over the content hash (the doctrine_hash field, sha256:hex encoded) as a UTF-8 string. That hash is itself the SHA-256 of the canonicalised doctrine bundle (files sorted by path, raw bytes, paths relative to the content root). The computation script is in the repo: scripts/build-archive-bundle.ts.

Failure modes

  • signature INVALID the content was tampered with OR the wrong public key is in use (wrong acf-mcp version). Re-download the output end to end and re-verify.
  • ERR_OSSL_EVP_DECODE_ERROR the signature or key is not valid base64. Check that no character was lost on copy-paste.
  • UnsupportedKeyType Node < 12. Upgrade to a supported version (node ≥ 18 per our engine).