Metadata-Version: 2.4
Name: allowly-receipt-format
Version: 4.3.0
Summary: Reference Python verifier for the Allowly Receipt Format
Author-email: Allowly <support@allowly.ai>
License-Expression: Apache-2.0
Project-URL: Homepage, https://allowly.ai
Project-URL: Repository, https://github.com/Allowly-AI/allowly-receipt-format
Project-URL: Specification, https://github.com/Allowly-AI/allowly-receipt-format/tree/main/spec
Project-URL: Issues, https://github.com/Allowly-AI/allowly-receipt-format/issues
Keywords: allowly,receipts,audit,ed25519,verification
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
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
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=42.0
Requires-Dist: rfc8785<1,>=0.1.4
Dynamic: license-file

# Python Reference Verifier

Packaged Python verifier for Allowly Receipt Format wire version 4.

## Install

```bash
pip install allowly-receipt-format
```

Runtime dependencies are `cryptography` for Ed25519 verification and `rfc8785`
for SEAL record canonicalization.

## CLI

Verify a single receipt:

```bash
allowly-receipt-verify \
  --workspace-id "$ALLOWLY_WORKSPACE_ID" \
  --trusted-key-fingerprint "$ALLOWLY_TRUSTED_KEY_FINGERPRINT" \
  path/to/receipt.json path/to/keys.json
```

Verify a whole export or audit-package chain in one go (`.jsonl` or `.jsonl.gz`):

```bash
# Each line is either a bare receipt (audit-package chain.jsonl) or a
# {"receipt_id", ..., "receipt": {...}} export wrapper — both are handled.
allowly-receipt-verify \
  --export chain.jsonl \
  --workspace-id "$ALLOWLY_WORKSPACE_ID" \
  --trusted-key-fingerprint "$ALLOWLY_TRUSTED_KEY_FINGERPRINT" \
  keys.json
```

If the export includes checkpoint evidence, recompute every listed checkpoint
from the already verified receipt lines:

```bash
allowly-receipt-verify \
  --export chain.jsonl \
  --checkpoint-evidence checkpoint_evidence.json \
  --workspace-id "$ALLOWLY_WORKSPACE_ID" \
  --trusted-key-fingerprint "$ALLOWLY_TRUSTED_KEY_FINGERPRINT" \
  keys.json
```

Verify only one authorization's chain and check its structure (exactly one
`authorization.create`, at most one `authorization.revoke`, well-formed
timestamps), printing the timeline:

```bash
allowly-receipt-verify \
  --export export.jsonl.gz \
  --authorization-id auth_01HXZ2... \
  --workspace-id "$ALLOWLY_WORKSPACE_ID" \
  --trusted-key-fingerprint "$ALLOWLY_TRUSTED_KEY_FINGERPRINT" \
  keys.json
```

Replay conditional policy evidence for every selected action receipt while
verifying an export. Signed `authorization.create` receipts in the export are
used as the historical rule snapshots:

```bash
allowly-receipt-verify \
  --export chain.jsonl \
  --check-policy-evaluation \
  --workspace-id "$ALLOWLY_WORKSPACE_ID" \
  --trusted-key-fingerprint "$ALLOWLY_TRUSTED_KEY_FINGERPRINT" \
  keys.json
```

The policy mode prints one bounded line containing the receipt ID, status, and
diagnostic for each action receipt, followed by matched, mismatch, and
not-checked counts. It does not print receipt context or evaluation payloads.
Lifecycle receipts provide creation evidence and are not counted as action
results. Missing creation receipts, unsupported historical versions, and action
receipts without `policy_eval` remain visible as `not_checked`.

`--workspace-id` and at least one `--trusted-key-fingerprint` are required.
Fingerprints use `sha256:<64 lowercase hex>` over the decoded raw 32-byte
Ed25519 public key. Repeat the fingerprint flag for every trusted rotation key
that may have signed the selected receipts.

For local development without installing from PyPI:

```bash
pip install -e .
python verifier.py \
  --workspace-id "$ALLOWLY_WORKSPACE_ID" \
  --trusted-key-fingerprint "$ALLOWLY_TRUSTED_KEY_FINGERPRINT" \
  path/to/receipt.json path/to/keys.json
```

Exit codes:

- `0` — all presented receipts are valid; in policy mode, at least one action receipt was selected and every selected action receipt is `matched`
- `1` — invalid receipt evidence, a checkpoint failure, or a chain anomaly (reason on stderr)
- `2` — command-line usage error
- `3` — policy mode found one or more `mismatch` results; this takes precedence over incomplete coverage
- `4` — policy mode found `not_checked`, selected no action receipts, or received an empty unscoped export

## Library

```python
from allowly_receipt_format import verify_receipt, VerificationError, load_keys_from_json
import json
import os

with open("receipt.json") as f:
    receipt = json.load(f)
with open("keys.json") as f:
    keys_doc = json.load(f)
configured_workspace_id = os.environ["ALLOWLY_WORKSPACE_ID"]
trusted_fingerprints = {os.environ["ALLOWLY_TRUSTED_KEY_FINGERPRINT"]}
if keys_doc.get("workspace_id") != configured_workspace_id:
    raise ValueError("key document workspace does not match configuration")
keys = load_keys_from_json(keys_doc)

try:
    verify_receipt(
        receipt,
        keys,
        expected_workspace_id=configured_workspace_id,
        trusted_key_fingerprints=trusted_fingerprints,
    )
    print("valid")
except VerificationError as e:
    print(f"invalid: {e}")
```

