Metadata-Version: 2.5
Name: norcaster-verify
Version: 0.4.0
Summary: Standalone verifier for Norcaster audit bundles: manifest content digest, integrity references, and public Rekor transparency-log anchors. Verifies evidence without a Norcaster account.
Project-URL: Homepage, https://www.norcaster.com
Project-URL: Documentation, https://www.norcaster.com/trust/integrity/algorithm
Project-URL: Repository, https://github.com/norcaster/verify
Project-URL: Issues, https://github.com/norcaster/verify/issues
Author-email: "Norcaster (Navego AB)" <security@norcaster.com>
License: Apache-2.0
License-File: LICENSE
Keywords: ai-governance,audit,eu-ai-act,evidence,rekor,soc2,verification
Requires-Python: >=3.10
Requires-Dist: httpx>=0.24
Provides-Extra: signatures
Requires-Dist: cryptography>=42; extra == 'signatures'
Description-Content-Type: text/markdown

# norcaster-verify

Independent verification tooling for [Norcaster](https://www.norcaster.com) evidence. It verifies
without a Norcaster account, without API access and without trusting Norcaster. Everything below
runs offline.

Two uses:

1. **Verify an audit bundle**: the evidence package Norcaster exports for SOC 2 examinations and
   EU AI Act readiness reviews (Python, `norcaster-verify` on PyPI).
2. **Verify a Shared Record exchange or dispute export**: what two companies' agents exchanged,
   signed by both and logged as hashes (Python or Node; the protocol is a draft, see below).

The examples run from a clone of this repository, against files committed in it:

```bash
git clone https://github.com/norcaster/verify.git && cd verify
```

## 1. Verify an audit bundle

```bash
pip install norcaster-verify
norcaster-verify examples/audit-bundle.json --offline
```

`examples/audit-bundle.json` is a small synthetic bundle. Expected result: `PASS`, with the
content digest, governing configuration and approval provenance checks passing, and the trace
chain snapshot and public Rekor anchors reported as `SKIP` because the example carries none. A
SKIP is never a PASS.

Change one recorded decision and verify again:

```bash
python -c "import json; b = json.load(open('examples/audit-bundle.json')); b['sections']['enforcement_records'][2]['outcome'] = 'allowed'; json.dump(b, open('tampered-bundle.json', 'w'))"
norcaster-verify tampered-bundle.json --offline
```

Expected result: `FAIL` on the bundle content digest, exit code 1.

For a real bundle, run `norcaster-verify bundle.json` (online, it looks up the Rekor anchors),
`--offline` (it uses the Rekor snapshots embedded in the bundle) or `--json` (a machine-readable
report). Exit code 0 means no check failed.

What it checks:

- the manifest's content digest;
- the internal integrity references;
- the public Sigstore Rekor anchors;
- **governing-configuration consistency**: every enforcement decision's
  `deployment_policy_version` / `policy_content_hash` resolves to a policy snapshot in the bundle
  whose hash recomputes. Org-scoped decisions, which have no deployment, resolve their
  `org_policy_version` against `org_policy_version_snapshots`;
- **approval provenance**: policy versions produced by a config proposal name an independent
  approver and appear in `change_history`.

For anchors written by a signing-enabled chain it also checks that the Rekor entry embeds the
tenant's public key named in the manifest and a valid ES256 signature over the segment root. That
needs `pip install "norcaster-verify[signatures]"`; without `cryptography` that part reports
SKIP, not PASS.

The same two files (`verify_audit_bundle.py`, `audit_bundle_verify_lib.py`) ship inside every
Norcaster audit-bundle zip. This package and the bundled copies are interchangeable, and a test
enforces that they are identical byte for byte.

## 2. Verify a Shared Record exchange or dispute export

A Shared Record export is one JSON evidence file: the signed receipts, acknowledgements and
missing-acknowledgement records for the exchanges in question, their log proofs, a witnessed
checkpoint, both organisations' key documents and, for exchanges a party chose to reveal, the
content and salt. You verify it against **your own** trust file: the root fingerprints you pinned
for each organisation and the log and witness keys you trust.

Each test vector in `spec/vectors/` holds both, plus the expected result. Split one into the two
files the verifier takes:

```bash
python -c "import json; v = json.load(open('spec/vectors/valid-acknowledgement.json')); json.dump(v['evidence'], open('evidence.json', 'w')); json.dump(v['trust'], open('trust.json', 'w'))"
```

Python:

```bash
pip install "norcaster-verify[signatures]"
python -m norcaster_verify.shared_record verify evidence.json --trust trust.json
```

Node (18 or later), the same checks:

```bash
npx --yes --package @norcaster/verify norcaster-verify-record evidence.json --trust trust.json
```

Expected result: the exchange `state=acknowledged`, `Result: no check failed` and exit code 0.
Every check passes except two that do not apply here and say so as `SKIP`: there is no
missing-acknowledgement record, and the evidence holds one checkpoint, so there is no earlier one
to prove consistency with. The receipt's content commitment is checked against the revealed
content and salt (`content_commitment: revealed content and salt match the commitment`).

Now a copy whose receipt was changed after it was signed:

```bash
python -c "import json; v = json.load(open('spec/vectors/tampered-content-hash.json')); json.dump(v['evidence'], open('evidence.json', 'w')); json.dump(v['trust'], open('trust.json', 'w'))"
python -m norcaster_verify.shared_record verify evidence.json --trust trust.json
```

Expected result: `FAIL` on the receipt signature, the content commitment and the receipt's log
inclusion, and exit code 1. Add `--json` to either command for a machine-readable report. Without
the `signatures` extra the Python verifier reports signature checks as SKIP, never PASS; Node
always checks them. Python and Node give the same report, byte for byte, on every input.

**Profiles.** The default, `--profile basic`, reports what the evidence shows. For a dispute, add
`--profile dispute`: every log check (receipts, acknowledgements, missing-acknowledgement records,
key-document versions and root rotations) must then PASS, and a SKIP counts as a FAIL. Either way
the report says, per organisation (`evidence_complete_for`), that the result assumes the export
holds the organisation's complete key history, which v0 evidence cannot show:

```bash
python -m norcaster_verify.shared_record verify evidence.json --trust trust.json --profile dispute
```

**Input.** Both verifiers read the evidence and trust files as text under a strict JSON profile
(spec §4.1): no number with a fraction or an exponent, no `NaN`, no duplicate member names. A file
outside it is unusable input: the report's `input` section says why, and the exit code is 2. From
code, use `verify_evidence_text(evidence_text, trust_text, profile="basic")` in Python or
`verifyEvidenceText(evidenceText, trustText, { profile })` in Node; `verify_evidence` and
`verifyEvidence` take parsed JSON, which can no longer show those mistakes.

The 25 vectors and what each must give are listed in `spec/vectors/`, and 52 negative vectors
(malformed input, as text) in `spec/vectors/negative/`. An export from a Norcaster organisation
(`POST /orgs/{org}/shared-record/dispute-exports`) or from the self-hosted counterparty component
(`norcaster-counterparty export`) verifies the same way.

### Shared Record status

In development · pilot pairs by application. A pilot version is built and in testing. It is not
in production or available to customers.

`spec/shared-record-v0.md` is a **draft protocol, in development, published for review**. It is
designed on the IETF SCITT architecture (RFC 9943), using COSE hash envelopes (RFC 9995), COSE
Receipts (RFC 9942) and the RFC 9162 Merkle tree. It may change. The Python module
`norcaster_verify.shared_record` is its reference implementation and `lib/shared-record/` its
Node verifier. `spec/REVIEW.md` is the brief for an outside review of the protocol. The record
shows what each party signed and what the log recorded; it does not decide who is right.

## 3. Verify a trace integrity proof (Node)

Verifies a per-run **trace integrity proof bundle** (`norcaster-proof-<run_id>.json`): the local
hash-chain proof, the Merkle path, and one Rekor lookup to confirm the segment root against the
public transparency log. This one is online.

```bash
npx --yes --package @norcaster/verify norcaster-verify-proof --proof norcaster-proof-<run_id>.json
```

What it checks: the recorded chain-proof result carried in the bundle, the Merkle inclusion path
when one is present, the tenant-key (ES256) signature over the entry and, where the Merkle path
allows, over the segment root, and that the segment root and public key in the Rekor entry match
what the bundle carries. It does not recompute the full hash chain, and the Rekor URL comes from
the bundle itself. See `docs/algorithm.md` for exactly what each step proves. Unsigned
(version 1) bundles are reported as `unsigned`, never as verified. The npm package installs two
commands, `norcaster-verify-proof` and `norcaster-verify-record`, distinct from the Python
`norcaster-verify` command.

## What verification does not prove

Completeness of an AI estate, compliance with any framework, or authenticity of the export
source. Norcaster produces evidence; only a licensed CPA firm can attest under SOC 2, and EU AI
Act conformity determinations require qualified counsel. A Shared Record verification shows what
was signed and logged, not which party is right about a dispute.

## Source and releases

This repository is a published snapshot of `packages/norcaster-verify` in Norcaster's main
repository, which is the source of truth. The Python files here are byte-for-byte the ones that
ship inside every audit-bundle zip, and a test in the main repository enforces that. Open issues
here.

Releases are cut from version tags (`v0.4.0`). The `release.yml` workflow builds the Python sdist
and wheel and publishes them to PyPI, and publishes the npm package, using each registry's
trusted-publishing (OIDC) flow, with no long-lived tokens. The versions in `pyproject.toml` and
`package.json` and the top entry of `CHANGELOG.md` must all equal the tag. CI runs the Node and
Python tests on every push.

Security reports: see [SECURITY.md](SECURITY.md).

Apache-2.0 · security@norcaster.com
