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.
@@ -0,0 +1,7 @@
1
+ .nomic/
2
+ .benchmarks/
3
+ *.db
4
+ *.db-shm
5
+ *.db-wal
6
+ dist/
7
+ *.egg-info/
@@ -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,6 @@
1
+ from __future__ import annotations
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -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()