k_card/fido2_probe.py

139 lines
3.8 KiB
Python
Executable File

#!/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())