### Replay conditional policy evidence

`verify_policy_evaluation` authenticates the action receipt and every supplied
authorization receipt before replay. The expected workspace and trusted key
fingerprints are required. Its result uses the same snake-case fields as the
TypeScript verifier:

```python
from allowly_receipt_format import verify_policy_evaluation

result = verify_policy_evaluation(
    action_receipt,
    authorization_creation_receipts,
    keys,
    expected_workspace_id=configured_workspace_id,
    trusted_key_fingerprints=trusted_fingerprints,
)

if result["status"] == "matched":
    print("recorded conditional evidence matches the replay")
elif result["status"] == "mismatch":
    print("recorded conditional evidence differs from the replay")
else:
    print("policy evidence could not be checked:", result["diagnostic"])
```

The supported profile is `allowly-conditional-evaluation-v1`. A match compares
only the complete `policy_eval` object. It does not claim that the final
`allow`, `deny`, `confirm`, or `escalate` decision was reproduced; budgets,
revocation, rate limits, approvals, and other state can affect that decision.
See `spec/profiles/allowly-conditional-evaluation-v1.md` for the exact engine,
operator, typed-comparison, missing-field, and context-reconstruction rules.

Always pass `expected_workspace_id` to bind the receipt to a workspace — a
`key_id` alone does not (spec §7, "Workspace binding"). Take that ID from
caller-trusted configuration, never from the receipt or key document, and
reject a key document that declares a different workspace. The CLI requires
that caller-trusted workspace ID plus one or more caller-trusted key
fingerprints. It checks the workspace against the key document and receipts,
and requires every selected receipt key to be pinned. A workspace ID or
fingerprint copied from the same untrusted bundle as the receipts is not an
independent trust anchor:

```python
verify_receipt(
    receipt,
    keys,
    expected_workspace_id="ws_01HXA1B2C3D4E5F6G7H8J9K0L1",
    trusted_key_fingerprints={"sha256:<64 lowercase hex>"},
)
```

`public_key_fingerprint(key)` returns that canonical fingerprint over the
decoded raw 32-byte Ed25519 public key. `load_keys_from_json` validates an
advertised `public_key_fingerprint`, but the advertised value is not itself a
trust anchor.

The package exposes typed verifier exceptions:

- `SchemaError`
- `UnknownKeyError`
- `KeyOutsideActiveWindowError`
- `SignatureMismatchError`

All inherit from `VerificationError`.

### Hash and verify a SEAL record

Use the raw function when JSON text comes from a file, form, or workflow. It
can reject duplicate decoded keys and number tokens that a normal parser would
silently change:

```python
from allowly_receipt_format import hash_seal_json, verify_seal_json

record_sha256 = hash_seal_json(raw_json)
result = verify_seal_json(
    raw_json,
    signed_receipt,
    keys,
    expected_workspace_id=configured_workspace_id,
    trusted_key_fingerprints=trusted_fingerprints,
)
assert result.signature_verified and result.record_matches
```

`hash_seal_value` and `verify_seal_value` are the explicit already-parsed
boundary. They cannot recover duplicate member names or original number-token
spellings. `SealInputError.code` identifies strict-input failures. A SEAL
verification result always reports signature verification separately from
record matching.

### Verify a daily checkpoint

`verify_checkpoint` verifies the checkpoint and member signatures, period,
count, Merkle root, and optional prior linkage. The workspace id must come from
caller-trusted configuration, and key fingerprints should come from an
authenticated channel:

```python
from allowly_receipt_format import verify_checkpoint

verify_checkpoint(
    checkpoint,
    member_receipts,
    keys,
    expected_workspace_id=configured_workspace_id,
    trusted_key_fingerprints=trusted_fingerprints,
    previous_checkpoint=previous_checkpoint,
)
```

Success proves the supplied set matches the signed commitment. Without an
external anchor it does not prove the issuer registered or retained every
receipt before constructing the checkpoint.

### Match a keyed pseudonym reference

`matches_ref` implements the optional `hmac-v1` convention in specification
Appendix A. Decode the show-once integration key, then match locally:

```python
import base64
from allowly_receipt_format import matches_ref

encoded_key = "<pseudonym_key_b64url>"
key = base64.urlsafe_b64decode(encoded_key + "=" * (-len(encoded_key) % 4))

assert matches_ref(
    key,
    "record",
    "MRN-48291",
    receipt["context"]["record_ref"],
)
```

Use the `context.ref_key_version` recorded in the receipt to select the retained
key version. Matching occurs entirely offline; it does not ask Allowly to
resolve an identifier.

`verify_receipt` accepts an already-parsed object, so it cannot detect duplicate
raw JSON names that a normal parser has already overwritten. The CLI uses a
duplicate-aware parser and rejects them. The verifier also rejects parsed
floating-point values, including the results of `1.0` and `1e0`; callers using
a different parser must preserve that distinction (spec §4.2).

## Test vectors

Run against the shared test vectors:

```bash
pip install -e .
python test_vectors.py ../../test-vectors.json
python test_exception_types.py ../../test-vectors.json
python test_seal.py ../../vectors/seal/profile-v1.json ../../vectors/seal/verification-v1.json
pytest -q test_policy_evaluation.py
```

All `should_verify` vectors must pass; all `should_reject` vectors must be rejected with the expected reason.

## License

Apache 2.0.
