proof-aware-crypto-tooling-.../provider/src/pacta_provider/cli.py

178 lines
7 KiB
Python
Raw Normal View History

2026-07-03 11:03:58 +00:00
from __future__ import annotations
import argparse
import json
from pathlib import Path
from pacta.config import load_config
from pacta.signing import generate_ed25519_keypair
from pacta.yamlio import dump_data
from .discovery import discover_toolchains
from .service import build_attestation
2026-07-03 12:09:34 +00:00
from .transparency_log import TransparencyLog
2026-07-03 11:03:58 +00:00
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
try:
return args.func(args)
except Exception as exc:
print(f"error: {exc}")
return 2
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="pacta-provider", description="PACTA proof-checking attestation provider.")
sub = parser.add_subparsers(dest="command", required=True)
discover = sub.add_parser("discover", help="Find reusable local Lean/Aeneas toolchains.")
discover.add_argument("--root", action="append")
discover.add_argument("--max-depth", type=int, default=6)
discover.set_defaults(func=cmd_discover)
init_key = sub.add_parser("init-key", help="Create an Ed25519 provider signing keypair.")
init_key.add_argument("--key-dir", default="provider/state/local-provider")
init_key.set_defaults(func=cmd_init_key)
check = sub.add_parser("check", help="Run proof checks and emit a signed attestation.")
check.add_argument("--config", required=True)
check.add_argument("--repo-name", required=True)
check.add_argument("--repo", required=True)
check.add_argument("--provider", required=True)
check.add_argument("--private-key", required=True)
check.add_argument("--public-key", required=True)
check.add_argument("--env-script")
check.add_argument("--lean-project-dir")
check.add_argument("--timeout", type=int, default=120)
check.add_argument("--log-dir", default="provider/out/logs")
check.add_argument("--out", required=True)
check.set_defaults(func=cmd_check)
2026-07-03 12:09:34 +00:00
log_init = sub.add_parser("log-init", help="Initialize a local RFC9162-style transparency log.")
log_init.add_argument("--log-dir", default="provider/state/transparency-log")
log_init.add_argument("--provider", required=True)
log_init.add_argument("--public-key", required=True)
log_init.set_defaults(func=cmd_log_init)
log_append = sub.add_parser("log-append", help="Append a signed proof-check attestation and emit an inclusion receipt.")
log_append.add_argument("--log-dir", default="provider/state/transparency-log")
log_append.add_argument("--attestation", required=True)
log_append.add_argument("--private-key", required=True)
log_append.add_argument("--public-key", required=True)
log_append.add_argument("--out", required=True)
log_append.set_defaults(func=cmd_log_append)
Log accountability: STH pinning, consistency enforcement, freshness, monitor A transparency log without split-view defense is just a signature with extra steps: the provider could serve one tree to the agent and another to the world, or roll the log back, and standalone receipt verification would never notice. The primitives (RFC 9162 consistency proofs) were already implemented and correct; this closes the loop on the AGENT side. - src/pacta/sthstore.py: a local STH pin store. Unknown log -> pin (trust-on-first-use, recorded as such). Same tree size -> the root must match the pin byte-for-byte; a mismatch is named EQUIVOCATION and is a hard rejection. Larger tree -> a consistency proof FROM THE PINNED SIZE is required and verified before the pin advances (receipts already embed a from-previous anchor; the anchor's root is itself checked against the pin so a lying anchor cannot bridge a split view). Smaller tree -> LOG ROLLBACK, hard rejection. - Freshness policy: --max-sth-age-seconds rejects stale (or future-dated) tree heads - an old-but-valid STH can hide later entries. - Wired into receipt-verify, claims, and agent (--sth-store, --consistency-proof, --max-sth-age-seconds); evidence records the pin action; any accountability failure fails the receipt closed. - Provider: log-consistency --from-size N (serve proofs for pinning agents whose pin is older than the receipt's embedded anchor) and log-audit (monitor self-check: recompute the tree, verify the stored STH and per-entry leaf hashes). Live drill in this commit's validation: pin-on-first-use -> matched -> grown-with-proof advance -> a real forged same-size split view REJECTED with the equivocation diagnostic -> freshness rejection -> clean self-audit. tests/test_sthstore.py covers pin/match/equivocation, growth-without-proof, lying consistency anchors, rollback, freshness. 45/45 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 08:08:34 +00:00
log_consistency = sub.add_parser("log-consistency", help="Emit a consistency proof from an earlier tree size (for pinning agents).")
log_consistency.add_argument("--log-dir", required=True)
log_consistency.add_argument("--from-size", type=int, required=True)
log_consistency.add_argument("--out", help="Write the proof document here (default: stdout).")
log_consistency.set_defaults(func=cmd_log_consistency)
log_audit = sub.add_parser("log-audit", help="Monitor check: recompute the tree, verify the stored STH and append-only structure.")
log_audit.add_argument("--log-dir", required=True)
log_audit.set_defaults(func=cmd_log_audit)
2026-07-03 12:09:34 +00:00
log_sth = sub.add_parser("log-sth", help="Sign and print the latest transparency-log tree head.")
log_sth.add_argument("--log-dir", default="provider/state/transparency-log")
log_sth.add_argument("--private-key", required=True)
log_sth.add_argument("--public-key", required=True)
log_sth.set_defaults(func=cmd_log_sth)
2026-07-03 11:03:58 +00:00
return parser
def cmd_discover(args: argparse.Namespace) -> int:
candidates = discover_toolchains(args.root, max_depth=args.max_depth)
print(json.dumps([candidate.to_dict() for candidate in candidates], indent=2))
return 0 if candidates else 1
def cmd_init_key(args: argparse.Namespace) -> int:
key_dir = Path(args.key_dir)
private_key = key_dir / "provider.ed25519.key"
public_key = key_dir / "provider.ed25519.pub"
if private_key.exists() or public_key.exists():
raise ValueError(f"Refusing to overwrite existing key files in {key_dir}")
generate_ed25519_keypair(private_key, public_key)
print(f"private_key: {private_key}")
print(f"public_key: {public_key}")
return 0
def cmd_check(args: argparse.Namespace) -> int:
config = load_config(args.config)
repo = config.repo_named(args.repo_name)
attestation = build_attestation(
repo,
args.repo,
provider=args.provider,
private_key=args.private_key,
public_key=args.public_key,
env_script=args.env_script,
lean_project_dir=args.lean_project_dir,
timeout=args.timeout,
log_dir=args.log_dir,
)
dump_data(attestation, args.out)
print(f"attestation: {args.out}")
return 0
2026-07-03 12:09:34 +00:00
def cmd_log_init(args: argparse.Namespace) -> int:
metadata = TransparencyLog(args.log_dir).init(args.provider, args.public_key)
print(json.dumps(metadata, indent=2, sort_keys=True))
return 0
def cmd_log_append(args: argparse.Namespace) -> int:
receipt = TransparencyLog(args.log_dir).append_attestation(
args.attestation,
private_key_path=args.private_key,
public_key_path=args.public_key,
receipt_out=args.out,
)
print(f"receipt: {args.out}")
print(f"log_id: {receipt['log_id']}")
print(f"tree_size: {receipt['tree_size']}")
print(f"leaf_hash: {receipt['leaf_hash']}")
print(f"ed25519_sth_signature: {receipt['sth']['signatures']['ed25519']['status']}")
print(f"ml_dsa_sth_signature: {receipt['sth']['signatures']['ml_dsa']['status']}")
return 0
Log accountability: STH pinning, consistency enforcement, freshness, monitor A transparency log without split-view defense is just a signature with extra steps: the provider could serve one tree to the agent and another to the world, or roll the log back, and standalone receipt verification would never notice. The primitives (RFC 9162 consistency proofs) were already implemented and correct; this closes the loop on the AGENT side. - src/pacta/sthstore.py: a local STH pin store. Unknown log -> pin (trust-on-first-use, recorded as such). Same tree size -> the root must match the pin byte-for-byte; a mismatch is named EQUIVOCATION and is a hard rejection. Larger tree -> a consistency proof FROM THE PINNED SIZE is required and verified before the pin advances (receipts already embed a from-previous anchor; the anchor's root is itself checked against the pin so a lying anchor cannot bridge a split view). Smaller tree -> LOG ROLLBACK, hard rejection. - Freshness policy: --max-sth-age-seconds rejects stale (or future-dated) tree heads - an old-but-valid STH can hide later entries. - Wired into receipt-verify, claims, and agent (--sth-store, --consistency-proof, --max-sth-age-seconds); evidence records the pin action; any accountability failure fails the receipt closed. - Provider: log-consistency --from-size N (serve proofs for pinning agents whose pin is older than the receipt's embedded anchor) and log-audit (monitor self-check: recompute the tree, verify the stored STH and per-entry leaf hashes). Live drill in this commit's validation: pin-on-first-use -> matched -> grown-with-proof advance -> a real forged same-size split view REJECTED with the equivocation diagnostic -> freshness rejection -> clean self-audit. tests/test_sthstore.py covers pin/match/equivocation, growth-without-proof, lying consistency anchors, rollback, freshness. 45/45 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 08:08:34 +00:00
def cmd_log_consistency(args) -> int:
from pacta.yamlio import dump_data
log = TransparencyLog(args.log_dir)
document = log.consistency_from(args.from_size)
if args.out:
dump_data(document, args.out)
print(f"consistency proof: {args.out}")
else:
for key in ("log_id", "from_tree_size", "from_root_hash", "to_tree_size", "to_root_hash"):
print(f"{key}: {document[key]}")
for item in document["proof"]:
print(f" {item}")
return 0
def cmd_log_audit(args) -> int:
log = TransparencyLog(args.log_dir)
report = log.audit()
print(f"tree_size: {report['tree_size']}")
print(f"computed_root: {report['computed_root']}")
print(f"stored_sth_root: {report['stored_sth_root']}")
if report["problems"]:
print("problems:")
for problem in report["problems"]:
print(f" - {problem}")
print(f"ok: {str(report['ok']).lower()}")
return 0 if report["ok"] else 1
2026-07-03 12:09:34 +00:00
def cmd_log_sth(args: argparse.Namespace) -> int:
sth = TransparencyLog(args.log_dir).latest_sth(args.private_key, args.public_key)
print(json.dumps(sth, indent=2, sort_keys=True))
return 0