asexec 0.3.14__py3-none-any.whl
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.
- asexec/__init__.py +17 -0
- asexec/__main__.py +4 -0
- asexec/canonical.py +61 -0
- asexec/cli.py +455 -0
- asexec/drand.py +164 -0
- asexec/errors.py +21 -0
- asexec/hashing.py +109 -0
- asexec/identity.py +75 -0
- asexec/keys.py +94 -0
- asexec/manifest.py +203 -0
- asexec/models.py +142 -0
- asexec/roughtime.py +407 -0
- asexec/verifier.py +422 -0
- asexec-0.3.14.dist-info/METADATA +271 -0
- asexec-0.3.14.dist-info/RECORD +18 -0
- asexec-0.3.14.dist-info/WHEEL +4 -0
- asexec-0.3.14.dist-info/entry_points.txt +3 -0
- asexec-0.3.14.dist-info/licenses/LICENSE +202 -0
asexec/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""asexec - a pre-registration & notarization primitive for AI evaluations.
|
|
2
|
+
|
|
3
|
+
Local-first, pseudonymous, offline-verifiable. An evaluator signs a
|
|
4
|
+
*pre-registration* before a run and *receipts* after, publishing them to a
|
|
5
|
+
public git repo whose witnessed history is the (social) ceiling that gives
|
|
6
|
+
"pre" its meaning. A third party can verify the commitment -> fulfillment /
|
|
7
|
+
gap offline, trusting only a keypair and the published files.
|
|
8
|
+
|
|
9
|
+
See the module docstrings and the README for what this does!
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "0.3.14"
|
|
13
|
+
|
|
14
|
+
SCHEMA_VERSION = "asexec"
|
|
15
|
+
PREDICATE_TYPE = "https://asexec.dev/manifest"
|
|
16
|
+
|
|
17
|
+
__all__ = ["__version__", "SCHEMA_VERSION", "PREDICATE_TYPE"]
|
asexec/__main__.py
ADDED
asexec/canonical.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Canonical serialization and the PAE signing input.
|
|
2
|
+
|
|
3
|
+
We sign the PAE (Pre-Authentication Encoding, borrowed from DSSE) of a fixed,
|
|
4
|
+
documented canonical byte serialization of the payload.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
# Media type of the signed payload (an asexec manifest body).
|
|
13
|
+
PAYLOAD_TYPE = b"application/vnd.asexec+json"
|
|
14
|
+
|
|
15
|
+
# Domain-separation prefix for our PAE construction (cf. DSSE's "DSSEv1").
|
|
16
|
+
_PAE_PREFIX = b"asexec-PAE/v1"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def canonical_bytes(payload: Any) -> bytes:
|
|
20
|
+
"""Deterministically serialize a JSON-compatible object to bytes.
|
|
21
|
+
|
|
22
|
+
Rules (documented so any implementation can reproduce them):
|
|
23
|
+
- UTF-8
|
|
24
|
+
- object keys sorted (lexicographic, by Unicode code point)
|
|
25
|
+
- no insignificant whitespace (compact separators)
|
|
26
|
+
- non-ASCII preserved (ensure_ascii=False)
|
|
27
|
+
|
|
28
|
+
The payload passed here must NOT contain the outer signature block; only
|
|
29
|
+
the manifest body is signed.
|
|
30
|
+
"""
|
|
31
|
+
return json.dumps(
|
|
32
|
+
payload,
|
|
33
|
+
sort_keys=True,
|
|
34
|
+
separators=(",", ":"),
|
|
35
|
+
ensure_ascii=False,
|
|
36
|
+
).encode("utf-8")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def pae(payload_type: bytes, body: bytes) -> bytes:
|
|
40
|
+
"""Pre-Authentication Encoding.
|
|
41
|
+
|
|
42
|
+
PAE(t, b) = PREFIX SP LEN(t) SP t SP LEN(b) SP b
|
|
43
|
+
|
|
44
|
+
where SP is a single ASCII space and LEN is ASCII decimal of the byte
|
|
45
|
+
length. Signing/verifying always happens over this string, never over raw
|
|
46
|
+
JSON.
|
|
47
|
+
"""
|
|
48
|
+
return b" ".join(
|
|
49
|
+
[
|
|
50
|
+
_PAE_PREFIX,
|
|
51
|
+
str(len(payload_type)).encode("ascii"),
|
|
52
|
+
payload_type,
|
|
53
|
+
str(len(body)).encode("ascii"),
|
|
54
|
+
body,
|
|
55
|
+
]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def signing_input(payload: Any) -> bytes:
|
|
60
|
+
"""The exact bytes to sign/verify for a manifest payload."""
|
|
61
|
+
return pae(PAYLOAD_TYPE, canonical_bytes(payload))
|
asexec/cli.py
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"""asexec command-line interface.
|
|
2
|
+
|
|
3
|
+
Commands: keygen, prereg, postreg, verify, identity.
|
|
4
|
+
|
|
5
|
+
``verify`` is fully offline; only the sign-time drand (``--floor``)/Roughtime (``--ceiling``)
|
|
6
|
+
fetches and ``identity match`` touch the network.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import uuid
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from typing import TypeVar
|
|
19
|
+
|
|
20
|
+
from . import __version__, drand, hashing, identity, keys, manifest, verifier
|
|
21
|
+
from .errors import VerificationError
|
|
22
|
+
|
|
23
|
+
OK = "✓"
|
|
24
|
+
NO = "✗"
|
|
25
|
+
T = TypeVar("T")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# --------------------------------------------------------------------------- #
|
|
29
|
+
# input helpers
|
|
30
|
+
# --------------------------------------------------------------------------- #
|
|
31
|
+
def _load_json(path: str) -> str | dict:
|
|
32
|
+
"""Load JSON from the given path."""
|
|
33
|
+
try:
|
|
34
|
+
with open(path) as f:
|
|
35
|
+
return json.load(f)
|
|
36
|
+
except json.JSONDecodeError as e:
|
|
37
|
+
raise SystemExit(f"error: {path} must contain valid JSON: {e}") from e
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _get_arg_or_file(
|
|
41
|
+
args, arg_name: str, file_arg_name: str, load_file: Callable[[str], T]
|
|
42
|
+
) -> str | T | None:
|
|
43
|
+
"""Load a value from either ``--<arg>`` or ``--<file-arg>``, but not both."""
|
|
44
|
+
value = getattr(args, arg_name, None)
|
|
45
|
+
file_path = getattr(args, file_arg_name, None)
|
|
46
|
+
if value and file_path:
|
|
47
|
+
raise SystemExit(
|
|
48
|
+
f"error: provide --{arg_name.replace('_', '-')} "
|
|
49
|
+
f"OR --{file_arg_name.replace('_', '-')}, not both"
|
|
50
|
+
)
|
|
51
|
+
if value:
|
|
52
|
+
return value
|
|
53
|
+
if file_path:
|
|
54
|
+
try:
|
|
55
|
+
return load_file(file_path)
|
|
56
|
+
except FileNotFoundError:
|
|
57
|
+
raise SystemExit(
|
|
58
|
+
f"error: --{file_arg_name.replace('_', '-')} not found: {file_path}"
|
|
59
|
+
) from None
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _get_target(args) -> str | dict | None:
|
|
64
|
+
"""Retrieve the target value (from arg xor file), if any."""
|
|
65
|
+
return _get_arg_or_file(args, "target", "target_file", _load_json)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _get_declaration(args) -> str | dict | None:
|
|
69
|
+
"""Retrieve the declaration value (from arg xor file), if any."""
|
|
70
|
+
return _get_arg_or_file(args, "declaration", "declaration_file", _load_json)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _get_notes(args) -> str | dict | None:
|
|
74
|
+
"""Retrieve the notes value (from arg xor file), if any."""
|
|
75
|
+
return _get_arg_or_file(args, "notes", "notes_file", _load_json)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _get_due(args) -> str | None:
|
|
79
|
+
"""Validate the provided ``--due`` ISO-8601 deadline; return it verbatim."""
|
|
80
|
+
if not args.due:
|
|
81
|
+
return None
|
|
82
|
+
try:
|
|
83
|
+
datetime.fromisoformat(args.due.replace("Z", "+00:00"))
|
|
84
|
+
except ValueError:
|
|
85
|
+
raise SystemExit(f"error: --due is not a valid ISO-8601 timestamp: {args.due}") from None
|
|
86
|
+
return args.due
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _get_floor(args) -> dict | None:
|
|
90
|
+
"""Fetch a drand freshness floor (anchor.floor) at sign time, or None."""
|
|
91
|
+
if not args.floor:
|
|
92
|
+
return None
|
|
93
|
+
try:
|
|
94
|
+
return drand.fetch_floor()
|
|
95
|
+
except Exception as e:
|
|
96
|
+
sys.stderr.write(f"warning: drand fetch failed ({e}); continuing without freshness floor\n")
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _build_subject(paths: list[str] | None, hash_alg: str) -> list | None:
|
|
101
|
+
"""Construct subject data (a list of filenames/dirnames and their associated
|
|
102
|
+
digest hashes) given source paths and a hash algorithm."""
|
|
103
|
+
return hashing.build_subject(paths, hash_alg) if paths else None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _resolve_ref(value: str) -> str:
|
|
107
|
+
"""A --fulfills/--prev value may be a manifest file path or a literal ref."""
|
|
108
|
+
if os.path.isfile(value):
|
|
109
|
+
return manifest.ref(manifest.get_body(manifest.load(value)))
|
|
110
|
+
return value
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _attach_ceiling(mani: dict, body: dict, want_ceiling: bool) -> None:
|
|
114
|
+
"""Optionally fetch a Roughtime ceiling witness and attach it (sign-time,
|
|
115
|
+
network). The nonce is the body ref, so this must run *after* signing; it
|
|
116
|
+
does not perturb ref."""
|
|
117
|
+
if not want_ceiling:
|
|
118
|
+
return
|
|
119
|
+
from . import roughtime
|
|
120
|
+
|
|
121
|
+
nonce = manifest.ref(body) # sha-256:<hex>
|
|
122
|
+
try:
|
|
123
|
+
ceiling = roughtime.fetch_ceiling(nonce)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
sys.stderr.write(
|
|
126
|
+
f"warning: ceiling witness fetch failed ({e}); continuing without a ceiling\n"
|
|
127
|
+
)
|
|
128
|
+
return
|
|
129
|
+
manifest.attach_ceiling(mani, ceiling)
|
|
130
|
+
print(
|
|
131
|
+
f" ceiling : {ceiling.get('ceiling_type')} witness {ceiling.get('witness_id')} "
|
|
132
|
+
f"@ {ceiling.get('midpoint')} (±{ceiling.get('radius')}s)"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# --------------------------------------------------------------------------- #
|
|
137
|
+
# commands
|
|
138
|
+
# --------------------------------------------------------------------------- #
|
|
139
|
+
def cmd_keygen(args) -> int:
|
|
140
|
+
"""Generate a cryptographic key for signing pre- and post-registrations."""
|
|
141
|
+
out = args.out or f"asexec-{uuid.uuid4()}.key"
|
|
142
|
+
priv, _pub = keys.generate()
|
|
143
|
+
kid = keys.save(priv, out)
|
|
144
|
+
print(f"{OK} generated ed25519 key")
|
|
145
|
+
print(f" secret : {out} (keep private; mode 0600)")
|
|
146
|
+
print(f" public : {out}.pub")
|
|
147
|
+
print(f" keyid : {kid}")
|
|
148
|
+
return 0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def cmd_prereg(args) -> int:
|
|
152
|
+
"""Write a signed pre-registration, signalling the intended execution."""
|
|
153
|
+
target = _get_target(args)
|
|
154
|
+
if target is None:
|
|
155
|
+
raise SystemExit("error: give --target or --target-file (the only mandatory claim)")
|
|
156
|
+
priv, pub = keys.load_signing_key(args.key)
|
|
157
|
+
subject = _build_subject(args.subject, args.hash_alg)
|
|
158
|
+
body = manifest.build_prereg(
|
|
159
|
+
target,
|
|
160
|
+
due=_get_due(args),
|
|
161
|
+
declaration=_get_declaration(args),
|
|
162
|
+
subject=subject,
|
|
163
|
+
hash_alg=args.hash_alg,
|
|
164
|
+
floor=_get_floor(args),
|
|
165
|
+
notes=_get_notes(args),
|
|
166
|
+
)
|
|
167
|
+
mani = manifest.sign(body, priv, pub)
|
|
168
|
+
_attach_ceiling(mani, body, args.ceiling)
|
|
169
|
+
manifest.save(mani, args.out)
|
|
170
|
+
print(f"{OK} pre-registration written: {args.out}")
|
|
171
|
+
print(f" ref : {manifest.ref(body)}")
|
|
172
|
+
if body.get("due"):
|
|
173
|
+
print(f" due : {body['due']}")
|
|
174
|
+
return 0
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def cmd_postreg(args) -> int:
|
|
178
|
+
"""Write a signed post-registration, documenting the execution and fulfilling
|
|
179
|
+
a pre-registration as promised."""
|
|
180
|
+
priv, pub = keys.load_signing_key(args.key)
|
|
181
|
+
|
|
182
|
+
prereg_body = None
|
|
183
|
+
if os.path.isfile(args.fulfills):
|
|
184
|
+
prereg_body = manifest.get_body(manifest.load(args.fulfills))
|
|
185
|
+
|
|
186
|
+
# target / due / declaration / hash_alg inherit from the fulfilled prereg
|
|
187
|
+
# unless overridden on this postreg.
|
|
188
|
+
target = _get_target(args)
|
|
189
|
+
if target is None:
|
|
190
|
+
if prereg_body is None:
|
|
191
|
+
raise SystemExit("error: no --target and --fulfills is not a readable prereg")
|
|
192
|
+
target = prereg_body["target"]
|
|
193
|
+
|
|
194
|
+
due = _get_due(args) or (prereg_body or {}).get("due")
|
|
195
|
+
declaration = _get_declaration(args) or (prereg_body or {}).get("declaration")
|
|
196
|
+
hash_alg = args.hash_alg or (prereg_body or {}).get("hash_alg") or hashing.DEFAULT_ALG
|
|
197
|
+
|
|
198
|
+
subject = _build_subject(args.subject, hash_alg)
|
|
199
|
+
body = manifest.build_postreg(
|
|
200
|
+
target,
|
|
201
|
+
fulfills=_resolve_ref(args.fulfills),
|
|
202
|
+
due=due,
|
|
203
|
+
declaration=declaration,
|
|
204
|
+
subject=subject,
|
|
205
|
+
hash_alg=hash_alg,
|
|
206
|
+
prev_hash=_resolve_ref(args.prev) if args.prev else None,
|
|
207
|
+
floor=_get_floor(args),
|
|
208
|
+
notes=_get_notes(args),
|
|
209
|
+
provenance=args.provenance,
|
|
210
|
+
repro_recipe=json.loads(args.repro_recipe) if args.repro_recipe else None,
|
|
211
|
+
)
|
|
212
|
+
mani = manifest.sign(body, priv, pub)
|
|
213
|
+
_attach_ceiling(mani, body, args.ceiling)
|
|
214
|
+
manifest.save(mani, args.out)
|
|
215
|
+
print(f"{OK} post-registration written: {args.out}")
|
|
216
|
+
print(f" ref : {manifest.ref(body)}")
|
|
217
|
+
print(f" fulfills : {body['fulfills']}")
|
|
218
|
+
if body.get("prev_hash"):
|
|
219
|
+
print(f" prev : {body['prev_hash']}")
|
|
220
|
+
return 0
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def cmd_verify(args) -> int:
|
|
224
|
+
"""Run a series of verification tests on the prereg-postreg cycle to prove that
|
|
225
|
+
this execution met the desired standards. Produces a code summarizing the tests
|
|
226
|
+
performed and the results of each."""
|
|
227
|
+
try:
|
|
228
|
+
tests = verifier.parse_tests(args.tests)
|
|
229
|
+
except VerificationError as e:
|
|
230
|
+
raise SystemExit(f"error: {e}") from e
|
|
231
|
+
|
|
232
|
+
report = verifier.verify_paths(args.paths, tests, artifacts_dir=args.artifacts)
|
|
233
|
+
|
|
234
|
+
print("=== manifests ===")
|
|
235
|
+
for m in report["manifests"]:
|
|
236
|
+
sig = m["signature"]
|
|
237
|
+
s = OK if (sig.get("signature_ok") and sig.get("keyid_ok")) else NO
|
|
238
|
+
print(f"{s} {m['path']} [{m.get('phase')}] keyid={m.get('keyid')}")
|
|
239
|
+
fl = m["floor"]
|
|
240
|
+
if fl["status"] != "absent":
|
|
241
|
+
fs = OK if fl["status"] == "verified" else NO
|
|
242
|
+
extra = (
|
|
243
|
+
f" (created no earlier than {fl.get('created_no_earlier_than')})"
|
|
244
|
+
if fl["status"] == "verified"
|
|
245
|
+
else ""
|
|
246
|
+
)
|
|
247
|
+
print(f" {fs} drand freshness floor round {fl.get('round', '?')}{extra}")
|
|
248
|
+
cl = m["ceiling"]
|
|
249
|
+
if cl["status"] != "absent":
|
|
250
|
+
cs = OK if cl["status"] == "verified" else NO
|
|
251
|
+
extra = (
|
|
252
|
+
f" (created no later than {cl.get('midpoint')} ±{cl.get('radius')}s, "
|
|
253
|
+
f"witness {cl.get('witness_id')})"
|
|
254
|
+
if cl["status"] == "verified"
|
|
255
|
+
else f" ({cl.get('error', cl['status'])})"
|
|
256
|
+
)
|
|
257
|
+
print(f" {cs} ceiling witness{extra}")
|
|
258
|
+
c = m["content"]
|
|
259
|
+
if c["status"] not in ("skipped",):
|
|
260
|
+
cs = OK if c["status"] == "ok" else NO
|
|
261
|
+
print(f" {cs} content hashes {c['status']}")
|
|
262
|
+
for e in c.get("entries", []):
|
|
263
|
+
if not e["ok"]:
|
|
264
|
+
print(f" {NO} {e['name']}: {e.get('reason', 'digest mismatch')}")
|
|
265
|
+
|
|
266
|
+
print("\n=== commitments ===")
|
|
267
|
+
if not report["commitments"]:
|
|
268
|
+
print(" (no pre-registrations among the provided manifests)")
|
|
269
|
+
for c in report["commitments"]:
|
|
270
|
+
print(f" [{c['state'].upper()}] prereg {c['ref']}")
|
|
271
|
+
print(f" due : {c.get('due') or '(none declared)'}")
|
|
272
|
+
print(
|
|
273
|
+
f" postregs : {len(c['receipts'])}"
|
|
274
|
+
+ ("" if c["chain_ok"] else f" {NO} {c['chain_note']}")
|
|
275
|
+
)
|
|
276
|
+
if not c["key_consistent"]:
|
|
277
|
+
print(f" {NO} postregs signed by a different key than the pre-registration")
|
|
278
|
+
if report["notarization_only"]:
|
|
279
|
+
print("\n=== notarization-only (postregs with no matching pre-registration) ===")
|
|
280
|
+
for n in report["notarization_only"]:
|
|
281
|
+
print(f" {n['ref']} (fulfills {n.get('fulfills')})")
|
|
282
|
+
|
|
283
|
+
print("\n=== tests ===")
|
|
284
|
+
for t in report["tests"]:
|
|
285
|
+
r = report["results"][t]
|
|
286
|
+
s = OK if r["result"] == "PASS" else NO
|
|
287
|
+
print(f" {s} {t}={r['result']} ({r['reason']})")
|
|
288
|
+
|
|
289
|
+
if report["ceiling_trust"]:
|
|
290
|
+
print("\n=== ceiling trust (what you are accepting) ===")
|
|
291
|
+
for line in report["ceiling_trust"]:
|
|
292
|
+
print(f" - {line}")
|
|
293
|
+
|
|
294
|
+
print("\n=== what this does NOT prove ===")
|
|
295
|
+
for nc in report["non_claims"]:
|
|
296
|
+
print(f" - {nc}")
|
|
297
|
+
|
|
298
|
+
print(f"\n{report['code']}")
|
|
299
|
+
print(f"DISCLAIMER: {report['disclaimer']}")
|
|
300
|
+
return 0 if report["ok"] else 2
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def cmd_identity(args) -> int:
|
|
304
|
+
"""Standalone emit/match function pair for tying a known public identity via
|
|
305
|
+
domain to a public key, no certificate authority (CA) needed. ``emit`` a json file
|
|
306
|
+
which embeds the key and domain and publish it. Anyone may then check via
|
|
307
|
+
``match`` that the asserted identity is placed at the given domain."""
|
|
308
|
+
if args.identity_cmd == "emit":
|
|
309
|
+
_priv, pub = keys.load_signing_key(args.key)
|
|
310
|
+
pair = {"keyid": keys.keyid_for(pub), "pubkey": pub.hex()}
|
|
311
|
+
doc = identity.build_wellknown([pair], domain=args.domain)
|
|
312
|
+
identity.write_wellknown(doc, args.out)
|
|
313
|
+
print(f"{OK} wrote {args.out}")
|
|
314
|
+
print(" publish at: https://<your-domain>/.well-known/asexec.json")
|
|
315
|
+
print(f" keyid : {pair['keyid']}")
|
|
316
|
+
return 0
|
|
317
|
+
if args.identity_cmd == "match":
|
|
318
|
+
keyid = args.keyid
|
|
319
|
+
pubkey = args.pubkey
|
|
320
|
+
if args.key:
|
|
321
|
+
_priv, pub = keys.load_signing_key(args.key)
|
|
322
|
+
keyid = keys.keyid_for(pub)
|
|
323
|
+
res = identity.match_binding(args.domain, keyid=keyid, pubkey_hex=pubkey)
|
|
324
|
+
s = OK if res["bound"] else NO
|
|
325
|
+
print(
|
|
326
|
+
f"{s} key {'IS' if res['bound'] else 'is NOT'} asserted by {args.domain} "
|
|
327
|
+
f"({res['listed_count']} key(s) listed)"
|
|
328
|
+
)
|
|
329
|
+
print(f" caveat: {res['caveat']}")
|
|
330
|
+
return 0 if res["bound"] else 2
|
|
331
|
+
raise SystemExit("error: use 'identity emit' or 'identity match'")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# --------------------------------------------------------------------------- #
|
|
335
|
+
# parser
|
|
336
|
+
# --------------------------------------------------------------------------- #
|
|
337
|
+
def _add_anchor_flags(p):
|
|
338
|
+
"""Time-anchor parsing helper, shared by prereg and postreg."""
|
|
339
|
+
p.add_argument(
|
|
340
|
+
"--floor",
|
|
341
|
+
action="store_true",
|
|
342
|
+
help="attach a drand freshness floor (proves created no earlier than T; network)",
|
|
343
|
+
)
|
|
344
|
+
p.add_argument(
|
|
345
|
+
"--ceiling",
|
|
346
|
+
action="store_true",
|
|
347
|
+
help="attach a Roughtime ceiling witness (proves created no later than T; network)",
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
352
|
+
"""``asexec`` parsing core."""
|
|
353
|
+
p = argparse.ArgumentParser(
|
|
354
|
+
prog="asexec",
|
|
355
|
+
description="Pre-registration & notarization primitive for AI evaluations. "
|
|
356
|
+
"Does NOT prove identity, eval quality, provenance, or cryptographic "
|
|
357
|
+
"'pre' (see 'verify' output).",
|
|
358
|
+
)
|
|
359
|
+
p.add_argument("--version", action="version", version=f"asexec {__version__}")
|
|
360
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
361
|
+
|
|
362
|
+
# asexec keygen
|
|
363
|
+
kg = sub.add_parser("keygen", help="generate an ed25519 keypair (no CA)")
|
|
364
|
+
kg.add_argument("--out", default=None, help="secret key file (default: asexec-<uuid>.key)")
|
|
365
|
+
kg.set_defaults(func=cmd_keygen)
|
|
366
|
+
|
|
367
|
+
# asexec prereg
|
|
368
|
+
pr = sub.add_parser("prereg", help="sign a pre-registration before a run")
|
|
369
|
+
pr.add_argument("--key", required=True, help="key that signs the prereg")
|
|
370
|
+
pr.add_argument("--target", help="plain-text target details (what you commit to run)")
|
|
371
|
+
pr.add_argument("--target-file", help="structured JSON target details")
|
|
372
|
+
pr.add_argument(
|
|
373
|
+
"--due",
|
|
374
|
+
help="disclosure deadline, ISO-8601 (optional; e.g. 2026-08-30T00:00:00Z)",
|
|
375
|
+
)
|
|
376
|
+
pr.add_argument("--declaration", help="plain-language commitment text")
|
|
377
|
+
pr.add_argument("--declaration-file", help="structured JSON declaration")
|
|
378
|
+
pr.add_argument(
|
|
379
|
+
"--subject",
|
|
380
|
+
nargs="+",
|
|
381
|
+
help="path(s) to harness/eval to hash (optional at prereg time)",
|
|
382
|
+
)
|
|
383
|
+
pr.add_argument(
|
|
384
|
+
"--hash-alg",
|
|
385
|
+
default=hashing.DEFAULT_ALG,
|
|
386
|
+
choices=hashing.available_algorithms(),
|
|
387
|
+
)
|
|
388
|
+
pr.add_argument("--notes", help="free-form context (hypothesis/methodology)")
|
|
389
|
+
pr.add_argument("--notes-file", help="structured JSON notes")
|
|
390
|
+
_add_anchor_flags(pr)
|
|
391
|
+
pr.add_argument("--out", default="preregistration.json")
|
|
392
|
+
pr.set_defaults(func=cmd_prereg)
|
|
393
|
+
|
|
394
|
+
# asexec postreg
|
|
395
|
+
po = sub.add_parser("postreg", help="sign a post-registration (receipt) after a run")
|
|
396
|
+
po.add_argument("--key", required=True)
|
|
397
|
+
po.add_argument(
|
|
398
|
+
"--fulfills",
|
|
399
|
+
required=True,
|
|
400
|
+
help="pre-registration file (or literal ref) this fulfils",
|
|
401
|
+
)
|
|
402
|
+
po.add_argument("--target", help="override target (default: inherit from prereg)")
|
|
403
|
+
po.add_argument("--target-file")
|
|
404
|
+
po.add_argument("--due", help="override disclosure deadline (default: inherit from prereg)")
|
|
405
|
+
po.add_argument("--declaration")
|
|
406
|
+
po.add_argument("--declaration-file")
|
|
407
|
+
po.add_argument("--subject", nargs="+", help="path(s) to outputs/transcript/harness to hash")
|
|
408
|
+
po.add_argument("--hash-alg", default=None, choices=hashing.available_algorithms())
|
|
409
|
+
po.add_argument("--prev", help="prior postreg file (or ref) in this commitment's chain")
|
|
410
|
+
po.add_argument("--provenance", choices=["asserted", "reproducible"], default="asserted")
|
|
411
|
+
po.add_argument(
|
|
412
|
+
"--repro-recipe",
|
|
413
|
+
help="JSON: {seed, decode, runtime} if provenance=reproducible",
|
|
414
|
+
)
|
|
415
|
+
po.add_argument("--notes")
|
|
416
|
+
po.add_argument("--notes-file")
|
|
417
|
+
_add_anchor_flags(po)
|
|
418
|
+
po.add_argument("--out", default="postregistration.json")
|
|
419
|
+
po.set_defaults(func=cmd_postreg)
|
|
420
|
+
|
|
421
|
+
# asexec verify
|
|
422
|
+
vy = sub.add_parser("verify", help="verify manifests offline; produce a canonical verify code")
|
|
423
|
+
vy.add_argument("paths", nargs="+", help="manifest file(s) or a directory of them")
|
|
424
|
+
vy.add_argument(
|
|
425
|
+
"--tests",
|
|
426
|
+
required=True,
|
|
427
|
+
help="comma-separated tests to run (MUST include 'BDR'). "
|
|
428
|
+
f"available: {', '.join(verifier.TEST_CATALOG)}",
|
|
429
|
+
)
|
|
430
|
+
vy.add_argument("--artifacts", help="directory of original artifacts, to check content hashes")
|
|
431
|
+
vy.set_defaults(func=cmd_verify)
|
|
432
|
+
|
|
433
|
+
# asexec identity
|
|
434
|
+
idp = sub.add_parser("identity", help="key<->domain binding via .well-known (no CA)")
|
|
435
|
+
isub = idp.add_subparsers(dest="identity_cmd", required=True)
|
|
436
|
+
ie = isub.add_parser("emit", help="write a .well-known/asexec.json for your key")
|
|
437
|
+
ie.add_argument("--key", required=True)
|
|
438
|
+
ie.add_argument("--domain")
|
|
439
|
+
ie.add_argument("--out", default="asexec.json")
|
|
440
|
+
ie.set_defaults(func=cmd_identity)
|
|
441
|
+
iv = isub.add_parser(
|
|
442
|
+
"match", help="check a key matches that which is asserted by a domain (network)"
|
|
443
|
+
)
|
|
444
|
+
iv.add_argument("--domain", required=True)
|
|
445
|
+
iv.add_argument("--keyid")
|
|
446
|
+
iv.add_argument("--pubkey")
|
|
447
|
+
iv.add_argument("--key", help="a key file, to derive the keyid")
|
|
448
|
+
iv.set_defaults(func=cmd_identity)
|
|
449
|
+
|
|
450
|
+
return p
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def main(argv: list[str] | None = None) -> int:
|
|
454
|
+
args = build_parser().parse_args(argv)
|
|
455
|
+
return args.func(args)
|