# MARSAD proof format (v1.0)

**Status:** open specification. Anyone may implement a verifier from
this document. The reference verifier is `tools/marsad-verifier/
verify_dossier.py` (also embedded in every dossier as
`verify-dossier.py`).

A MARSAD **audit dossier** is a zip that a bank hands to an auditor or
regulator. This spec defines how a third party verifies it **without any
MARSAD software, licence, or network access** -- and, for the integrity
and chain layers, **without any shared secret**.

## Goals

1. **Bundle integrity** -- prove no file was altered. (key-free)
2. **Chain truth** -- prove every hash chain recomputes from genesis, so
   the *content* is tamper-evident, not merely the container. (key-free)
3. **Authenticity** -- prove the dossier was signed by the issuing bank.
   (Ed25519 public-key, or HMAC shared-secret.)
4. **Key provenance** -- (optional) pin the embedded signing key to an
   out-of-band published source so authenticity is the *bank's*, not a
   self-asserted key. (Ed25519 only.)

Layers 1-2 are the differentiator: a regulator recomputes MARSAD's
tamper-evidence independently, with nothing but a SHA-256 implementation.

## Zip contents

```
00-cover.pdf / .json          executive summary + metadata
10..13-chain-*.pdf / .json     per-Twin-chain timelines (human + machine)
14-chain-workflow.pdf / .json  CAP workflow transitions
20..100-*.pdf / .json          audit events, connector runs, board, etc.
proof.json                     self-describing, key-free chain proof
manifest.json                  per-file SHA-256 + signature
verify-dossier.py              the reference verifier (stdlib)
```

## manifest.json

```jsonc
{
  "marsad_dossier_version": "1.0",
  "dossier_id": "...", "bank_name": "...", "as_of": "...",
  "audit_window_since": "...", "audit_window_until": "...",
  "files": [ { "path": "00-cover.pdf", "sha256": "<hex>", "size_bytes": 1234 }, ... ],
  "manifest_signature": { ... }       // see below
}
```

**Layer 1 (bundle):** for every entry in `files`, recompute
`SHA-256(zip_member_bytes)` and compare to `sha256`. Any mismatch =
tamper. `proof.json` and every section file are listed here.

## proof.json -- key-free chain truth (Layer 2)

```jsonc
{
  "proof_version": "1.0",
  "hash_algorithm": "SHA-256",
  "hash_version": 1,
  "field_separator_hex": "1f",
  "genesis_prev_hash_hex": "0000...0000",   // 32 zero bytes
  "recipe": "row_hash = SHA256( fromhex(prev_hash) + fromhex(field_separator_hex) + b64decode(canonical_b64) )",
  "chains": [
    {
      "chain": "resilience-state",
      "rows": [
        {
          "entity_kind": "ibs_service",
          "entity_id": "<uuid>",
          "prev_hash": "<hex>",
          "row_hash": "<hex>",
          "canonical_b64": "<base64 of the exact bytes that were hashed>"
        }
      ]
    }
  ]
}
```

**Verification (per row, in file order):**

1. `recomputed = SHA-256( bytes.fromhex(prev_hash) +`
   `bytes.fromhex(field_separator_hex) + base64decode(canonical_b64) )`.
   It MUST equal `row_hash`. (A mismatch means the sealed content was
   altered.)
2. **Linkage:** rows are grouped by `(chain, entity_kind, entity_id)` and
   ordered. The first row of each group MUST have `prev_hash ==
   genesis_prev_hash_hex`; every later row's `prev_hash` MUST equal the
   previous row's `row_hash`. (A mismatch means a row was inserted,
   removed, or reordered.)

The verifier needs **no per-chain knowledge** -- `canonical_b64` carries
the exact bytes MARSAD hashed, so the check is a generic SHA-256 walk.
`canonical_b64` decodes to the MARSAD canonical row encoding
(`v=<hash_version>` followed by `0x1F`-separated `field=value` pairs);
a verifier never has to parse it -- it only re-hashes it.

## manifest_signature -- authenticity (Layer 3)

The signature covers the canonical JSON of the manifest **minus the
`manifest_signature` field**:
`canonical = json.dumps(body, sort_keys=True, separators=(",", ":"))`.

### Ed25519 (recommended -- third-party verifiable, no shared secret)

```jsonc
{ "algorithm": "Ed25519", "key_id": "...", "signature": "<hex>", "public_key": "<hex, 32-byte raw>" }
```

Verify the Ed25519 `signature` over `canonical` using the embedded
`public_key`. The embedded key alone is only **self-asserted** -- the
embedded signature check proves "the manifest wasn't modified after it
was signed by *this* key", not "this key is the bank's". Cross-check it
out-of-band (Layer 4) to close that gap.

### Layer 4 -- key provenance (out-of-band cross-check)

The issuing bank publishes its signing public key(s) at an
**unauthenticated** endpoint:

```
GET /v1/proof/public-keys
-> { "keys": [ { "key_id": "...", "algorithm": "Ed25519",
                 "public_key": "<hex>", "status": "active" | "retired" } ] }
```

A verifier pins the manifest's embedded `public_key` to this source by
matching on `key_id`. The reference verifier does this with either flag:

```
python verify_dossier.py dossier.zip --expected-key <hex>
python verify_dossier.py dossier.zip --keys-url https://<bank>/v1/proof/public-keys
```

A mismatch fails the verification -- this defeats a forger who tampers a
manifest and re-signs it with their *own* keypair (the embedded-key
check would otherwise pass). Examiners should obtain the bank's
`key_id -> public_key` mapping through a trusted channel (the published
endpoint over TLS, or a one-off out-of-band exchange) and pin to it.

**Key rotation.** Rotated-out keys stay published with `status:
"retired"` (server-side via `MARSAD_PROOF_RETIRED_PUBLIC_KEYS`) so
artefacts signed by an old key still cross-check after rotation.

### HMAC-SHA256 (internal / fallback -- needs the shared key)

```jsonc
{ "algorithm": "HMAC-SHA256", "key_id": "...", "signature": "<hex>" }
```

Recompute `HMAC-SHA256(key, canonical)` and compare. Only usable by a
holder of the shared key; not suitable for third-party authenticity.

## Versioning

`hash_version` / `proof_version` / `marsad_dossier_version` are explicit
so a verifier can refuse a format it doesn't understand. The hash recipe
is frozen for `hash_version: 1`; any change to the canonical encoding is
a version bump (see `docs/adr-hash-version-evolution.md`).

## Notes / scope

- `proof.json` covers **every registered Twin-shaped chain** -- the set
  is derived from the chain registry at export time (14 chains as of
  v0.313.0: resilience-state, blast-radius, process, obligation,
  service-methodology, app-settings, connector-posture, mvb-pack,
  ibs-admission, engine-output, framework-analyser,
  analyser-suggestions, resilience-maturity, document-ingest), sorted
  by slug for a stable, diffable proof. The Framework workflow chain
  (different row shape) is verified via its own section today; a future
  proof-format revision may fold it in.
- Layers 1-2 are sufficient to prove tamper-evidence. Layer 3 adds "who
  signed it." A regulator with no `cryptography` install still gets full
  key-free integrity + chain truth.
