Metadata-Version: 2.4
Name: certanix-verify
Version: 0.14.0
Summary: Offline verification of Certanix evidence packs.
Project-URL: Homepage, https://certanix.eu/verify/
Project-URL: Schemas, https://schema.certanix.eu/
Author: GovStrat SRL
Maintainer: GovStrat SRL
License-Expression: BSD-3-Clause-Clear
License-File: LICENSE
License-File: NOTICE
License-File: PATENTS-NOTICE.md
Keywords: ecdsa,evidence,rfc3161,signature,verification
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Security :: Cryptography
Requires-Python: >=3.10
Requires-Dist: asn1crypto==1.5.1
Requires-Dist: cryptography==48.0.1
Description-Content-Type: text/markdown

# certanix-verify

Verify a Certanix evidence pack offline. The standard library plus two pinned
dependencies, no network, no `cosign` binary, no transparency-log lookup, and nothing
from the Certanix platform.

**The two dependencies, and why** (from 0.6.0):

- `cryptography` checks signatures (ECDSA, RSA) and X.509 certificates.
- `asn1crypto`, pure Python with no compiled code, parses ASN.1: the RFC 3161 timestamp
  tokens and RFC 4998 evidence records of the Certanix Vault, which `cryptography` cannot
  read. Hand-written DER parsing was rejected as security-sensitive code with no second
  reviewer, and a separate package was rejected because a person checking a disclosure
  in 2036 should need to install one tool.


## Quickstart

```bash
pip install certanix-verify
certanix-verify verify evidence-pack.json --key cosign.pub
echo $?          # 0 = verified; any other value is the failure class (see below)
```

## What it checks, and what it does not

It checks that the pack's **canonical bytes** — recomputed from the document exactly as
you supply it — match the detached signature made by the key you pass in, that the pack
satisfies the evidence-pack schema, and that it does not claim a capability its own
recorded capability gate denies.

It does **not** tell you whose key that is. A signature verifies against whatever key
you hand it, so confirm the key's fingerprint against an out-of-band source (a Certanix
deck appendix, printed collateral, a channel you already trust) before trusting what a
green result means. A verifier that implied otherwise would be lying by omission.

## Exit codes

The exit code **is** the failure class, so a script can branch on *why* verification
failed without parsing text:

| Code | Class | Meaning |
|---|---|---|
| 0 | `OK` | Signature valid over the canonical bytes; schema valid; no gate violation. |
| 1 | `USAGE` | Could not begin: unreadable pack, missing key, bad invocation. Says nothing about the pack. |
| 2 | `MALFORMED_INPUT` | Not parseable as a JSON object (e.g. truncated). |
| 3 | `SCHEMA_INVALID` | Parses, but violates the schema: missing required field, unknown field, value outside a closed vocabulary. |
| 4 | `UNSUPPORTED_SCHEMA_VERSION` | A schema version this verifier does not know. Refused rather than guessed at. |
| 5 | `SIGNATURE_MISSING` | The pack carries no signature — an unsigned pack, not a failed check. |
| 6 | `SIGNATURE_MALFORMED` | A signature is present but its container is not decodable. |
| 7 | `SIGNATURE_MISMATCH` | The cryptographic check failed: these are not the bytes that were signed. **The tamper class.** |
| 8 | `KEY_MISMATCH` | The signature names a different signer than the key supplied. |
| 9 | `GATE_VIOLATION` | Signature and schema are valid, but the pack claims a capability its recorded gate state denies. |
| 10 | `EVIDENCE_MISMATCH` | `vault` only: the evidence record does not cover these bytes. They changed after they were timestamped. |
| 11 | `TIMESTAMP_INVALID` | `vault` only: the RFC 3161 token is not valid (modified, or its signature fails). |
| 12 | `TSA_UNTRUSTED` | `vault` only: the token's signer does not chain to a trust anchor you supplied, is not a time-stamping certificate, or was not valid when the token was issued. |
| 13 | `TSA_EXPIRED` | `vault` only: the time-stamping certificate has expired at the verification time and nothing in the record renews it. |

