Vupt Docs

Verify your cost-audit chain externally

Vupt publishes a public bash verifier — scripts/verify-cost-chain.sh — that runs on your machine, offline, against any NDJSON export bundle. The per-organization Ed25519 public key is embedded on every row, so no network call to Vupt is required. This page is the auditor guide.

Why verify externally

Trust without verification is unsuitable for regulated workloads. Following the Kyriba 2026 Operational Resilience Index's verify, then trust framework, Vupt ships the cryptographic primitives, the public verification keys, and an auditor-runnable script — together with the audit data itself. ADR-011 commits Vupt to per-org Ed25519 audit chains; this page closes the loop by giving you the tool to audit them independently.

Prerequisites

The verifier is a single bash script with portable dependencies. No Node runtime, no language SDK, no package manager. Installation is whatever your distribution already ships. OpenSSL 3.0+ is required because Ed25519 verification via pkeyutl needs the -rawin flag added in OpenSSL 3.0; the older 1.1.1 ships Ed25519 keys but cannot verify them via the pkeyutl command.

  • bash 4.0+
  • jq 1.5+
  • openssl 3.0+ (required for Ed25519 verification via pkeyutl -rawin)
  • sha256sum + base64 (GNU coreutils or BSD)

Quick start

bash scripts/verify-cost-chain.sh <ndjson-file>

For continuous-integration pipelines, --json emits a structured, locale-neutral payload you can pipe through jq.

bash scripts/verify-cost-chain.sh --json <ndjson-file>

The verifier dispatches human-readable output on the LANG env var. Set pt_BR.UTF-8 for Brazilian Portuguese.

LANG=pt_BR.UTF-8 bash scripts/verify-cost-chain.sh <ndjson-file>

Output examples

Success

OK: chain verified — 1432 rows — signature OK — chain-tip a4b2c8d1

Chain broken (exit 1)

VERIFICATION FAILED: row 1342 prev_hash mismatch (expected abc123..., got def456...)

Signature invalid (exit 2)

VERIFICATION FAILED: row 27 signature invalid (Ed25519 verify failed)

Malformed input (exit 3)

VERIFICATION FAILED: malformed NDJSON at line 813 (JSON parse failed)

Exit codes

  • 0Chain verified — every signature valid + every prev_hash matches.
  • 1Chain broken — at least one row's prev_hash did not match the SHA-256 of the previous row.
  • 2Signature invalid — at least one row's Ed25519 signature did not verify against the embedded public key.
  • 3Malformed input — NDJSON parse error, missing required field, missing input file, or missing tooling.

JSON output (--json)

The --json flag emits a single-line JSON object suitable for CI pipelines. The shape is fixed; status is either ok or failed; reason is one of prev_hash_mismatch, signature_invalid, or malformed.

{"status":"ok","rows":1432,"chain_tip":"a4b2c8d1e9f7..."}
{"status":"failed","reason":"prev_hash_mismatch","line":1342,"detail":"..."}

NDJSON envelope schema

Each line in the export bundle is one JSON object. The full envelope contract — what Vupt's export worker writes and what the verifier consumes:

{
  "id": "<ulid>",
  "prev_hash": "<64-char hex SHA-256>",
  "payload": { /* JCS-canonicalizable JSON */ },
  "signature": "<base64 Ed25519 signature>",
  "signing_key_pem": "<PEM SPKI Ed25519 public key>",
  "key_id": "<rotation history identifier>",
  "active_key_id": "<currently-active per-org key id>",
  "created_at": 1716285600000,
  "action": "cost.attribution_recorded",
  "jcs_canonical_bytes_b64": "<base64 of RFC 8785 JCS bytes>"
}

Air-gapped verification

Because every NDJSON row embeds the per-organization signing_key_pem, the verifier runs offline. No outbound network access is required. This is the same script you would use in an air-gapped audit room, on a regulator's sealed laptop, or inside a customer's own VPC. Vupt does not need to be reachable to validate your audit chain.

Source and further reading

The verifier is MIT-licensed and lives in the Vupt monorepo. All design decisions are public.

The verify-cost-chain.sh script ships bundled with the desktop installer for offline inspection — open it from your installation directory under scripts/. Source repository is private during the design-partner phase; public re-publication is planned alongside v1.4.