Attestation

Cryptographic attestation of every response

Deterministic JSON serialization per RFC 8785 (JCS), then ML-DSA-65 per NIST FIPS 204.

Records generated before the May 30, 2026 migration were signed with Ed25519 (RFC 8032) and remain verifiable with the legacy public key.

Why attestation

Every Stratalize MCP and x402 JSON response that passes the signing path carries an ML-DSA-65 signature over a JCS-canonicalized payload, allowing offline verification that bytes originated from Stratalize and were not altered after signing. The mechanism is standards-based (RFC 8785 + NIST FIPS 204), not a proprietary MAC string.

The `_stratalize` envelope

The attestation object is attached at the top level as _stratalize alongside tool-specific fields (for example flat tool fields, or a data wrapper on HTTP x402 routes — always verify over the exact JSON object returned).

// Tool response (other tool-specific top-level fields omitted).
interface StratalizeAttestedResponse {
  data?: unknown;
  _stratalize: {
    signed_at: string;
    signature: string;
    algorithm: "ML-DSA-65";
    public_key_url: string;
    canonicalization: "JCS-RFC8785";
    version: "1";
    unsigned?: true;
    /**
     * Per-agent signature over an agent-scoped payload (nested attestation).
     * Q3 2026 — not yet emitted by production; do not rely on this field today.
     */
    _agent?: {
      agent_id: string;
      signed_at: string;
      signature: string;
      public_key_url?: string;
      canonicalization: "JCS-RFC8785";
      version: "1";
    };
  };
}

Implementation reference: lib/mcp/response-signer.ts. Signing strips _stratalize before canonicalization, then signs the UTF-8 bytes of the canonical string.

Verification algorithm

  1. Parse the response JSON. Read _stratalize.signature, signed_at, and public_key_url.
  2. If unsigned: true, treat the response as not cryptographically attested.
  3. Build a shallow copy of the response object and delete _stratalize. The signed material is the remaining object only (RFC 8785 JCS via the same canonicalizer Stratalize uses in production).
  4. Compute canonical = canonicalize(payloadWithoutEnvelope). If canonicalization fails, verification fails.
  5. Fetch signing keys: GET public_key_url returns JSON with a keys array. Select the entry whose algorithm matches _stratalize.algorithm (typically ML-DSA-65, hex-encoded public key per NIST FIPS 204). Cache aggressively; honor Cache-Control on that endpoint.
  6. Verify ML-DSA-65: ml_dsa65.verify( base64url_decode(signature), UTF-8(canonical), publicKeyHex ) per NIST FIPS 204. Node does not ship ML-DSA-65 natively; use @noble/post-quantum/ml-dsa.js or an equivalent library.
  7. If verification fails, treat the payload as untrusted for compliance or billing evidence.

TypeScript verifier

Node 20+. Uses the same canonicalize package, ML-DSA-65 verify path, and base64url wire format as lib/mcp/response-signer.ts. Install: npm install canonicalize @noble/post-quantum.

import canonicalize from "canonicalize";
import { ml_dsa65 } from "@noble/post-quantum/ml-dsa.js";

type SigningKeyEntry = { algorithm: string; format: string; key: string };

export interface VerificationResult {
  valid: boolean;
  signedAt?: string;
  reason?: string;
}

export async function verifyStratalizeResponse(
  response: Record<string, unknown>,
  publicKeyHex?: string,
): Promise<VerificationResult> {
  const env = response["_stratalize"];
  if (env === null || typeof env !== "object" || Array.isArray(env)) {
    return { valid: false, reason: "missing _stratalize envelope" };
  }
  const att = env as Record<string, unknown>;
  if (att["unsigned"] === true) {
    return { valid: false, reason: "envelope marked unsigned" };
  }
  const sig = att["signature"];
  if (typeof sig !== "string" || !sig) {
    return { valid: false, reason: "missing signature" };
  }
  const signedAt = typeof att["signed_at"] === "string" ? att["signed_at"] : undefined;
  const algorithm = typeof att["algorithm"] === "string" ? att["algorithm"] : "ML-DSA-65";
  let keyHex = publicKeyHex;
  if (!keyHex) {
    const url = att["public_key_url"];
    if (typeof url !== "string" || !url) return { valid: false, reason: "missing public_key_url" };
    const res = await fetch(url);
    if (!res.ok) return { valid: false, reason: `fetch signing key HTTP ${res.status}` };
    const doc = (await res.json()) as { keys?: SigningKeyEntry[] };
    const entry = doc.keys?.find((k) => k.algorithm === algorithm);
    if (!entry || typeof entry.key !== "string") {
      return { valid: false, reason: `signing-key JSON missing ${algorithm} key` };
    }
    keyHex = entry.key;
  }
  const body: Record<string, unknown> = { ...response };
  delete body["_stratalize"];
  const canonical = canonicalize(body);
  if (canonical === undefined) return { valid: false, reason: "JCS canonicalize returned undefined" };
  try {
    if (algorithm !== "ML-DSA-65") {
      return { valid: false, reason: `unsupported algorithm: ${algorithm}` };
    }
    const ok = ml_dsa65.verify(
      Buffer.from(sig, "base64url"),
      Buffer.from(canonical, "utf8"),
      Buffer.from(keyHex, "hex"),
    );
    return ok ? { valid: true, signedAt } : { valid: false, signedAt, reason: "ML-DSA-65 verify failed" };
  } catch (e) {
    return { valid: false, reason: e instanceof Error ? e.message : "verify threw" };
  }
}

Public key distribution

The active verification key is published at https://www.stratalize.com/api/trust/signing-key as JSON: a keys array with algorithm: "ML-DSA-65" (hex public key, NIST FIPS 204), current_algorithm, and legacy_public_key for pre-migration verification. This is not a JWKS document; consumers select the matching keys[] entry by algorithm.

Rotation. Keys rotate on a quarterly cadence for operational hygiene. After rotation, prior keys remain valid for verifying historically signed responses for one year. The envelope’s public_key_url always points at the current fetch path; callers should key cache entries by key_version when present in the signing-key JSON.

What signatures prove (and do not)

ML-DSA-65 signatures prove

  • Provenance — the canonicalized payload was processed by Stratalize code holding the signing key at signed_at (subject to your trust model for key custody).
  • Integrity — any bit flip in the signed JSON object after signing breaks verification.
  • Non-repudiation (operational) — Stratalize cannot plausibly deny having emitted this exact canonical byte sequence without compromising the private key.

They do not prove

  • Upstream factual accuracy — the signature covers Stratalize’s output, not independent truth of every numeric field against the physical world or third-party systems.
  • Data freshness as-of external sources signed_at is when the response was signed, not a universal timestamp for every upstream feed’s last refresh.
  • Future replay or stability — the same logical question at a later time may yield a different signed object as inputs change; signatures are not a commitment to repeatability.
  • Client or transport safety — TLS still protects bytes in flight; attestation binds the JSON object Stratalize intended to return, not your local storage after you transform it.

Audit lineage

Each signed production response is suitable for inclusion in Stratalize’s audit records (response fingerprint, signing time, tool name, org tenancy where applicable). Synthesis and governance lineage views for authenticated organizations are exposed through org MCP tools documented on /docs/mcp; this page does not specify internal hash algorithms beyond “cryptographic” or database schemas for lineage tables.

Related: x402, Tool catalog, Docs home.