# Evidence Fabric Sample Package v1.0.0

A buyer-runnable, auditor-runnable sample of an Evidence Fabric package as described in the [Vertical Edge AI Evidence Fabric Methodology v1.1.0](https://verticaledgeai.ai/methodology.html).

**This is synthetic data. The cryptography is real.**

---

## What is in this package

| File | Purpose |
|---|---|
| `receipt.json` | Signed sample receipt for a synthetic healthcare workflow |
| `manifest.json` | Lists every artifact in the package **except itself**, with SHA-256 hash and byte count; the manifest is itself signed, which is why it cannot appear in its own artifact list |
| `framework_mapping.md` | Illustrative topic crosswalk for NIST AI RMF and EU AI Act topics; HIPAA and Reg S-P control mappings are excluded, and no control-satisfaction claim is made |
| `verifier_cli.py` | Standalone Python verifier; depends only on `cryptography` and `rfc8785` |
| `sign_and_verify.py` | Build script: generates Ed25519 keypair, signs artifacts, runs verifier, captures transcript |
| `public_key.json` | Synthetic Ed25519 public key (`vke_synthetic_2026-05`) for offline verification; ships pre-committed and is required by the verify command |
| `session_export.json` | Synthetic session export: a three-event hash-linked chain signed under `rfc8785-jcs-v1`, the legacy-canonicalized receipt, and a chain anchor |
| `public_keyring.json` | Public-only keyring, one active key and one retired key, for verification across key rotation |
| `verifier_transcript.txt` | Recorded verifier run output, every mode; deterministic, and hashed in `manifest.json` like every other artifact |
| `README.md` | This file |

The package contains **11 files**. `manifest.json` lists **10 of them — every artifact except itself**; a signed manifest cannot carry its own hash, and nothing else is excluded. Nothing else means nothing else: a file you name on the command line is still a package artifact if it sits inside the package, so `--session-export session_export.json --public-keyring-file public_keyring.json` does not buy either of those files a pass out of the manifest set.

**The manifest-path grammar.** One grammar, enforced identically by the verifier and by the site build that produces the ZIP, so a package that builds is a package that verifies. A descriptor path is:

- **relative and `/`-separated** — no drive letter, no leading separator, no backslash;
- **NFC-normalized**;
- **every segment matching `^[A-Za-z0-9][A-Za-z0-9._-]*$`** — ASCII letters, digits, `.`, `_` and `-`, starting with a letter or digit — and no `.` or `..` segment;
- **no segment ending in `.` or a space**;
- **no `:` anywhere**;
- **no Windows reserved device name** — `CON`, `PRN`, `AUX`, `NUL`, `COM1`–`COM9`, `LPT1`–`LPT9` — in any case, with or without an extension.

Everything past "relative" is there because **string-level uniqueness is not filesystem-object uniqueness**. Win32 strips a trailing dot or space, so `README.md.` and `README.md` are one file addressed by two descriptors — which is how a manifest carrying eleven descriptors over ten physical files could report `11 artifact hashes match`. `README.md:x` names an NTFS alternate data stream of `README.md`. `NUL.txt` opens the null device and reads as an empty file rather than a missing one. Two Unicode spellings of one name are one file on macOS and two strings everywhere else.

A grammar can only close the aliases that are decidable from the string. After the no-symlink walk the verifier therefore also requires **unique resolved file identity** — `(st_dev, st_ino)` across every descriptor target — so a hard link that gives one file two perfectly in-grammar names is refused by name too. A host that reports no inode cannot answer that question and the check says nothing there rather than reporting uniqueness it did not establish.

`public_key.json` and `public_keyring.json` ship pre-committed so the verify commands run without first building. Running `sign_and_verify.py --sign` regenerates every signature and rewrites five files from scratch — `public_key.json`, `public_keyring.json`, `receipt.json`, `session_export.json` and `manifest.json`; the full `sign_and_verify.py` cycle also re-records `verifier_transcript.txt`.

**Byte-reproducibility.** Every signing input is fixed: the synthetic package key, the synthetic seed the self-test derives its throwaway keys from (`ve-sample-package-self-test-v1`, published in `verifier_cli.py` and no more secret than the package key), and the fixture data. Two consecutive full `sign_and_verify.py` cycles therefore reproduce **all 11 files byte-for-byte**, `verifier_transcript.txt` included. Because the transcript is both an output of the verifier run and an input to the manifest that same run checks, the build iterates to the fixed point — it signs the manifest over the transcript on disk, runs the verifier against exactly that pair, and stops when the run reproduces the transcript it was verified against.

The **downloadable ZIP** is reproducible too, and this is a narrower claim than the one above: its container bytes are a function of the eleven members' bytes, the eleven member names, and one **repository-pinned member timestamp** — `1980-01-01T00:00:00Z`, the epoch the ZIP format counts from — and of nothing else. Member order is sorted. So two builds of the same package produce the same archive digest: on the same host, on a different host, after a checkout that reset every file's modification time, and in any environment.

**`SOURCE_DATE_EPOCH` is ignored.** The build used to read it when the environment set it, which put an ambient input nobody had declared into the formula: the same eleven identical members produced two different archives depending on a variable, while this paragraph said nothing else could move the digest. It is out of the formula now — a published digest that any environment can change is not a digest anyone can check — and a build that finds the variable set says on its own output that it ignored it. The pinned constant and the digest of the release archive at this tip are recorded in `RELEASE_PINS.json` at the repository root, and the build prints the digest on every run.

Before member timestamps were fixed at all, touching a file without changing a byte of it changed the archive's digest while all eleven members stayed identical, which made the download's hash useless as a reproducibility check.

---

## How to verify (3 steps)

### Step 1: Install the dependencies

```bash
pip install cryptography rfc8785
```

Those are the only Python packages required. `cryptography` does Ed25519 verification; `rfc8785` does the RFC 8785 JSON canonicalization used by artifacts stamped `canonicalization_version: "rfc8785-jcs-v1"`. Requires Python 3.9+; developed and verified on Python 3.12 with `cryptography` 46.x and `rfc8785` 0.1.x.

### Step 2: Build the signatures (first-time only)

If you downloaded the package fresh (signatures contain `PLACEHOLDER`), run the build script to generate the synthetic keypair and sign the artifacts:

```bash
python sign_and_verify.py --sign
```

This rewrites the same five files listed above — `public_key.json`, `public_keyring.json`, `receipt.json`, `session_export.json` and `manifest.json` — with real Ed25519 signatures. The private keys (synthetic, for demonstration) are embedded in the script for reproducibility.

If you downloaded the package already-signed (from `verticaledgeai.ai/sample-evidence-package.zip`), skip Step 2.

### Step 3: Verify

```bash
python verifier_cli.py --receipt receipt.json --public-key-file public_key.json
```

Expected output, recorded from the shipped tool:

<!-- recorded-output: verifier_transcript.txt -->
```
== VeilEngine Receipt Verifier ==
Loading receipt: receipt.json
Receipt: VERIFIED
  receipt rcpt_2026-05-18T14:32:11Z_a1b2c3d4: signature verified [ve-canonical-json-v1]; declares key id vke_synthetic_2026-05

Overall: PASS
```

The verifier exits with code 0 on PASS.

To verify the manifest too:

```bash
python verifier_cli.py --manifest manifest.json --public-key-file public_key.json
```

<!-- recorded-output: verifier_transcript.txt -->
```
== VeilEngine Receipt Verifier ==
Loading manifest: manifest.json
Manifest: VERIFIED
  signature verified [ve-canonical-json-v1]; 10 artifact hashes match; declares key id vke_synthetic_2026-05

Overall: PASS
```

To walk the hash-linked event chain in the session export:

```bash
python verifier_cli.py --chain session_export.json --public-key-file public_key.json
```

To verify the export whole -- envelope signature, chain, every receipt under its own canonicalization, the manifest hashes, the binding that holds the bundle, the chain wrapper, every event and every receipt to one session and one tenant, and the recomputed chain-tip anchor:

```bash
python verifier_cli.py --session-export session_export.json --public-key-file public_key.json
```

Two of those layers exist because signature checks alone cannot catch them:

- **Binding.** A validly signed receipt or event from another session, or one carrying another tenant, can be mixed into this export and every signature still verifies. The binding layer compares the identity every signed object declares and fails by naming the layer that does not match. Its schema contract is explicit and fail-closed: **every layer that can carry identity must carry it, and every identity it carries must match.** `bundle.session_id`, `bundle.tenant_id`, `chain.session_id` and `chain.tenant_id` are all required; every event must name the bundle's session and tenant; every receipt must name both. Receipt identity is read at `request.session_id` / `request.tenant_id` in this schema and at the top level in the gateway schema -- **every path present is read and they must agree.** A receipt that names one session nested and a different one at the top level is refused by name, not admitted under whichever path the verifier consulted first; a receipt that names neither is rejected, not skipped. The success line lists only the layers and identities that were actually compared. This is stricter than the schemas themselves require, and stricter than the internal gateway-side verifier, which leaves `bundle.tenant_id` and the chain wrapper's tenant unrequired.
- **Chain anchor.** `chain.anchor` commits to the chain TIP: algorithm, event count, last event hash. Without it an export can be truncated to a shorter prefix of its own chain and every remaining layer still verifies. Keyring verification therefore requires a structured anchor; explicit single-key verification keeps one narrow, reported exception so bundles frozen before the anchor existed stay verifiable. Whenever an anchor is present it is recomputed and must match exactly.

The same export verifies against the pinned keyring instead of a single key, which is how verification survives a signing-key rotation:

```bash
python verifier_cli.py --session-export session_export.json --public-keyring-file public_keyring.json
```

The keyring is an externally pinned trust input: the verifier checks its shape, status, validity window and uniqueness and binds each signed object to the key that object names, but it does not establish the keyring's own authenticity -- pin it out of band. Descriptor validation fails closed before any key is resolved: `valid_from` must be a non-empty RFC 3339 timestamp, `valid_until` an RFC 3339 timestamp or null, a window may not run backwards, key ids must be unique, no two descriptors may name the same public key, and private-key-shaped fields are rejected by name. A revoked key refuses to resolve and says so with its key id; a retired key still resolves, so events signed before a rotation stay verifiable.

**What "validity window" means here: compared, not merely shaped.** `valid_from` and `valid_until` are parsed when the keyring loads and enforced when a key is resolved. An object that predates or postdates the window of the key it names is refused, naming the key id and the bound it crossed. The comparison is **per signed object, against that object's own declared instant** — never against the date the bundle was packaged, because an event signed before a key rotation is older than the export carrying it, and measuring it by the export's date would refuse exactly the historical verification the retired key exists for. `bundle.export_generated_utc` dates the envelope and is required under a pinned keyring; `request.timestamp_utc` dates a receipt; `payload.received_utc` dates an event. An object that declares no instant has nothing to compare, and the run says so: `--session-export` prints a `validity` line naming which objects were compared, against how many, and which declared no timestamp; a bare `--manifest` or `--chain` under a keyring says the same on its result line. One documented divergence from the gateway loader: the gateway derives every key id from its public key (first 16 hex of SHA-256) and requires the declared id to equal that derivation. This package's keyring uses human-readable ids (`vke_synthetic_2026-05`), so the derived-id binding is enforced conditionally -- an id in the derived shape (exactly 16 lowercase hex characters) must equal the derivation for its key, and any other id is treated as a label.

To check the verifier itself before you trust its verdict, run its offline self-test. It generates throwaway keypairs, signs a payload that canonicalizes differently under each supported scheme, and confirms the verifier accepts what it should and rejects a modified payload, a wrong key, a signature made under the wrong canonicalization, an unrecognized canonicalization identifier, a cross-session receipt mix, a mixed-tenant receipt, a receipt naming no session or tenant, a bundle or chain wrapper naming no tenant, a chain wrapper naming a different tenant, a receipt declaring conflicting session or tenant values at its nested and top-level paths, an identity declared at a present path as a number, array, object, boolean, null or empty string, conflicting key ids at an object's two key-id paths, a derived-shape key id that is not the supplied key's, a pinned `--expect-key-id` a layer does not match, a wrong root shape for every mode, a manifest descriptor that is empty, untyped, absolute or escaping the package directory, a descriptor path outside the manifest-path grammar (a trailing dot or space, alternate-stream syntax, a reserved device name, a backslash, a non-NFC spelling, a segment that does not start alphanumeric), a JSON object naming one key twice, an event declaring a false hash of itself, unsigned fields beside the signed export in its wrapper, an artifact dated outside its key's validity window, a missing anchor under the keyring, a malformed or stale anchor, and keyring descriptors with malformed validity timestamps, a reversed window, duplicate ids, an id that does not derive, an aliased public key, or private-key material:

```bash
python verifier_cli.py --self-test
```

Or run the full sign-and-verify cycle (regenerates signatures, then verifies) in one shot:

```bash
python sign_and_verify.py
```

The verifier returns exit code 0 on PASS, 1 on FAIL, 2 on argument/environment error.

---

## How this verifier differs from the internal one

`verifier_cli.py` is a customer-facing subset of the internal VeilEngine receipt verifier. Every behavioural difference is listed here — the same list appears in the tool's own docstring — so "documented superset" is something you can check rather than take on trust.

| # | Direction | What differs |
|---|---|---|
| 1 | **Stricter** | `bundle.tenant_id` and the chain wrapper's `tenant_id` are **required**; the internal verifier leaves both unrequired. An export asserted to hold one tenant that some layer never declared is an export whose tenant claim is weaker than it reads. |
| 2 | **Conditional** | The internal verifier derives every key id from its public key (first 16 hex of SHA-256) and requires the declared id to equal that derivation. This package's keyring uses human-readable labels, which no derivation can produce, so the rule is enforced conditionally: an id in the derived **shape** must equal the derivation, and any other id is treated as a label. The same-public-key check closes the aliasing hole the derivation rule otherwise covers. |
| 3 | **Aligned (was looser; closed 2026-09-02)** | Explicit single-key mode used to ignore the key id a signed object declares, which was **looser** than the internal verifier. It now binds a derived-shape declared id to the supplied key, prints a label id on every layer line, and honours `--expect-key-id`. Listed so the record of what changed is not lost. |
| 4 | **Stricter** | An export carrying **no receipts FAILS** here; the internal verifier reports SKIP, treating an empty receipts array as legitimate for chain-only sessions. This package publishes a receipt-bearing export and a binding layer that reads the receipts, so an empty array is more likely an export that lost them than a chain-only session. |

---

## What this package demonstrates

- The Evidence Fabric methodology produces real, structured, cryptographically-signed artifacts.
- The verifier runs without vendor dependencies (only `cryptography` and `rfc8785` + Python 3) and makes no network calls.
- An auditor with the public key can verify a receipt's signature, and the manifest's artifact hashes, without trusting Vertical Edge AI.
- The hash-linked chain-of-receipts pattern ships in the verifier: `--chain` walks the links and per-event signatures, and `--session-export` checks the envelope, the chain, every receipt under its own canonicalization, the manifest hashes, the cross-object session/tenant binding, and the recomputed chain-tip anchor in one run. What remains on the roadmap is the cross-session transparency log and its inclusion evidence.
- The signing path is documented and byte-for-byte reproducible.

## What this package does NOT demonstrate

- That Vertical Edge AI has deployed this in production with customers (sample is synthetic).
- That the methodology is correct for your specific regulatory framework (your auditor and counsel interpret).
- That the AI providers retained or did not retain data (provider-side claim; this package records what the operator did, not what the provider then did).

---

## Synthetic-data notice

- All names, MRNs, DOBs, diagnosis codes, clinician identifiers, and session IDs are fictional and labeled `"synthetic": true` in the receipt and manifest metadata.
- The signer key `vke_synthetic_2026-05` is a synthetic Ed25519 keypair generated solely for the sample. The private key is intentionally exposed in `sign_and_verify.py` so anyone can re-verify. In production, the signer key is the operator's, not Vertical Edge AI's.
- The cryptographic primitives (Ed25519 signatures, SHA-256 hashes, JSON canonicalization) are real algorithms used the same way they would be in a production gateway.

---

## License

`LICENSE` is the MIT License, and the grant covers the two Python files in this package: **`verifier_cli.py` and `sign_and_verify.py`**. Both carry an `SPDX-License-Identifier: MIT` header. Audit, modify, and redistribute them freely.

The grant does not extend to the rest of the package. `README.md` and `framework_mapping.md` are documentation, and `receipt.json`, `manifest.json`, `session_export.json`, `public_key.json`, `public_keyring.json` and `verifier_transcript.txt` are synthetic sample data — published so anyone can reproduce the verification, not offered under an open-source grant.

We ask that you not alter the verification logic in `verifier_cli.py` without re-publishing a documented diff -- its value rests on byte-for-byte alignment with the gateway's signing path.

---

## Provenance

- **Methodology:** [Evidence Fabric Methodology v1.1.0](https://verticaledgeai.ai/methodology.html)
- **Specification:** [Sample Evidence Package Specification v1.0.0](https://verticaledgeai.ai/sample-evidence-package.html)
- **Verifier lineage:** customer-facing subset of the internal VeilEngine receipt verifier, published here for buyer and auditor inspection
- **Authored:** 2026-05-19 by Vertical Edge AI LLC (Austin, TX)

---

## Contact

[Contact](https://www.verticaledgeai.ai/contact.html) | [verticaledgeai.ai](https://verticaledgeai.ai)

Vertical Edge AI is not an audit firm. The package is published to demonstrate the Evidence Fabric methodology, not to substitute for an audit opinion.