## Vault evidence records (0.6.0)

```bash
certanix-verify vault evidence-record-0001.ers \
    --data artefact --data manifest.json --tsa-root tsa-root.pem [--at 2036-01-01T00:00:00Z]
```

It shows that the data objects are unchanged since the time the TSA states, as attested
by the trust anchor you supply. From 0.7.0 it reads renewed records (RFC 4998 section 5):
a later timestamp over an earlier one, or a new chain under a newer hash algorithm. Each
timestamp must have been covered by the next before its certificate expired, and the
newest must be valid at the verification time; a renewal that came too late is
`TSA_EXPIRED`, and a broken link is `EVIDENCE_MISMATCH`. From 0.8.0 records are read
exactly as Bouncy Castle's independent RFC 4998 implementation reads them
(`org.bouncycastle.tsp.ers` 1.86, cross-checked 2026-10-08), and each record names the
hash-tree convention it was built with. It checks no revocation (it is offline), and it says
nothing about whether the content is true.

Note what code 3 does *not* mean: the verifier stops at the first failed check, so
`SCHEMA_INVALID` means it never got as far as the signature. It is not a statement that
the signature was fine.

Two failure modes are deliberately **not** distinguished. A wrong key and a mutated byte
are cryptographically identical from the outside; code 8 is reported only when the
signature bundle positively names a different signer, and otherwise the honest answer is
code 7. This verifier does not upgrade an ambiguous failure into a more specific
accusation.

## Machine-readable output

```bash
certanix-verify verify evidence-pack.json --key cosign.pub --json
```

```json
{
  "verified": false,
  "status": "SIGNATURE_MISMATCH",
  "exit_code": 7,
  "schema_version": "1.7",
  "detail": "recomputed canonical bytes do not match the signed digest",
  "verifier_version": "certanix-verify 0.3.0"
}
```

`exit_code` always equals the process exit code. `verified` is carried explicitly so a
consumer never has to know that zero means success.

## Two modes, deliberately different

**Pack mode** (the default) takes the pack JSON and *recomputes* its canonical form. A
pack that has been re-serialised somewhere in your toolchain — different indentation,
different key order, same content — still verifies. The signature binds the content, not
the transport encoding, and reporting a benign JSON round-trip as tampering would be a
false accusation.

**Byte-exact mode** takes a canonical-bytes file and its detached bundle:

```bash
certanix-verify verify evidence-pack.canonical.json --bundle evidence-pack.bundle --key cosign.pub
```

Here nothing is recomputed, so **any** byte change fails — including a re-serialisation
that pack mode accepts. That is the correct behaviour for an archived artifact whose
exact bytes are the claim. The two modes disagreeing on that one input is by design.

**Standalone mode** takes a signed artifact that is not a pack and names its own type in
`artifact_type`: a delta report or delta refusal, self-evidence, or an EU Control and
Origin Attestation (`attestation`):

```bash
certanix-verify verify attestation.json --standalone --key cosign.pub
```

The checks are the same as for a pack (structure, then the signature over the canonical
bytes recomputed from the document as presented), against that type's bundled schema. An
unknown or missing `artifact_type` is refused as an unsupported schema, never guessed.

## Provenance

`VENDORED.json` records where this verifier came from: the platform source file and its
SHA-256, the platform commit, and the schema version the bundled descriptors were taken
from. The verification logic here is generated verbatim from the platform's single
source — it is a copy, never a reimplementation — and the platform's CI asserts that
this copy and the platform agree on every case in the verification conformance corpus.

## Licence

BSD-3-Clause-Clear: see [LICENSE](LICENSE). Who holds the copyright, and the assignment
to come: see [NOTICE](NOTICE). Patents: see [PATENTS-NOTICE.md](PATENTS-NOTICE.md); the
licence itself grants no patent rights.
