mineracks-ckbunker-hsm-sign/hsm_validate.py
mineracks 9d380f5013 Initial import: CKBunker HSM validation harness
WebSocket client + CLI harness + pytest suite that exercises each axis of
a CKBunker + Coldcard Mk4 policy and asserts the expected outcomes, including
the critical negative test that a large PSBT without TOTP is rejected with
a specific 'rule #1: need user(s) confirmation' reason.

Configuration via .env / YAML / CLI flags, two pre-crafted test PSBTs as
fixtures (generation guide in fixtures/README.md), dashboard counter
scraper as sanity check, design rationale in docs/.
2026-04-14 10:50:04 +10:00

95 lines
3.0 KiB
Python

#!/usr/bin/env python3
"""CKBunker HSM production validator — CLI entrypoint.
Runs a short, structured sequence of tests against a live CKBunker + Coldcard
deployment and exits non-zero if anything fails. Safe to run in CI or as a
periodic monitor; all signing uses pre-crafted test PSBTs that you supply.
Usage:
./hsm_validate.py # env/.env only
./hsm_validate.py --config config.yaml
./hsm_validate.py --url http://10.x.y.z:9823 --tests connectivity message_signing
Exits:
0 all enabled tests passed (or were skipped)
1 at least one test failed
2 configuration error
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from ckbunker_hsm_sign import Harness, load_config
from ckbunker_hsm_sign.harness import Verdict
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(
description="Validate a CKBunker + Coldcard HSM deployment",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__.split("Usage:")[1],
)
p.add_argument("--config", type=Path, default=None,
help="YAML configuration file (see config.example.yaml)")
p.add_argument("--env", type=Path, default=Path(".env"),
help="dotenv file to read (default: .env)")
p.add_argument("--url", default=None,
help="override CKBunker URL")
p.add_argument("--tests", nargs="+", default=None,
help="only run these tests (by name)")
p.add_argument("--skip", nargs="+", default=None,
help="skip these tests (by name)")
p.add_argument("--verbose", "-v", action="store_true",
help="dump every WebSocket frame")
p.add_argument("--save-signed", default=None,
help="write signed PSBTs from sign tests into this directory")
p.add_argument("--list-tests", action="store_true",
help="print test names and exit")
return p.parse_args()
def main() -> int:
args = parse_args()
try:
overrides = {
"url": args.url,
"verbose": args.verbose,
"save_signed_dir": args.save_signed,
}
cfg = load_config(
yaml_path=args.config,
dotenv_path=args.env if args.env.exists() else None,
overrides={k: v for k, v in overrides.items() if v is not None},
)
except SystemExit as e:
print(f"configuration error: {e}", file=sys.stderr)
return 2
if args.list_tests:
for name in cfg.tests:
print(name)
return 0
if args.tests:
for k in cfg.tests:
cfg.tests[k] = k in args.tests
if args.skip:
for k in args.skip:
if k in cfg.tests:
cfg.tests[k] = False
harness = Harness(cfg)
outcomes = harness.run_all()
if any(o.verdict == Verdict.FAIL for o in outcomes):
return 1
return 0
if __name__ == "__main__":
sys.exit(main())