#!/usr/bin/env python3 """Command line for Sigil. sigil.py seed one-time enrolment secret sigil.py keygen [pass] write an Ed25519 key (0600, encrypted if given) sigil.py enrol sigil.py challenge nonce to sign sigil.py verify-chain check the audit log for tampering sigil.py sign [pass] signature, base64 sigil.py verify agent session token sigil.py code [pass] one-time code, proving the key sigil.py redeem human_via_agent session token sigil.py whoami describe a session sigil.py events [actor] audit trail Uses SIGIL_DB (default ./sigil.db) and SIGIL_KEY (default ./agent.key). """ from __future__ import annotations import base64 import json import os import sys from sigil import errors, identity, keys, sessions, viewcodes from sigil.store import Store DB = os.environ.get("SIGIL_DB", "./sigil.db") KEY = os.environ.get("SIGIL_KEY", "./agent.key") def out(obj) -> None: print(json.dumps(obj, indent=2, sort_keys=True)) def main(argv) -> int: if len(argv) < 2: print(__doc__) return 2 cmd = argv[1] store = Store(DB) try: if cmd == "seed": agent = argv[2] out({"agent": agent, "secret": identity.seed_bootstrap(store, agent)}) elif cmd == "keygen": path = argv[2] if len(argv) > 2 else KEY private, public = keys.generate() passphrase = argv[3] if len(argv) > 3 else None keys.save_private(private, path, passphrase) out({ "private_key": path, "fingerprint": keys.fingerprint(public), "public_b64": keys.export_public_b64(public), }) elif cmd == "enrol": out({"fingerprint": identity.enrol(store, argv[2], argv[3], argv[4])}) elif cmd == "challenge": out(identity.challenge(store, argv[2])) elif cmd == "sign": private = keys.load_private(argv[2], argv[4] if len(argv) > 4 else None) nonce = argv[3] out({"nonce": nonce, "signature": base64.b64encode( keys.sign(private, nonce.encode("utf-8"))).decode("ascii")}) elif cmd == "verify": out(identity.verify(store, argv[2], argv[3])) elif cmd == "code": # Mints durable authority, so it proves key possession rather than # trusting a name on the command line. if len(argv) < 4: print("usage: sigil.py code [passphrase]") return 2 private = keys.load_private(argv[3], argv[4] if len(argv) > 4 else None) chal = identity.challenge(store, argv[2]) signed = keys.sign(private, chal["nonce"].encode("utf-8")) sig = base64.b64encode(signed).decode("ascii") token = identity.verify(store, chal["nonce"], sig)["token"] out(viewcodes.issue_code(store, token)) elif cmd == "redeem": out(viewcodes.redeem_code(store, argv[2])) elif cmd == "whoami": session = sessions.resolve(store, argv[2]) may_act = True try: sessions.assert_may_act_as_agent(session) except errors.AgentImpersonationDenied: may_act = False session["may_act_as_agent"] = may_act out(session) elif cmd == "verify-chain": result = store.verify_chain() out(result) return 0 if result["ok"] else 1 elif cmd == "events": out(store.events(argv[2] if len(argv) > 2 else None, int(argv[3]) if len(argv) > 3 else 50)) else: print(__doc__) return 2 except errors.SigilError as exc: print(json.dumps({"error": type(exc).__name__, "message": str(exc)})) return 1 except (ValueError, OSError) as exc: # Wrong passphrase, unreadable key file. Report it as a plain result # rather than a traceback: it is an ordinary operator mistake. print(json.dumps({"error": type(exc).__name__, "message": str(exc)[:200]})) return 1 return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv))