Metadata-Version: 2.5
Name: sanning-proof
Version: 0.14.0
Summary: Verification kernel for the Sanning verification stack: RFC 8785 canonicalization, SHA-256, Ed25519 envelope sign/verify, RFC 9162 Merkle inclusion proofs, and RSA-PSS attested-evidence-export verification.
Project-URL: Homepage, https://sanning.io
Project-URL: Documentation, https://docs.sanning.io
Author: Sanning Inc.
License: MIT License
        
        Copyright (c) 2026 Sanning Inc.
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: arweave,ed25519,jcs,merkle,provenance,rfc8785,rfc9162,sanning,verification
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security :: Cryptography
Requires-Python: >=3.10
Requires-Dist: cryptography>=41.0.0
Requires-Dist: jcs>=0.2.1
Requires-Dist: pynacl>=1.5.0
Provides-Extra: dev
Requires-Dist: black>=24.0; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Description-Content-Type: text/markdown

# Sanning Proof

Offline verification for the Sanning evidence plane. If someone handed you an
evidence pack and asked you to check it, this package is the whole answer:
no account, no API key, no network call, and no relationship with Sanning.
**Sanning is never in the trust path.**

Two kernels, one contract. This page covers the Python one.

- **Python**: `sanning-proof` on PyPI. A library, with no command of its own.
- **TypeScript**: [`@sanning/proof`](https://www.npmjs.com/package/@sanning/proof)
  on npm, browser and Node ≥ 20. Ships the `proof` command.

If instead you are *producing* evidence, the write SDK is
[`sanning-anchor`](https://pypi.org/project/sanning-anchor/) and the hosted
plane is [console.sanning.io](https://console.sanning.io). For what the
evidence plane is and how anchoring, reading and verifying fit together, see
the [Sanning documentation](https://docs.sanning.io).

## Install

```bash
pip install sanning-proof
```

## Check a pack you were handed

A pack is one zip holding `bundle.json` beside a `logs/` directory. Unzip it
and check it from your terminal:

```bash
sanning-proof verify pack/bundle.json --logs pack/logs
```

Exit codes are the contract, so a script can read them, and every verb shares
one table: **0** verified, or the export was written, **1** failed, **2** usage
or malformed, **3** undetermined (`partial`, `pending`, `incomplete` or
`not_found`), **4** mixed. `3` is deliberately not `1`: undetermined means a
check could not be run, and reporting that as failed would say the evidence is
bad when it is not. Since 0.13.0 `partial` exits 3 whatever caused it; it exited
0 unless a gateway was unreachable.

Several things leave a run undetermined. An unreachable gateway leaves the
on-chain half unanswered, which is the network being down rather than the
evidence being bad. A stamp no witness has published is `partial`. And a record
nobody hashed leaves the PACK `incomplete`: the headline is
the verdict on the pack, the signed bundle plus whatever disclosed bytes
travelled beside it, which is the same verdict the verify page prints. A pack
can be incomplete while the bundle it carries is impeccable, so both verdicts
are printed whenever they differ, along with every reason the pack is not
verified.

Add `--gateways <url,url>` to check the on-chain half as well; omit
them for a fully offline verification, which needs no account, no API key and
no network. Only `arweave` witnesses are re-fetched. A witness of any other
`kind` is not checked, and with gateways passed the run answers exit 3.

The same command exists in the TypeScript kernel as
`npx @sanning/proof verify`. A cross-kernel leg runs both over the same
bundles and compares their **exit codes**, including the `--logs` lanes, so a
verdict cannot drift between them unnoticed. Their printed output is not
identical: the TypeScript command reports more per-event detail.

Or from Python, if you would rather have the result as an object than as an
exit code:

```python
import json
import pathlib

from sanning_proof import verify_evidence_bundle

pack = pathlib.Path("pack")
bundle = json.loads((pack / "bundle.json").read_text())
content = {p.stem: p.read_bytes() for p in (pack / "logs").glob("*.json")}

result = verify_evidence_bundle(bundle, content=content)
print(result.status)                                    # "verified"
print(sum(1 for e in result.events if e.ok), "verified")
```

`content=` binds each disclosed raw log to the commitment inside its own signed
record. Omit it and every event's content check is *undetermined* rather than
failed: absence of a disclosure is not evidence of tampering.

Nothing leaves your machine. The kernel does no I/O of its own.

## Read several packs together: a case reading

When a case spans more than one pack (two agents, two organizations, one
hand-off between them), check the packs together:

```bash
sanning-proof case assessor-pack/ challenger-pack/
unzip packs.zip -d case/ && sanning-proof case case/   # a zip of several packs
sanning-proof case case/ --json               # the full reading, for a script
```

Each argument is a folder: one pack (`bundle.json` plus `logs/`), or a folder
that holds several packs, such as an unzipped folder of packs. `case` does not
open archives. Given a `.zip`, it exits 2 and says to unzip it first and pass
the folder.

Packs are found by the verify page's splitting rule, so the page and this
command apply the same rule to the same unpacked tree: every `bundle.json` is a
pack, a file belongs to the deepest pack whose `bundle.json` or `logs/` it sits
in, and every other file is listed as outside every pack and never opened. A
`.zip` inside the folder is a file like any other. Several folders are read as
one folder holding them, each under its own name. Each `bundle.json` is read
with `verify`'s own reader, so the two commands classify one bundle the same
way.

The walk is bounded and follows no link:

- A symbolic link is never followed. It is listed as outside every pack and not
  read, even under a pack's `logs/`, and a link named on the command line exits 2.
- A folder nested more than 32 levels deep, more than 50,000 files and folders
  in all, or a name that is not valid UTF-8 exits 2. A bad name is refused
  rather than escaped, because two different names would then read as one.
- A `bundle.json` nested more than 512 levels deep exits 2, from `case` and
  from `verify` alike.

Known differences from the verify page: the page opens a dropped `.zip`; the
page drops a leading byte-order mark from `bundle.json`, which `verify`'s reader
refuses; the page reads whatever the browser hands it for a link, a bad name or
a very deep tree; and the page loses a record named `__proto__`, which this
command keeps.

The first line is the case verdict. Then come the kernel's one-line summary
and every exception the reading names, in the kernel's own words, followed by
the packs, the files outside them, and the on-chain half.

| Verdict | Exit |
|---|---|
| `verified` | 0 |
| `failed` | 1 |
| `malformed` | 2 (also usage, an input that cannot be read as a case, or an unexpected error in the command) |
| `incomplete` | 3 |
| `partial` | 3 |
| `mixed` | 4 |

A proven failure keeps the headline: if anything in the case failed a check,
the case reads `failed` and exits 1, and everything else the reading found is
still listed beside it (evidence-bundle §5.5.4, v1.16). `mixed` means the case
has more than one outcome and none of them failed, so read the exceptions. It
has its own code because each of the others would claim something a mixed case
has not established: 0 would soften a case with an unsettled or unreadable
outcome in it, 3 would hide the outcomes that did settle, and 1 would accuse a
case in which nothing was proven wrong.

**What a case reading establishes.** Every pack is verified offline. Every
record every pack supplied is read in one pile under the hand-off rules of
envelope-spec §4.1, never partitioned by pack, issuer or organization. A
hand-off record with no counterpart among the case's records is always named,
and when a pack signed by the key it names is in the case, the reading says
that pack holds no counterpart, unless every candidate it had was accounted
for by another record (A20), which reads as no counterpart and names no pack.

**What it does not establish.** That the case is complete, since a party can
decline to hand over a pack. That a party which presented no pack under the
named key recorded nothing. That two organizations are independent of each
other. That any witness was re-fetched on chain: the reading makes no network
call, and the output says `on-chain NOT CHECKED` unless you pass
`--gateways <url,url>`. With gateways, each pack's witnesses are re-fetched
pack by pack and reported beside the reading, which they never change. A
witness that does not match exits 1, and an on-chain half that could not be
settled turns 0 into 3.

An unexpected error inside the command exits 2, never 1, so a defect in the
command is never reported as failed evidence.

The same command exists as `npx @sanning/proof case`, and a cross-kernel leg
runs both over every `case/` corpus vector and every tree in
`ts/vectors/case-split.json`, and requires identical output, identical `--json`
and identical exit codes.

## Compose an attested export

An attested export (`sanning.evidence.export/v2`, [`specs/evidence-export.md`](specs/evidence-export.md))
carries operator attestations over a source bundle, signed with your own
Ed25519 key:

```bash
sanning-proof export source.json --attestations attestations.json --key exporter.key --out export.json
sanning-proof verify export.json
```

`export` exits 0 when it wrote the export, 1 when the recomputed export is
`failed` (it still writes it, so you can inspect it), and 2 on a bad flag or
malformed input. The TypeScript kernel ships the same command, and
`cross-kernel/run_export_cli.sh` requires the two to write the same bytes.

## What a verdict claims

A verdict is recomputed from the artifact, never read off it. A bundle whose
own asserted verdict disagrees with the recomputed one is reported as the
recomputed one.

**A `verified` verdict is offline, and it does not mean the chain was
consulted.** A bundle *names* the witness that holds its bytes; asking that
witness whether it does is a separate, online act.

What a verifier can prove, and what it must not claim, is specified in
`specs/evidence-bundle.md` §5.3.4, which ships inside this package, under
"The standard". It draws four boundaries, covering
completeness, the chain, identity and per-agent slices. Read it before you
write down what a pack proved. A verifier that overclaims is worth less than no
verifier. The plain reading is at
[what a verdict means](https://docs.sanning.io/verify/what-a-verdict-means).

## The rest of the family

To check one signed envelope on its own, rather than a whole bundle:

```python
from sanning_proof import verify_envelope

result = verify_envelope(envelope)
result.ok   # spec_version accepted + payload binding + Ed25519 signature
```

Three arguments cover the rest:

- `verify_envelope(envelope, expected_content_hash=...)` binds an artifact you
  already hold to the provenance an envelope commits to, and reports which role
  it matched.
- `verify_envelope(envelope, payload_bytes=...)` checks an external-commitment
  envelope against the committed bytes.
- `verify_agent_proof(bundle)` proves a leaf event was in a signed checkpoint.

The TypeScript kernel has parity, including the RFC 9162 Merkle primitives.

## Sign an envelope

Most producers never call this kernel to sign: the write SDK, the agent daemon
and the MLflow plugin all sign through it, and reaching for
[`sanning-anchor`](https://pypi.org/project/sanning-anchor/) is the shorter
path. For a custom producer:

```python
from sanning_proof import jcs, sha256_hex, sign_envelope, signing_key_from_seed_hex

key = signing_key_from_seed_hex(SEED_HEX)
envelope = sign_envelope({
    "spec_version": "sanning.agent/v1",
    "event_id": EVENT_ID,
    "signed_at": "2026-01-01T00:00:00Z",
    "payload": payload,
    "payload_hash": sha256_hex(jcs(payload)),
}, key)
```

Replace `SEED_HEX` with a 32-byte hex Ed25519 seed and `EVENT_ID` with the
event's UUID. `sign_envelope` takes the envelope minus `signature` and returns
it with `public_key` and `signature` filled in, per the spec.

## The standard

**The specifications ship inside this package**, beside the kernel that
implements them, so the contract and the implementation claiming to satisfy it
arrive together and can be read against each other:

```bash
pip download --no-deps --no-binary :all: sanning-proof
tar xzf sanning_proof-*.tar.gz && ls sanning_proof-*/specs/
```

| Specification | Covers |
|---|---|
| `envelope-spec.md` | The producer-neutral Verifiable Event Envelope family contract |
| `evidence-bundle.md` | The `sanning.evidence/v1` report wrapper, its body types, and the verdict boundaries in §5.3.4 |
| `evidence-export.md` | The `sanning.evidence.export/v2` wire format |
| `log-store.md` | The `sanning.logstore/v1` store a pack's `logs/` is materialized from |
| `architecture.md` | The kernel, producer, connector and transport factoring standard |
| `governance.md` | Who decides, and how |

Three envelope **profiles** are registered against the family contract:
`sanning.agent/v1`, the agent daemon's inline-payload profile;
`sanning.mlflow/v1`, the MLflow plugin's external-commitment profile; and
`sanning.events/v2`, the anchor SDK's minimal-disclosure profile (`sanning.events/v1`
until the producer-to-agent cut, and no longer accepted). This Python kernel
accepts all three. Additive minors are accepted within a major, while unknown
majors and malformed versions fail closed.

**Conformance discipline:** both kernels reproduce one pinned corpus byte for
byte, across JCS-canonical bytes, payload hashes, envelope-for-signature bytes,
deterministic signatures, Merkle roots and audit paths. Neither passes by
agreeing with the other. If this package disagrees with a vector, the package
is wrong and the vector is not.

## Kernel scope

Deliberately small: canonicalization (RFC 8785), SHA-256 hashing, Ed25519 sign
and verify, RFC 9162 binary Merkle inclusion proofs, and the profile registry.
**No I/O, no networking, no key lifecycle.** Gateway fetching, attestation
polling and key storage belong to the products that import it.

Three dependencies, and adding to them is a design decision rather than a
convenience: [`PyNaCl`](https://pypi.org/project/PyNaCl/) and
[`jcs`](https://pypi.org/project/jcs/) for the kernel primitives, and
[`cryptography`](https://pypi.org/project/cryptography/) for RSA-PSS
attestation verification.

A malformed envelope returns a failed result rather than raising. A verifier
that crashes on a hostile artifact has handed that artifact a way to avoid
being checked, so both kernels treat adversarial input as something to report
on, not something to fall over on.

## Security

Report vulnerabilities to security@sanning.io. We support responsible
disclosure and ask for a reasonable opportunity to investigate before public
disclosure.

## License

MIT. The verifier is deliberately open-licensed so anyone can audit it and
verify evidence independently of Sanning.
