provenex-verifier 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- provenex_verifier-1.0.0/CHANGELOG.md +26 -0
- provenex_verifier-1.0.0/CODEOWNERS +4 -0
- provenex_verifier-1.0.0/CODE_OF_CONDUCT.md +65 -0
- provenex_verifier-1.0.0/CONTRIBUTING.md +39 -0
- provenex_verifier-1.0.0/LICENSE +21 -0
- provenex_verifier-1.0.0/MANIFEST.in +15 -0
- provenex_verifier-1.0.0/Makefile +17 -0
- provenex_verifier-1.0.0/PKG-INFO +130 -0
- provenex_verifier-1.0.0/README.md +98 -0
- provenex_verifier-1.0.0/SECURITY.md +49 -0
- provenex_verifier-1.0.0/provenex_verifier/__init__.py +33 -0
- provenex_verifier-1.0.0/provenex_verifier/canonical.py +57 -0
- provenex_verifier-1.0.0/provenex_verifier/cli.py +109 -0
- provenex_verifier-1.0.0/provenex_verifier/merkle.py +108 -0
- provenex_verifier-1.0.0/provenex_verifier/py.typed +0 -0
- provenex_verifier-1.0.0/provenex_verifier/verifier.py +240 -0
- provenex_verifier-1.0.0/provenex_verifier.egg-info/PKG-INFO +130 -0
- provenex_verifier-1.0.0/provenex_verifier.egg-info/SOURCES.txt +35 -0
- provenex_verifier-1.0.0/provenex_verifier.egg-info/dependency_links.txt +1 -0
- provenex_verifier-1.0.0/provenex_verifier.egg-info/entry_points.txt +2 -0
- provenex_verifier-1.0.0/provenex_verifier.egg-info/requires.txt +5 -0
- provenex_verifier-1.0.0/provenex_verifier.egg-info/top_level.txt +1 -0
- provenex_verifier-1.0.0/pyproject.toml +81 -0
- provenex_verifier-1.0.0/setup.cfg +4 -0
- provenex_verifier-1.0.0/test-vectors/README.md +25 -0
- provenex_verifier-1.0.0/test-vectors/ed25519_public_key.pem +3 -0
- provenex_verifier-1.0.0/test-vectors/generate.py +226 -0
- provenex_verifier-1.0.0/test-vectors/hmac_receipt.json +36 -0
- provenex_verifier-1.0.0/test-vectors/tampered_field_receipt.json +78 -0
- provenex_verifier-1.0.0/test-vectors/valid_full_receipt.json +78 -0
- provenex_verifier-1.0.0/test-vectors/valid_minimal_receipt.json +36 -0
- provenex_verifier-1.0.0/tests/__init__.py +0 -0
- provenex_verifier-1.0.0/tests/conftest.py +137 -0
- provenex_verifier-1.0.0/tests/test_canonical.py +83 -0
- provenex_verifier-1.0.0/tests/test_cli.py +123 -0
- provenex_verifier-1.0.0/tests/test_merkle.py +238 -0
- provenex_verifier-1.0.0/tests/test_verifier.py +319 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
4
|
+
|
|
5
|
+
## [1.0.0] - 2026-05-25
|
|
6
|
+
|
|
7
|
+
Initial release.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
* `verify_receipt(receipt, public_key)` returning `VerificationResult`.
|
|
12
|
+
* `canonicalize(receipt)` exposed publicly so callers can audit the canonical-payload step.
|
|
13
|
+
* `verify_inclusion_proof(leaf_hash, proof, tree_root, leaf_index, tree_size)` for RFC 6962 Merkle inclusion proofs.
|
|
14
|
+
* `provenex-verify` CLI with stdin support and standard exit codes (0/1/2).
|
|
15
|
+
* Static test vectors under `test-vectors/` plus a generator script.
|
|
16
|
+
* 100% line and branch coverage on the verifier modules.
|
|
17
|
+
* CI matrix across Python 3.9, 3.10, 3.11, 3.12, and 3.13.
|
|
18
|
+
|
|
19
|
+
### Design notes
|
|
20
|
+
|
|
21
|
+
* Ed25519 only. HMAC receipts are rejected with a clear error rather than silently accepted, because HMAC is symmetric and out of scope for third-party verification.
|
|
22
|
+
* No network calls. Safe to run in air-gapped environments.
|
|
23
|
+
* No dependency on `provenex-core`. The library implements the receipt-format spec directly.
|
|
24
|
+
* Single runtime dependency: `cryptography` (for Ed25519 primitives).
|
|
25
|
+
|
|
26
|
+
[1.0.0]: https://github.com/aoptimystic/provenex-verifier-python/releases/tag/v1.0.0
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our
|
|
6
|
+
community a harassment-free experience for everyone, regardless of age, body
|
|
7
|
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
8
|
+
identity and expression, level of experience, education, socio-economic status,
|
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity
|
|
10
|
+
and orientation.
|
|
11
|
+
|
|
12
|
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
13
|
+
diverse, inclusive, and healthy community.
|
|
14
|
+
|
|
15
|
+
## Our Standards
|
|
16
|
+
|
|
17
|
+
Examples of behavior that contributes to a positive environment for our
|
|
18
|
+
community include:
|
|
19
|
+
|
|
20
|
+
* Demonstrating empathy and kindness toward other people
|
|
21
|
+
* Being respectful of differing opinions, viewpoints, and experiences
|
|
22
|
+
* Giving and gracefully accepting constructive feedback
|
|
23
|
+
* Accepting responsibility and apologizing to those affected by our mistakes,
|
|
24
|
+
and learning from the experience
|
|
25
|
+
* Focusing on what is best not just for us as individuals, but for the overall
|
|
26
|
+
community
|
|
27
|
+
|
|
28
|
+
Examples of unacceptable behavior include:
|
|
29
|
+
|
|
30
|
+
* The use of sexualized language or imagery, and sexual attention or advances of
|
|
31
|
+
any kind
|
|
32
|
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
33
|
+
* Public or private harassment
|
|
34
|
+
* Publishing others' private information, such as a physical or email address,
|
|
35
|
+
without their explicit permission
|
|
36
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
|
37
|
+
professional setting
|
|
38
|
+
|
|
39
|
+
## Enforcement Responsibilities
|
|
40
|
+
|
|
41
|
+
Community leaders are responsible for clarifying and enforcing our standards of
|
|
42
|
+
acceptable behavior and will take appropriate and fair corrective action in
|
|
43
|
+
response to any behavior that they deem inappropriate, threatening, offensive,
|
|
44
|
+
or harmful.
|
|
45
|
+
|
|
46
|
+
## Scope
|
|
47
|
+
|
|
48
|
+
This Code of Conduct applies within all community spaces, and also applies when
|
|
49
|
+
an individual is officially representing the community in public spaces.
|
|
50
|
+
|
|
51
|
+
## Enforcement
|
|
52
|
+
|
|
53
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
54
|
+
reported to the community leaders responsible for enforcement at
|
|
55
|
+
**contact@provenex.ai**. All complaints will be reviewed and investigated
|
|
56
|
+
promptly and fairly.
|
|
57
|
+
|
|
58
|
+
## Attribution
|
|
59
|
+
|
|
60
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
61
|
+
version 2.1, available at
|
|
62
|
+
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
|
63
|
+
|
|
64
|
+
[homepage]: https://www.contributor-covenant.org
|
|
65
|
+
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing. The verifier is intentionally small: scope discipline is the project's defining property.
|
|
4
|
+
|
|
5
|
+
## Ground rules
|
|
6
|
+
|
|
7
|
+
1. **No new runtime dependencies.** The library depends on `cryptography` and nothing else. Proposals to add dependencies require a clear rationale and will be considered carefully.
|
|
8
|
+
2. **No network access at runtime.** The library must run unmodified in an air-gapped environment.
|
|
9
|
+
3. **No bidirectional dependency on `provenex-core`.** The verifier implements the receipt-format spec directly. If the spec changes, this library changes; if `provenex-core` adds an unrelated feature, this library does not.
|
|
10
|
+
4. **Spec changes are major versions.** Any change that breaks a previously valid receipt requires a major version bump and a corresponding CHANGELOG entry.
|
|
11
|
+
|
|
12
|
+
## Development
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
git clone https://github.com/aoptimystic/provenex-verifier-python
|
|
16
|
+
cd provenex-verifier-python
|
|
17
|
+
python -m pip install -e ".[test]"
|
|
18
|
+
make test
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Tests must:
|
|
22
|
+
|
|
23
|
+
* Maintain 100% line and branch coverage on the `provenex_verifier` package.
|
|
24
|
+
* Complete in under 5 seconds (`pytest -xvs`).
|
|
25
|
+
* Pass on Python 3.9 through 3.13.
|
|
26
|
+
|
|
27
|
+
## Adding a test
|
|
28
|
+
|
|
29
|
+
Negative cases are first-class. Every new validation path should ship with a test for the failure mode it guards.
|
|
30
|
+
|
|
31
|
+
## Pull requests
|
|
32
|
+
|
|
33
|
+
* Open a PR against `main`.
|
|
34
|
+
* Reference the issue or use case in the description.
|
|
35
|
+
* The CI matrix must pass on all supported Python versions before merge.
|
|
36
|
+
|
|
37
|
+
## Releases
|
|
38
|
+
|
|
39
|
+
Releases are tagged `vX.Y.Z`, signed with Sigstore cosign, and published to PyPI with an SBOM. The first stable release is v1.0.0. See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Saurabh Kulkarni
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
include LICENSE
|
|
2
|
+
include README.md
|
|
3
|
+
include CHANGELOG.md
|
|
4
|
+
include SECURITY.md
|
|
5
|
+
include CONTRIBUTING.md
|
|
6
|
+
include CODE_OF_CONDUCT.md
|
|
7
|
+
include CODEOWNERS
|
|
8
|
+
include Makefile
|
|
9
|
+
include pyproject.toml
|
|
10
|
+
recursive-include provenex_verifier *.py py.typed
|
|
11
|
+
recursive-include tests *.py
|
|
12
|
+
recursive-include test-vectors *.py *.json *.pem *.md
|
|
13
|
+
global-exclude __pycache__
|
|
14
|
+
global-exclude *.py[cod]
|
|
15
|
+
global-exclude .DS_Store
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.PHONY: install test cov lint clean build
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
python -m pip install -e ".[test]"
|
|
5
|
+
|
|
6
|
+
test:
|
|
7
|
+
pytest -xvs
|
|
8
|
+
|
|
9
|
+
cov:
|
|
10
|
+
pytest --cov=provenex_verifier --cov-report=term-missing --cov-report=html
|
|
11
|
+
|
|
12
|
+
clean:
|
|
13
|
+
rm -rf build dist *.egg-info .coverage htmlcov .pytest_cache
|
|
14
|
+
|
|
15
|
+
build:
|
|
16
|
+
python -m pip install --upgrade build
|
|
17
|
+
python -m build
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: provenex-verifier
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Standalone, offline verifier for Provenex receipts. Implements the receipt-format spec; no network, no telemetry, no dependencies on provenex-core.
|
|
5
|
+
Author-email: Provenex <contact@provenex.ai>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://provenex.ai
|
|
8
|
+
Project-URL: Source, https://github.com/aoptimystic/provenex-verifier-python
|
|
9
|
+
Project-URL: Issues, https://github.com/aoptimystic/provenex-verifier-python/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/aoptimystic/provenex-verifier-python#readme
|
|
11
|
+
Keywords: provenex,receipt,verifier,ed25519,merkle,rfc6962,ai-compliance,rag,llm-security
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Information Technology
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Security :: Cryptography
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: cryptography>=41.0.0
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
30
|
+
Requires-Dist: pytest-cov>=4.0; extra == "test"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# provenex-verifier
|
|
34
|
+
|
|
35
|
+
[](https://github.com/aoptimystic/provenex-verifier-python/actions/workflows/ci.yml)
|
|
36
|
+
[](https://pypi.org/project/provenex-verifier/)
|
|
37
|
+
[](https://pypi.org/project/provenex-verifier/)
|
|
38
|
+
[](LICENSE)
|
|
39
|
+
|
|
40
|
+
Standalone, offline verifier for [Provenex](https://provenex.ai) receipts.
|
|
41
|
+
|
|
42
|
+
A Provenex receipt is a signed JSON artifact that a regulator or third party can validate without contacting the issuing organization. This library is the reference verifier: it takes a receipt and an Ed25519 public key, and tells you whether the signature is valid over the canonical payload.
|
|
43
|
+
|
|
44
|
+
* **No network.** Runs in air-gapped environments. No telemetry, no analytics, no phone-home.
|
|
45
|
+
* **No `provenex-core` dependency.** Implements the receipt-format spec directly.
|
|
46
|
+
* **One runtime dependency** (`cryptography`) for Ed25519. Everything else is stdlib.
|
|
47
|
+
* **Tiny.** Under 500 lines of actual code.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
pip install provenex-verifier
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 30-second example
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import json
|
|
59
|
+
from provenex_verifier import verify_receipt
|
|
60
|
+
|
|
61
|
+
with open("receipt.json") as fh:
|
|
62
|
+
receipt = json.load(fh)
|
|
63
|
+
with open("issuer_public_key.pem", "rb") as fh:
|
|
64
|
+
public_key = fh.read()
|
|
65
|
+
|
|
66
|
+
result = verify_receipt(receipt, public_key)
|
|
67
|
+
|
|
68
|
+
if result.valid:
|
|
69
|
+
print(f"Receipt OK. Signed by {result.signer} at {result.signed_at}.")
|
|
70
|
+
else:
|
|
71
|
+
print("Receipt INVALID:")
|
|
72
|
+
for err in result.errors:
|
|
73
|
+
print(f" - {err}")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Command line
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
provenex-verify receipt.json --public-key issuer_public_key.pem
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Exit codes: `0` valid, `1` invalid, `2` usage error. Reads from stdin if the receipt argument is `-`.
|
|
83
|
+
|
|
84
|
+
## API
|
|
85
|
+
|
|
86
|
+
| Symbol | Purpose |
|
|
87
|
+
| ------ | ------- |
|
|
88
|
+
| `verify_receipt(receipt, public_key) -> VerificationResult` | Verify an Ed25519 receipt signature. |
|
|
89
|
+
| `VerificationResult` | NamedTuple: `valid`, `signer`, `signed_at`, `errors`, `warnings`. |
|
|
90
|
+
| `canonicalize(receipt) -> bytes` | Reproduce the canonical signed-payload bytes. Exposed so callers can audit the canonicalization step independently of signature verification. |
|
|
91
|
+
| `verify_inclusion_proof(leaf_hash, proof, tree_root, leaf_index, tree_size) -> bool` | Verify an RFC 6962 Merkle inclusion proof. |
|
|
92
|
+
|
|
93
|
+
Every public function carries an inline docstring. See the source for parameter shapes and edge-case behavior.
|
|
94
|
+
|
|
95
|
+
## What this library does *not* do
|
|
96
|
+
|
|
97
|
+
The verifier is deliberately narrow:
|
|
98
|
+
|
|
99
|
+
* No fingerprinting, normalization, indexing, or retrieval logic.
|
|
100
|
+
* No policy evaluation. Receipts record decisions; this library does not re-interpret them.
|
|
101
|
+
* No HTTP client, no key-fetching, no TSA lookups.
|
|
102
|
+
* No automatic Merkle inclusion check, no trajectory walking. Those are layered on top by callers.
|
|
103
|
+
|
|
104
|
+
If you need to issue receipts, use `provenex-core`. This library is purely the verification half.
|
|
105
|
+
|
|
106
|
+
## Algorithm support
|
|
107
|
+
|
|
108
|
+
* **Ed25519** -- yes. The verifier accepts public keys in PEM (`SubjectPublicKeyInfo`), DER, or 32-byte raw form.
|
|
109
|
+
* **HMAC-SHA256** -- explicitly rejected. HMAC is symmetric: anyone able to verify can forge. A third-party verifier is by definition not the producer, so HMAC receipts are out of scope. Receipts using HMAC fail with a clear `unsupported signature algorithm` error.
|
|
110
|
+
|
|
111
|
+
## Spec compatibility
|
|
112
|
+
|
|
113
|
+
This library implements:
|
|
114
|
+
|
|
115
|
+
* Receipt schema **2.5.0** canonicalization rules (`docs/receipt_format.md` in `provenex-core`).
|
|
116
|
+
* RFC 6962 Merkle inclusion proofs.
|
|
117
|
+
|
|
118
|
+
Test vectors live in [`test-vectors/`](test-vectors/) and are versioned alongside the library. The cross-compatibility test in `tests/test_merkle.py` pins a proof generated by `provenex-core` and asserts this verifier accepts it.
|
|
119
|
+
|
|
120
|
+
## Stability promise
|
|
121
|
+
|
|
122
|
+
This library is at **v1.0.0** because the receipt format is. Any change that breaks a previously valid receipt is a breaking change and bumps the major version.
|
|
123
|
+
|
|
124
|
+
## Security
|
|
125
|
+
|
|
126
|
+
Report vulnerabilities to **contact@provenex.ai** (see [SECURITY.md](SECURITY.md)). The library has no network surface area, but a verifier that mis-validates a tampered receipt is a security issue.
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# provenex-verifier
|
|
2
|
+
|
|
3
|
+
[](https://github.com/aoptimystic/provenex-verifier-python/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/provenex-verifier/)
|
|
5
|
+
[](https://pypi.org/project/provenex-verifier/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Standalone, offline verifier for [Provenex](https://provenex.ai) receipts.
|
|
9
|
+
|
|
10
|
+
A Provenex receipt is a signed JSON artifact that a regulator or third party can validate without contacting the issuing organization. This library is the reference verifier: it takes a receipt and an Ed25519 public key, and tells you whether the signature is valid over the canonical payload.
|
|
11
|
+
|
|
12
|
+
* **No network.** Runs in air-gapped environments. No telemetry, no analytics, no phone-home.
|
|
13
|
+
* **No `provenex-core` dependency.** Implements the receipt-format spec directly.
|
|
14
|
+
* **One runtime dependency** (`cryptography`) for Ed25519. Everything else is stdlib.
|
|
15
|
+
* **Tiny.** Under 500 lines of actual code.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
pip install provenex-verifier
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 30-second example
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import json
|
|
27
|
+
from provenex_verifier import verify_receipt
|
|
28
|
+
|
|
29
|
+
with open("receipt.json") as fh:
|
|
30
|
+
receipt = json.load(fh)
|
|
31
|
+
with open("issuer_public_key.pem", "rb") as fh:
|
|
32
|
+
public_key = fh.read()
|
|
33
|
+
|
|
34
|
+
result = verify_receipt(receipt, public_key)
|
|
35
|
+
|
|
36
|
+
if result.valid:
|
|
37
|
+
print(f"Receipt OK. Signed by {result.signer} at {result.signed_at}.")
|
|
38
|
+
else:
|
|
39
|
+
print("Receipt INVALID:")
|
|
40
|
+
for err in result.errors:
|
|
41
|
+
print(f" - {err}")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Command line
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
provenex-verify receipt.json --public-key issuer_public_key.pem
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Exit codes: `0` valid, `1` invalid, `2` usage error. Reads from stdin if the receipt argument is `-`.
|
|
51
|
+
|
|
52
|
+
## API
|
|
53
|
+
|
|
54
|
+
| Symbol | Purpose |
|
|
55
|
+
| ------ | ------- |
|
|
56
|
+
| `verify_receipt(receipt, public_key) -> VerificationResult` | Verify an Ed25519 receipt signature. |
|
|
57
|
+
| `VerificationResult` | NamedTuple: `valid`, `signer`, `signed_at`, `errors`, `warnings`. |
|
|
58
|
+
| `canonicalize(receipt) -> bytes` | Reproduce the canonical signed-payload bytes. Exposed so callers can audit the canonicalization step independently of signature verification. |
|
|
59
|
+
| `verify_inclusion_proof(leaf_hash, proof, tree_root, leaf_index, tree_size) -> bool` | Verify an RFC 6962 Merkle inclusion proof. |
|
|
60
|
+
|
|
61
|
+
Every public function carries an inline docstring. See the source for parameter shapes and edge-case behavior.
|
|
62
|
+
|
|
63
|
+
## What this library does *not* do
|
|
64
|
+
|
|
65
|
+
The verifier is deliberately narrow:
|
|
66
|
+
|
|
67
|
+
* No fingerprinting, normalization, indexing, or retrieval logic.
|
|
68
|
+
* No policy evaluation. Receipts record decisions; this library does not re-interpret them.
|
|
69
|
+
* No HTTP client, no key-fetching, no TSA lookups.
|
|
70
|
+
* No automatic Merkle inclusion check, no trajectory walking. Those are layered on top by callers.
|
|
71
|
+
|
|
72
|
+
If you need to issue receipts, use `provenex-core`. This library is purely the verification half.
|
|
73
|
+
|
|
74
|
+
## Algorithm support
|
|
75
|
+
|
|
76
|
+
* **Ed25519** -- yes. The verifier accepts public keys in PEM (`SubjectPublicKeyInfo`), DER, or 32-byte raw form.
|
|
77
|
+
* **HMAC-SHA256** -- explicitly rejected. HMAC is symmetric: anyone able to verify can forge. A third-party verifier is by definition not the producer, so HMAC receipts are out of scope. Receipts using HMAC fail with a clear `unsupported signature algorithm` error.
|
|
78
|
+
|
|
79
|
+
## Spec compatibility
|
|
80
|
+
|
|
81
|
+
This library implements:
|
|
82
|
+
|
|
83
|
+
* Receipt schema **2.5.0** canonicalization rules (`docs/receipt_format.md` in `provenex-core`).
|
|
84
|
+
* RFC 6962 Merkle inclusion proofs.
|
|
85
|
+
|
|
86
|
+
Test vectors live in [`test-vectors/`](test-vectors/) and are versioned alongside the library. The cross-compatibility test in `tests/test_merkle.py` pins a proof generated by `provenex-core` and asserts this verifier accepts it.
|
|
87
|
+
|
|
88
|
+
## Stability promise
|
|
89
|
+
|
|
90
|
+
This library is at **v1.0.0** because the receipt format is. Any change that breaks a previously valid receipt is a breaking change and bumps the major version.
|
|
91
|
+
|
|
92
|
+
## Security
|
|
93
|
+
|
|
94
|
+
Report vulnerabilities to **contact@provenex.ai** (see [SECURITY.md](SECURITY.md)). The library has no network surface area, but a verifier that mis-validates a tampered receipt is a security issue.
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Email **contact@provenex.ai** with the subject line `[verifier] security`.
|
|
6
|
+
|
|
7
|
+
Please include:
|
|
8
|
+
|
|
9
|
+
* A description of the issue and its impact.
|
|
10
|
+
* Steps to reproduce or a proof-of-concept.
|
|
11
|
+
* Affected versions.
|
|
12
|
+
* Your name or handle if you would like credit; otherwise we will publish the disclosure anonymously.
|
|
13
|
+
|
|
14
|
+
Do **not** open public GitHub issues for security reports.
|
|
15
|
+
|
|
16
|
+
## Response targets
|
|
17
|
+
|
|
18
|
+
| Step | Target |
|
|
19
|
+
| ---- | ------ |
|
|
20
|
+
| Acknowledgement of report | 2 business days |
|
|
21
|
+
| Initial triage and severity assessment | 5 business days |
|
|
22
|
+
| Fix or mitigation in a tagged release | Critical: 7 days. High: 30 days. Lower: best-effort. |
|
|
23
|
+
| Public disclosure | Coordinated with reporter; default 90 days after fix is shipped. |
|
|
24
|
+
|
|
25
|
+
## Scope
|
|
26
|
+
|
|
27
|
+
In scope:
|
|
28
|
+
|
|
29
|
+
* Bugs that cause `verify_receipt` to return `valid=True` for a receipt whose signature does not match its canonical payload.
|
|
30
|
+
* Bugs that cause `verify_inclusion_proof` to return `True` for an invalid proof.
|
|
31
|
+
* Canonicalization mismatches that would let a producer and verifier disagree on the signed bytes.
|
|
32
|
+
* Resource-exhaustion attacks reachable through public APIs (CPU, memory, file descriptors) on adversarial input.
|
|
33
|
+
|
|
34
|
+
Out of scope:
|
|
35
|
+
|
|
36
|
+
* Issues that require an attacker already holding the issuer's private key.
|
|
37
|
+
* Policy interpretation, trajectory validation, or anything beyond cryptographic envelope verification: this library deliberately does not perform those checks.
|
|
38
|
+
* Bugs in upstream `cryptography` (please report to PyCA).
|
|
39
|
+
|
|
40
|
+
## Supported versions
|
|
41
|
+
|
|
42
|
+
We support the latest minor release of the `1.x` line. Security fixes for older minor releases are best-effort.
|
|
43
|
+
|
|
44
|
+
## Cryptographic primitives
|
|
45
|
+
|
|
46
|
+
* Ed25519 via [pyca/cryptography](https://github.com/pyca/cryptography). Constant-time signature verification at the primitive level.
|
|
47
|
+
* SHA-256 (stdlib `hashlib`) for RFC 6962 Merkle hashing.
|
|
48
|
+
|
|
49
|
+
There is no random-number generation, no key derivation, and no symmetric encryption in this library. Public-key loading is offline (no key-fetching).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Standalone verifier for Provenex receipts.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
|
|
5
|
+
verify_receipt(receipt, public_key) -> VerificationResult
|
|
6
|
+
Verify the Ed25519 signature on a receipt.
|
|
7
|
+
|
|
8
|
+
canonicalize(receipt) -> bytes
|
|
9
|
+
Produce the canonical signed-payload bytes for a receipt.
|
|
10
|
+
|
|
11
|
+
verify_inclusion_proof(leaf_hash, proof, tree_root, leaf_index, tree_size) -> bool
|
|
12
|
+
Verify an RFC 6962 Merkle inclusion proof.
|
|
13
|
+
|
|
14
|
+
VerificationResult
|
|
15
|
+
NamedTuple returned by verify_receipt.
|
|
16
|
+
|
|
17
|
+
The library has a single runtime dependency (``cryptography``) and makes no
|
|
18
|
+
network calls. It is safe to run in an air-gapped environment.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .canonical import canonicalize
|
|
22
|
+
from .merkle import verify_inclusion_proof
|
|
23
|
+
from .verifier import VerificationResult, verify_receipt
|
|
24
|
+
|
|
25
|
+
__version__ = "1.0.0"
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"VerificationResult",
|
|
29
|
+
"__version__",
|
|
30
|
+
"canonicalize",
|
|
31
|
+
"verify_inclusion_proof",
|
|
32
|
+
"verify_receipt",
|
|
33
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Canonical JSON serialization for Provenex receipts.
|
|
2
|
+
|
|
3
|
+
The canonical form is what gets signed. Any verifier that wants to reproduce
|
|
4
|
+
the signed bytes must follow the same rules. The serialization is exposed
|
|
5
|
+
publicly so callers can audit the canonicalization step independently of
|
|
6
|
+
signature verification.
|
|
7
|
+
|
|
8
|
+
Rules:
|
|
9
|
+
|
|
10
|
+
* The ``signature`` block is stripped before serialization. Signing a receipt
|
|
11
|
+
is signing every byte of its canonical form *except* the signature itself.
|
|
12
|
+
* Keys are sorted lexicographically at every level of nesting.
|
|
13
|
+
* Separators are ``","`` and ``":"`` (no whitespace).
|
|
14
|
+
* Non-ASCII characters (smart quotes, accented letters, CJK) are emitted as
|
|
15
|
+
raw UTF-8, not as ``\\uXXXX`` escapes. This matches the producer side and
|
|
16
|
+
keeps the form portable to non-Python verifiers.
|
|
17
|
+
* ``NaN``, ``Infinity``, and ``-Infinity`` are rejected.
|
|
18
|
+
|
|
19
|
+
Matches provenex-core ``ProvenanceReceipt.canonical_payload`` as of receipt
|
|
20
|
+
schema 2.5.0.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
from typing import Any, Mapping
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def canonicalize(receipt: Mapping[str, Any]) -> bytes:
|
|
30
|
+
"""Return the canonical signed-payload bytes for a receipt.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
receipt: The receipt dict (typically the output of ``json.loads`` on a
|
|
34
|
+
receipt JSON file). The input is not mutated.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
UTF-8 encoded canonical JSON bytes, with any ``signature`` block
|
|
38
|
+
omitted.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
TypeError: If ``receipt`` is not a mapping, or contains values JSON
|
|
42
|
+
cannot serialize.
|
|
43
|
+
ValueError: If the receipt contains ``NaN``, ``Infinity``, or
|
|
44
|
+
``-Infinity``.
|
|
45
|
+
"""
|
|
46
|
+
if not isinstance(receipt, Mapping):
|
|
47
|
+
raise TypeError(
|
|
48
|
+
f"receipt must be a mapping (dict), got {type(receipt).__name__}"
|
|
49
|
+
)
|
|
50
|
+
payload = {k: v for k, v in receipt.items() if k != "signature"}
|
|
51
|
+
return json.dumps(
|
|
52
|
+
payload,
|
|
53
|
+
sort_keys=True,
|
|
54
|
+
separators=(",", ":"),
|
|
55
|
+
ensure_ascii=False,
|
|
56
|
+
allow_nan=False,
|
|
57
|
+
).encode("utf-8")
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""``provenex-verify`` command-line entry point.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
provenex-verify <receipt.json> --public-key <key.pem>
|
|
6
|
+
cat receipt.json | provenex-verify - --public-key key.pem
|
|
7
|
+
|
|
8
|
+
Exit codes:
|
|
9
|
+
|
|
10
|
+
* ``0`` -- signature verified.
|
|
11
|
+
* ``1`` -- signature did not verify (tampered receipt, wrong key, etc.).
|
|
12
|
+
* ``2`` -- usage error (missing file, malformed JSON, bad arguments).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
from typing import List, Optional, TextIO
|
|
21
|
+
|
|
22
|
+
from . import __version__
|
|
23
|
+
from .verifier import verify_receipt
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
27
|
+
parser = argparse.ArgumentParser(
|
|
28
|
+
prog="provenex-verify",
|
|
29
|
+
description=(
|
|
30
|
+
"Verify the Ed25519 signature on a Provenex receipt. "
|
|
31
|
+
"Reads receipt JSON from a file (or '-' for stdin) and an "
|
|
32
|
+
"Ed25519 public key (PEM or DER) from disk."
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"receipt",
|
|
37
|
+
help="Path to receipt JSON file, or '-' to read from stdin.",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--public-key",
|
|
41
|
+
required=True,
|
|
42
|
+
help="Path to Ed25519 public key (PEM SubjectPublicKeyInfo or DER).",
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--quiet",
|
|
46
|
+
action="store_true",
|
|
47
|
+
help="Suppress per-field output; rely on exit code only.",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--version",
|
|
51
|
+
action="version",
|
|
52
|
+
version=f"provenex-verify {__version__}",
|
|
53
|
+
)
|
|
54
|
+
return parser
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main(
|
|
58
|
+
argv: Optional[List[str]] = None,
|
|
59
|
+
stdin: Optional[TextIO] = None,
|
|
60
|
+
stdout: Optional[TextIO] = None,
|
|
61
|
+
stderr: Optional[TextIO] = None,
|
|
62
|
+
) -> int:
|
|
63
|
+
"""Entry point. Returns an exit code; never raises on normal failures."""
|
|
64
|
+
stdin = stdin or sys.stdin
|
|
65
|
+
stdout = stdout or sys.stdout
|
|
66
|
+
stderr = stderr or sys.stderr
|
|
67
|
+
|
|
68
|
+
parser = _build_parser()
|
|
69
|
+
args = parser.parse_args(argv)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
if args.receipt == "-":
|
|
73
|
+
receipt = json.load(stdin)
|
|
74
|
+
else:
|
|
75
|
+
with open(args.receipt, "r", encoding="utf-8") as fh:
|
|
76
|
+
receipt = json.load(fh)
|
|
77
|
+
except OSError as exc:
|
|
78
|
+
print(f"error: could not read receipt: {exc}", file=stderr)
|
|
79
|
+
return 2
|
|
80
|
+
except json.JSONDecodeError as exc:
|
|
81
|
+
print(f"error: receipt is not valid JSON: {exc}", file=stderr)
|
|
82
|
+
return 2
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
with open(args.public_key, "rb") as fh:
|
|
86
|
+
key_bytes = fh.read()
|
|
87
|
+
except OSError as exc:
|
|
88
|
+
print(f"error: could not read public key: {exc}", file=stderr)
|
|
89
|
+
return 2
|
|
90
|
+
|
|
91
|
+
result = verify_receipt(receipt, key_bytes)
|
|
92
|
+
|
|
93
|
+
if not args.quiet:
|
|
94
|
+
print(f"valid: {result.valid}", file=stdout)
|
|
95
|
+
print(f"signer: {result.signer or '(missing)'}", file=stdout)
|
|
96
|
+
print(
|
|
97
|
+
f"signed_at: {result.signed_at.isoformat() if result.signed_at else '(unknown)'}",
|
|
98
|
+
file=stdout,
|
|
99
|
+
)
|
|
100
|
+
for err in result.errors:
|
|
101
|
+
print(f"error: {err}", file=stderr)
|
|
102
|
+
for warn in result.warnings:
|
|
103
|
+
print(f"warning: {warn}", file=stderr)
|
|
104
|
+
|
|
105
|
+
return 0 if result.valid else 1
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__": # pragma: no cover
|
|
109
|
+
sys.exit(main())
|