← Files OpsTruthARCHIVED FILE

test/donestate-verification-contract-v2.test.js

6.01 KB · Oct 5, 2026 · 18:32 UTC

↓ Download file

import assert from "node:assert/strict";
import { createHash, createPublicKey, verify } from "node:crypto";
import { readFile } from "node:fs/promises";
import test from "node:test";
import { canonicalJson } from "../src/canonical.js";
import { validateAgainstSchema } from "../scripts/validate-contracts.mjs";

const root = new URL("../contracts/donestate/", import.meta.url);
const readJson = async (path) => JSON.parse(await readFile(new URL(path, root), "utf8"));
const readBytes = async (path) => readFile(new URL(path, root));

function gitBlobSha(bytes) {
  return createHash("sha1").update(`blob ${bytes.length}\0`).update(bytes).digest("hex");
}

function sha256(value) {
  return createHash("sha256").update(value).digest("hex");
}

function responseErrors(response, responseSchema, reportSchema, attestationSchema) {
  const errors = [];
  const allowed = new Set(Object.keys(responseSchema.properties));
  for (const field of responseSchema.required) {
    if (!Object.hasOwn(response, field)) errors.push(`$.${field}: required property missing`);
  }
  for (const field of Object.keys(response)) {
    if (!allowed.has(field)) errors.push(`$.${field}: unknown property`);
  }
  if (response.contractVersion !== responseSchema.properties.contractVersion.const) {
    errors.push("$.contractVersion: const mismatch");
  }
  if (response.report) errors.push(...validateAgainstSchema(response.report, reportSchema, reportSchema, "$.report"));
  if (response.attestation) errors.push(...validateAgainstSchema(response.attestation, attestationSchema, attestationSchema, "$.attestation"));
  return errors;
}

async function verifyVector(vector, expectedDecision, schemas) {
  assert.equal(vector.schemaVersion, "2.0.0");
  assert.equal(vector.response.contractVersion, "donestate.verification-contract.v2");
  assert.equal(vector.response.report.decision, expectedDecision);
  assert.equal(vector.response.attestation.decision, expectedDecision);
  assert.deepEqual(responseErrors(vector.response, ...schemas), []);

  const handoff = { ...vector.handoff };
  delete handoff.handoffDigest;
  assert.equal(
    vector.handoff.handoffDigest,
    sha256(`donestate.verification-handoff.v2\0${canonicalJson(handoff)}`),
  );
  assert.equal(vector.response.report.runId, vector.handoff.runId);
  assert.equal(vector.response.report.handoffDigest, vector.handoff.handoffDigest);
  assert.equal(vector.response.report.verificationNonce, vector.handoff.verificationNonce);
  assert.equal(vector.response.attestation.runId, vector.handoff.runId);
  assert.equal(vector.response.attestation.handoffDigest, vector.handoff.handoffDigest);
  assert.equal(vector.response.attestation.verificationNonce, vector.handoff.verificationNonce);
  assert.equal(
    vector.response.attestation.verificationReportDigest,
    sha256(`opstruth.donestate-verification-report.v1\0${canonicalJson(vector.response.report)}`),
  );

  const { signature, ...unsigned } = vector.response.attestation;
  const publicKey = createPublicKey(signature.publicKeyPem);
  const der = publicKey.export({ type: "spki", format: "der" });
  assert.equal(signature.signerFingerprint, sha256(der));
  assert.equal(
    verify(
      null,
      Buffer.from(`donestate.verification-attestation.v2\0${canonicalJson(unsigned)}`),
      publicKey,
      Buffer.from(signature.signatureBase64, "base64"),
    ),
    true,
  );
}

test("OpsTruth consumes the byte-identical DoneState verification contract v2 corpus", async () => {
  const manifest = await readJson("manifest.json");
  assert.equal(manifest.sourceCommit, "9e33a7e4c8505eabd24df775e8292bfaa2906f43");
  for (const [path, expectedBlob] of Object.entries(manifest.artifacts)) {
    assert.equal(gitBlobSha(await readBytes(path)), expectedBlob, `${path} drifted from DoneState`);
  }

  const responseSchema = await readJson("verification-response-v2.schema.json");
  const reportSchema = await readJson("verification-report-v1.schema.json");
  const attestationSchema = await readJson("verification-attestation-v2.schema.json");
  const schemas = [responseSchema, reportSchema, attestationSchema];
  await verifyVector(await readJson("vectors/verification-contract-v2-verified.json"), "verified", schemas);
  await verifyVector(await readJson("vectors/verification-contract-v2-failed.json"), "failed", schemas);
  await verifyVector(await readJson("vectors/verification-contract-v2-uncertain.json"), "uncertain", schemas);
});

test("OpsTruth consumes the complete DoneState negative mutation catalogue and rejects strict-envelope drift", async () => {
  const responseSchema = await readJson("verification-response-v2.schema.json");
  const reportSchema = await readJson("verification-report-v1.schema.json");
  const attestationSchema = await readJson("verification-attestation-v2.schema.json");
  const schemas = [responseSchema, reportSchema, attestationSchema];
  const negative = await readJson("vectors/verification-contract-v2-negative.json");
  const ids = negative.mutations.map((mutation) => mutation.id).sort();
  assert.deepEqual(ids, [
    "altered_handoff",
    "decision_mismatch",
    "extra_attestation_field",
    "extra_response_field",
    "extra_signature_field",
    "future_observation",
    "missing_requirement",
    "replayed_nonce",
    "revoked_signer",
    "stale_observation",
    "unsupported_contract",
  ]);

  const verified = await readJson("vectors/verification-contract-v2-verified.json");
  for (const id of ["unsupported_contract", "extra_response_field", "extra_attestation_field", "extra_signature_field"]) {
    const mutation = negative.mutations.find((item) => item.id === id);
    const response = structuredClone(verified.response);
    if (id === "unsupported_contract") response.contractVersion = mutation.value;
    if (id === "extra_response_field") response.unsupported = mutation.value;
    if (id === "extra_attestation_field") response.attestation.unsupported = mutation.value;
    if (id === "extra_signature_field") response.attestation.signature.unsupported = mutation.value;
    assert.notDeepEqual(responseErrors(response, ...schemas), [], `${id} must fail closed`);
  }
});

SHA-256: c79dc03af63845eecd526ab25c5f2eb6730c73ab249fd61549467cd36082b261