Metadata-Version: 2.4
Name: evidencevault-verify
Version: 1.0.0
Summary: Offline verification of EvidenceVault certificates (Ed25519 + hybrid post-quantum ML-DSA-65 & SLH-DSA-SHA2-128f).
Author: Karl F. Ambrosius
License: MIT License
        
        Copyright (c) 2026 Karl F. Ambrosius
        
        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.
        
Project-URL: Homepage, https://github.com/EvidenceVault/evidencevault
Project-URL: Repository, https://github.com/EvidenceVault/evidencevault
Keywords: evidencevault,certificate,verification,post-quantum,ml-dsa,slh-dsa,ed25519,offline,provenance
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Legal Industry
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: Topic :: Security :: Cryptography
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=42
Requires-Dist: dilithium-py>=1.1
Requires-Dist: slh-dsa>=0.2.3
Provides-Extra: test
Requires-Dist: pytest>=7; extra == "test"
Dynamic: license-file

# evidencevault-verify

Offline, dependency-light verification of **EvidenceVault certificates** — for third
parties, auditors, and courts. `pip install evidencevault-verify` gives you a CLI and a
Python library that verify a certificate's cryptographic and structural integrity using
only its public artifacts, with **zero network access**.

This is a faithful Python port of the EvidenceVault TypeScript verifier
(`tools/cert-verify` + `packages/verify-core` + `packages/shared`). It produces
**byte-identical canonicalization and hashes** and the **same pass/fail verdicts** as the
reference `cert-verify` tool — proven by a cross-validation test suite that diffs the two
verifiers against real deployed certificates.

## What it verifies

For each certificate, exactly the checks the TypeScript verifier reports:

| Check | Meaning |
|-------|---------|
| `identityMatches` | The payload's `cert_id` / `issuing_tenant_id` match what you asked for |
| `canonicalHashMatches` | RFC 8785 JCS canonicalization of the payload → SHA-256 → unpadded base64url equals `signatures.canonical_payload_sha256` |
| `ed25519Valid` | Ed25519 signature over the canonical payload bytes (key resolved through the tenant registry, incl. time-bounded agent subkey certificates) |
| `mlDsa65Valid` | **ML-DSA-65** (FIPS 204) post-quantum co-signature |
| `slhDsaSha2128fValid` | **SLH-DSA-SHA2-128f** (FIPS 205) post-quantum co-signature |
| `merkleInclusionValid` | RFC 6962 inclusion proof (0x00 leaf / 0x01 node) binds the claimed leaf to `source_evidence_merkle_root` — `null` when no proof is present |
| `withinValidity` | `issued_at <= now < expires_at` |
| `dualSignatureValid` | Licensee co-signature for dual-signed classes: licensee credential present, platform-certified, `verified` and in-window — `null` when neither present nor required, **fail-closed** when required but missing |
| `predicatesValid` | Every declared predicate certificate exists, has status `issued`, and is within validity — **fail-closed** when predicates are declared but unresolved |
| `otsAnchorPresent` / `otsProofFound` | An OpenTimestamps `.ots` proof is present and non-empty (Bitcoin confirmation is delegated to the `ots` CLI via `--ots-cli`) |

A certificate is `ok` only when all mandatory checks pass and no optional check is `false`
— the identical conjunction the TypeScript verifier uses.

