aragora-verify 0.1.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.
- aragora_verify-0.1.0/.gitignore +7 -0
- aragora_verify-0.1.0/CHANGELOG.md +20 -0
- aragora_verify-0.1.0/LICENSE +21 -0
- aragora_verify-0.1.0/PKG-INFO +136 -0
- aragora_verify-0.1.0/README.md +102 -0
- aragora_verify-0.1.0/pyproject.toml +71 -0
- aragora_verify-0.1.0/src/aragora_verify/__init__.py +53 -0
- aragora_verify-0.1.0/src/aragora_verify/__main__.py +6 -0
- aragora_verify-0.1.0/src/aragora_verify/cli.py +104 -0
- aragora_verify-0.1.0/src/aragora_verify/jcs.py +136 -0
- aragora_verify-0.1.0/src/aragora_verify/odr_schema.json +305 -0
- aragora_verify-0.1.0/src/aragora_verify/py.typed +0 -0
- aragora_verify-0.1.0/src/aragora_verify/schema.py +209 -0
- aragora_verify-0.1.0/src/aragora_verify/verifier.py +431 -0
- aragora_verify-0.1.0/tests/_fixtures.py +79 -0
- aragora_verify-0.1.0/tests/test_cli.py +81 -0
- aragora_verify-0.1.0/tests/test_example_live_receipt.py +33 -0
- aragora_verify-0.1.0/tests/test_example_merge_quorum_receipt.py +39 -0
- aragora_verify-0.1.0/tests/test_jcs.py +63 -0
- aragora_verify-0.1.0/tests/test_review_fixes.py +142 -0
- aragora_verify-0.1.0/tests/test_verifier.py +222 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `aragora-verify` are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/) and the project uses semantic
|
|
5
|
+
versioning.
|
|
6
|
+
|
|
7
|
+
## [0.1.0] — unreleased
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Initial release: standalone offline verifier for Open Decision Receipts (ODR v0.1).
|
|
11
|
+
- `aragora-verify <receipt.json> [--pubkey KEY] [--chain JSONL] [--json]` CLI.
|
|
12
|
+
- Library API: `verify`, `load_public_key`, `compute_key_id`, `validate_structure`,
|
|
13
|
+
`jcs_canonicalize`, `odr_content_digest`.
|
|
14
|
+
- Checks: ODR v0.1 schema conformance (stdlib structural validator, with optional
|
|
15
|
+
`jsonschema` rigor), RFC 8785 (JCS) canonical digest recomputation, Ed25519
|
|
16
|
+
detached-signature verification, quorum participant consistency, and hash-chain
|
|
17
|
+
linkage/anchoring.
|
|
18
|
+
- Absent markers and `"undisclosed"` model families surfaced as non-failing
|
|
19
|
+
weakening signals.
|
|
20
|
+
- Dependencies: Python standard library plus `cryptography`; `jsonschema` optional.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aragora Contributors
|
|
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,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aragora-verify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Standalone offline verifier for Open Decision Receipts (ODR) -- check schema, JCS canonical digest, Ed25519 signature, and hash-chain linkage with no Aragora install or account.
|
|
5
|
+
Project-URL: Homepage, https://github.com/synaptent/aragora
|
|
6
|
+
Project-URL: Documentation, https://github.com/synaptent/aragora/blob/main/docs/specs/OPEN_DECISION_RECEIPT.md
|
|
7
|
+
Project-URL: Repository, https://github.com/synaptent/aragora/tree/main/aragora-verify
|
|
8
|
+
Project-URL: Changelog, https://github.com/synaptent/aragora/blob/main/aragora-verify/CHANGELOG.md
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/synaptent/aragora/issues
|
|
10
|
+
Author-email: Aragora <team@aragora.dev>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: ai-governance,audit-trail,decision-integrity,decision-receipt,ed25519,eu-ai-act,jcs,odr,offline-verification,open-decision-receipt,rfc8785
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Legal Industry
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Security :: Cryptography
|
|
25
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: cryptography>=41.0
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
31
|
+
Provides-Extra: schema
|
|
32
|
+
Requires-Dist: jsonschema>=4.0; extra == 'schema'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# aragora-verify
|
|
36
|
+
|
|
37
|
+
**Verify an [Open Decision Receipt](https://github.com/synaptent/aragora/blob/main/docs/specs/OPEN_DECISION_RECEIPT.md) offline — no Aragora install, no server, no account.**
|
|
38
|
+
|
|
39
|
+
Action-level receipts (Microsoft AGT, SCITT, in-toto/SLSA) prove *what happened
|
|
40
|
+
and whether policy allowed it*. An **Open Decision Receipt (ODR)** proves the
|
|
41
|
+
layer above: *why it was decided, who adversarially examined it with what model
|
|
42
|
+
diversity, who dissented, how calibrated the confidence was, and whether an
|
|
43
|
+
accountable human accepted the risk.*
|
|
44
|
+
|
|
45
|
+
`aragora-verify` is the free, standalone tool that lets anyone — an auditor, a
|
|
46
|
+
customer, a skeptic — check such a receipt is genuine and well-formed:
|
|
47
|
+
|
|
48
|
+
- **Schema conformance** to the ODR v0.1 content profile.
|
|
49
|
+
- **Canonical digest** — recomputes `SHA-256(JCS(receipt − signatures))` per
|
|
50
|
+
RFC 8785, the value any detached signature covers.
|
|
51
|
+
- **Ed25519 signature** — verifies detached signatures with only the public key.
|
|
52
|
+
- **Quorum consistency** — every supporting/dissenting agent is a disclosed
|
|
53
|
+
participant (a mismatch is a tamper/malformed signal).
|
|
54
|
+
- **Hash-chain linkage** — when a chain is supplied, the receipt is anchored in
|
|
55
|
+
it and the links are continuous.
|
|
56
|
+
|
|
57
|
+
It depends only on the Python standard library plus `cryptography`.
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install aragora-verify
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Use
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Structural + canonical-digest check
|
|
69
|
+
aragora-verify receipt.odr.json
|
|
70
|
+
|
|
71
|
+
# Full authenticity check against the issuer's published public key
|
|
72
|
+
aragora-verify receipt.odr.json --pubkey aragora-odr-signing-key.pem
|
|
73
|
+
|
|
74
|
+
# Also confirm the receipt is anchored in a hash chain
|
|
75
|
+
aragora-verify receipt.odr.json --pubkey key.pem --chain intent-chain.jsonl
|
|
76
|
+
|
|
77
|
+
# Machine-readable result
|
|
78
|
+
aragora-verify receipt.odr.json --pubkey key.pem --json
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Exit code `0` means verified (no failed checks, and any present signatures were
|
|
82
|
+
checked); `1` means a check failed; `2` is a usage/input error; `3` means the
|
|
83
|
+
receipt is structurally OK but carries signatures that were **not** checked
|
|
84
|
+
(no `--pubkey` supplied) — authenticity is unestablished, so it is deliberately
|
|
85
|
+
not reported as `0`/VERIFIED.
|
|
86
|
+
|
|
87
|
+
The public key for receipts emitted by an Aragora deployment is published at
|
|
88
|
+
`GET /.well-known/aragora-odr-signing-key` and `GET /api/v2/receipts/signing-key`.
|
|
89
|
+
|
|
90
|
+
### Weakening vs. failing
|
|
91
|
+
|
|
92
|
+
Absent markers (`{"status": "absent", ...}`) and `"undisclosed"` model families
|
|
93
|
+
are **honesty signals** — a receipt full of them is visibly weak, not a
|
|
94
|
+
strong-looking fabrication. They are reported as *weakening signals* and do
|
|
95
|
+
**not** fail verification; the policy thresholds (e.g. "require ≥2 model
|
|
96
|
+
families", "require human attestation") are yours to apply on top.
|
|
97
|
+
|
|
98
|
+
### Known limitations (v0.1)
|
|
99
|
+
|
|
100
|
+
The verifier is deliberately conservative and these are documented, not silent:
|
|
101
|
+
|
|
102
|
+
- **Hash-chain (`--chain`) is anchoring + self-consistency, not integrity.** It
|
|
103
|
+
confirms the receipt's content digest appears in the chain and that declared
|
|
104
|
+
`prev_hash`/`hash` links are internally consistent, but it does **not** recompute
|
|
105
|
+
entry hashes — so it reports `chain_link` as `WARN` when links are present. A
|
|
106
|
+
party who controls the chain file can fabricate consistent-looking linkage; the
|
|
107
|
+
chain is corroborating evidence, not a tamper proof on its own.
|
|
108
|
+
- **Signature verification is single-key, Ed25519-only.** It verifies that at least
|
|
109
|
+
one `signatures[]` entry validates against the supplied `--pubkey` (and fails if
|
|
110
|
+
an entry targeting that key fails). Richer multi-signer / threshold policies are
|
|
111
|
+
out of scope for v0.1.
|
|
112
|
+
- **I-JSON numeric range.** Canonicalization assumes IEEE-754-double-safe numbers
|
|
113
|
+
(per RFC 8785 / I-JSON). Integers at or beyond 1e21 are not expected in ODR
|
|
114
|
+
payloads and are not specially handled.
|
|
115
|
+
|
|
116
|
+
## Library
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from aragora_verify import verify, load_public_key
|
|
120
|
+
|
|
121
|
+
result = verify(receipt_dict, public_key=load_public_key(pem_bytes))
|
|
122
|
+
print(result.ok, result.odr_digest)
|
|
123
|
+
for check in result.checks:
|
|
124
|
+
print(check.name, check.status, check.detail)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## What this is part of
|
|
128
|
+
|
|
129
|
+
ODR-3 of the [Open Decision Receipt epic](https://github.com/synaptent/aragora/issues/8223).
|
|
130
|
+
The verifier is free and standalone by design — the *emitter* (adversarial
|
|
131
|
+
debate + signed decision receipts) is the product. See the
|
|
132
|
+
[content-profile spec](https://github.com/synaptent/aragora/blob/main/docs/specs/OPEN_DECISION_RECEIPT.md).
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# aragora-verify
|
|
2
|
+
|
|
3
|
+
**Verify an [Open Decision Receipt](https://github.com/synaptent/aragora/blob/main/docs/specs/OPEN_DECISION_RECEIPT.md) offline — no Aragora install, no server, no account.**
|
|
4
|
+
|
|
5
|
+
Action-level receipts (Microsoft AGT, SCITT, in-toto/SLSA) prove *what happened
|
|
6
|
+
and whether policy allowed it*. An **Open Decision Receipt (ODR)** proves the
|
|
7
|
+
layer above: *why it was decided, who adversarially examined it with what model
|
|
8
|
+
diversity, who dissented, how calibrated the confidence was, and whether an
|
|
9
|
+
accountable human accepted the risk.*
|
|
10
|
+
|
|
11
|
+
`aragora-verify` is the free, standalone tool that lets anyone — an auditor, a
|
|
12
|
+
customer, a skeptic — check such a receipt is genuine and well-formed:
|
|
13
|
+
|
|
14
|
+
- **Schema conformance** to the ODR v0.1 content profile.
|
|
15
|
+
- **Canonical digest** — recomputes `SHA-256(JCS(receipt − signatures))` per
|
|
16
|
+
RFC 8785, the value any detached signature covers.
|
|
17
|
+
- **Ed25519 signature** — verifies detached signatures with only the public key.
|
|
18
|
+
- **Quorum consistency** — every supporting/dissenting agent is a disclosed
|
|
19
|
+
participant (a mismatch is a tamper/malformed signal).
|
|
20
|
+
- **Hash-chain linkage** — when a chain is supplied, the receipt is anchored in
|
|
21
|
+
it and the links are continuous.
|
|
22
|
+
|
|
23
|
+
It depends only on the Python standard library plus `cryptography`.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install aragora-verify
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Use
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Structural + canonical-digest check
|
|
35
|
+
aragora-verify receipt.odr.json
|
|
36
|
+
|
|
37
|
+
# Full authenticity check against the issuer's published public key
|
|
38
|
+
aragora-verify receipt.odr.json --pubkey aragora-odr-signing-key.pem
|
|
39
|
+
|
|
40
|
+
# Also confirm the receipt is anchored in a hash chain
|
|
41
|
+
aragora-verify receipt.odr.json --pubkey key.pem --chain intent-chain.jsonl
|
|
42
|
+
|
|
43
|
+
# Machine-readable result
|
|
44
|
+
aragora-verify receipt.odr.json --pubkey key.pem --json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Exit code `0` means verified (no failed checks, and any present signatures were
|
|
48
|
+
checked); `1` means a check failed; `2` is a usage/input error; `3` means the
|
|
49
|
+
receipt is structurally OK but carries signatures that were **not** checked
|
|
50
|
+
(no `--pubkey` supplied) — authenticity is unestablished, so it is deliberately
|
|
51
|
+
not reported as `0`/VERIFIED.
|
|
52
|
+
|
|
53
|
+
The public key for receipts emitted by an Aragora deployment is published at
|
|
54
|
+
`GET /.well-known/aragora-odr-signing-key` and `GET /api/v2/receipts/signing-key`.
|
|
55
|
+
|
|
56
|
+
### Weakening vs. failing
|
|
57
|
+
|
|
58
|
+
Absent markers (`{"status": "absent", ...}`) and `"undisclosed"` model families
|
|
59
|
+
are **honesty signals** — a receipt full of them is visibly weak, not a
|
|
60
|
+
strong-looking fabrication. They are reported as *weakening signals* and do
|
|
61
|
+
**not** fail verification; the policy thresholds (e.g. "require ≥2 model
|
|
62
|
+
families", "require human attestation") are yours to apply on top.
|
|
63
|
+
|
|
64
|
+
### Known limitations (v0.1)
|
|
65
|
+
|
|
66
|
+
The verifier is deliberately conservative and these are documented, not silent:
|
|
67
|
+
|
|
68
|
+
- **Hash-chain (`--chain`) is anchoring + self-consistency, not integrity.** It
|
|
69
|
+
confirms the receipt's content digest appears in the chain and that declared
|
|
70
|
+
`prev_hash`/`hash` links are internally consistent, but it does **not** recompute
|
|
71
|
+
entry hashes — so it reports `chain_link` as `WARN` when links are present. A
|
|
72
|
+
party who controls the chain file can fabricate consistent-looking linkage; the
|
|
73
|
+
chain is corroborating evidence, not a tamper proof on its own.
|
|
74
|
+
- **Signature verification is single-key, Ed25519-only.** It verifies that at least
|
|
75
|
+
one `signatures[]` entry validates against the supplied `--pubkey` (and fails if
|
|
76
|
+
an entry targeting that key fails). Richer multi-signer / threshold policies are
|
|
77
|
+
out of scope for v0.1.
|
|
78
|
+
- **I-JSON numeric range.** Canonicalization assumes IEEE-754-double-safe numbers
|
|
79
|
+
(per RFC 8785 / I-JSON). Integers at or beyond 1e21 are not expected in ODR
|
|
80
|
+
payloads and are not specially handled.
|
|
81
|
+
|
|
82
|
+
## Library
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from aragora_verify import verify, load_public_key
|
|
86
|
+
|
|
87
|
+
result = verify(receipt_dict, public_key=load_public_key(pem_bytes))
|
|
88
|
+
print(result.ok, result.odr_digest)
|
|
89
|
+
for check in result.checks:
|
|
90
|
+
print(check.name, check.status, check.detail)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## What this is part of
|
|
94
|
+
|
|
95
|
+
ODR-3 of the [Open Decision Receipt epic](https://github.com/synaptent/aragora/issues/8223).
|
|
96
|
+
The verifier is free and standalone by design — the *emitter* (adversarial
|
|
97
|
+
debate + signed decision receipts) is the product. See the
|
|
98
|
+
[content-profile spec](https://github.com/synaptent/aragora/blob/main/docs/specs/OPEN_DECISION_RECEIPT.md).
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aragora-verify"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Standalone offline verifier for Open Decision Receipts (ODR) -- check schema, JCS canonical digest, Ed25519 signature, and hash-chain linkage with no Aragora install or account."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Aragora", email = "team@aragora.dev" },
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"decision-receipt",
|
|
17
|
+
"open-decision-receipt",
|
|
18
|
+
"odr",
|
|
19
|
+
"offline-verification",
|
|
20
|
+
"ed25519",
|
|
21
|
+
"jcs",
|
|
22
|
+
"rfc8785",
|
|
23
|
+
"audit-trail",
|
|
24
|
+
"ai-governance",
|
|
25
|
+
"eu-ai-act",
|
|
26
|
+
"decision-integrity",
|
|
27
|
+
]
|
|
28
|
+
classifiers = [
|
|
29
|
+
"Development Status :: 4 - Beta",
|
|
30
|
+
"Intended Audience :: Developers",
|
|
31
|
+
"Intended Audience :: Legal Industry",
|
|
32
|
+
"License :: OSI Approved :: MIT License",
|
|
33
|
+
"Operating System :: OS Independent",
|
|
34
|
+
"Programming Language :: Python :: 3",
|
|
35
|
+
"Programming Language :: Python :: 3.10",
|
|
36
|
+
"Programming Language :: Python :: 3.11",
|
|
37
|
+
"Programming Language :: Python :: 3.12",
|
|
38
|
+
"Programming Language :: Python :: 3.13",
|
|
39
|
+
"Topic :: Security :: Cryptography",
|
|
40
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
41
|
+
"Typing :: Typed",
|
|
42
|
+
]
|
|
43
|
+
dependencies = [
|
|
44
|
+
"cryptography>=41.0",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.optional-dependencies]
|
|
48
|
+
schema = ["jsonschema>=4.0"]
|
|
49
|
+
dev = ["pytest>=8.0"]
|
|
50
|
+
|
|
51
|
+
[project.scripts]
|
|
52
|
+
aragora-verify = "aragora_verify.cli:main"
|
|
53
|
+
|
|
54
|
+
[project.urls]
|
|
55
|
+
Homepage = "https://github.com/synaptent/aragora"
|
|
56
|
+
Documentation = "https://github.com/synaptent/aragora/blob/main/docs/specs/OPEN_DECISION_RECEIPT.md"
|
|
57
|
+
Repository = "https://github.com/synaptent/aragora/tree/main/aragora-verify"
|
|
58
|
+
Changelog = "https://github.com/synaptent/aragora/blob/main/aragora-verify/CHANGELOG.md"
|
|
59
|
+
"Bug Tracker" = "https://github.com/synaptent/aragora/issues"
|
|
60
|
+
|
|
61
|
+
[tool.hatch.build.targets.sdist]
|
|
62
|
+
exclude = ["CLAUDE.md", "**/CLAUDE.md", ".nomic/", ".benchmarks/"]
|
|
63
|
+
|
|
64
|
+
[tool.hatch.build.targets.wheel]
|
|
65
|
+
packages = ["src/aragora_verify"]
|
|
66
|
+
|
|
67
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
68
|
+
"src/aragora_verify/odr_schema.json" = "aragora_verify/odr_schema.json"
|
|
69
|
+
|
|
70
|
+
[tool.pytest.ini_options]
|
|
71
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""aragora-verify — standalone offline verifier for Open Decision Receipts.
|
|
2
|
+
|
|
3
|
+
Validate an ODR v0.1 receipt with nothing but the receipt JSON (and optionally
|
|
4
|
+
a public key and a hash chain): schema conformance, RFC 8785 (JCS) canonical
|
|
5
|
+
digest, Ed25519 detached signature, quorum consistency, and hash-chain linkage.
|
|
6
|
+
|
|
7
|
+
No Aragora install, no server, no account. The emitter is the product; the
|
|
8
|
+
verifier is free.
|
|
9
|
+
|
|
10
|
+
from aragora_verify import verify, load_public_key
|
|
11
|
+
result = verify(receipt_dict, public_key=load_public_key(pem_bytes))
|
|
12
|
+
assert result.ok
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from .jcs import jcs_canonicalize, odr_content_digest
|
|
18
|
+
from .schema import ODR_PROFILE_URI, ODR_VERSION, validate_structure
|
|
19
|
+
from .verifier import (
|
|
20
|
+
Check,
|
|
21
|
+
VerificationError,
|
|
22
|
+
VerifyResult,
|
|
23
|
+
compute_key_id,
|
|
24
|
+
load_public_key,
|
|
25
|
+
verify,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Single-source the version from installed package metadata (pyproject.toml is
|
|
29
|
+
# the only place the number lives). Falls back when running from a source tree
|
|
30
|
+
# with no installed dist metadata.
|
|
31
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
__version__ = _pkg_version("aragora-verify")
|
|
35
|
+
except PackageNotFoundError: # pragma: no cover - source tree without metadata
|
|
36
|
+
__version__ = "0.0.0+source"
|
|
37
|
+
|
|
38
|
+
del PackageNotFoundError, _pkg_version
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"__version__",
|
|
42
|
+
"verify",
|
|
43
|
+
"load_public_key",
|
|
44
|
+
"compute_key_id",
|
|
45
|
+
"validate_structure",
|
|
46
|
+
"jcs_canonicalize",
|
|
47
|
+
"odr_content_digest",
|
|
48
|
+
"Check",
|
|
49
|
+
"VerifyResult",
|
|
50
|
+
"VerificationError",
|
|
51
|
+
"ODR_VERSION",
|
|
52
|
+
"ODR_PROFILE_URI",
|
|
53
|
+
]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""``aragora-verify`` command-line interface.
|
|
2
|
+
|
|
3
|
+
aragora-verify receipt.json [--pubkey key.pem] [--chain chain.jsonl] [--json]
|
|
4
|
+
|
|
5
|
+
Exit status: ``0`` when the receipt verifies (no failed checks and any present
|
|
6
|
+
signatures were checked), ``1`` when any check fails, ``2`` for usage/input
|
|
7
|
+
errors, ``3`` when the receipt is structurally OK but carries signatures that
|
|
8
|
+
were never checked (no ``--pubkey`` supplied). With ``--json`` the structured
|
|
9
|
+
:class:`~aragora_verify.verifier.VerifyResult` is printed instead of the report.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
from typing import Sequence
|
|
18
|
+
|
|
19
|
+
from . import __version__
|
|
20
|
+
from .verifier import VerificationError, VerifyResult, verify_path
|
|
21
|
+
|
|
22
|
+
_GLYPH = {"pass": "PASS", "fail": "FAIL", "warn": "WARN", "skip": "----"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _render(result: VerifyResult) -> str:
|
|
26
|
+
lines: list[str] = []
|
|
27
|
+
if not result.ok:
|
|
28
|
+
verdict = "FAILED"
|
|
29
|
+
elif result.authenticity_unverified:
|
|
30
|
+
verdict = "UNVERIFIED"
|
|
31
|
+
else:
|
|
32
|
+
verdict = "VERIFIED"
|
|
33
|
+
lines.append(f"Open Decision Receipt — {verdict}")
|
|
34
|
+
lines.append(f" receipt_id: {result.receipt_id or '<missing>'}")
|
|
35
|
+
if result.odr_digest:
|
|
36
|
+
lines.append(f" odr_digest: sha-256:{result.odr_digest}")
|
|
37
|
+
if verdict == "UNVERIFIED":
|
|
38
|
+
lines.append(
|
|
39
|
+
" note: signatures are present but were NOT checked (no --pubkey); "
|
|
40
|
+
"authenticity is NOT established"
|
|
41
|
+
)
|
|
42
|
+
lines.append("")
|
|
43
|
+
lines.append(" checks:")
|
|
44
|
+
for check in result.checks:
|
|
45
|
+
lines.append(f" [{_GLYPH.get(check.status, check.status)}] {check.name}: {check.detail}")
|
|
46
|
+
if result.warnings:
|
|
47
|
+
lines.append("")
|
|
48
|
+
lines.append(" weakening signals (do not fail verification):")
|
|
49
|
+
for warning in result.warnings:
|
|
50
|
+
lines.append(f" ! {warning}")
|
|
51
|
+
lines.append("")
|
|
52
|
+
lines.append(f" => {verdict}")
|
|
53
|
+
return "\n".join(lines)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
57
|
+
parser = argparse.ArgumentParser(
|
|
58
|
+
prog="aragora-verify",
|
|
59
|
+
description=(
|
|
60
|
+
"Offline verifier for Open Decision Receipts (ODR v0.1): schema "
|
|
61
|
+
"conformance, JCS canonical digest, Ed25519 signature, hash-chain "
|
|
62
|
+
"link, and quorum consistency. No Aragora install or account required."
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument("receipt", help="path to the ODR receipt JSON")
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--pubkey",
|
|
68
|
+
metavar="KEY",
|
|
69
|
+
help="Ed25519 public key (PEM/DER/raw/base64/hex) to verify signatures with",
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--chain",
|
|
73
|
+
metavar="JSONL",
|
|
74
|
+
help="hash-chain file (JSONL); checks the receipt is anchored and the chain links",
|
|
75
|
+
)
|
|
76
|
+
parser.add_argument("--json", action="store_true", help="emit the structured result as JSON")
|
|
77
|
+
parser.add_argument("--version", action="version", version=f"aragora-verify {__version__}")
|
|
78
|
+
return parser
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
82
|
+
args = build_parser().parse_args(argv)
|
|
83
|
+
try:
|
|
84
|
+
result = verify_path(args.receipt, pubkey_path=args.pubkey, chain_path=args.chain)
|
|
85
|
+
except FileNotFoundError as exc:
|
|
86
|
+
print(f"error: file not found: {exc.filename}", file=sys.stderr)
|
|
87
|
+
return 2
|
|
88
|
+
except (json.JSONDecodeError, VerificationError) as exc:
|
|
89
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
90
|
+
return 2
|
|
91
|
+
|
|
92
|
+
if args.json:
|
|
93
|
+
print(json.dumps(result.to_dict(), indent=2, sort_keys=True))
|
|
94
|
+
else:
|
|
95
|
+
print(_render(result))
|
|
96
|
+
if not result.ok:
|
|
97
|
+
return 1
|
|
98
|
+
if result.authenticity_unverified:
|
|
99
|
+
return 3 # structurally OK but present signatures were never checked
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""RFC 8785 (JSON Canonicalization Scheme) for Open Decision Receipts.
|
|
2
|
+
|
|
3
|
+
A dependency-free port of ``aragora.gauntlet.odr_export.jcs_canonicalize`` so
|
|
4
|
+
that ``aragora-verify`` can recompute an ODR receipt's content digest without
|
|
5
|
+
installing Aragora. Byte-for-byte identical output to the reference emitter is
|
|
6
|
+
the whole point: the digest a verifier computes here must match the digest the
|
|
7
|
+
signatures cover.
|
|
8
|
+
|
|
9
|
+
Canonicalization rules (RFC 8785):
|
|
10
|
+
|
|
11
|
+
- UTF-8 output, no insignificant whitespace;
|
|
12
|
+
- object members sorted by UTF-16 code units;
|
|
13
|
+
- strings minimally escaped per JSON with lowercase ``\\u00xx`` for controls;
|
|
14
|
+
- numbers serialized with the ECMAScript ``Number::toString`` shortest
|
|
15
|
+
round-trip algorithm; ``NaN``/``Infinity`` are forbidden.
|
|
16
|
+
|
|
17
|
+
ODR payloads are I-JSON-safe (no numbers needing more than IEEE-754 double
|
|
18
|
+
precision), so any conforming JCS implementation produces identical bytes.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import hashlib
|
|
24
|
+
import json
|
|
25
|
+
import math
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
__all__ = ["jcs_canonicalize", "odr_content_digest"]
|
|
29
|
+
|
|
30
|
+
_ES_INT_LIMIT = 10**21 # ECMAScript switches to exponent notation at 1e21.
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _es_number_to_string(value: float) -> str:
|
|
34
|
+
"""Serialize a float per ECMAScript ``Number::toString`` (RFC 8785 3.2.2.3)."""
|
|
35
|
+
if math.isnan(value) or math.isinf(value):
|
|
36
|
+
raise ValueError("NaN and Infinity cannot be canonicalized per RFC 8785")
|
|
37
|
+
if value == 0:
|
|
38
|
+
# Covers -0.0 as well: JCS serializes negative zero as "0".
|
|
39
|
+
return "0"
|
|
40
|
+
|
|
41
|
+
sign = "-" if value < 0 else ""
|
|
42
|
+
# Python's repr() yields the shortest digit string that round-trips the
|
|
43
|
+
# IEEE-754 double, the same digit selection ECMAScript uses. Only the
|
|
44
|
+
# *formatting* rules differ; they are applied below.
|
|
45
|
+
text = repr(abs(value))
|
|
46
|
+
if "e" in text or "E" in text:
|
|
47
|
+
mantissa, _, exp_text = text.lower().partition("e")
|
|
48
|
+
exponent = int(exp_text)
|
|
49
|
+
else:
|
|
50
|
+
mantissa, exponent = text, 0
|
|
51
|
+
|
|
52
|
+
if "." in mantissa:
|
|
53
|
+
int_part, frac_part = mantissa.split(".", 1)
|
|
54
|
+
else:
|
|
55
|
+
int_part, frac_part = mantissa, ""
|
|
56
|
+
|
|
57
|
+
digits = int_part + frac_part
|
|
58
|
+
point = len(int_part) + exponent
|
|
59
|
+
|
|
60
|
+
stripped = digits.lstrip("0")
|
|
61
|
+
point -= len(digits) - len(stripped)
|
|
62
|
+
digits = stripped.rstrip("0")
|
|
63
|
+
|
|
64
|
+
k = len(digits)
|
|
65
|
+
n = point
|
|
66
|
+
if k <= n <= 21:
|
|
67
|
+
out = digits + "0" * (n - k)
|
|
68
|
+
elif 0 < n <= 21:
|
|
69
|
+
out = digits[:n] + "." + digits[n:]
|
|
70
|
+
elif -6 < n <= 0:
|
|
71
|
+
out = "0." + "0" * (-n) + digits
|
|
72
|
+
else:
|
|
73
|
+
e = n - 1
|
|
74
|
+
head = digits[0] + ("." + digits[1:] if k > 1 else "")
|
|
75
|
+
out = f"{head}e{'+' if e >= 0 else '-'}{abs(e)}"
|
|
76
|
+
return sign + out
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _jcs_serialize(value: Any, out: list[str]) -> None:
|
|
80
|
+
"""Append the JCS serialization of ``value`` to ``out``."""
|
|
81
|
+
if value is None:
|
|
82
|
+
out.append("null")
|
|
83
|
+
elif value is True:
|
|
84
|
+
out.append("true")
|
|
85
|
+
elif value is False:
|
|
86
|
+
out.append("false")
|
|
87
|
+
elif isinstance(value, str):
|
|
88
|
+
out.append(json.dumps(value, ensure_ascii=False))
|
|
89
|
+
elif isinstance(value, int):
|
|
90
|
+
if abs(value) < _ES_INT_LIMIT:
|
|
91
|
+
out.append(str(value))
|
|
92
|
+
else:
|
|
93
|
+
out.append(_es_number_to_string(float(value)))
|
|
94
|
+
elif isinstance(value, float):
|
|
95
|
+
out.append(_es_number_to_string(value))
|
|
96
|
+
elif isinstance(value, (list, tuple)):
|
|
97
|
+
out.append("[")
|
|
98
|
+
for i, item in enumerate(value):
|
|
99
|
+
if i:
|
|
100
|
+
out.append(",")
|
|
101
|
+
_jcs_serialize(item, out)
|
|
102
|
+
out.append("]")
|
|
103
|
+
elif isinstance(value, dict):
|
|
104
|
+
out.append("{")
|
|
105
|
+
# RFC 8785 sorts member names by UTF-16 code units; comparing the
|
|
106
|
+
# UTF-16BE encodings byte-wise is equivalent.
|
|
107
|
+
keys = sorted(value.keys(), key=lambda k: str(k).encode("utf-16-be"))
|
|
108
|
+
for i, key in enumerate(keys):
|
|
109
|
+
if i:
|
|
110
|
+
out.append(",")
|
|
111
|
+
if not isinstance(key, str):
|
|
112
|
+
raise TypeError(f"JCS object member names must be strings, got {type(key)!r}")
|
|
113
|
+
out.append(json.dumps(key, ensure_ascii=False))
|
|
114
|
+
out.append(":")
|
|
115
|
+
_jcs_serialize(value[key], out)
|
|
116
|
+
out.append("}")
|
|
117
|
+
else:
|
|
118
|
+
raise TypeError(f"Type {type(value)!r} is not JCS-serializable")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def jcs_canonicalize(value: Any) -> bytes:
|
|
122
|
+
"""Canonicalize ``value`` to RFC 8785 (JCS) UTF-8 bytes."""
|
|
123
|
+
out: list[str] = []
|
|
124
|
+
_jcs_serialize(value, out)
|
|
125
|
+
return "".join(out).encode("utf-8")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def odr_content_digest(odr: dict[str, Any]) -> str:
|
|
129
|
+
"""SHA-256 hex digest over the JCS bytes of the ODR payload.
|
|
130
|
+
|
|
131
|
+
The ``signatures`` array is excluded so that attaching detached signatures
|
|
132
|
+
never changes the digest they cover. This mirrors
|
|
133
|
+
``aragora.gauntlet.odr_export.odr_content_digest`` exactly.
|
|
134
|
+
"""
|
|
135
|
+
payload = {k: v for k, v in odr.items() if k != "signatures"}
|
|
136
|
+
return hashlib.sha256(jcs_canonicalize(payload)).hexdigest()
|