Skip to content
CNMLCertificat Numérique de Métrologie Légale
← Docs

Guides

Verifying a certificate

2 min read · Guides

Verifying a certificate

This guide covers the CNML verification pipeline: the nine checks, how to run them, and how to interpret the results.

The nine-check pipeline

Every CNML certificate is verified through a data-driven pipeline. Each check is a module that exports a Check object. The pipeline iterates the CHECKS array and renders results generically.

Order Check What it verifies
1 XML well-formed The document is parseable XML
2 Schema valid The document conforms to the per-Recommendation JSON Schema
3 Signature valid The XMLDSig signature verifies against the embedded X.509 cert
4 Dimensional co-signatures Each co-signer (tester, calibration authority) verified on the same canonical payload
5 Scope valid The signer's IA is authorized for the Recommendation on the cert, and scope conditions hold
6 CRL status The certificate is not on a revocation list, and no bound authority state is revoked
7 Evaluation report bound The evaluation report digest matches the certificate's binding
8 Timestamp An OpenTimestamps proof anchors the cert to Bitcoin
9 Transparency The cert appears in the public Merkle transparency log

Earlier checks short-circuit later ones. If the XML is malformed, there is no point checking the signature.

Running the pipeline

import { runChecks } from "@oiml/cnml-crypto/checks";

const results = await runChecks(xmlString, {
  trustedKeys: [cryptoKey],
});

for (const result of results) {
  console.log(`${result.checkId}: ${result.status}`);
  // status: "pass" | "fail" | "warn" | "skip"
}
import { runChecks } from "@oiml/cnml-crypto/checks";

const results = await runChecks(xmlString, {
  trustedKeys: [cryptoKey],
});

for (const result of results) {
  console.log(`${result.checkId}: ${result.status}`);
  // status: "pass" | "fail" | "warn" | "skip"
}

The check context (ctx) accumulates state across checks: the parsed cert, the recommendation ID, the trusted certs. Later checks read from the context populated by earlier ones.

Adding a new check

The pipeline is open and closed. Adding a check requires one new file and one line in the CHECKS array:

// src/checks/my-check.ts
import type { Check, CheckResult } from "./types.ts";

export const myCheck: Check = {
  id: "my-check",
  label: "9. My custom check",
  continueOnFail: false,
  run: async (xml, ctx): Promise<CheckResult> => {
    // ... verification logic ...
    return { checkId: "my-check", status: "pass", reason: "OK" };
  },
};
// src/checks/my-check.ts
import type { Check, CheckResult } from "./types.ts";

export const myCheck: Check = {
  id: "my-check",
  label: "9. My custom check",
  continueOnFail: false,
  run: async (xml, ctx): Promise<CheckResult> => {
    // ... verification logic ...
    return { checkId: "my-check", status: "pass", reason: "OK" };
  },
};
// src/checks/index.ts
import { myCheck } from "./my-check.ts";
export const CHECKS: Check[] = [ /* ... */ , myCheck];
// src/checks/index.ts
import { myCheck } from "./my-check.ts";
export const CHECKS: Check[] = [ /* ... */ , myCheck];

Offline verification

The verifier downloads a trust-anchor bundle once. After that, it can verify certificates indefinitely without network access. The bundle contains the root CA certificates and their SHA-256 fingerprints.

The service worker at /sw.js caches the verify page and the trust-anchor bundle, making verification usable offline after one online visit.

Next steps