diff --git a/sigil.py b/sigil.py index 5a08b5a..5f1c182 100755 --- a/sigil.py +++ b/sigil.py @@ -2,12 +2,13 @@ """Command line for Sigil. sigil.py seed one-time enrolment secret - sigil.py keygen write an Ed25519 private key (0600) + sigil.py keygen [pass] write an Ed25519 key (0600, encrypted if given) sigil.py enrol sigil.py challenge nonce to sign - sigil.py sign signature, base64 + 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 one-time code for a human to view + 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 @@ -48,7 +49,8 @@ def main(argv) -> int: elif cmd == "keygen": path = argv[2] if len(argv) > 2 else KEY private, public = keys.generate() - keys.save_private(private, path) + passphrase = argv[3] if len(argv) > 3 else None + keys.save_private(private, path, passphrase) out({ "private_key": path, "fingerprint": keys.fingerprint(public), @@ -62,7 +64,7 @@ def main(argv) -> int: out(identity.challenge(store, argv[2])) elif cmd == "sign": - private = keys.load_private(argv[2]) + 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")}) @@ -71,7 +73,17 @@ def main(argv) -> int: out(identity.verify(store, argv[2], argv[3])) elif cmd == "code": - out(viewcodes.issue_code(store, argv[2])) + # 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])) @@ -86,6 +98,11 @@ def main(argv) -> int: 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)) @@ -97,6 +114,11 @@ def main(argv) -> int: 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