Both post-quantum families are **required** (a break in one family cannot forge a
certificate on its own), and both signatures were produced by
[`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum) (FIPS 204 / FIPS
205 final). See [Post-quantum interoperability](#post-quantum-interoperability) below.

## Install

```bash
pip install evidencevault-verify
```

Requires Python 3.10+. Dependencies (all pure-Python or pip-wheel, no native OQS build):
`cryptography` (Ed25519), `dilithium-py` (ML-DSA-65), `slh-dsa` (SLH-DSA-SHA2-128f).

## CLI

```bash
# Verify from a directory of public artifacts
evidencevault-verify --cert-id cert_01KY7HNX7T89K8HNNC17PBK3YD \
                     --tenant-id tnt_demo \
                     --dir ./bundle

# Verify from a self-contained bundle (base64-embedded files)
evidencevault-verify --cert-id <id> --tenant-id <tnt> --bundle ./bundle.json

# Machine-readable output
evidencevault-verify --cert-id <id> --tenant-id <tnt> --dir ./bundle --json

# Also run Bitcoin-tier OpenTimestamps confirmation (requires `ots` on PATH)
evidencevault-verify --cert-id <id> --tenant-id <tnt> --dir ./bundle --ots-cli
```

Output (byte-identical to the reference `cert-verify`):

```
✓ cert_01KY7HNX7T89K8HNNC17PBK3YD — {"tenantRegistryFound":true,...,"otsProofFound":true}
```

Exit code `0` when the certificate is valid, `1` when it is not, `2` on a usage error.

## Library API

```python
from evidencevault_verify import verify_certificate, DirSource

result = verify_certificate(DirSource("./bundle"), "cert_01KY7HNX...", "tnt_demo")

print(result.ok)                       # overall verdict
print(result.checks.ed25519_valid)     # individual checks (snake_case attributes)
print(result.checks.ml_dsa65_valid)
print(result.checks.slh_dsa_sha2_128f_valid)
print(result.checks.to_ts_dict())      # exact camelCase check map the CLI prints
for err in result.errors:
    print(err)
```

Convenience wrappers: `verify_certificate_from_dir(base_dir, cert_id, tenant_id)` and
`verify_certificate_from_bundle(bundle_path, cert_id, tenant_id)`. Pass `now=<datetime>`
to any of them to verify against a fixed time instead of the current clock.

`verify_certificate` accepts any object with a `read(relative_path) -> bytes | None`
method (the `ArtifactSource` protocol), so you can back it with a directory
(`DirSource`), an in-memory bundle (`BundleSource`), or your own storage.

## Artifact layout

Identical to the reference `cert-verify`. In `--dir` mode the base directory contains:

```
public/tenants/<tenant_id>.json              # tenant public key registry (trust root)
public/certificates/<cert_id>.json           # existence records (for predicate resolution)
certificates/<cert_id>/payload.json          # the signed certificate payload
certificates/<cert_id>/signatures.json       # Ed25519 + hybrid PQC signature block
certificates/<cert_id>/merkle-proof.json     # optional RFC 6962 inclusion proof
certificates/<cert_id>/cert.ots              # optional OpenTimestamps proof
```

In `--bundle` mode a single JSON file carries every file above as base64-encoded bytes.
Both modes contact **no network** and read **no EvidenceVault infrastructure**.

## Post-quantum interoperability

The load-bearing risk in a Python port is whether a Python PQC library can verify
signatures produced by `@noble/post-quantum`. It can — proven against the **real**
signatures on the shipped campaign certificates:

- **ML-DSA-65** → [`dilithium-py`](https://pypi.org/project/dilithium-py/) (pure-Python
  FIPS 204). `ML_DSA_65.verify(pubkey, msg, sig)` accepts @noble's signature and rejects a
  tampered message.
- **SLH-DSA-SHA2-128f** → [`slh-dsa`](https://pypi.org/project/slh-dsa/) (pure-Python FIPS
  205). Verified with the **pure** variant and an **empty context string**
  (`PublicKey.from_digest(pubkey, slhdsa.sha2_128f).verify_pure(msg, sig)`), which is what
  `@noble`'s `slh_dsa_sha2_128f.sign` produces. Note: the pre-hashed `verify` variant does
  *not* accept these signatures — pure is required.

Both libraries are pure-Python (no WASM, no native liboqs build), which keeps the verifier
portable and easy to audit. The interop is asserted for every campaign certificate in
`tests/test_pqc_interop.py`.

> Note on `quantcrypt` and `liboqs`: `quantcrypt` was evaluated first but on some platforms
> it has no pre-built wheel and its source build JIT-downloads PQClean from GitHub at
> runtime — unsuitable for an offline court tool. `liboqs`/`oqs` requires a CMake/C build of
> liboqs. The pure-Python `dilithium-py` + `slh-dsa` pair verifies the same `@noble`
> signatures with none of that friction, so it is the chosen dependency set.

## Cross-validation (the correctness proof)

`tests/test_cross_validation.py` runs the Python verifier and the reference TypeScript
`cert-verify` against the same real certificates and asserts their check maps are
**equal, field-for-field**. Where a Node toolchain is present the TS verifier is run live
and diffed; the pure-Python assertions (all-green verdict, canonical-hash parity, PQC
interop, fail-closed tampering) run unconditionally.

```bash
pip install -e ".[test]"
pytest
```

## For the operator: publishing to PyPI

The package is ready to publish. This repository does **not** publish it (that needs the
operator's PyPI credentials). To publish:

```bash
cd tools/evidencevault-verify-py
python -m build            # produces dist/*.tar.gz and dist/*.whl
twine upload dist/*        # requires your PyPI API token
```

Run `twine check dist/*` first to validate the metadata.

## License

MIT — see [LICENSE](./LICENSE).
