Governed Attestation Contract (Node 0.30.1)
Exact governed /api/attest request and response, receipt and verification-envelope signature procedures, stateless verification, persistence and visibility for Node 0.30.1.
Every field on this page comes from Node production source and executable tests for the released Node 0.30.1 source, with @nexart/governed-execution@0.4.2 as the package contract. Placeholders in angle brackets mark dynamic values only. No fields from AI Execution CERs are included. See the source ledger.
Two different request wrappers
| Route | Auth | Body |
|---|---|---|
POST /api/attest | Authorization: Bearer <api_key> | The complete sealed CER itself, top-level |
POST /v1/cer/verify | None | { "bundle": <sealed CER> } |
{ bundle } for /api/attest, or send it unwrapped to /v1/cer/verify.Attestation request
| Property | Contract |
|---|---|
| Method and path | POST /api/attest |
| Content type | application/json |
| Family | Exact bundleType: "cer.governed.execution.v1" |
| CER version | "0.1" or "0.2". Not the Node version, SDK version or AI protocol version. |
| Logical execution identity | evidence.bundle.bundleId, not threadId |
| Signer selection | The Node selects the key. Neither kid nor nodeId is sent. |
X-NexArt-Visibility | Optional; exactly public or private. hidden is not an accepted header value. Omitted: existing proof's visibility, otherwise hidden. |
X-NexArt-Readable-Fields | Rejected for governed records with READABLE_FIELDS_UNSUPPORTED. Consequential-family feature only. |
POST /api/attest
Authorization: Bearer <TEMPORARY_API_KEY>
Content-Type: application/json{
"bundleType": "cer.governed.execution.v1",
"createdAt": "<RFC3339_CREATED_AT>",
"version": "0.1",
"profile": {
"id": "cage-governance-v1",
"version": "1",
"contract": "urn:cage:governance:v1:attestation-bundle",
"source": { "system": "CAGE", "profile": "provider_02" }
},
"evidence": {
"bundle": {
"bundleId": "<UUID_BUNDLE>",
"threadId": "<CORRELATION_ID>",
"startedAt": "<RFC3339_START>",
"completedAt": "<RFC3339_COMPLETION>",
"terminalPath": "happy_path",
"steps": [
{
"stepId": "<UUID_STEP_1>",
"nodeName": "execution_analyst",
"parentStepIds": [],
"timestampUtc": "<RFC3339_STEP_1>",
"durationMs": 1,
"signals": {},
"metadata": {},
"stateHash": "<64_LOWERCASE_HEX>"
},
{
"stepId": "<UUID_STEP_2>",
"nodeName": "evaluator",
"parentStepIds": ["<UUID_STEP_1>"],
"timestampUtc": "<RFC3339_STEP_2>",
"durationMs": 1,
"signals": {},
"metadata": {},
"stateHash": "<64_LOWERCASE_HEX>"
}
]
},
"topology": {
"nodes": ["evaluator", "execution_analyst"],
"parentEdges": {
"evaluator": ["execution_analyst"],
"execution_analyst": ["evaluator"]
},
"terminalNode": "evaluator"
}
},
"protectedSet": {
"stabilitySchemeId": "jcs-v1",
"protectedSetId": "nexart.governed.execution.v1.protected-set.v1",
"protectedFields": ["bundleType", "createdAt", "version", "profile", "evidence"]
},
"certificateHash": "sha256:<64_LOWERCASE_HEX>"
}Processing order: JSON parser, structure guard, API key, header checks, quota, family recognition, owner-scoped replay lookup, SDK verifyGovernedExecution (rejected unless ok === true), mutation and replay handling, transactional signing and persistence. The Node does not replace the submitted certificateHash.
Source-default limits
- HTTP JSON body: 8 MiB default, configurable via
MAX_JSON_BODYup to 32 MiB. - Structure guard: depth 64, 100,000 total JSON nodes, 10,000 items per container (413
REQUEST_STRUCTURE_TOO_COMPLEX). - SDK evidence: depth 32, 4,096 array items, 4,096 object keys, 1 MiB canonical evidence.
- CAGE SDK: 256 steps, 1,024 topology nodes, 4,096 topology edges.
- Monthly account quota applies.
/api/attestdoes not mount the authenticated per-minute limiter.
These are source defaults and caps, not observations of production configuration.
Successful response (HTTP 200)
{
"ok": true,
"bundleType": "cer.governed.execution.v1",
"certificateHash": "sha256:<CERTIFICATE_HEX>",
"attestationId": "<UUID_ATTESTATION>",
"nodeRuntimeHash": "sha256:<RUNTIME_HEX>",
"protocolVersion": "0.1",
"issuedAt": "<RFC3339_ATTESTED_AT>",
"kid": "<KID>",
"attestorKeyId": "<KID>",
"attestation": {
"attestedAt": "<RFC3339_ATTESTED_AT>",
"attestationId": "<UUID_ATTESTATION>",
"bundleType": "cer.governed.execution.v1",
"certificateHash": "sha256:<CERTIFICATE_HEX>",
"nodeRuntimeHash": "sha256:<RUNTIME_HEX>",
"protocolVersion": "0.1",
"requestId": "<UUID_ATTESTATION>",
"verified": true,
"checks": ["governed_execution"]
},
"receipt": {
"receiptVersion": "attestation.receipt.v1",
"attestationId": "<UUID_ATTESTATION>",
"certificateHash": "sha256:<CERTIFICATE_HEX>",
"nodeRuntimeHash": "sha256:<RUNTIME_HEX>",
"protocolVersion": "0.1",
"attestedAt": "<RFC3339_ATTESTED_AT>",
"timestampType": "node-issued",
"nodeId": "<NODE_ID>",
"attestorKeyId": "<KID>"
},
"signature": "<BASE64URL_RECEIPT_SIGNATURE>",
"verificationEnvelope": {
"algorithm": "Ed25519",
"attestation": {
"attestationId": "<UUID_ATTESTATION>",
"attestedAt": "<RFC3339_ATTESTED_AT>",
"kid": "<KID>",
"nodeRuntimeHash": "sha256:<RUNTIME_HEX>",
"protocolVersion": "0.1"
},
"canonicalization": "jcs",
"envelopeType": "nexart.verification.envelope.v2",
"kid": "<KID>",
"scope": "whitelist",
"signedFields": {
"attestation": ["attestationId", "attestedAt", "kid", "nodeRuntimeHash", "protocolVersion"],
"bundle": ["bundleType", "version", "createdAt", "profile", "evidence", "protectedSet"]
},
"specVersion": "v0.16.1"
},
"verificationEnvelopeSignature": "<BASE64URL_ENVELOPE_SIGNATURE>",
"visibility": "hidden",
"governedVerification": {
"certificateIntegrity": "valid",
"cageSchemaValidity": "valid",
"causalGraphValidity": "valid",
"topologyValidation": "not-supplied",
"resourceSafety": "valid",
"stateHashVerification": "not-performed",
"producerAuthentication": "not-performed",
"completeness": "not-claimed",
"warnings": []
},
"verificationUrl": "https://verify.nexart.io/c/sha256%3A<CERTIFICATE_HEX>"
}| Field | Meaning |
|---|---|
ok | true |
certificateHash | The caller's sealed hash, unchanged |
attestationId | Node-generated UUID; equals attestation.requestId on the fresh path |
nodeRuntimeHash | sha256:<hex> of the canonical Node runtime descriptor |
protocolVersion | Mirrors the CER version: "0.1" or "0.2" |
issuedAt | Node attestedAt |
kid, attestorKeyId | The selected signing key ID |
attestation.checks | Array ["governed_execution"]. Not the stateless checks object. |
receipt, signature | Receipt and its base64url Ed25519 signature (procedure) |
verificationEnvelope, verificationEnvelopeSignature | v2 descriptor and projection signature (procedure) |
visibility | Actual local visibility: hidden, public or private |
governedVerification | Nine-key report (fields) |
verificationUrl | Formatted string. Not a read-after-write publication check. |
timestamp | Optional; present only when a TSA is configured |
Absent from this response: the CER, top-level status, top-level nodeId (it is inside the receipt), receiptSignature, signatureEncoding, top-level signedFields (inside the envelope), persisted, published, publicRecordId and readableFields. For the cyclic-topology request above, topologyValidation is "valid".
With a configured, successful RFC 3161 TSA, the receipt adds timestampStatus: "ok" and timestampProvider, timestampType becomes "rfc3161", and the response adds:
{
"timestamp": {
"type": "rfc3161",
"status": "ok",
"provider": "<TSA_PROVIDER>",
"token": "<BASE64_DER_TOKEN>"
}
}Token bytes are outside the certificate, receipt and envelope signed perimeters. No production TSA configuration is implied.
Replay and mutation
- An exact owner-authorized replay returns the original response with
X-Idempotent-Replay: true. No new identity is minted. - The same
bundleIdwith a different valid hash returns 409EXECUTION_MUTATION_DETECTED.
Failure responses
| HTTP | Condition |
|---|---|
| 400 | Invalid JSON or shape, unsupported family, invalid visibility or readability headers |
| 400 | SDK rejection: { "error": "INVALID_BUNDLE", "reasonCode": "<SDK_CODE>", "details": ["<SDK_ERROR>"] } |
| 401 | Missing, invalid or inactive Bearer API key (UNAUTHORIZED) |
| 404 | Owner or privacy authorization failure, indistinguishable from a nonexistent record |
| 409 | Logical execution mutation, visibility conflict, conflicting replay |
| 413 | PAYLOAD_TOO_LARGE or REQUEST_STRUCTURE_TOO_COMPLEX |
| 429 | Monthly QUOTA_EXCEEDED; the failed-authentication limiter may also reject |
| 503 | Auth database unavailable under fail-closed metering; bounded lock or service-unavailable paths |
| 500 | Persistence, signing or internal failure. Do not assume success. |
Error shapes are branch-specific, not a uniform object.
Receipt signature verification
The receipt contains, in construction order:
receiptVersion, attestationId, certificateHash, nodeRuntimeHash,
protocolVersion, attestedAt, timestampType, nodeId, attestorKeyId
(+ timestampStatus, timestampProvider when supplied)Every field actually present in the receipt is signed. There is no signed-field subset.
bytes = UTF8(canonicalize(receipt)) // @nexart/ai-execution toCanonicalJson
signature = base64url(Ed25519.sign(bytes)) // URL-safe, no paddingThe canonicalizer sorts object keys, preserves array order, uses JSON string escaping and ECMAScript number serialization, emits no whitespace, rejects non-finite numbers and omits undefined members. Do not normalize Unicode, sort arrays or substitute ordinary JSON serialization.
- Obtain
receiptandsignature. - Read
receipt.attestorKeyId; cross-check with responsekidandverificationEnvelope.attestation.kid. - Resolve that exact
kidinkeys[]ofGET /.well-known/nexart-node.json, notkeys[0]oractiveKid. Rotated or deprecated keys can verify older signatures. - Build the Ed25519 public key from
publicKeyJwk,publicKeySpkiB64or the raw 32-byte base64urlpublicKey(SPKI DER prefix302a300506032b6570032100). - Apply key lifecycle policy: reject unknown or revoked keys, enforce explicit validity windows. A boot-time
notBeforeon the active key is not an authoritative lower bound for old records. - Base64url-decode the signature and verify Ed25519 over the canonical receipt bytes.
- Cross-check
certificateHash,attestationId,nodeRuntimeHashandprotocolVersionagainst the CER and envelope.
import { createPublicKey, verify } from "node:crypto";
import { toCanonicalJson } from "@nexart/ai-execution"; // the canonicalizer the Node uses
const NODE = process.env.NEXART_NODE_ENDPOINT; // e.g. https://node.nexart.io
const { receipt, signature, kid } = attestResponse;
// 1-2. Key ID comes from the receipt; cross-check the response and envelope.
const receiptKid = receipt.attestorKeyId;
if (receiptKid !== kid || receiptKid !== attestResponse.verificationEnvelope.attestation.kid) {
throw new Error("kid mismatch");
}
// 3. Resolve that exact kid. Never use keys[0] or activeKid by position.
const discovery = await (await fetch(`${NODE}/.well-known/nexart-node.json`)).json();
const key = discovery.keys.find((k) => k.kid === receiptKid);
if (!key || key.revoked || key.status === "revoked") throw new Error("unknown or revoked key");
// 4. Raw 32-byte base64url key -> SPKI DER with the Ed25519 prefix.
const spki = Buffer.concat([
Buffer.from("302a300506032b6570032100", "hex"),
Buffer.from(key.publicKey, "base64url"),
]);
const publicKey = createPublicKey({ key: spki, format: "der", type: "spki" });
// 6. Ed25519 over UTF-8 canonical JSON of the WHOLE receipt.
const bytes = Buffer.from(toCanonicalJson(receipt), "utf8");
const receiptOk = verify(null, bytes, publicKey, Buffer.from(signature, "base64url"));
if (!receiptOk) throw new Error("receipt signature invalid");
// 7. Bind the receipt to the CER you hold.
if (receipt.certificateHash !== cer.certificateHash) throw new Error("hash mismatch");
if (receipt.protocolVersion !== cer.version) throw new Error("version mismatch");A valid receipt signature proves that the resolved Node key signed these receipt facts and bound the certificate hash to a Node-issued time and runtime descriptor. It does not prove state preimages, execution truth, producer authentication, completeness, public publication or independent wall-clock authority. Only a separately verified RFC 3161 token adds external time evidence.
Verification-envelope signature verification
Descriptor fields: algorithm (Ed25519), attestation, canonicalization (jcs), envelopeType (nexart.verification.envelope.v2), kid, scope (whitelist), signedFields, specVersion (v0.16.1, an envelope specification label, not the Node version).
| CER version | attestation fields | bundle fields |
|---|---|---|
"0.1" | attestationId, attestedAt, kid, nodeRuntimeHash, protocolVersion | bundleType, version, createdAt, profile, evidence, protectedSet, then context, contextSummary if present |
"0.2" | Same five | Same six, then identity, confidentiality if present, in that order |
The signed message is the canonical document of exactly this object:
{
"attestation": {
"attestationId": "<UUID>",
"attestedAt": "<RFC3339>",
"kid": "<KID>",
"nodeRuntimeHash": "sha256:<HEX>",
"protocolVersion": "0.1"
},
"bundle": {
"bundleType": "cer.governed.execution.v1",
"version": "0.1",
"createdAt": "<RFC3339>",
"profile": "<COPY THE COMPLETE PROFILE OBJECT>",
"evidence": "<COPY THE COMPLETE EVIDENCE OBJECT>",
"protectedSet": "<COPY THE COMPLETE PROTECTED-SET OBJECT>"
}
}The last three slots are the original objects, not JSON-encoded strings. The wrapper has only attestation and bundle. certificateHash, meta, receipt, signature, the rendered envelope and unknown extras are excluded.
JCS(verificationEnvelope). That is the older envelope-v1 helper, not the governed /api/attest path, and produces invalid results.- Retain the submitted sealed CER.
/api/attestdoes not return it. - Read
verificationEnvelope.attestationand itskid. - Validate family and version and use your own expected whitelist. Do not accept the returned
signedFieldsas authority. - Reconstruct
{ attestation, bundle }from the five attestation fields and the required plus actually-present optional bundle fields. - Canonicalize with the same rules and verify
verificationEnvelopeSignaturewith the key for thatkidunder your lifecycle policy. - Verify the CER with Governed SDK 0.4.2 and verify the receipt. The projection omits
certificateHash, so the hash binding needs those checks.
// Reuses publicKey resolution from the receipt procedure, keyed by
// verificationEnvelope.attestation.kid.
const env = attestResponse.verificationEnvelope;
// 3. Use YOUR expected whitelist for this family/version, not env.signedFields.
const ATTESTATION_FIELDS = ["attestationId", "attestedAt", "kid", "nodeRuntimeHash", "protocolVersion"];
const BUNDLE_REQUIRED = ["bundleType", "version", "createdAt", "profile", "evidence", "protectedSet"];
const BUNDLE_OPTIONAL = cer.version === "0.2" ? ["identity", "confidentiality"] : ["context", "contextSummary"];
const pick = (obj, keys) => Object.fromEntries(keys.filter((k) => k in obj).map((k) => [k, obj[k]]));
// 4. Reconstruct exactly { attestation, bundle }. No other wrapper keys.
const signable = {
attestation: pick(env.attestation, ATTESTATION_FIELDS),
bundle: pick(cer, [...BUNDLE_REQUIRED, ...BUNDLE_OPTIONAL]),
};
// 5. Ed25519 over UTF-8 canonical JSON of the projection, NOT of the envelope descriptor.
const envelopeOk = verify(
null,
Buffer.from(toCanonicalJson(signable), "utf8"),
publicKey,
Buffer.from(attestResponse.verificationEnvelopeSignature, "base64url"),
);
if (!envelopeOk) throw new Error("envelope signature invalid");
// 6. certificateHash is NOT in the projection: also run verifyGovernedExecution(cer)
// and the receipt procedure to verify the hash binding.For Node-side checking, attach the descriptor and signature at bundle.meta.verificationEnvelope and bundle.meta.verificationEnvelopeSignature, then send { "bundle": bundle } to /v1/cer/verify. Outer-level placement is not equivalent. The route does not verify the receipt signature for you. Node-side checking is not independent verification.
A valid envelope signature proves the resolved Node key signed this family projection and five attestation facts. It does not authenticate the producer, validate state preimages, guarantee complete capture or certify public visibility.
governedVerification fields
| Field | Values | Also in stateless checks |
|---|---|---|
certificateIntegrity | valid | invalid | not-performed | Yes |
cageSchemaValidity | valid | invalid | not-performed | Yes |
causalGraphValidity | valid | invalid | not-performed | Yes |
topologyValidation | valid | invalid | not-supplied | Yes |
resourceSafety | valid | invalid | not-performed | Yes |
stateHashVerification | not-performed | No |
producerAuthentication | not-performed | No |
completeness | not-claimed | No |
warnings | string array, e.g. TERMINAL_PATH_UNKNOWN | No |
The object has no ok, code, errors or details. SDK codes: OK, SCHEMA_ERROR, CAUSAL_ERROR, TOPOLOGY_ERROR, CERTIFICATE_HASH_MISMATCH, CANONICALIZATION_ERROR, RESOURCE_ERROR. The Node may add INVALID_STRUCTURE and a defensive GOVERNED_VERIFICATION_FAILED. Missing SDK fields default to not-performed, not-supplied or not-claimed, never to success.
Stateless verification: POST /v1/cer/verify
POST /v1/cer/verify
Content-Type: application/json
{ "bundle": <complete sealed CER object> }- No Bearer header. Public, rate-limited (source default 60/min) and structure-guarded.
- HTTP 200 for both
verifiedandfailedon a recognized family. HTTP 400 for a missing bundle or unsupported family. computedCertificateHashequals the submitted hash only on SDK success, otherwisenull.- Does not persist, mint a receipt or envelope, or publish. It may read stored TSA evidence and key metadata.
checks.signatureispass,failorskipped.
{
"status": "verified",
"reason": "All checks passed",
"reasonCode": "OK",
"checks": {
"certificateIntegrity": "valid",
"cageSchemaValidity": "valid",
"causalGraphValidity": "valid",
"topologyValidation": "not-supplied",
"resourceSafety": "valid",
"signature": "skipped"
},
"certificateHash": "sha256:<CERTIFICATE_HEX>",
"submittedCertificateHash": "sha256:<CERTIFICATE_HEX>",
"computedCertificateHash": "sha256:<CERTIFICATE_HEX>",
"keyUsed": null,
"keySource": null,
"anchorStatus": { "checked": false, "valid": true },
"timestampStatus": { "checked": false, "valid": true },
"timestampVerdict": "NOT_PRESENT",
"timestampType": null,
"timestampProvider": null,
"timestamp": { "type": "node-issued", "provider": null, "status": "untrusted" },
"context": null,
"classification": "VERIFIED",
"assurance": null,
"relaxations": [],
"verifiedUnder": null,
"code": "OK",
"classificationReason": "All checks passed",
"contentMode": null,
"verifiedAt": "<RFC3339_VERIFICATION_TIME>",
"node": "<NODE_ID>",
"protocolVersion": "0.1",
"governedVerification": {
"certificateIntegrity": "valid",
"cageSchemaValidity": "valid",
"causalGraphValidity": "valid",
"topologyValidation": "not-supplied",
"resourceSafety": "valid",
"stateHashVerification": "not-performed",
"producerAuthentication": "not-performed",
"completeness": "not-claimed",
"warnings": []
}
}Failed example (abridged; unlisted fields match the verified shape). The malformed stateHash was inserted before sealing a synthetic test fixture so that integrity stays valid. This describes building a test input, not a reseal workflow.
{
"status": "failed",
"reason": "bundle.steps[1].stateHash: lowercase 64-character hexadecimal required",
"reasonCode": "SCHEMA_ERROR",
"checks": {
"certificateIntegrity": "valid",
"cageSchemaValidity": "invalid",
"causalGraphValidity": "valid",
"topologyValidation": "not-supplied",
"resourceSafety": "valid",
"signature": "skipped"
},
"certificateHash": "sha256:<CERTIFICATE_HEX>",
"submittedCertificateHash": "sha256:<CERTIFICATE_HEX>",
"computedCertificateHash": null,
"classification": "FAILED",
"code": "SCHEMA_ERROR",
"node": "<NODE_ID>",
"protocolVersion": "0.1",
"governedVerification": {
"certificateIntegrity": "valid",
"cageSchemaValidity": "invalid",
"causalGraphValidity": "valid",
"topologyValidation": "not-supplied",
"resourceSafety": "valid",
"stateHashVerification": "not-performed",
"producerAuthentication": "not-performed",
"completeness": "not-claimed",
"warnings": []
}
}Persistence versus publication
- A successful attestation is persisted in the Node's local
cer_proofsstore with the cleaned CER, receipt, signatures, envelope andgovernedVerification.execution_idisbundleId;sdk_versionis null. - Default visibility is
hidden. Hidden, private, deleted and missing records all return indistinguishable 404s on public reads. - Governed records are never forwarded to external CER ingest, even when local visibility is public.
- An authenticated owner (Bearer API key that owns the record) may call
POST /api/proofs/:certificateHash/unhide(localhiddentopublic) orPOST /api/proofs/:certificateHash/visibilitywith{ "visibility": "public" }or"private". Visibility is Node record state (visibility_state), not a CER field, so it is outside the protected hash projection. The transition creates no new CER, receipt or signature and the record resolves locally under the same certificate hash. - Node 0.30.1 supports an authenticated visibility transition that does not change the protected certificate hash. This source audit does not establish the external verifier's storage or lookup implementation.
verificationUrl is formatted, not checked. Because the verify.nexart.io backend is outside the audited repository, a successful attestation, even with public local visibility, is not a guarantee that the URL resolves.
Route status
| Route | Status | Use |
|---|---|---|
POST /api/attest | Current | Authenticated governed attestation |
POST /v1/cer/verify | Current | Public, non-persisting verification |
GET /.well-known/nexart-node.json | Current | Key discovery |
GET /v1/resolve/cer/:certificateHash | Current | Exact-hash public resolution of local records |
GET /v1/cer/public | Current | Public read by certificate_hash or execution_id |
GET /api/public-cer-proof | Current, older surface | Public proof read; not an attestation alias |
POST /api/proofs/:certificateHash/unhide | Current | Owner hidden to public |
POST /api/proofs/:certificateHash/visibility | Current | Owner public/private management |
POST /verify | Current, legacy-shaped | Authenticated render/snapshot verification; not governed |
POST /v1/cage/bundles | Current, separate contract | Native CAGE bundle registration |
POST /api/stamp | Governed unsupported | Rejects governed CERs |
POST /api/stamp-from-execution | Cached replay | Not a governed reseal |
/certifyDecision | Nonexistent | SDK helper name, not an HTTP route |
/verify/{hash} | Nonexistent | Not POST /verify or the external /c/ page |
/v1/attestation/cage-governance | Nonexistent | No alias |
Absence statements come from route registration in source, not production probing.
Key discovery
{
"nodeId": "<NODE_ID>",
"operator": "<OPERATOR>",
"activeKid": "<ACTIVE_KID>",
"keys": [
{
"kid": "<KID>",
"alg": "Ed25519",
"algorithm": "Ed25519",
"use": "sig",
"status": "active",
"publicKey": "<BASE64URL_32_BYTE_PUBLIC_KEY>",
"publicKeyJwk": { "kty": "OKP", "crv": "Ed25519", "x": "<BASE64URL_PUBLIC_KEY>" },
"publicKeySpkiB64": "<BASE64_PUBLIC_SPKI>",
"createdAt": "<RFC3339>",
"revoked": false,
"notBefore": "<RFC3339>",
"validFrom": "<RFC3339>",
"validTo": null
}
]
}Additional and deprecated keys may appear with deprecatedAt, notAfter and validTo. Status is active, deprecated or revoked. Always select by the signature's kid.