Minimum Integration, 3 Steps
- Load, Parse the CER bundle JSON (use
importCer(json)orJSON.parse). - Verify locally, Call
verify(bundle)to check certificate hash and snapshot hashes. - Verify stamp, If a signed receipt is present, call
verifyBundleAttestation(bundle, { nodeUrl }).
Local Integrity Verification
Local verification re-computes all hashes from the bundle's data and compares them to the recorded values. No network call is needed. This works entirely offline.
import { verify } from '@nexart/ai-execution';
// or for Code Mode:
// import { verify } from '@nexart/codemode-sdk';
const bundle = JSON.parse(fs.readFileSync('record.cer.json', 'utf8'));
const result = verify(bundle);
if (result.ok) {
console.log('✓ Record intact, hashes match');
} else {
console.log('✗ Integrity breach:', result.code);
console.log(' Reason:', result.details?.reason);
console.log(' Expected:', result.details?.expected);
console.log(' Actual:', result.details?.actual);
}What gets checked:
certificateHash, SHA-256 of canonical JSON of{ bundleType, version, createdAt, snapshot }inputHash, SHA-256 of the input fieldoutputHash, SHA-256 of the output field- Schema validity, correct
bundleType,version, required fields present
Node Stamp Verification
If the bundle includes a signed receipt (from a canonical attestation node), you can verify the Ed25519 signature offline. The only network call is fetching the node's public keys from /.well-known/nexart-node.json, verifyBundleAttestation does not call /api/attest or submit anything to the node.
import { verifyBundleAttestation } from '@nexart/ai-execution';
const result = await verifyBundleAttestation(bundle, {
nodeUrl: 'https://nexart-canonical-renderer-production.up.railway.app',
});
console.log(result.ok); // true or false
console.log(result.code); // "OK", "ATTESTATION_INVALID_SIGNATURE", etc.
// Detailed mismatch info (when result.ok is false):
console.log(result.details);
// { reason: "signature_mismatch", kid: "key-2025-01", ... }This performs the following automatically:
- Extracts the signed receipt and signature from the bundle
- Cross-checks
receipt.certificateHash === bundle.certificateHash(prevents receipt-swapping) - Fetches the node's public keys from
/.well-known/nexart-node.json - Selects the correct key by
kidoractiveKid - Verifies the Ed25519 signature over the canonical JSON bytes of the receipt
Browser vs Server Verification
| Environment | SHA-256 | Ed25519 | Notes |
|---|---|---|---|
| Browser | WebCrypto (crypto.subtle.digest) | SDK verifier (@noble/ed25519) | No native Ed25519 in WebCrypto; SDK bundles a pure-JS verifier |
| Node.js | crypto.createHash | @noble/ed25519 or built-in crypto.verify | SDK uses noble by default; may use built-in crypto when available |
The SDK handles this automatically. verify() and verifyBundleAttestation() work in both environments without configuration.
Recânon: For a zero-setup browser verification experience, upload your CER JSON to recanon.xyz. Verification runs entirely in your browser. No data is sent to any server.
What "Stamp Incomplete" Means
A bundle may have been attested but lack the full signed receipt fields introduced in v0.5.0. This is not an error, it means the bundle was attested under an earlier SDK version.
| Fields Present | Status | What You Can Verify |
|---|---|---|
receipt + signature + attestorKeyId | Fully signed | Local integrity + node signature |
attestationId + nodeRuntimeHash (no signature) | Legacy attestation | Local integrity only |
| No attestation fields | Not attested | Local integrity only |
Legacy stamp fields may appear either at the top level of the bundle or nested under meta.attestation, depending on the producer. The SDK's getAttestationReceipt() normalizes both locations automatically.
verifyBundleAttestation() returns ATTESTATION_MISSING when no signed receipt is found. This does not invalidate the bundle itself. Local integrity can still pass.
Verification semantics
status: "verified"means integrity match only. It does not imply that a signature was verified.checks.signaturemust equal"pass"for authenticity.- Unsigned bundles may still return
"verified", integrity passed, no signed receipt was present. verifyCer()provides classification; the node recomputes verification independently and aligns with the SDK classification.
Trust boundaries
- Integrity, fully independent. Bundle + SHA-256 + matching canonicalisation is sufficient.
- Authenticity, depends on trust in the binding between the public key and the node operator.
- Timestamp, two layers: a node-issued timestamp providing an ordering guarantee within the node's signing chain, and an RFC 3161 timestamp issued via DigiCert's public timestamp authority (applied by default) providing an independent anchor of when the record existed.
- Transparency, internal to the node today; no public Merkle log or third-party inclusion proof.
- Completeness, not enforced by the substrate; ensuring every relevant execution is submitted is an integration-level responsibility.
Stranger verification (no NexArt code)
A third party can verify a CER with only the bundle JSON, the node's published public key, and standard crypto libraries:
- Recompute
certificateHash= SHA-256 of the canonical JSON of{ bundleType, version, createdAt, snapshot, context?, contextSummary?, policyEvaluation? }. - Reconstruct the signable payload by canonicalising the
receiptobject, and cross-checkreceipt.certificateHashmatches the bundle. - Verify the Ed25519 signature with any standard library, using the public key from
/.well-known/nexart-node.jsonselected bykid.
Verification uses protocol-pinned canonicalisation: nexart-v1 for Protocol 1.2.0, RFC 8785 (JCS) for Protocol 1.3.0. You are not required to trust NexArt's implementation, Protocol 1.3.0 enables verification using a public standard (RFC 8785) and standard Ed25519 libraries.
Fix: Re-attest the bundle with a current node to obtain receipt + signature + kid.