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/.
95 lines
3.0 KiB
Python
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())
|