uibc-core 0.3.0__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.
uibc_core/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ """uibc-core: UIBC Core reference implementation v0.3.0 [PROPOSAL].
2
+
3
+ Implements the minimal object model from the LGD/UIBC master archive (v0.1.0, SS9-SS22):
4
+ Agent, Identity, Event, Evidence, Run/Manifest, Evidence Root, Verification, Seal Signature.
5
+
6
+ v0.2: adds seal signatures (HMAC-SHA256 PROVISIONAL, Ed25519 = v0.3 target),
7
+ verifier check S6, and strict verification mode (--key). Closes the v0.1
8
+ 'malicious' fixture blind spot (self-consistent forgery without the key).
9
+
10
+ v0.2.2: two verdict-affecting fixes surfaced by the new memory mutation corpus
11
+ (see fixtures/generate_memory_fixtures.py) - an injected duplicate memory_id in
12
+ a migration target is no longer silently discarded, and an empty-to-empty
13
+ migration is no longer a PASS. Registered as a breaking change in CHANGELOG.md.
14
+
15
+ Status: PROPOSAL. Not a ratified standard. Verification results are scoped proofs:
16
+ a PASS means "no violation detected within the observed evidence boundary".
17
+ """
18
+
19
+ __version__ = "0.3.0"
20
+ SPEC_VERSION = "uibc-core/0.3.0-proposal"
21
+ VERIFIER_ID = "uibc-core-verifier"
uibc_core/canonical.py ADDED
@@ -0,0 +1,45 @@
1
+ """Canonical serialization + hashing (archive SS13).
2
+
3
+ PROVISIONAL implementation: JSON with sorted keys, no whitespace, UTF-8.
4
+ A future version MUST migrate to full RFC 8785 (JCS) compliance before any
5
+ signature/Merkle construction is ratified - JSON formatting differences would
6
+ otherwise break hash comparability (archive warning, SS13).
7
+ """
8
+
9
+ import hashlib
10
+ import json
11
+
12
+
13
+ def canonicalize(obj) -> bytes:
14
+ """Provisional canonical JSON encoding (sorted keys, compact, UTF-8)."""
15
+ return json.dumps(
16
+ obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False
17
+ ).encode("utf-8")
18
+
19
+
20
+ def sha256_hex(data: bytes) -> str:
21
+ return hashlib.sha256(data).hexdigest()
22
+
23
+
24
+ def hash_obj(obj) -> str:
25
+ """SHA-256 over the canonical serialization of a JSON-able object."""
26
+ return sha256_hex(canonicalize(obj))
27
+
28
+
29
+ def hash_file(path) -> str:
30
+ h = hashlib.sha256()
31
+ with open(path, "rb") as f:
32
+ for chunk in iter(lambda: f.read(65536), b""):
33
+ h.update(chunk)
34
+ return h.hexdigest()
35
+
36
+
37
+ def evidence_root(content_hashes: list) -> str:
38
+ """Evidence Root v0.1: SHA-256 over the canonical serialization of the
39
+ sorted list of evidence content hashes.
40
+
41
+ PROVISIONAL: not yet a Merkle tree. Upgrading to a proper Merkle/Hash Tree
42
+ (archive SS13) requires the ratified canonical serialization first. The
43
+ algorithm identifier is recorded in the verification report.
44
+ """
45
+ return sha256_hex(canonicalize(sorted(content_hashes)))
@@ -0,0 +1,134 @@
1
+ """Certificates (route step 10) - v0.1 PROPOSAL, pure stdlib.
2
+
3
+ A certificate is a SIGNED STATEMENT that a package passed verification:
4
+ it carries the evidence_root, the signer key_id, an optional expiry, and an
5
+ optional binding to a subject package. It is Evidence-with-signature, NOT
6
+ Trust (archive discipline: Fact != Evidence != ... != Trust).
7
+
8
+ v0.1 honest scope:
9
+ 1. HMAC-SHA256 (symmetric) - same boundary as v0.2 seals: only key holders
10
+ can issue AND verify. Third-party verifiable certs are the Ed25519/v0.3
11
+ target. Do NOT represent this as "publicly verifiable certification".
12
+ 2. "LGD Certified" branding is explicitly OUT OF SCOPE (route note 17:
13
+ certification must be cautious). This module issues machine-checkable
14
+ attestation objects, not brand endorsements.
15
+ 3. Certificate revocation is NOT in v0.1 (revocation overlay at the gate
16
+ layer covers packages; cert revocation list is a v0.2 target).
17
+ 4. Expiry uses the local clock - timestamps are not independently anchored
18
+ (same limitation as the verifier report).
19
+ """
20
+
21
+ import base64
22
+ import datetime
23
+ import hashlib
24
+ import hmac
25
+ import json
26
+ import os
27
+ import uuid
28
+
29
+ from .signing import ALGORITHM, key_id
30
+ from . import SPEC_VERSION
31
+
32
+ CERT_SCHEMA = "uibc-certificate/0.1"
33
+
34
+
35
+ def _canonical(body: dict) -> bytes:
36
+ return json.dumps(body, sort_keys=True, separators=(",", ":"),
37
+ ensure_ascii=False).encode("utf-8")
38
+
39
+
40
+ def _sign(key: bytes, body: dict) -> str:
41
+ return base64.b64encode(
42
+ hmac.new(key, _canonical(body), hashlib.sha256).digest()).decode("ascii")
43
+
44
+
45
+ def issue_certificate(key: bytes, identity: dict, manifest: dict,
46
+ expires_at: str = None, statement: str = None,
47
+ cert_id: str = None) -> dict:
48
+ """Issue a certificate binding agent identity to an evidence root."""
49
+ body = {
50
+ "schema": CERT_SCHEMA,
51
+ "spec_version": SPEC_VERSION,
52
+ "certificate_id": cert_id or f"c-{uuid.uuid4()}",
53
+ "algorithm": ALGORITHM,
54
+ "key_id": key_id(key),
55
+ "subject_agent": identity.get("agent_id"),
56
+ "owner": identity.get("owner"),
57
+ "evidence_root": manifest.get("evidence_root"),
58
+ "issued_at": datetime.datetime.now(datetime.timezone.utc)
59
+ .strftime("%Y-%m-%dT%H:%M:%SZ"),
60
+ "expires_at": expires_at,
61
+ "statement": statement or ("package with this evidence_root passed "
62
+ "verification at issue time"),
63
+ }
64
+ body["signature"] = _sign(key, body)
65
+ return body
66
+
67
+
68
+ def verify_certificate(key: bytes, cert: dict,
69
+ package_dir: str = None, now_utc: str = None) -> dict:
70
+ """Verify a certificate. Returns a scoped report (never a trust claim).
71
+
72
+ package_dir: if given, additionally checks the cert is bound to that
73
+ package (evidence_root equality).
74
+ now_utc: override 'now' for expiry check (ISO8601 Z); tests use this
75
+ to travel in time deterministically.
76
+ """
77
+ checks = []
78
+
79
+ def record(cid, name, passed, detail=""):
80
+ checks.append({"id": cid, "name": name,
81
+ "result": "PASS" if passed else "FAIL", "detail": detail})
82
+ return passed
83
+
84
+ ok = True
85
+ for field in ("schema", "certificate_id", "algorithm", "key_id",
86
+ "subject_agent", "evidence_root", "issued_at", "signature"):
87
+ if not record("C1", "required fields", bool(cert.get(field)),
88
+ f"missing field {field!r}" if not cert.get(field) else "present"):
89
+ ok = False
90
+ break
91
+
92
+ if ok:
93
+ ok &= record("C2", "schema", cert["schema"] == CERT_SCHEMA,
94
+ f"{cert['schema']!r}")
95
+ ok &= record("C3", "algorithm", cert["algorithm"] == ALGORITHM,
96
+ f"{cert['algorithm']!r}")
97
+ kid = key_id(key)
98
+ ok &= record("C4", "signer key", cert["key_id"] == kid,
99
+ "signer key mismatch: not issued by this key")
100
+ body = {k: v for k, v in cert.items() if k != "signature"}
101
+ try:
102
+ mac = base64.b64decode(cert["signature"], validate=True)
103
+ valid = hmac.compare_digest(
104
+ hmac.new(key, _canonical(body), hashlib.sha256).digest(), mac)
105
+ except Exception:
106
+ valid = False
107
+ ok &= record("C5", "signature", valid,
108
+ "signature invalid: certificate body was modified")
109
+
110
+ if ok and cert.get("expires_at"):
111
+ now = now_utc or datetime.datetime.now(datetime.timezone.utc)\
112
+ .strftime("%Y-%m-%dT%H:%M:%SZ")
113
+ expired = now > cert["expires_at"]
114
+ ok &= record("C6", "expiry", not expired,
115
+ f"expired at {cert['expires_at']}" if expired
116
+ else f"valid until {cert['expires_at']}")
117
+
118
+ if ok and package_dir is not None:
119
+ with open(os.path.join(package_dir, "manifest.json"),
120
+ "r", encoding="utf-8") as f:
121
+ root = json.load(f).get("evidence_root")
122
+ ok &= record("C7", "package binding", root == cert.get("evidence_root"),
123
+ f"cert root {cert.get('evidence_root')!r} vs package root {root!r}")
124
+
125
+ return {
126
+ "result": "PASS" if ok else "FAIL",
127
+ "certificate_id": cert.get("certificate_id"),
128
+ "subject_agent": cert.get("subject_agent"),
129
+ "checks": checks,
130
+ "scope": "Certificate signature and (optionally) expiry and package "
131
+ "binding. NOT checked: certificate revocation (v0.2 target), "
132
+ "independent timestamp anchoring, behavioral quality.",
133
+ "statement_bound": cert.get("statement"),
134
+ }
uibc_core/cli.py ADDED
@@ -0,0 +1,461 @@
1
+ """uibc CLI (archive SS23): init / register / event / evidence / submit / verify / inspect."""
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import shutil
7
+ import sys
8
+ import uuid
9
+ from datetime import datetime, timezone
10
+
11
+ from .canonical import hash_file
12
+ from .signing import (ALGORITHM, ALGORITHM_ED25519, ed_generate_seed,
13
+ ed_key_id, ed_public_key, ed_seal_sign, generate_key,
14
+ key_id, seal_sign)
15
+ from .verify import verify
16
+ from . import SPEC_VERSION, __version__
17
+
18
+ REQUIRED = ["manifest.json", "identity.json", "lifecycle.json", "evidence/index.json"]
19
+
20
+
21
+ def _now() -> str:
22
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
23
+
24
+
25
+ def _write(path, obj):
26
+ with open(path, "w", encoding="utf-8", newline="\n") as f:
27
+ json.dump(obj, f, ensure_ascii=False, indent=2)
28
+
29
+
30
+ def _read(path):
31
+ with open(path, "r", encoding="utf-8") as f:
32
+ return json.load(f)
33
+
34
+
35
+ def cmd_init(args):
36
+ pkg = os.path.abspath(args.path)
37
+ if os.path.exists(pkg):
38
+ sys.exit(f"error: {pkg} already exists")
39
+ os.makedirs(os.path.join(pkg, "evidence", "files"), exist_ok=True)
40
+ for f in REQUIRED:
41
+ _write(os.path.join(pkg, f), {"_note": f"TODO: run 'uibc register' / add evidence", "schema": SPEC_VERSION})
42
+ _write(os.path.join(pkg, "evidence", "index.json"), {"entries": []})
43
+ print(f"initialized package skeleton at {pkg}")
44
+ print(f"next: uibc register {pkg} --agent-id <id> --owner <owner>")
45
+
46
+
47
+ def cmd_register(args):
48
+ pkg = os.path.abspath(args.path)
49
+ agent_id = args.agent_id
50
+ now = _now()
51
+ reg_event = {
52
+ "event_id": str(uuid.uuid4()),
53
+ "event_type": "REGISTER",
54
+ "agent_id": agent_id,
55
+ "timestamp": now,
56
+ "actor": args.owner,
57
+ "payload_hash": None,
58
+ "previous_event": None,
59
+ "signature": None, # signing not ratified in v0.1
60
+ }
61
+ _write(os.path.join(pkg, "identity.json"), {
62
+ "schema": SPEC_VERSION,
63
+ "agent_id": agent_id,
64
+ "agent_type": args.agent_type,
65
+ "owner": args.owner,
66
+ "created_at": now,
67
+ "status": "REGISTERED",
68
+ "version": args.version,
69
+ })
70
+ _write(os.path.join(pkg, "lifecycle.json"), {"schema": SPEC_VERSION, "events": [reg_event]})
71
+ print(f"registered agent '{agent_id}' (owner: {args.owner}) at {now}")
72
+
73
+
74
+ def cmd_event(args):
75
+ pkg = os.path.abspath(args.path)
76
+ lc = _read(os.path.join(pkg, "lifecycle.json"))
77
+ events = lc["events"]
78
+ events.append({
79
+ "event_id": str(uuid.uuid4()),
80
+ "event_type": args.type,
81
+ "agent_id": events[0]["agent_id"],
82
+ "timestamp": _now(),
83
+ "actor": args.actor,
84
+ "payload_hash": None,
85
+ "previous_event": events[-1]["event_id"],
86
+ "signature": None,
87
+ })
88
+ _write(os.path.join(pkg, "lifecycle.json"), lc)
89
+ print(f"appended event {args.type} ({events[-1]['event_id']})")
90
+
91
+
92
+ def cmd_evidence(args):
93
+ pkg = os.path.abspath(args.path)
94
+ src = os.path.abspath(args.file)
95
+ if not os.path.isfile(src):
96
+ sys.exit(f"error: file not found: {src}")
97
+ idx = _read(os.path.join(pkg, "evidence", "index.json"))
98
+ fname = os.path.basename(src)
99
+ rel = os.path.join("files", fname)
100
+ dst = os.path.join(pkg, "evidence", rel)
101
+ if os.path.exists(dst):
102
+ fname = f"{uuid.uuid4().hex[:8]}_{fname}"
103
+ rel = os.path.join("files", fname)
104
+ dst = os.path.join(pkg, "evidence", rel)
105
+ shutil.copy2(src, dst)
106
+ idx["entries"].append({
107
+ "evidence_id": str(uuid.uuid4()),
108
+ "type": args.type,
109
+ "path": rel.replace("\\", "/"),
110
+ "content_hash": hash_file(dst),
111
+ "created_at": _now(),
112
+ "media_type": args.media_type,
113
+ "note": args.note or "",
114
+ })
115
+ _write(os.path.join(pkg, "evidence", "index.json"), idx)
116
+ print(f"evidence added: {args.type} {rel} (hash recorded)")
117
+
118
+
119
+ def _load_key(path):
120
+ """Load a seal key from a hex text file (owner secret, never in package)."""
121
+ if not os.path.isfile(path):
122
+ sys.exit(f"error: key file not found: {path}")
123
+ with open(path, "r", encoding="ascii") as f:
124
+ raw = f.read().strip()
125
+ try:
126
+ return bytes.fromhex(raw)
127
+ except ValueError:
128
+ sys.exit(f"error: key file is not 64-char hex: {path}")
129
+
130
+
131
+ def _load_seal_key(path):
132
+ """Load a seal key file: ("hmac", key) or ("ed25519", seed).
133
+
134
+ HMAC files are the v0.2 bare 64-hex format; Ed25519 files start with
135
+ "ed25519:" followed by the 64-hex seed (uibc keygen --ed25519).
136
+ """
137
+ if not os.path.isfile(path):
138
+ sys.exit(f"error: key file not found: {path}")
139
+ with open(path, "r", encoding="ascii") as f:
140
+ raw = f.read().strip()
141
+ if raw.startswith("ed25519:"):
142
+ try:
143
+ seed = bytes.fromhex(raw[len("ed25519:"):].strip())
144
+ except ValueError:
145
+ sys.exit(f"error: Ed25519 key file is not valid hex: {path}")
146
+ if len(seed) != 32:
147
+ sys.exit(f"error: Ed25519 seed must be 32 bytes: {path}")
148
+ return "ed25519", seed
149
+ return "hmac", _load_key(path)
150
+
151
+
152
+ def _load_pubkey(path):
153
+ """Load a pinned Ed25519 public key file (bare 64-hex of the key)."""
154
+ if not os.path.isfile(path):
155
+ sys.exit(f"error: pubkey file not found: {path}")
156
+ with open(path, "r", encoding="ascii") as f:
157
+ raw = f.read().strip()
158
+ try:
159
+ pub = bytes.fromhex(raw)
160
+ except ValueError:
161
+ sys.exit(f"error: pubkey file is not hex: {path}")
162
+ if len(pub) != 32:
163
+ sys.exit(f"error: Ed25519 public key must be 32 bytes: {path}")
164
+ return pub
165
+
166
+
167
+ def cmd_keygen(args):
168
+ out = os.path.abspath(args.out)
169
+ if os.path.exists(out):
170
+ sys.exit(f"error: {out} already exists (refusing to overwrite a key)")
171
+ if getattr(args, "ed25519", False):
172
+ seed = ed_generate_seed()
173
+ with open(out, "w", encoding="ascii") as f:
174
+ f.write("ed25519:" + seed.hex())
175
+ pub = ed_public_key(seed)
176
+ with open(out + ".pub", "w", encoding="ascii") as f:
177
+ f.write(pub.hex())
178
+ print(f"Ed25519 seal key written to {out}")
179
+ print(f"public key written to {out}.pub")
180
+ print(f"key_id (of the public key, safe to share): {ed_key_id(pub)}")
181
+ print("keep the seed file secret - anyone holding it can sign as you; "
182
+ "the .pub file is for verifiers and is safe to publish")
183
+ return
184
+ key = generate_key()
185
+ with open(out, "w", encoding="ascii") as f:
186
+ f.write(key.hex())
187
+ print(f"seal key written to {out}")
188
+ print(f"key_id (public, safe to share): {key_id(key)}")
189
+ print("keep this file secret - anyone holding it can sign packages as you")
190
+
191
+
192
+ def cmd_submit(args):
193
+ pkg = os.path.abspath(args.path)
194
+ identity = _read(os.path.join(pkg, "identity.json"))
195
+ if "_note" in identity and identity.get("agent_id") is None:
196
+ sys.exit("error: package not registered yet. run: "
197
+ f"uibc register {args.path} --agent-id <id> --owner <owner>")
198
+ idx = _read(os.path.join(pkg, "evidence", "index.json"))
199
+ hashes = []
200
+ for e in idx["entries"]:
201
+ hashes.append(hash_file(os.path.join(pkg, "evidence", e["path"])))
202
+ from .canonical import evidence_root
203
+ root = evidence_root(hashes)
204
+ manifest = {
205
+ "schema": SPEC_VERSION,
206
+ "agent_id": identity["agent_id"],
207
+ "agent_version": identity.get("version"),
208
+ "created_at": _now(),
209
+ "evidence_count": len(idx["entries"]),
210
+ "evidence_root": root,
211
+ "status": "SUBMITTED",
212
+ }
213
+ _write(os.path.join(pkg, "manifest.json"), manifest)
214
+ msg = f"submission sealed: agent={identity['agent_id']} evidence={len(idx['entries'])} root={root}"
215
+ if getattr(args, "key", None):
216
+ kind, secret = _load_seal_key(args.key)
217
+ sig_dir = os.path.join(pkg, "signatures")
218
+ os.makedirs(sig_dir, exist_ok=True)
219
+ if kind == "ed25519":
220
+ sig, pub_b64 = ed_seal_sign(secret, identity, manifest)
221
+ pub = ed_public_key(secret)
222
+ _write(os.path.join(sig_dir, "seal.json"), {
223
+ "schema": SPEC_VERSION,
224
+ "algorithm": ALGORITHM_ED25519,
225
+ "key_id": ed_key_id(pub),
226
+ "public_key": pub_b64,
227
+ "signed": "identity+manifest",
228
+ "signature": sig,
229
+ "created_at": _now(),
230
+ })
231
+ msg += " | Ed25519 seal signed (key_id %s...; third-party verifiable)" % ed_key_id(pub)[:16]
232
+ else:
233
+ _write(os.path.join(sig_dir, "seal.json"), {
234
+ "schema": SPEC_VERSION,
235
+ "algorithm": ALGORITHM,
236
+ "key_id": key_id(secret),
237
+ "signed": "identity+manifest",
238
+ "signature": seal_sign(secret, identity, manifest),
239
+ "created_at": _now(),
240
+ })
241
+ msg += " | seal signed (key_id %s...)" % key_id(secret)[:16]
242
+ print(msg)
243
+
244
+
245
+ def cmd_demo(args):
246
+ """One-command end-to-end demo (v0.3): build -> public seal -> a stranger
247
+ verifies -> tamper -> catch -> forge-and-reseal -> pinning catches it."""
248
+ import tempfile
249
+ from .verify import verify as _verify
250
+
251
+ base = tempfile.mkdtemp(prefix="uibc-demo-")
252
+ pkg = os.path.join(base, "demo")
253
+ key_file = os.path.join(base, "owner.key")
254
+
255
+ def _step(n, text):
256
+ print(f"[{n}/8] {text}")
257
+
258
+ _step(1, "init package")
259
+ cmd_init(type("A", (), {"path": pkg})())
260
+ _step(2, "register agent (Registry)")
261
+ cmd_register(type("A", (), {"path": pkg, "agent_id": "demo-agent",
262
+ "owner": "alice", "agent_type": "software-agent",
263
+ "version": "0.1.0"})())
264
+ ev = os.path.join(base, "report.txt")
265
+ with open(ev, "w", encoding="utf-8", newline="\n") as f:
266
+ f.write("demo evidence: agent action log\n")
267
+ _step(3, "add evidence (Evidence)")
268
+ cmd_evidence(type("A", (), {"path": pkg, "type": "ACTION", "file": ev,
269
+ "media_type": "text/plain", "note": ""})())
270
+ _step(4, "generate an Ed25519 seal key + seal the package (Gates)")
271
+ cmd_keygen(type("A", (), {"out": key_file, "ed25519": True})())
272
+ cmd_submit(type("A", (), {"path": pkg, "key": key_file})())
273
+ _step(5, "a STRANGER verifies - no key, no permission, public seal only")
274
+ r = _verify(pkg)
275
+ print(f" -> result: {r['result']} (expected PASS; assurance: {r['assurance']})")
276
+ _step(6, "tamper with the evidence behind the owner's back")
277
+ ev_files = os.path.join(pkg, "evidence", "files")
278
+ with open(os.path.join(ev_files, os.listdir(ev_files)[0]), "a",
279
+ encoding="utf-8", newline="\n") as f:
280
+ f.write("forged line added by attacker\n")
281
+ r = _verify(pkg)
282
+ print(f" -> result: {r['result']} (expected FAIL: tampered content)")
283
+ _step(7, "stronger attack: recompute every hash and re-seal with the "
284
+ "attacker's OWN key, then verify unpinned")
285
+ forge = os.path.join(base, "forged")
286
+ shutil.copytree(pkg, forge)
287
+ idx = _read(os.path.join(forge, "evidence", "index.json"))
288
+ for e in idx["entries"]:
289
+ e["content_hash"] = hash_file(os.path.join(forge, "evidence", e["path"]))
290
+ _write(os.path.join(forge, "evidence", "index.json"), idx)
291
+ from .canonical import evidence_root
292
+ man = _read(os.path.join(forge, "manifest.json"))
293
+ man["evidence_root"] = evidence_root([e["content_hash"] for e in idx["entries"]])
294
+ _write(os.path.join(forge, "manifest.json"), man)
295
+ atk_key = os.path.join(base, "attacker.key")
296
+ cmd_keygen(type("A", (), {"out": atk_key, "ed25519": True})())
297
+ cmd_submit(type("A", (), {"path": forge, "key": atk_key})())
298
+ r = _verify(forge)
299
+ print(f" -> result: {r['result']} (the attacker's seal is valid against the "
300
+ "attacker's key - a substituted key verifies against itself)")
301
+ _step(8, "verify the same package PINNED to the owner's public key")
302
+ r = _verify(forge, pubkey=_load_pubkey(key_file + ".pub"))
303
+ print(f" -> result: {r['result']} (expected FAIL: key substitution caught "
304
+ "by pinning - publish your .pub where verifiers can pin it)")
305
+ print("Try it yourself: python -m uibc_core.cli verify "
306
+ + pkg + " (no key needed - the seal is public)")
307
+ print(" python -m uibc_core.cli verify "
308
+ + forge + " --pubkey " + key_file + ".pub")
309
+
310
+
311
+ def cmd_cert_issue(args):
312
+ from .certificate import issue_certificate
313
+ pkg = os.path.abspath(args.path)
314
+ key = _load_key(args.key)
315
+ identity = _read(os.path.join(pkg, "identity.json"))
316
+ manifest = _read(os.path.join(pkg, "manifest.json"))
317
+ expires_at = None
318
+ if args.expires_days:
319
+ import datetime
320
+ expires_at = (datetime.datetime.now(datetime.timezone.utc)
321
+ + datetime.timedelta(days=args.expires_days))\
322
+ .strftime("%Y-%m-%dT%H:%M:%SZ")
323
+ cert = issue_certificate(key, identity, manifest, expires_at=expires_at)
324
+ out = os.path.abspath(args.out) if args.out else pkg + ".cert.json"
325
+ if os.path.exists(out):
326
+ sys.exit(f"error: {out} already exists (refusing to overwrite a certificate)")
327
+ _write(out, cert)
328
+ print(f"certificate issued: {cert['certificate_id']} "
329
+ f"subject={cert['subject_agent']} root={cert['evidence_root'][:16]}...")
330
+ print(f"written to {out}")
331
+
332
+
333
+ def cmd_cert_verify(args):
334
+ from .certificate import verify_certificate
335
+ key = _load_key(args.key)
336
+ cert = _read(os.path.abspath(args.cert))
337
+ pkg = os.path.abspath(args.package) if args.package else None
338
+ report = verify_certificate(key, cert, package_dir=pkg)
339
+ print(json.dumps(report, ensure_ascii=False, indent=2))
340
+ sys.exit(0 if report["result"] == "PASS" else 1)
341
+
342
+
343
+ def cmd_gate(args):
344
+ from .gate import gate
345
+ pkg = os.path.abspath(args.path)
346
+ if not os.path.isdir(pkg):
347
+ sys.exit(f"error: package dir not found: {pkg}")
348
+ key = _load_key(args.key) if args.key else None
349
+ pubkey = _load_pubkey(args.pubkey) if getattr(args, "pubkey", None) else None
350
+ report = gate(pkg, key=key, pubkey=pubkey,
351
+ revocation_path=args.revocation, dispute_path=args.dispute)
352
+ print(json.dumps(report, ensure_ascii=False, indent=2))
353
+ sys.exit(report["exit_code"])
354
+
355
+
356
+ def cmd_verify(args):
357
+ pkg = os.path.abspath(args.path)
358
+ if not os.path.isdir(pkg):
359
+ sys.exit(f"error: package dir not found: {pkg}")
360
+ key = _load_key(args.key) if args.key else None
361
+ pubkey = _load_pubkey(args.pubkey) if getattr(args, "pubkey", None) else None
362
+ report = verify(pkg, key=key, pubkey=pubkey)
363
+ print(json.dumps(report, ensure_ascii=False, indent=2))
364
+ sys.exit(0 if report["result"] == "PASS" else 1)
365
+
366
+
367
+ def cmd_inspect(args):
368
+ pkg = os.path.abspath(args.path)
369
+ manifest = _read(os.path.join(pkg, "manifest.json"))
370
+ identity = _read(os.path.join(pkg, "identity.json"))
371
+ lc = _read(os.path.join(pkg, "lifecycle.json"))
372
+ idx = _read(os.path.join(pkg, "evidence", "index.json"))
373
+ print(f"agent: {identity.get('agent_id')} (owner {identity.get('owner')})")
374
+ print(f"status: {identity.get('status')} / manifest: {manifest.get('status')}")
375
+ print(f"events: {' -> '.join(e['event_type'] for e in lc.get('events', []))}")
376
+ print(f"evidence: {len(idx.get('entries', []))} entries")
377
+ print(f"root: {manifest.get('evidence_root')}")
378
+
379
+
380
+ def main():
381
+ ap = argparse.ArgumentParser(prog="uibc", description="UIBC Core CLI v%s [PROPOSAL]" % __version__)
382
+ sub = ap.add_subparsers(dest="cmd", required=True)
383
+
384
+ p = sub.add_parser("init", help="create submission package skeleton")
385
+ p.add_argument("path"); p.set_defaults(func=cmd_init)
386
+
387
+ p = sub.add_parser("register", help="register agent (writes identity + REGISTER event)")
388
+ p.add_argument("path"); p.add_argument("--agent-id", required=True)
389
+ p.add_argument("--owner", required=True); p.add_argument("--agent-type", default="software-agent")
390
+ p.add_argument("--version", default="0.1.0"); p.set_defaults(func=cmd_register)
391
+
392
+ p = sub.add_parser("event", help="append lifecycle event")
393
+ p.add_argument("path"); p.add_argument("--type", required=True); p.add_argument("--actor", default="owner")
394
+ p.set_defaults(func=cmd_event)
395
+
396
+ p = sub.add_parser("evidence", help="add evidence file (hash recorded)")
397
+ p.add_argument("path"); p.add_argument("--type", required=True); p.add_argument("--file", required=True)
398
+ p.add_argument("--media-type", default="text/plain"); p.add_argument("--note", default="")
399
+ p.set_defaults(func=cmd_evidence)
400
+
401
+ p = sub.add_parser("submit", help="seal package (compute evidence_root into manifest)")
402
+ p.add_argument("path")
403
+ p.add_argument("--key", default=None, metavar="KEYFILE",
404
+ help="key file: write a seal signature (HMAC hex key, or an "
405
+ "ed25519: key file for a third-party-verifiable seal)")
406
+ p.set_defaults(func=cmd_submit)
407
+
408
+ p = sub.add_parser("verify", help="verify package (exit 0 = PASS)")
409
+ p.add_argument("path")
410
+ p.add_argument("--key", default=None, metavar="KEYFILE",
411
+ help="hex owner key: strict mode - unsigned/mismatched seal FAILS (S6)")
412
+ p.add_argument("--pubkey", default=None, metavar="PUBFILE",
413
+ help="pinned Ed25519 public key (hex file): the seal's recorded "
414
+ "public key must match it, else S6 FAILS")
415
+ p.set_defaults(func=cmd_verify)
416
+
417
+ p = sub.add_parser("keygen", help="generate a 256-bit seal key (hex file)")
418
+ p.add_argument("--out", required=True, metavar="KEYFILE")
419
+ p.add_argument("--ed25519", action="store_true",
420
+ help="generate an Ed25519 seal key instead: writes an ed25519: "
421
+ "seed file plus a publishable .pub file (v0.3)")
422
+ p.set_defaults(func=cmd_keygen)
423
+
424
+ p = sub.add_parser("cert-issue", help="issue a certificate for a verified package")
425
+ p.add_argument("path")
426
+ p.add_argument("--key", required=True, metavar="KEYFILE")
427
+ p.add_argument("--out", default=None, metavar="FILE")
428
+ p.add_argument("--expires-days", type=int, default=None, metavar="N")
429
+ p.set_defaults(func=cmd_cert_issue)
430
+
431
+ p = sub.add_parser("cert-verify", help="verify a certificate (exit 0 = PASS)")
432
+ p.add_argument("cert", metavar="CERTFILE")
433
+ p.add_argument("--key", required=True, metavar="KEYFILE")
434
+ p.add_argument("--package", default=None, metavar="PKG",
435
+ help="also check the certificate is bound to this package")
436
+ p.set_defaults(func=cmd_cert_verify)
437
+
438
+ p = sub.add_parser("gate", help="governed decision: ALLOW(0)/DENY(2)/HOLD(3)")
439
+ p.add_argument("path")
440
+ p.add_argument("--key", default=None, metavar="KEYFILE",
441
+ help="hex owner key: strict verify + revocation signature check")
442
+ p.add_argument("--pubkey", default=None, metavar="PUBFILE",
443
+ help="pinned Ed25519 public key (hex file) for the verify step")
444
+ p.add_argument("--revocation", default=None, metavar="FILE",
445
+ help="revocation.json overlay (governance.md section 3)")
446
+ p.add_argument("--dispute", default=None, metavar="FILE",
447
+ help="dispute.json overlay (governance.md section 1)")
448
+ p.set_defaults(func=cmd_gate)
449
+
450
+ p = sub.add_parser("demo", help="one-command end-to-end demo (build/seal/verify/tamper/catch)")
451
+ p.set_defaults(func=cmd_demo)
452
+
453
+ p = sub.add_parser("inspect", help="human-readable package summary")
454
+ p.add_argument("path"); p.set_defaults(func=cmd_inspect)
455
+
456
+ args = ap.parse_args()
457
+ args.func(args)
458
+
459
+
460
+ if __name__ == "__main__":
461
+ main()