trustchain-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,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: trustchain-verify
3
+ Version: 0.1.0
4
+ Summary: Open-source CLI verifier for Arkava Trust Chain witness manifests
5
+ Author: Arkava Ltd
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/Arkava-Ai/arkava-trustchain
8
+ Project-URL: Source, https://github.com/Arkava-Ai/arkava-trustchain
9
+ Project-URL: Documentation, https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/TRUSTCHAIN_VERIFY_CLI.md
10
+ Keywords: arkava,trust-chain,audit,sovereign-ai,verifier
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Security :: Cryptography
19
+ Classifier: Topic :: System :: Systems Administration
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: click>=8.1
23
+ Requires-Dist: trustchain-core
24
+ Requires-Dist: requests>=2.31
25
+
26
+ # trustchain-verify
27
+
28
+ Open-source CLI verifier for Arkava® Trust Chain witness manifests.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install trustchain-verify
34
+ ```
35
+
36
+ Requires Python 3.11+.
37
+
38
+ ## Quick Start
39
+
40
+ ```bash
41
+ # Verify a local manifest file
42
+ trustchain-verify verify-manifest manifest.json
43
+
44
+ # Verify a remote manifest
45
+ trustchain-verify verify-manifest https://raw.githubusercontent.com/Arkava-Ai/arkava-trustchain/main/witness-history/2026/06/26/manifest_2026-06-26.json
46
+
47
+ # Batch-verify multiple manifests
48
+ trustchain-verify verify-batch manifest_1.json manifest_2.json
49
+
50
+ # Validate schema only (no signature check)
51
+ trustchain-verify validate-schema manifest.json
52
+
53
+ # List published operator public keys
54
+ trustchain-verify list-keys
55
+ ```
56
+
57
+ ## Exit Codes
58
+
59
+ | Code | Meaning |
60
+ |---|---|
61
+ | 0 | Verification succeeded |
62
+ | 1 | Schema validation error |
63
+ | 2 | Signature verification error |
64
+ | 3 | Usage error |
65
+
66
+ ## Documentation
67
+
68
+ - CLI reference: [`docs/TRUSTCHAIN_VERIFY_CLI.md`](https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/TRUSTCHAIN_VERIFY_CLI.md)
69
+ - Architecture: [`docs/Arkava_TrustChain_Technical_Architecture_v0.2.md`](https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/Arkava_TrustChain_Technical_Architecture_v0.2.md) §2.9
70
+ - ADR: [TC-ADR-0011](https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/adr/TC-ADR-0011-api-and-verification.md)
71
+
72
+ ## Licence
73
+
74
+ Apache-2.0. Arkava® Trust Chain is a registered trademark of Arkava Ltd.
75
+
76
+ © 2026 Arkava® / All rights reserved.
@@ -0,0 +1,51 @@
1
+ # trustchain-verify
2
+
3
+ Open-source CLI verifier for Arkava® Trust Chain witness manifests.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install trustchain-verify
9
+ ```
10
+
11
+ Requires Python 3.11+.
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ # Verify a local manifest file
17
+ trustchain-verify verify-manifest manifest.json
18
+
19
+ # Verify a remote manifest
20
+ trustchain-verify verify-manifest https://raw.githubusercontent.com/Arkava-Ai/arkava-trustchain/main/witness-history/2026/06/26/manifest_2026-06-26.json
21
+
22
+ # Batch-verify multiple manifests
23
+ trustchain-verify verify-batch manifest_1.json manifest_2.json
24
+
25
+ # Validate schema only (no signature check)
26
+ trustchain-verify validate-schema manifest.json
27
+
28
+ # List published operator public keys
29
+ trustchain-verify list-keys
30
+ ```
31
+
32
+ ## Exit Codes
33
+
34
+ | Code | Meaning |
35
+ |---|---|
36
+ | 0 | Verification succeeded |
37
+ | 1 | Schema validation error |
38
+ | 2 | Signature verification error |
39
+ | 3 | Usage error |
40
+
41
+ ## Documentation
42
+
43
+ - CLI reference: [`docs/TRUSTCHAIN_VERIFY_CLI.md`](https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/TRUSTCHAIN_VERIFY_CLI.md)
44
+ - Architecture: [`docs/Arkava_TrustChain_Technical_Architecture_v0.2.md`](https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/Arkava_TrustChain_Technical_Architecture_v0.2.md) §2.9
45
+ - ADR: [TC-ADR-0011](https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/adr/TC-ADR-0011-api-and-verification.md)
46
+
47
+ ## Licence
48
+
49
+ Apache-2.0. Arkava® Trust Chain is a registered trademark of Arkava Ltd.
50
+
51
+ © 2026 Arkava® / All rights reserved.
@@ -0,0 +1,41 @@
1
+ [project]
2
+ name = "trustchain-verify"
3
+ version = "0.1.0"
4
+ description = "Open-source CLI verifier for Arkava Trust Chain witness manifests"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = { text = "Apache-2.0" }
8
+ authors = [{ name = "Arkava Ltd" }]
9
+ keywords = ["arkava", "trust-chain", "audit", "sovereign-ai", "verifier"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Environment :: Console",
13
+ "Intended Audience :: Developers",
14
+ "Intended Audience :: Information Technology",
15
+ "License :: OSI Approved :: Apache Software License",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Topic :: Security :: Cryptography",
19
+ "Topic :: System :: Systems Administration",
20
+ ]
21
+ dependencies = [
22
+ "click>=8.1",
23
+ "trustchain-core",
24
+ "requests>=2.31",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/Arkava-Ai/arkava-trustchain"
29
+ Source = "https://github.com/Arkava-Ai/arkava-trustchain"
30
+ Documentation = "https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/TRUSTCHAIN_VERIFY_CLI.md"
31
+
32
+ [project.scripts]
33
+ trustchain-verify = "trustchain_verify.cli:main"
34
+
35
+ [build-system]
36
+ requires = ["setuptools>=69", "wheel"]
37
+ build-backend = "setuptools.build_meta"
38
+
39
+ [tool.setuptools.packages.find]
40
+ where = ["src"]
41
+ include = ["trustchain_verify*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,210 @@
1
+ """trustchain-verify CLI — open-source manifest verifier (ARK-3560, TC-ADR-0011).
2
+
3
+ Commands:
4
+ verify-manifest <path-or-url> Verify a single manifest (schema + Ed25519 signature)
5
+ verify-batch <path1> <path2>… Batch-verify multiple manifests
6
+ validate-schema <path> Validate manifest against JSON Schema only
7
+ list-keys List published operator public keys
8
+ list-manifests <dir> List manifest files in a witness-history directory
9
+
10
+ Exit codes:
11
+ 0 — success (all verifications passed)
12
+ 1 — schema validation error
13
+ 2 — signature verification error
14
+ 3 — usage error (bad arguments, file not found, network error)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import sys
21
+ import tempfile
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ import click
26
+
27
+ from trustchain_core.witness import (
28
+ PublicKeyStore,
29
+ VerificationResult,
30
+ verify_manifest_file,
31
+ verify_manifest_schema,
32
+ )
33
+
34
+
35
+ def _fetch_remote(url: str) -> str:
36
+ """Download a manifest from a URL to a temp file and return its path."""
37
+ import requests
38
+
39
+ try:
40
+ resp = requests.get(url, timeout=30, allow_redirects=True)
41
+ resp.raise_for_status()
42
+ except requests.RequestException as exc:
43
+ click.echo(json.dumps({"error": f"Failed to fetch {url}: {exc}", "valid": False}))
44
+ sys.exit(3)
45
+
46
+ tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
47
+ tmp.write(resp.text)
48
+ tmp.close()
49
+ return tmp.name
50
+
51
+
52
+ def _result_to_json(result: VerificationResult, source: str) -> dict[str, Any]:
53
+ """Convert a VerificationResult to a JSON-serialisable dict."""
54
+ output: dict[str, Any] = {
55
+ "source": source,
56
+ "valid": result.valid,
57
+ "schema_valid": result.schema_valid,
58
+ "signature_valid": result.signature_valid,
59
+ "errors": result.errors,
60
+ }
61
+ if result.manifest is not None:
62
+ output["manifest"] = result.manifest.to_dict()
63
+ return output
64
+
65
+
66
+ @click.group()
67
+ @click.version_option(version="0.1.0")
68
+ def cli() -> None:
69
+ """Arkava Trust Chain — open-source manifest verifier."""
70
+
71
+
72
+ @cli.command("verify-manifest")
73
+ @click.argument("source")
74
+ @click.option("--keys", default=None, help="Path to operator public keys JSON file")
75
+ def verify_manifest(source: str, keys: str | None) -> None:
76
+ """Verify a single manifest (schema + Ed25519 signature).
77
+
78
+ SOURCE is a local file path or a URL (http/https).
79
+ """
80
+ key_store = PublicKeyStore.from_file(keys) if keys else PublicKeyStore.from_default()
81
+
82
+ if source.startswith("http://") or source.startswith("https://"):
83
+ local_path = _fetch_remote(source)
84
+ else:
85
+ local_path = source
86
+
87
+ if not Path(local_path).exists():
88
+ click.echo(json.dumps({"error": f"File not found: {source}", "valid": False}))
89
+ sys.exit(3)
90
+
91
+ result = verify_manifest_file(local_path, key_store)
92
+ output = _result_to_json(result, source)
93
+ click.echo(json.dumps(output, indent=2, sort_keys=True))
94
+
95
+ if not result.schema_valid:
96
+ sys.exit(1)
97
+ if result.signature_valid is False:
98
+ sys.exit(2)
99
+ if not result.valid:
100
+ sys.exit(3)
101
+ sys.exit(0)
102
+
103
+
104
+ @cli.command("verify-batch")
105
+ @click.argument("sources", nargs=-1, required=True)
106
+ @click.option("--keys", default=None, help="Path to operator public keys JSON file")
107
+ def verify_batch(sources: tuple[str, ...], keys: str | None) -> None:
108
+ """Batch-verify multiple manifests.
109
+
110
+ Each SOURCE is a local file path or URL. Exits 0 only if ALL pass.
111
+ """
112
+ key_store = PublicKeyStore.from_file(keys) if keys else PublicKeyStore.from_default()
113
+
114
+ results: list[dict[str, Any]] = []
115
+ all_valid = True
116
+ has_schema_error = False
117
+ has_sig_error = False
118
+
119
+ for source in sources:
120
+ if source.startswith("http://") or source.startswith("https://"):
121
+ local_path = _fetch_remote(source)
122
+ else:
123
+ local_path = source
124
+
125
+ result = verify_manifest_file(local_path, key_store)
126
+ entry = _result_to_json(result, source)
127
+ results.append(entry)
128
+
129
+ if not result.valid:
130
+ all_valid = False
131
+ if not result.schema_valid:
132
+ has_schema_error = True
133
+ if result.signature_valid is False:
134
+ has_sig_error = True
135
+
136
+ batch_output: dict[str, Any] = {
137
+ "total": len(sources),
138
+ "valid": sum(1 for r in results if r["valid"]),
139
+ "invalid": sum(1 for r in results if not r["valid"]),
140
+ "results": results,
141
+ }
142
+ click.echo(json.dumps(batch_output, indent=2, sort_keys=True))
143
+
144
+ if has_schema_error:
145
+ sys.exit(1)
146
+ if has_sig_error:
147
+ sys.exit(2)
148
+ if not all_valid:
149
+ sys.exit(3)
150
+ sys.exit(0)
151
+
152
+
153
+ @cli.command("validate-schema")
154
+ @click.argument("path")
155
+ def validate_schema(path: str) -> None:
156
+ """Validate a manifest file against the v1 JSON Schema only (no signature check)."""
157
+ file_path = Path(path)
158
+ if not file_path.exists():
159
+ click.echo(json.dumps({"error": f"File not found: {path}", "valid": False}))
160
+ sys.exit(3)
161
+
162
+ try:
163
+ with open(file_path, "r", encoding="utf-8") as f:
164
+ data = json.load(f)
165
+ except json.JSONDecodeError as exc:
166
+ click.echo(json.dumps({"error": f"JSON parse error: {exc}", "valid": False}))
167
+ sys.exit(3)
168
+
169
+ valid = verify_manifest_schema(data)
170
+ click.echo(json.dumps({"path": path, "schema_valid": valid}, indent=2, sort_keys=True))
171
+ sys.exit(0 if valid else 1)
172
+
173
+
174
+ @cli.command("list-keys")
175
+ @click.option("--keys", default=None, help="Path to operator public keys JSON file")
176
+ def list_keys(keys: str | None) -> None:
177
+ """List published operator public keys."""
178
+ key_store = PublicKeyStore.from_file(keys) if keys else PublicKeyStore.from_default()
179
+ dids = key_store.list_dids()
180
+ output: list[dict[str, str]] = []
181
+ for did in dids:
182
+ pk = key_store.lookup(did)
183
+ if pk:
184
+ output.append({"participant_did": did, "public_key_hex": pk})
185
+ click.echo(json.dumps({"keys": output}, indent=2, sort_keys=True))
186
+ sys.exit(0)
187
+
188
+
189
+ @cli.command("list-manifests")
190
+ @click.argument("directory")
191
+ def list_manifests(directory: str) -> None:
192
+ """List manifest files in a witness-history directory."""
193
+ dir_path = Path(directory)
194
+ if not dir_path.exists():
195
+ click.echo(json.dumps({"error": f"Directory not found: {directory}"}))
196
+ sys.exit(3)
197
+
198
+ manifests = sorted(dir_path.rglob("manifest_*.json"))
199
+ output = [{"path": str(m.relative_to(dir_path))} for m in manifests]
200
+ click.echo(json.dumps({"directory": directory, "manifests": output}, indent=2, sort_keys=True))
201
+ sys.exit(0)
202
+
203
+
204
+ def main() -> None:
205
+ """Entry point for the trustchain-verify CLI."""
206
+ cli()
207
+
208
+
209
+ if __name__ == "__main__":
210
+ main()
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: trustchain-verify
3
+ Version: 0.1.0
4
+ Summary: Open-source CLI verifier for Arkava Trust Chain witness manifests
5
+ Author: Arkava Ltd
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/Arkava-Ai/arkava-trustchain
8
+ Project-URL: Source, https://github.com/Arkava-Ai/arkava-trustchain
9
+ Project-URL: Documentation, https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/TRUSTCHAIN_VERIFY_CLI.md
10
+ Keywords: arkava,trust-chain,audit,sovereign-ai,verifier
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Security :: Cryptography
19
+ Classifier: Topic :: System :: Systems Administration
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: click>=8.1
23
+ Requires-Dist: trustchain-core
24
+ Requires-Dist: requests>=2.31
25
+
26
+ # trustchain-verify
27
+
28
+ Open-source CLI verifier for Arkava® Trust Chain witness manifests.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install trustchain-verify
34
+ ```
35
+
36
+ Requires Python 3.11+.
37
+
38
+ ## Quick Start
39
+
40
+ ```bash
41
+ # Verify a local manifest file
42
+ trustchain-verify verify-manifest manifest.json
43
+
44
+ # Verify a remote manifest
45
+ trustchain-verify verify-manifest https://raw.githubusercontent.com/Arkava-Ai/arkava-trustchain/main/witness-history/2026/06/26/manifest_2026-06-26.json
46
+
47
+ # Batch-verify multiple manifests
48
+ trustchain-verify verify-batch manifest_1.json manifest_2.json
49
+
50
+ # Validate schema only (no signature check)
51
+ trustchain-verify validate-schema manifest.json
52
+
53
+ # List published operator public keys
54
+ trustchain-verify list-keys
55
+ ```
56
+
57
+ ## Exit Codes
58
+
59
+ | Code | Meaning |
60
+ |---|---|
61
+ | 0 | Verification succeeded |
62
+ | 1 | Schema validation error |
63
+ | 2 | Signature verification error |
64
+ | 3 | Usage error |
65
+
66
+ ## Documentation
67
+
68
+ - CLI reference: [`docs/TRUSTCHAIN_VERIFY_CLI.md`](https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/TRUSTCHAIN_VERIFY_CLI.md)
69
+ - Architecture: [`docs/Arkava_TrustChain_Technical_Architecture_v0.2.md`](https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/Arkava_TrustChain_Technical_Architecture_v0.2.md) §2.9
70
+ - ADR: [TC-ADR-0011](https://github.com/Arkava-Ai/arkava-trustchain/blob/develop/docs/adr/TC-ADR-0011-api-and-verification.md)
71
+
72
+ ## Licence
73
+
74
+ Apache-2.0. Arkava® Trust Chain is a registered trademark of Arkava Ltd.
75
+
76
+ © 2026 Arkava® / All rights reserved.
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/trustchain_verify/__init__.py
4
+ src/trustchain_verify/cli.py
5
+ src/trustchain_verify.egg-info/PKG-INFO
6
+ src/trustchain_verify.egg-info/SOURCES.txt
7
+ src/trustchain_verify.egg-info/dependency_links.txt
8
+ src/trustchain_verify.egg-info/entry_points.txt
9
+ src/trustchain_verify.egg-info/requires.txt
10
+ src/trustchain_verify.egg-info/top_level.txt
11
+ tests/test_cli.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ trustchain-verify = trustchain_verify.cli:main
@@ -0,0 +1,3 @@
1
+ click>=8.1
2
+ trustchain-core
3
+ requests>=2.31
@@ -0,0 +1 @@
1
+ trustchain_verify
@@ -0,0 +1,377 @@
1
+ """trustchain-verify CLI tests (ARK-3560 AC 7-10).
2
+
3
+ Acceptance criteria:
4
+ AC7: CLI verify-manifest <path-or-url> — verifies local + remote
5
+ AC8: CLI exit codes: 0=success, 1=schema error, 2=signature error, 3=usage error
6
+ AC9: CLI batch verification: verify-batch <path1> <path2> ...
7
+ AC10: CLI help text comprehensive with examples
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from click.testing import CliRunner
18
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
19
+ from cryptography.hazmat.primitives.serialization import (
20
+ Encoding,
21
+ NoEncryption,
22
+ PrivateFormat,
23
+ PublicFormat,
24
+ )
25
+
26
+ from trustchain_core.witness import Manifest
27
+ from trustchain_core.witness.canonical_encoding import ALGORITHM_ID_SHA3_512
28
+ from trustchain_core.witness.verifier import _reconstruct_signed_data
29
+ from trustchain_verify.cli import cli
30
+
31
+ _TEST_DID = "did:arkava:log-test"
32
+
33
+
34
+ def _extract_json(output: str) -> Any:
35
+ lines = output.strip().split("\n")
36
+ for i in range(len(lines) - 1, -1, -1):
37
+ if lines[i].strip().startswith("{"):
38
+ candidate = "\n".join(lines[i:])
39
+ try:
40
+ return json.loads(candidate)
41
+ except json.JSONDecodeError:
42
+ continue
43
+ return json.loads(lines[-1])
44
+
45
+
46
+ def _make_keypair() -> tuple[bytes, bytes]:
47
+ private_key = Ed25519PrivateKey.generate()
48
+ seed = private_key.private_bytes(
49
+ encoding=Encoding.Raw,
50
+ format=PrivateFormat.Raw,
51
+ encryption_algorithm=NoEncryption(),
52
+ )
53
+ public = private_key.public_key().public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
54
+ return seed, public
55
+
56
+
57
+ def _sha3_512_root(seed: str = "test") -> str:
58
+ return hashlib.sha3_512(seed.encode()).hexdigest()
59
+
60
+
61
+ def _sign_manifest(manifest: Manifest, tree_height: int, seed: bytes) -> bytes:
62
+ root_hash_bytes = bytes.fromhex(manifest.merkle_root)
63
+ signed_data = _reconstruct_signed_data(
64
+ tree_height,
65
+ manifest.participant_did,
66
+ manifest.timestamp,
67
+ root_hash_bytes,
68
+ )
69
+ key = Ed25519PrivateKey.from_private_bytes(seed)
70
+ return key.sign(signed_data)
71
+
72
+
73
+ def _make_signed_manifest_dict(
74
+ public_key_hex: str, seed: bytes, tree_height: int = 2
75
+ ) -> dict[str, Any]:
76
+ root = _sha3_512_root("cli-test-root")
77
+ timestamp = "2026-06-26T12:00:00Z"
78
+ manifest = Manifest(
79
+ timestamp=timestamp,
80
+ merkle_root=root,
81
+ participant_did=_TEST_DID,
82
+ batch_count=5,
83
+ stump_proof_path=[],
84
+ algorithm_id=ALGORITHM_ID_SHA3_512,
85
+ key_version="v1",
86
+ tree_height=tree_height,
87
+ )
88
+ signature = _sign_manifest(manifest, tree_height, seed)
89
+ return {
90
+ "manifest_version": "v1",
91
+ "timestamp": timestamp,
92
+ "merkle_root": root,
93
+ "participant_did": _TEST_DID,
94
+ "batch_count": 5,
95
+ "stump_proof_path": [],
96
+ "algorithm_id": ALGORITHM_ID_SHA3_512,
97
+ "key_version": "v1",
98
+ "signature_hex": signature.hex(),
99
+ "tree_height": tree_height,
100
+ }
101
+
102
+
103
+ def _make_key_file(tmp_path: Path, public_key_hex: str, did: str = _TEST_DID) -> Path:
104
+ key_file = tmp_path / "keys.json"
105
+ key_file.write_text(
106
+ json.dumps(
107
+ {
108
+ "keys": [
109
+ {"participant_did": did, "key_version": "v1", "public_key_hex": public_key_hex}
110
+ ]
111
+ }
112
+ ),
113
+ encoding="utf-8",
114
+ )
115
+ return key_file
116
+
117
+
118
+ def _write_manifest(
119
+ tmp_path: Path, manifest_dict: dict[str, Any], name: str = "manifest.json"
120
+ ) -> Path:
121
+ p = tmp_path / name
122
+ p.write_text(json.dumps(manifest_dict, sort_keys=True), encoding="utf-8")
123
+ return p
124
+
125
+
126
+ class TestAC7VerifyManifest:
127
+ """AC7: CLI verify-manifest <path-or-url> — verifies local + remote."""
128
+
129
+ def test_verify_valid_local_manifest_exit_0(self, tmp_path: Path) -> None:
130
+ seed, public = _make_keypair()
131
+ manifest_dict = _make_signed_manifest_dict(public.hex(), seed)
132
+ manifest_path = _write_manifest(tmp_path, manifest_dict)
133
+ key_file = _make_key_file(tmp_path, public.hex())
134
+
135
+ runner = CliRunner()
136
+ result = runner.invoke(
137
+ cli, ["verify-manifest", str(manifest_path), "--keys", str(key_file)]
138
+ )
139
+
140
+ assert result.exit_code == 0
141
+ output = _extract_json(result.output)
142
+ assert output["valid"] is True
143
+ assert output["schema_valid"] is True
144
+ assert output["signature_valid"] is True
145
+
146
+ def test_verify_schema_invalid_manifest_exit_1(self, tmp_path: Path) -> None:
147
+ bad_data: dict[str, Any] = {
148
+ "manifest_version": "v1",
149
+ "timestamp": "2026-06-26T12:00:00Z",
150
+ "merkle_root": _sha3_512_root(),
151
+ "participant_did": "",
152
+ "batch_count": 1,
153
+ "stump_proof_path": [],
154
+ "algorithm_id": ALGORITHM_ID_SHA3_512,
155
+ "key_version": "v1",
156
+ }
157
+ manifest_path = _write_manifest(tmp_path, bad_data, "bad.json")
158
+ key_file = _make_key_file(tmp_path, "a" * 64)
159
+
160
+ runner = CliRunner()
161
+ result = runner.invoke(
162
+ cli, ["verify-manifest", str(manifest_path), "--keys", str(key_file)]
163
+ )
164
+
165
+ assert result.exit_code == 1
166
+
167
+ def test_verify_tampered_signature_exit_2(self, tmp_path: Path) -> None:
168
+ seed, public = _make_keypair()
169
+ manifest_dict = _make_signed_manifest_dict(public.hex(), seed)
170
+ manifest_dict["merkle_root"] = _sha3_512_root("tampered")
171
+ manifest_path = _write_manifest(tmp_path, manifest_dict, "tampered.json")
172
+ key_file = _make_key_file(tmp_path, public.hex())
173
+
174
+ runner = CliRunner()
175
+ result = runner.invoke(
176
+ cli, ["verify-manifest", str(manifest_path), "--keys", str(key_file)]
177
+ )
178
+
179
+ assert result.exit_code == 2
180
+
181
+ def test_verify_nonexistent_file_exit_3(self, tmp_path: Path) -> None:
182
+ key_file = _make_key_file(tmp_path, "a" * 64)
183
+
184
+ runner = CliRunner()
185
+ result = runner.invoke(
186
+ cli, ["verify-manifest", str(tmp_path / "nope.json"), "--keys", str(key_file)]
187
+ )
188
+
189
+ assert result.exit_code == 3
190
+
191
+
192
+ class TestAC8ExitCodes:
193
+ """AC8: CLI exit codes: 0=success, 1=schema error, 2=signature error, 3=usage error."""
194
+
195
+ def test_exit_0_on_valid_manifest(self, tmp_path: Path) -> None:
196
+ seed, public = _make_keypair()
197
+ manifest_dict = _make_signed_manifest_dict(public.hex(), seed)
198
+ manifest_path = _write_manifest(tmp_path, manifest_dict)
199
+ key_file = _make_key_file(tmp_path, public.hex())
200
+
201
+ runner = CliRunner()
202
+ result = runner.invoke(
203
+ cli, ["verify-manifest", str(manifest_path), "--keys", str(key_file)]
204
+ )
205
+ assert result.exit_code == 0
206
+
207
+ def test_exit_1_on_schema_error(self, tmp_path: Path) -> None:
208
+ bad_data: dict[str, Any] = {"not": "valid"}
209
+ manifest_path = _write_manifest(tmp_path, bad_data, "bad.json")
210
+ key_file = _make_key_file(tmp_path, "a" * 64)
211
+
212
+ runner = CliRunner()
213
+ result = runner.invoke(
214
+ cli, ["verify-manifest", str(manifest_path), "--keys", str(key_file)]
215
+ )
216
+ assert result.exit_code == 1
217
+
218
+ def test_exit_2_on_signature_error(self, tmp_path: Path) -> None:
219
+ seed, public = _make_keypair()
220
+ manifest_dict = _make_signed_manifest_dict(public.hex(), seed)
221
+ manifest_dict["signature_hex"] = "00" * 64
222
+ manifest_path = _write_manifest(tmp_path, manifest_dict, "badsig.json")
223
+ key_file = _make_key_file(tmp_path, public.hex())
224
+
225
+ runner = CliRunner()
226
+ result = runner.invoke(
227
+ cli, ["verify-manifest", str(manifest_path), "--keys", str(key_file)]
228
+ )
229
+ assert result.exit_code == 2
230
+
231
+ def test_exit_3_on_missing_file(self, tmp_path: Path) -> None:
232
+ key_file = _make_key_file(tmp_path, "a" * 64)
233
+
234
+ runner = CliRunner()
235
+ result = runner.invoke(
236
+ cli, ["verify-manifest", "/nonexistent/path.json", "--keys", str(key_file)]
237
+ )
238
+ assert result.exit_code == 3
239
+
240
+
241
+ class TestAC9BatchVerification:
242
+ """AC9: CLI batch verification: verify-batch <path1> <path2> ..."""
243
+
244
+ def test_batch_all_valid_exit_0(self, tmp_path: Path) -> None:
245
+ seed, public = _make_keypair()
246
+ m1 = _make_signed_manifest_dict(public.hex(), seed, tree_height=1)
247
+ m2 = _make_signed_manifest_dict(public.hex(), seed, tree_height=2)
248
+ p1 = _write_manifest(tmp_path, m1, "m1.json")
249
+ p2 = _write_manifest(tmp_path, m2, "m2.json")
250
+ key_file = _make_key_file(tmp_path, public.hex())
251
+
252
+ runner = CliRunner()
253
+ result = runner.invoke(cli, ["verify-batch", str(p1), str(p2), "--keys", str(key_file)])
254
+
255
+ assert result.exit_code == 0
256
+ output = _extract_json(result.output)
257
+ assert output["total"] == 2
258
+ assert output["valid"] == 2
259
+ assert output["invalid"] == 0
260
+
261
+ def test_batch_one_invalid_exit_2(self, tmp_path: Path) -> None:
262
+ seed, public = _make_keypair()
263
+ m1 = _make_signed_manifest_dict(public.hex(), seed, tree_height=1)
264
+ m2 = _make_signed_manifest_dict(public.hex(), seed, tree_height=2)
265
+ m2["merkle_root"] = _sha3_512_root("tampered")
266
+ p1 = _write_manifest(tmp_path, m1, "m1.json")
267
+ p2 = _write_manifest(tmp_path, m2, "m2.json")
268
+ key_file = _make_key_file(tmp_path, public.hex())
269
+
270
+ runner = CliRunner()
271
+ result = runner.invoke(cli, ["verify-batch", str(p1), str(p2), "--keys", str(key_file)])
272
+
273
+ assert result.exit_code == 2
274
+ output = _extract_json(result.output)
275
+ assert output["total"] == 2
276
+ assert output["valid"] == 1
277
+ assert output["invalid"] == 1
278
+
279
+
280
+ class TestAC10HelpText:
281
+ """AC10: CLI help text comprehensive with examples."""
282
+
283
+ def test_top_level_help(self) -> None:
284
+ runner = CliRunner()
285
+ result = runner.invoke(cli, ["--help"])
286
+
287
+ assert result.exit_code == 0
288
+ assert "verify-manifest" in result.output
289
+ assert "verify-batch" in result.output
290
+ assert "validate-schema" in result.output
291
+ assert "list-keys" in result.output
292
+ assert "list-manifests" in result.output
293
+
294
+ def test_verify_manifest_help(self) -> None:
295
+ runner = CliRunner()
296
+ result = runner.invoke(cli, ["verify-manifest", "--help"])
297
+
298
+ assert result.exit_code == 0
299
+ assert "SOURCE" in result.output
300
+ assert "schema" in result.output
301
+ assert "signature" in result.output
302
+
303
+ def test_verify_batch_help(self) -> None:
304
+ runner = CliRunner()
305
+ result = runner.invoke(cli, ["verify-batch", "--help"])
306
+
307
+ assert result.exit_code == 0
308
+ assert "SOURCES" in result.output or "sources" in result.output.lower()
309
+
310
+ def test_version_flag(self) -> None:
311
+ runner = CliRunner()
312
+ result = runner.invoke(cli, ["--version"])
313
+
314
+ assert result.exit_code == 0
315
+ assert "0.1.0" in result.output
316
+
317
+
318
+ class TestAdditionalCommands:
319
+ """validate-schema, list-keys, list-manifests commands."""
320
+
321
+ def test_validate_schema_valid_exit_0(self, tmp_path: Path) -> None:
322
+ data: dict[str, Any] = {
323
+ "manifest_version": "v1",
324
+ "timestamp": "2026-06-26T12:00:00Z",
325
+ "merkle_root": _sha3_512_root(),
326
+ "participant_did": "did:arkava:test",
327
+ "batch_count": 1,
328
+ "stump_proof_path": [],
329
+ "algorithm_id": ALGORITHM_ID_SHA3_512,
330
+ "key_version": "v1",
331
+ }
332
+ manifest_path = _write_manifest(tmp_path, data, "valid.json")
333
+
334
+ runner = CliRunner()
335
+ result = runner.invoke(cli, ["validate-schema", str(manifest_path)])
336
+
337
+ assert result.exit_code == 0
338
+ output = _extract_json(result.output)
339
+ assert output["schema_valid"] is True
340
+
341
+ def test_validate_schema_invalid_exit_1(self, tmp_path: Path) -> None:
342
+ data: dict[str, Any] = {"not": "valid"}
343
+ manifest_path = _write_manifest(tmp_path, data, "invalid.json")
344
+
345
+ runner = CliRunner()
346
+ result = runner.invoke(cli, ["validate-schema", str(manifest_path)])
347
+
348
+ assert result.exit_code == 1
349
+
350
+ def test_list_keys(self, tmp_path: Path) -> None:
351
+ key_file = _make_key_file(tmp_path, "ab" * 32, did="did:arkava:list-test")
352
+
353
+ runner = CliRunner()
354
+ result = runner.invoke(cli, ["list-keys", "--keys", str(key_file)])
355
+
356
+ assert result.exit_code == 0
357
+ output = _extract_json(result.output)
358
+ assert len(output["keys"]) == 1
359
+ assert output["keys"][0]["participant_did"] == "did:arkava:list-test"
360
+
361
+ def test_list_manifests(self, tmp_path: Path) -> None:
362
+ witness_dir = tmp_path / "witness-history" / "2026" / "06" / "26"
363
+ witness_dir.mkdir(parents=True)
364
+ (witness_dir / "manifest_2026-06-26.json").write_text("{}", encoding="utf-8")
365
+
366
+ runner = CliRunner()
367
+ result = runner.invoke(cli, ["list-manifests", str(tmp_path)])
368
+
369
+ assert result.exit_code == 0
370
+ output = _extract_json(result.output)
371
+ assert len(output["manifests"]) == 1
372
+
373
+ def test_list_manifests_nonexistent_dir_exit_3(self, tmp_path: Path) -> None:
374
+ runner = CliRunner()
375
+ result = runner.invoke(cli, ["list-manifests", str(tmp_path / "nope")])
376
+
377
+ assert result.exit_code == 3