diff --git a/src/pyhdwallet.py b/src/pyhdwallet.py index 1926c73..3162a54 100644 --- a/src/pyhdwallet.py +++ b/src/pyhdwallet.py @@ -251,6 +251,19 @@ def interactive_mnemonic_word_by_word() -> str: # ----------------------------------------------------------------------------- # Fingerprint # ----------------------------------------------------------------------------- +def normalize_fpr(s: str) -> str: + s = (s or "").strip().replace(" ", "").replace(":", "").replace("-", "") + if s.lower().startswith("0x"): + s = s[2:] + return s.upper() + +def require_fingerprint_match(actual_fpr: str, expected_fpr: str, context: str) -> None: + a = normalize_fpr(actual_fpr) + e = normalize_fpr(expected_fpr) + if not e: + return + if a != e: + raise ValueError(f"{context}: PGP fingerprint mismatch (expected {e}, got {a})") def get_master_fingerprint(seed_bytes: bytes) -> str: @@ -315,12 +328,20 @@ def sha256_hex_bytes(data: bytes) -> str: return hashlib.sha256(data).hexdigest() -def cmd_fetchkey(url: str, out_path: Optional[str], timeout: int, off_screen: bool) -> None: +def cmd_fetchkey( + url: str, + out_path: Optional[str], + timeout: int, + off_screen: bool, + expected_fingerprint: str = "", +) -> None: + require_for_fetchkey() armored, raw = fetch_ascii_armored_text(url, timeout=timeout) s256 = sha256_hex_bytes(raw) fpr = pgp_fingerprint(armored) + require_fingerprint_match(fpr, expected_fingerprint, "fetchkey") temp_file = None if off_screen: @@ -699,7 +720,8 @@ def cmd_gen(args) -> None: if is_pgp: with open(args.pgp_pubkey_file, "r", encoding="utf-8") as f: pub = f.read() - + fpr = pgp_fingerprint(pub) + require_fingerprint_match(fpr, args.expected_fingerprint, "encrypt(gen)") payload = build_payload_gen( mnemonic=mnemonic, passphrase_used=bool(passphrase), @@ -802,7 +824,11 @@ def cmd_recover(args) -> None: if is_pgp: with open(args.pgp_pubkey_file, "r", encoding="utf-8") as f: pub = f.read() + fpr = pgp_fingerprint(pub) + require_fingerprint_match(fpr, args.expected_fingerprint, "encrypt(gen)") + fpr = pgp_fingerprint(pub) + require_fingerprint_match(fpr, args.expected_fingerprint, "encrypt") payload = build_payload_recover( fp=fp, sol_profile=args.sol_profile, @@ -902,6 +928,8 @@ def build_parser() -> argparse.ArgumentParser: action="store_true", help="Enable off-screen mode: no printing of sensitive data to stdout.", ) + p_fetch.add_argument("--expected-fingerprint", default="", help="Refuse if downloaded key fingerprint does not match (40 hex chars).") + def add_common(p: argparse.ArgumentParser) -> None: p.add_argument("--force", action="store_true", help="Allow printing sensitive output even when stdout is not a TTY (dangerous).") @@ -914,6 +942,7 @@ def build_parser() -> argparse.ArgumentParser: p.add_argument("--pgp-pubkey-file", default=None) p.add_argument("--pgp-ignore-usage-flags", action="store_true") + p.add_argument("--expected-fingerprint", default="", help="Refuse if PGP recipient key fingerprint does not match.") p.add_argument( "--sol-profile", @@ -970,7 +999,7 @@ def main() -> None: try: if args.cmd == "fetchkey": - cmd_fetchkey(args.url, args.out, args.timeout, args.off_screen) + cmd_fetchkey(args.url, args.out, args.timeout, args.off_screen, args.expected_fingerprint) return if args.cmd == "gen": cmd_gen(args)