keel-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.
- keel_verifier-1.0.0/LICENSE +21 -0
- keel_verifier-1.0.0/PKG-INFO +193 -0
- keel_verifier-1.0.0/README.md +156 -0
- keel_verifier-1.0.0/keel_verifier/__init__.py +21 -0
- keel_verifier-1.0.0/keel_verifier/__main__.py +4 -0
- keel_verifier-1.0.0/keel_verifier/cli.py +296 -0
- keel_verifier-1.0.0/keel_verifier/data/trust_root.json +30 -0
- keel_verifier-1.0.0/keel_verifier/keys/keel_checkpoint.pub.json +8 -0
- keel_verifier-1.0.0/keel_verifier/verifier.py +2015 -0
- keel_verifier-1.0.0/keel_verifier.egg-info/PKG-INFO +193 -0
- keel_verifier-1.0.0/keel_verifier.egg-info/SOURCES.txt +19 -0
- keel_verifier-1.0.0/keel_verifier.egg-info/dependency_links.txt +1 -0
- keel_verifier-1.0.0/keel_verifier.egg-info/entry_points.txt +2 -0
- keel_verifier-1.0.0/keel_verifier.egg-info/requires.txt +1 -0
- keel_verifier-1.0.0/keel_verifier.egg-info/top_level.txt +1 -0
- keel_verifier-1.0.0/pyproject.toml +31 -0
- keel_verifier-1.0.0/setup.cfg +4 -0
- keel_verifier-1.0.0/tests/test_canonical_byte_identity.py +75 -0
- keel_verifier-1.0.0/tests/test_smoke.py +36 -0
- keel_verifier-1.0.0/tests/test_verify_closure.py +102 -0
- keel_verifier-1.0.0/tests/test_walk_events.py +45 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Keel API, Inc.
|
|
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,193 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: keel-verifier
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Independent verifier for Keel governance evidence
|
|
5
|
+
Author: Keel API, Inc.
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Keel API, Inc.
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://keelapi.com/verify
|
|
29
|
+
Project-URL: Documentation, https://docs.keelapi.com/11-running-keel-verify
|
|
30
|
+
Project-URL: Repository, https://github.com/keelapi/keel-verifier
|
|
31
|
+
Project-URL: Changelog, https://github.com/keelapi/keel-verifier/releases
|
|
32
|
+
Requires-Python: >=3.10
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
License-File: LICENSE
|
|
35
|
+
Requires-Dist: cryptography>=42
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# keel-verifier
|
|
39
|
+
|
|
40
|
+
Independent verifier for Keel governance evidence.
|
|
41
|
+
|
|
42
|
+
It runs locally, requires no access to Keel, and makes no outbound network calls unless you explicitly ask it to fetch a public key or key manifest URL.
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
python -m pip install keel-verifier
|
|
48
|
+
keel-verify export --help
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
From a checkout:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
python -m pip install -e .
|
|
55
|
+
python -m keel_verifier --help
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The v0.2.0 invocation pattern still works:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python -m keel_verifier sample/export.json --self-attested
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## What It Verifies
|
|
65
|
+
|
|
66
|
+
`keel-verify export` verifies a signed compliance export in three layers:
|
|
67
|
+
|
|
68
|
+
1. The export bytes match the signed manifest `content_hash`.
|
|
69
|
+
2. The manifest Ed25519 signature verifies against a trusted key.
|
|
70
|
+
3. Optional Phase C/D checks walk bundled chain entries and verify closure records.
|
|
71
|
+
|
|
72
|
+
`keel-verify checkpoint` verifies integrity checkpoint JSON artifacts: the `chain_heads` composite hash, the Ed25519 checkpoint signature, and an embedded RFC 3161 timestamp MessageImprint when present.
|
|
73
|
+
|
|
74
|
+
## Obtaining a Signed Export
|
|
75
|
+
|
|
76
|
+
Request an audit export from Keel's compliance export API and include chain entries when you want full lifecycle walking:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
curl -sS -X POST "https://api.keelapi.com/v1/compliance/exports?include_chain_entries=true" -H "Authorization: Bearer $KEEL_API_KEY" -H "Content-Type: application/json" -d '{"project_id":"<project_uuid>","format":"json"}'
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Download both artifacts returned by the export workflow:
|
|
83
|
+
|
|
84
|
+
- the export payload, for example `export.json`
|
|
85
|
+
- the signed manifest, for example `manifest.json`
|
|
86
|
+
|
|
87
|
+
Then run:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
keel-verify export export.json manifest.json --walk-events --verify-closure
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The flag form used by the internal verifier is also supported:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
keel-verify export --export-file export.json --manifest manifest.json --walk-events --verify-closure
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Chain Walking
|
|
100
|
+
|
|
101
|
+
`--walk-events` parses `audit_export_bundle` files with `schema_version=2` and `include_chain_entries=true`.
|
|
102
|
+
|
|
103
|
+
It groups entries by `chain_scope`, sorts by `sequence_number`, recomputes every `record_hash`, verifies `prev_hash` continuity inside the export window, and fails closed on unknown `chain_format_version` values.
|
|
104
|
+
|
|
105
|
+
Schema version 1 exports remain backward compatible. They can still be verified at the export-signature layer, but they do not contain chain entries to walk.
|
|
106
|
+
|
|
107
|
+
## Closure Verification
|
|
108
|
+
|
|
109
|
+
`--verify-closure` verifies `permit.closed` entries.
|
|
110
|
+
|
|
111
|
+
For `closure_v1`, it verifies the closure Ed25519 signature and cross-references provider/client response digests against the bundled lifecycle events.
|
|
112
|
+
|
|
113
|
+
For `closure_v2`, it also verifies `dispatch_request_digest_v1` against the permit's `binding_request_hash`, proving that the dispatch-time request body is the one covered by the closure record.
|
|
114
|
+
|
|
115
|
+
Closure verification uses public keys with purpose `permit_binding_signing`. Pass a manifest explicitly when needed:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
keel-verify export export.json manifest.json --key-manifest permit-binding-keys.json --walk-events --verify-closure
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The bundled trust root lives at `keel_verifier/data/trust_root.json`. It includes the production export and checkpoint signing keys currently served by `https://api.keelapi.com/v1/compliance/keys`. The production permit-binding endpoint returned 404 on 2026-05-07, so maintainers should refresh the bundled manifest when `https://api.keelapi.com/v1/integrity/permit-binding-public-keys` is live.
|
|
122
|
+
|
|
123
|
+
## Tampering Matrix
|
|
124
|
+
|
|
125
|
+
The verifier emits stable `WALK_*` failure codes, including:
|
|
126
|
+
|
|
127
|
+
- `WALK_RECORD_HASH_MISMATCH`
|
|
128
|
+
- `WALK_PREV_HASH_DISCONTINUITY`
|
|
129
|
+
- `WALK_SEQUENCE_INVERSION`
|
|
130
|
+
- `WALK_UNKNOWN_CHAIN_FORMAT`
|
|
131
|
+
- `WALK_CLOSURE_SIGNATURE_INVALID`
|
|
132
|
+
- `WALK_CLOSURE_DIGEST_MISMATCH`
|
|
133
|
+
- `WALK_CLOSURE_DIGEST_MISSING`
|
|
134
|
+
- `WALK_CLOSURE_DISPATCH_DIGEST_MISMATCH`
|
|
135
|
+
- `WALK_UNKNOWN_CLOSURE_FORMAT`
|
|
136
|
+
|
|
137
|
+
The authoritative matrix is maintained in the Keel docs: https://docs.keelapi.com/12-tampering-detection-matrix
|
|
138
|
+
|
|
139
|
+
## Trust Model
|
|
140
|
+
|
|
141
|
+
There are two useful kinds of verification:
|
|
142
|
+
|
|
143
|
+
- Self-attested: the file agrees with itself. This proves internal consistency only.
|
|
144
|
+
- Trust-root verified: the artifact verifies against a key you trust, such as the bundled production trust root, a pinned public key, or a manifest fetched and saved out-of-band.
|
|
145
|
+
|
|
146
|
+
Trust sources, strongest first:
|
|
147
|
+
|
|
148
|
+
| Mode | Flags | Notes |
|
|
149
|
+
| --- | --- | --- |
|
|
150
|
+
| Pinned key | `--expected-public-key ed25519:...` or `--public-key ed25519:...` | Strongest when obtained out-of-band. |
|
|
151
|
+
| Key manifest | `--key-manifest keys.json` | Supports key rotation and active windows. |
|
|
152
|
+
| Key manifest URL | `--key-manifest-url URL` | Explicit network fetch. |
|
|
153
|
+
| Bundled trust root | none | Default. No phone-home. |
|
|
154
|
+
| Self-attested | `--self-attested` | Development/sample mode only. |
|
|
155
|
+
|
|
156
|
+
`--public-key-url` is also supported for checkpoint verification against the single live checkpoint public-key endpoint.
|
|
157
|
+
|
|
158
|
+
## CLI Examples
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
keel-verify export export.json manifest.json
|
|
162
|
+
keel-verify export export.json manifest.json --walk-events
|
|
163
|
+
keel-verify export export.json manifest.json --walk-events --verify-closure
|
|
164
|
+
keel-verify checkpoint checkpoint.json
|
|
165
|
+
python -m keel_verifier sample/export.json --self-attested
|
|
166
|
+
python -m keel_verifier sample/export.json --json --self-attested
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Exit code `0` means verified. Exit code `1` means verification failed. Exit code `2` means bad usage.
|
|
170
|
+
|
|
171
|
+
## Network Behavior
|
|
172
|
+
|
|
173
|
+
The verifier does not phone home. It reaches the network only when you pass `--public-key-url` or `--key-manifest-url`.
|
|
174
|
+
|
|
175
|
+
There is no telemetry.
|
|
176
|
+
|
|
177
|
+
## Library Use
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from keel_verifier import verify, verify_export_walk_events, verify_closure_record
|
|
181
|
+
|
|
182
|
+
result = verify("sample/export.json", self_attested=True)
|
|
183
|
+
if not result.ok:
|
|
184
|
+
raise SystemExit(result.error)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Versioning
|
|
188
|
+
|
|
189
|
+
v1.0.0 syncs the public verifier with the Phase A/B/C/D internal verifier. v0.2.0 users can keep using `python -m keel_verifier <artifact>`.
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# keel-verifier
|
|
2
|
+
|
|
3
|
+
Independent verifier for Keel governance evidence.
|
|
4
|
+
|
|
5
|
+
It runs locally, requires no access to Keel, and makes no outbound network calls unless you explicitly ask it to fetch a public key or key manifest URL.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
python -m pip install keel-verifier
|
|
11
|
+
keel-verify export --help
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
From a checkout:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
python -m pip install -e .
|
|
18
|
+
python -m keel_verifier --help
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The v0.2.0 invocation pattern still works:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
python -m keel_verifier sample/export.json --self-attested
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## What It Verifies
|
|
28
|
+
|
|
29
|
+
`keel-verify export` verifies a signed compliance export in three layers:
|
|
30
|
+
|
|
31
|
+
1. The export bytes match the signed manifest `content_hash`.
|
|
32
|
+
2. The manifest Ed25519 signature verifies against a trusted key.
|
|
33
|
+
3. Optional Phase C/D checks walk bundled chain entries and verify closure records.
|
|
34
|
+
|
|
35
|
+
`keel-verify checkpoint` verifies integrity checkpoint JSON artifacts: the `chain_heads` composite hash, the Ed25519 checkpoint signature, and an embedded RFC 3161 timestamp MessageImprint when present.
|
|
36
|
+
|
|
37
|
+
## Obtaining a Signed Export
|
|
38
|
+
|
|
39
|
+
Request an audit export from Keel's compliance export API and include chain entries when you want full lifecycle walking:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
curl -sS -X POST "https://api.keelapi.com/v1/compliance/exports?include_chain_entries=true" -H "Authorization: Bearer $KEEL_API_KEY" -H "Content-Type: application/json" -d '{"project_id":"<project_uuid>","format":"json"}'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Download both artifacts returned by the export workflow:
|
|
46
|
+
|
|
47
|
+
- the export payload, for example `export.json`
|
|
48
|
+
- the signed manifest, for example `manifest.json`
|
|
49
|
+
|
|
50
|
+
Then run:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
keel-verify export export.json manifest.json --walk-events --verify-closure
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The flag form used by the internal verifier is also supported:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
keel-verify export --export-file export.json --manifest manifest.json --walk-events --verify-closure
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Chain Walking
|
|
63
|
+
|
|
64
|
+
`--walk-events` parses `audit_export_bundle` files with `schema_version=2` and `include_chain_entries=true`.
|
|
65
|
+
|
|
66
|
+
It groups entries by `chain_scope`, sorts by `sequence_number`, recomputes every `record_hash`, verifies `prev_hash` continuity inside the export window, and fails closed on unknown `chain_format_version` values.
|
|
67
|
+
|
|
68
|
+
Schema version 1 exports remain backward compatible. They can still be verified at the export-signature layer, but they do not contain chain entries to walk.
|
|
69
|
+
|
|
70
|
+
## Closure Verification
|
|
71
|
+
|
|
72
|
+
`--verify-closure` verifies `permit.closed` entries.
|
|
73
|
+
|
|
74
|
+
For `closure_v1`, it verifies the closure Ed25519 signature and cross-references provider/client response digests against the bundled lifecycle events.
|
|
75
|
+
|
|
76
|
+
For `closure_v2`, it also verifies `dispatch_request_digest_v1` against the permit's `binding_request_hash`, proving that the dispatch-time request body is the one covered by the closure record.
|
|
77
|
+
|
|
78
|
+
Closure verification uses public keys with purpose `permit_binding_signing`. Pass a manifest explicitly when needed:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
keel-verify export export.json manifest.json --key-manifest permit-binding-keys.json --walk-events --verify-closure
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The bundled trust root lives at `keel_verifier/data/trust_root.json`. It includes the production export and checkpoint signing keys currently served by `https://api.keelapi.com/v1/compliance/keys`. The production permit-binding endpoint returned 404 on 2026-05-07, so maintainers should refresh the bundled manifest when `https://api.keelapi.com/v1/integrity/permit-binding-public-keys` is live.
|
|
85
|
+
|
|
86
|
+
## Tampering Matrix
|
|
87
|
+
|
|
88
|
+
The verifier emits stable `WALK_*` failure codes, including:
|
|
89
|
+
|
|
90
|
+
- `WALK_RECORD_HASH_MISMATCH`
|
|
91
|
+
- `WALK_PREV_HASH_DISCONTINUITY`
|
|
92
|
+
- `WALK_SEQUENCE_INVERSION`
|
|
93
|
+
- `WALK_UNKNOWN_CHAIN_FORMAT`
|
|
94
|
+
- `WALK_CLOSURE_SIGNATURE_INVALID`
|
|
95
|
+
- `WALK_CLOSURE_DIGEST_MISMATCH`
|
|
96
|
+
- `WALK_CLOSURE_DIGEST_MISSING`
|
|
97
|
+
- `WALK_CLOSURE_DISPATCH_DIGEST_MISMATCH`
|
|
98
|
+
- `WALK_UNKNOWN_CLOSURE_FORMAT`
|
|
99
|
+
|
|
100
|
+
The authoritative matrix is maintained in the Keel docs: https://docs.keelapi.com/12-tampering-detection-matrix
|
|
101
|
+
|
|
102
|
+
## Trust Model
|
|
103
|
+
|
|
104
|
+
There are two useful kinds of verification:
|
|
105
|
+
|
|
106
|
+
- Self-attested: the file agrees with itself. This proves internal consistency only.
|
|
107
|
+
- Trust-root verified: the artifact verifies against a key you trust, such as the bundled production trust root, a pinned public key, or a manifest fetched and saved out-of-band.
|
|
108
|
+
|
|
109
|
+
Trust sources, strongest first:
|
|
110
|
+
|
|
111
|
+
| Mode | Flags | Notes |
|
|
112
|
+
| --- | --- | --- |
|
|
113
|
+
| Pinned key | `--expected-public-key ed25519:...` or `--public-key ed25519:...` | Strongest when obtained out-of-band. |
|
|
114
|
+
| Key manifest | `--key-manifest keys.json` | Supports key rotation and active windows. |
|
|
115
|
+
| Key manifest URL | `--key-manifest-url URL` | Explicit network fetch. |
|
|
116
|
+
| Bundled trust root | none | Default. No phone-home. |
|
|
117
|
+
| Self-attested | `--self-attested` | Development/sample mode only. |
|
|
118
|
+
|
|
119
|
+
`--public-key-url` is also supported for checkpoint verification against the single live checkpoint public-key endpoint.
|
|
120
|
+
|
|
121
|
+
## CLI Examples
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
keel-verify export export.json manifest.json
|
|
125
|
+
keel-verify export export.json manifest.json --walk-events
|
|
126
|
+
keel-verify export export.json manifest.json --walk-events --verify-closure
|
|
127
|
+
keel-verify checkpoint checkpoint.json
|
|
128
|
+
python -m keel_verifier sample/export.json --self-attested
|
|
129
|
+
python -m keel_verifier sample/export.json --json --self-attested
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Exit code `0` means verified. Exit code `1` means verification failed. Exit code `2` means bad usage.
|
|
133
|
+
|
|
134
|
+
## Network Behavior
|
|
135
|
+
|
|
136
|
+
The verifier does not phone home. It reaches the network only when you pass `--public-key-url` or `--key-manifest-url`.
|
|
137
|
+
|
|
138
|
+
There is no telemetry.
|
|
139
|
+
|
|
140
|
+
## Library Use
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from keel_verifier import verify, verify_export_walk_events, verify_closure_record
|
|
144
|
+
|
|
145
|
+
result = verify("sample/export.json", self_attested=True)
|
|
146
|
+
if not result.ok:
|
|
147
|
+
raise SystemExit(result.error)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Versioning
|
|
151
|
+
|
|
152
|
+
v1.0.0 syncs the public verifier with the Phase A/B/C/D internal verifier. v0.2.0 users can keep using `python -m keel_verifier <artifact>`.
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Standalone verifier for Keel governance evidence."""
|
|
2
|
+
|
|
3
|
+
from keel_verifier.verifier import (
|
|
4
|
+
CHAIN_FORMAT_HASHERS,
|
|
5
|
+
CLOSURE_FORMAT_VERIFIERS,
|
|
6
|
+
VerifyResult,
|
|
7
|
+
verify,
|
|
8
|
+
verify_closure_record,
|
|
9
|
+
verify_export_walk_events,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"CHAIN_FORMAT_HASHERS",
|
|
14
|
+
"CLOSURE_FORMAT_VERIFIERS",
|
|
15
|
+
"VerifyResult",
|
|
16
|
+
"verify",
|
|
17
|
+
"verify_closure_record",
|
|
18
|
+
"verify_export_walk_events",
|
|
19
|
+
"__version__",
|
|
20
|
+
]
|
|
21
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Command-line interface for keel_verifier."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from keel_verifier import __version__
|
|
10
|
+
from keel_verifier.verifier import (
|
|
11
|
+
KEELAPI_CHECKPOINT_PUBLIC_KEY_URL,
|
|
12
|
+
KEELAPI_COMPLIANCE_KEYS_URL,
|
|
13
|
+
VerifyResult,
|
|
14
|
+
cmd_checkpoint,
|
|
15
|
+
cmd_export,
|
|
16
|
+
verify,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
LEGACY_COMMANDS = {"export", "checkpoint"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _public_key_alias(args: argparse.Namespace) -> None:
|
|
23
|
+
if getattr(args, "public_key", None) and getattr(args, "expected_public_key", None):
|
|
24
|
+
raise argparse.ArgumentTypeError(
|
|
25
|
+
"--public-key and --expected-public-key are aliases; pass only one"
|
|
26
|
+
)
|
|
27
|
+
if getattr(args, "expected_public_key", None) is None:
|
|
28
|
+
args.expected_public_key = getattr(args, "public_key", None)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _trust_flag_count(args: argparse.Namespace, *, include_public_key_url: bool) -> int:
|
|
32
|
+
values = [
|
|
33
|
+
getattr(args, "expected_public_key", None),
|
|
34
|
+
getattr(args, "key_manifest", None),
|
|
35
|
+
getattr(args, "key_manifest_url", None),
|
|
36
|
+
getattr(args, "self_attested", False),
|
|
37
|
+
]
|
|
38
|
+
if include_public_key_url:
|
|
39
|
+
values.append(getattr(args, "public_key_url", None))
|
|
40
|
+
return sum(bool(value) for value in values)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _add_key_manifest_args(p: argparse.ArgumentParser) -> None:
|
|
44
|
+
p.add_argument(
|
|
45
|
+
"--key-manifest",
|
|
46
|
+
help=(
|
|
47
|
+
"Local path to a Keel public key manifest JSON file. Defaults to "
|
|
48
|
+
"the bundled production trust root when no trust override is passed."
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
p.add_argument(
|
|
52
|
+
"--key-manifest-url",
|
|
53
|
+
help=f"URL to fetch the key manifest from (canonical: {KEELAPI_COMPLIANCE_KEYS_URL}).",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _add_common_trust_args(p: argparse.ArgumentParser) -> None:
|
|
58
|
+
p.add_argument(
|
|
59
|
+
"--expected-public-key",
|
|
60
|
+
help="ed25519:<base64> public key the artifact must be signed with.",
|
|
61
|
+
)
|
|
62
|
+
p.add_argument(
|
|
63
|
+
"--public-key",
|
|
64
|
+
help="Alias for --expected-public-key, preserved for v0.2.0 users.",
|
|
65
|
+
)
|
|
66
|
+
p.add_argument(
|
|
67
|
+
"--self-attested",
|
|
68
|
+
action="store_true",
|
|
69
|
+
help=(
|
|
70
|
+
"Verify against the artifact's embedded public_key. This only proves "
|
|
71
|
+
"internal consistency; it does not prove Keel signed the artifact."
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
p.add_argument(
|
|
75
|
+
"--offline",
|
|
76
|
+
action="store_true",
|
|
77
|
+
help=argparse.SUPPRESS,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _cmd_export_cli(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
|
|
82
|
+
try:
|
|
83
|
+
_public_key_alias(args)
|
|
84
|
+
except argparse.ArgumentTypeError as exc:
|
|
85
|
+
parser.error(str(exc))
|
|
86
|
+
args.export_file = args.export_file_flag or args.export_file_pos
|
|
87
|
+
args.manifest = args.manifest_flag or args.manifest_pos
|
|
88
|
+
if not args.export_file:
|
|
89
|
+
parser.error("export requires EXPORT_FILE or --export-file")
|
|
90
|
+
if not args.manifest:
|
|
91
|
+
parser.error("export requires MANIFEST or --manifest")
|
|
92
|
+
if _trust_flag_count(args, include_public_key_url=False) > 1:
|
|
93
|
+
parser.error(
|
|
94
|
+
"--expected-public-key/--public-key, --key-manifest, "
|
|
95
|
+
"--key-manifest-url, and --self-attested are mutually exclusive"
|
|
96
|
+
)
|
|
97
|
+
return cmd_export(args)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _cmd_checkpoint_cli(parser: argparse.ArgumentParser, args: argparse.Namespace) -> int:
|
|
101
|
+
try:
|
|
102
|
+
_public_key_alias(args)
|
|
103
|
+
except argparse.ArgumentTypeError as exc:
|
|
104
|
+
parser.error(str(exc))
|
|
105
|
+
args.checkpoint_file = args.checkpoint_file_flag or args.checkpoint_file_pos
|
|
106
|
+
if not args.checkpoint_file:
|
|
107
|
+
parser.error("checkpoint requires CHECKPOINT_FILE or --checkpoint-file")
|
|
108
|
+
if _trust_flag_count(args, include_public_key_url=True) > 1:
|
|
109
|
+
parser.error(
|
|
110
|
+
"--expected-public-key/--public-key, --public-key-url, --key-manifest, "
|
|
111
|
+
"--key-manifest-url, and --self-attested are mutually exclusive"
|
|
112
|
+
)
|
|
113
|
+
return cmd_checkpoint(args)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
117
|
+
parser = argparse.ArgumentParser(
|
|
118
|
+
prog="keel-verify",
|
|
119
|
+
description="Standalone verifier for Keel trust artifacts.",
|
|
120
|
+
epilog=(
|
|
121
|
+
"New export verification supports --walk-events and --verify-closure. "
|
|
122
|
+
"Backward compatible usage remains: python -m keel_verifier <checkpoint.json>."
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
parser.add_argument("--version", action="version", version=f"keel_verifier {__version__}")
|
|
126
|
+
sub = parser.add_subparsers(dest="cmd")
|
|
127
|
+
|
|
128
|
+
p_export = sub.add_parser("export", help="Verify a signed compliance export.")
|
|
129
|
+
p_export.add_argument("export_file_pos", nargs="?", metavar="EXPORT_FILE")
|
|
130
|
+
p_export.add_argument("manifest_pos", nargs="?", metavar="MANIFEST")
|
|
131
|
+
p_export.add_argument("--export-file", dest="export_file_flag")
|
|
132
|
+
p_export.add_argument("--manifest", dest="manifest_flag")
|
|
133
|
+
p_export.add_argument(
|
|
134
|
+
"--walk-events",
|
|
135
|
+
action="store_true",
|
|
136
|
+
help=(
|
|
137
|
+
"After export content hash and signature verification, parse an "
|
|
138
|
+
"audit export bundle and walk bundled chain_entries."
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
p_export.add_argument(
|
|
142
|
+
"--verify-closure",
|
|
143
|
+
action="store_true",
|
|
144
|
+
help=(
|
|
145
|
+
"After export content hash and signature verification, verify "
|
|
146
|
+
"permit.closed closure signatures and dispatch/provider/client digest "
|
|
147
|
+
"consistency from bundled chain_entries."
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
_add_common_trust_args(p_export)
|
|
151
|
+
_add_key_manifest_args(p_export)
|
|
152
|
+
p_export.set_defaults(func=lambda args: _cmd_export_cli(p_export, args))
|
|
153
|
+
|
|
154
|
+
p_cp = sub.add_parser("checkpoint", help="Verify an integrity checkpoint JSON file.")
|
|
155
|
+
p_cp.add_argument("checkpoint_file_pos", nargs="?", metavar="CHECKPOINT_FILE")
|
|
156
|
+
p_cp.add_argument("--checkpoint-file", dest="checkpoint_file_flag")
|
|
157
|
+
_add_common_trust_args(p_cp)
|
|
158
|
+
p_cp.add_argument(
|
|
159
|
+
"--public-key-url",
|
|
160
|
+
help=(
|
|
161
|
+
"URL to fetch the single checkpoint public key "
|
|
162
|
+
f"(canonical: {KEELAPI_CHECKPOINT_PUBLIC_KEY_URL})."
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
_add_key_manifest_args(p_cp)
|
|
166
|
+
p_cp.add_argument(
|
|
167
|
+
"--tsa-ca-bundle",
|
|
168
|
+
help="Optional CA bundle for TSA trust-chain validation (note only).",
|
|
169
|
+
)
|
|
170
|
+
p_cp.set_defaults(func=lambda args: _cmd_checkpoint_cli(p_cp, args))
|
|
171
|
+
return parser
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _build_legacy_parser() -> argparse.ArgumentParser:
|
|
175
|
+
parser = argparse.ArgumentParser(
|
|
176
|
+
prog="python -m keel_verifier",
|
|
177
|
+
description=(
|
|
178
|
+
"Backward-compatible v0.2.0 checkpoint verifier. For signed "
|
|
179
|
+
"compliance exports, use: keel-verify export --help."
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
parser.add_argument("export_file", help="Path to a sealed Keel checkpoint/export JSON file.")
|
|
183
|
+
parser.add_argument("--json", action="store_true", dest="as_json")
|
|
184
|
+
parser.add_argument("--no-tsa", action="store_true", help="Skip RFC 3161 TSA receipt verification.")
|
|
185
|
+
parser.add_argument("--public-key", metavar="ed25519:BASE64")
|
|
186
|
+
parser.add_argument(
|
|
187
|
+
"--public-key-url",
|
|
188
|
+
metavar="URL",
|
|
189
|
+
help=f"Fetch the trust-root public key from this URL (canonical: {KEELAPI_CHECKPOINT_PUBLIC_KEY_URL}).",
|
|
190
|
+
)
|
|
191
|
+
parser.add_argument(
|
|
192
|
+
"--self-attested",
|
|
193
|
+
action="store_true",
|
|
194
|
+
dest="self_attested",
|
|
195
|
+
help=(
|
|
196
|
+
"Verify against the artifact's own embedded public_key. This only "
|
|
197
|
+
"proves internal consistency; it does not prove Keel signed it."
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
parser.add_argument("--offline", action="store_true", help=argparse.SUPPRESS)
|
|
201
|
+
parser.add_argument("--version", action="version", version=f"keel_verifier {__version__}")
|
|
202
|
+
return parser
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _print_human(result: VerifyResult, export_path: str, stream) -> None:
|
|
206
|
+
p = lambda s="": print(s, file=stream)
|
|
207
|
+
|
|
208
|
+
if result.ok:
|
|
209
|
+
p(f"VERIFIED: {export_path}")
|
|
210
|
+
else:
|
|
211
|
+
p(f"FAILED: {export_path}")
|
|
212
|
+
if result.error:
|
|
213
|
+
for line in result.error.splitlines():
|
|
214
|
+
p(f" {line}")
|
|
215
|
+
|
|
216
|
+
if result.checkpoint_id:
|
|
217
|
+
p(f" Checkpoint: {result.checkpoint_id}")
|
|
218
|
+
if result.computed_at:
|
|
219
|
+
p(f" Computed at: {result.computed_at}")
|
|
220
|
+
if result.composite_hash:
|
|
221
|
+
p(f" Composite: {result.composite_hash}")
|
|
222
|
+
if result.chain_heads_count:
|
|
223
|
+
p(f" Chain heads: {result.chain_heads_count} scope(s)")
|
|
224
|
+
if result.public_key:
|
|
225
|
+
p(f" Public key: {result.public_key}")
|
|
226
|
+
if result.key_id:
|
|
227
|
+
p(f" Key id: {result.key_id}")
|
|
228
|
+
if result.trust_source:
|
|
229
|
+
p(f" Trust source: {result.trust_source}")
|
|
230
|
+
|
|
231
|
+
if result.tsa_present:
|
|
232
|
+
if not result.tsa_checked:
|
|
233
|
+
p(" TSA: present (skipped — --no-tsa)")
|
|
234
|
+
elif result.tsa_verified:
|
|
235
|
+
p(f" TSA: verified ({result.tsa_reason})")
|
|
236
|
+
if result.tsa_url:
|
|
237
|
+
p(f" url: {result.tsa_url}")
|
|
238
|
+
if result.tsa_requested_at:
|
|
239
|
+
p(f" stamped at: {result.tsa_requested_at}")
|
|
240
|
+
else:
|
|
241
|
+
p(f" TSA: FAILED ({result.tsa_reason})")
|
|
242
|
+
else:
|
|
243
|
+
p(" TSA: not present")
|
|
244
|
+
|
|
245
|
+
if result.ok and result.self_attested:
|
|
246
|
+
p()
|
|
247
|
+
p("WARNING: --self-attested verification only proves internal consistency.")
|
|
248
|
+
p("It does not prove that Keel signed this artifact. Drop --self-attested to")
|
|
249
|
+
p("verify against the bundled trust root, or pin explicitly with:")
|
|
250
|
+
p(f" --public-key-url {KEELAPI_CHECKPOINT_PUBLIC_KEY_URL}")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _main_legacy(argv: list[str]) -> int:
|
|
254
|
+
parser = _build_legacy_parser()
|
|
255
|
+
args = parser.parse_args(argv)
|
|
256
|
+
flags = (args.public_key, args.public_key_url, args.self_attested)
|
|
257
|
+
if sum(bool(x) for x in flags) > 1:
|
|
258
|
+
print(
|
|
259
|
+
"ERROR: --public-key, --public-key-url, and --self-attested are mutually exclusive.",
|
|
260
|
+
file=sys.stderr,
|
|
261
|
+
)
|
|
262
|
+
return 2
|
|
263
|
+
|
|
264
|
+
result = verify(
|
|
265
|
+
args.export_file,
|
|
266
|
+
public_key=args.public_key,
|
|
267
|
+
public_key_url=args.public_key_url,
|
|
268
|
+
self_attested=args.self_attested,
|
|
269
|
+
check_tsa=not args.no_tsa,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
if args.as_json:
|
|
273
|
+
print(json.dumps(result.to_dict(), indent=2, sort_keys=True))
|
|
274
|
+
if not result.ok and result.error:
|
|
275
|
+
print(result.error, file=sys.stderr)
|
|
276
|
+
else:
|
|
277
|
+
stream = sys.stdout if result.ok else sys.stderr
|
|
278
|
+
_print_human(result, args.export_file, stream)
|
|
279
|
+
return 0 if result.ok else 1
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def main(argv: list[str] | None = None) -> int:
|
|
283
|
+
raw = list(sys.argv[1:] if argv is None else argv)
|
|
284
|
+
if raw and raw[0] not in LEGACY_COMMANDS and raw[0] not in {"-h", "--help", "--version"}:
|
|
285
|
+
return _main_legacy(raw)
|
|
286
|
+
|
|
287
|
+
parser = _build_parser()
|
|
288
|
+
args = parser.parse_args(raw)
|
|
289
|
+
if not hasattr(args, "func"):
|
|
290
|
+
parser.print_help()
|
|
291
|
+
return 0
|
|
292
|
+
return args.func(args)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
if __name__ == "__main__":
|
|
296
|
+
raise SystemExit(main())
|