#!/usr/bin/env python3 """ Minimal host tool for checking CTAP2/FIDO2 connectivity to a ChromeCard. Requires: pip install fido2 """ from __future__ import annotations import argparse import json import sys from typing import Any try: from fido2.hid import CtapHidDevice from fido2.ctap2 import Ctap2 except Exception as exc: print("Missing dependency: python-fido2", file=sys.stderr) print("Install with: python3 -m pip install fido2", file=sys.stderr) print(f"Import error: {exc}", file=sys.stderr) sys.exit(2) def _json_default(value: Any) -> Any: if isinstance(value, bytes): return value.hex() if isinstance(value, set): return sorted(value) return str(value) def list_devices() -> list[CtapHidDevice]: return list(CtapHidDevice.list_devices()) def describe_device(dev: CtapHidDevice) -> dict[str, Any]: desc = getattr(dev, "descriptor", None) out = { "product_name": getattr(desc, "product_name", None), "manufacturer": getattr(desc, "manufacturer_string", None), "vendor_id": getattr(desc, "vid", None), "product_id": getattr(desc, "pid", None), "path": getattr(desc, "path", None), } return out def print_device_table(devs: list[CtapHidDevice]) -> None: if not devs: print("No CTAP HID devices found.") return for idx, dev in enumerate(devs): info = describe_device(dev) print( f"[{idx}] " f"vid:pid={info['vendor_id']}:{info['product_id']} " f"manufacturer={info['manufacturer']} " f"product={info['product_name']} " f"path={info['path']}" ) def get_info(dev: CtapHidDevice) -> dict[str, Any]: ctap2 = Ctap2(dev) info = ctap2.get_info() out = { "versions": getattr(info, "versions", None), "extensions": getattr(info, "extensions", None), "aaguid": getattr(info, "aaguid", None), "options": getattr(info, "options", None), "max_msg_size": getattr(info, "max_msg_size", None), "pin_uv_protocols": getattr(info, "pin_uv_protocols", None), "firmware_version": getattr(info, "firmware_version", None), "transports": getattr(info, "transports", None), "algorithms": getattr(info, "algorithms", None), } return out def main() -> int: parser = argparse.ArgumentParser(description="Probe USB FIDO2/CTAP2 devices") parser.add_argument( "--index", type=int, default=0, help="Device index from --list output (default: 0)", ) parser.add_argument( "--list", action="store_true", help="List devices only", ) parser.add_argument( "--json", action="store_true", help="Print CTAP2 info as JSON", ) args = parser.parse_args() devs = list_devices() if args.list: print_device_table(devs) return 0 if devs else 1 if not devs: print("No CTAP HID devices found.") return 1 if args.index < 0 or args.index >= len(devs): print(f"Invalid --index {args.index}; found {len(devs)} device(s).", file=sys.stderr) print_device_table(devs) return 2 dev = devs[args.index] dev_meta = describe_device(dev) info = get_info(dev) if args.json: payload = {"device": dev_meta, "ctap2_info": info} print(json.dumps(payload, indent=2, default=_json_default)) return 0 print("Selected device:") print( f" vid:pid={dev_meta['vendor_id']}:{dev_meta['product_id']} " f"manufacturer={dev_meta['manufacturer']} " f"product={dev_meta['product_name']}" ) print(f" path={dev_meta['path']}") print("CTAP2 getInfo:") print(json.dumps(info, indent=2, default=_json_default)) return 0 if __name__ == "__main__": raise SystemExit(main())