#!/usr/bin/env python3 """ Manual CTAPHID INIT probe for a specific hidraw node. This bypasses python-fido2's device bootstrap so we can see whether the raw HID transport itself exchanges packets on the expected FIDO interface. """ from __future__ import annotations import argparse import os import secrets import select import struct import sys from pathlib import Path CTAPHID_INIT = 0x06 TYPE_INIT = 0x80 BROADCAST_CID = 0xFFFFFFFF def build_init_packet(nonce: bytes) -> bytes: frame = struct.pack(">IBH", BROADCAST_CID, TYPE_INIT | CTAPHID_INIT, len(nonce)) + nonce return b"\0" + frame.ljust(64, b"\0") def main() -> int: parser = argparse.ArgumentParser(description="Manual CTAPHID INIT probe") parser.add_argument("--device-path", default="/dev/hidraw0") parser.add_argument("--timeout", type=float, default=3.0) args = parser.parse_args() path = Path(args.device_path) if not path.exists(): print(f"missing device: {path}", file=sys.stderr) return 2 nonce = secrets.token_bytes(8) packet = build_init_packet(nonce) print(f"device={path}") print(f"nonce={nonce.hex()}") print(f"write_len={len(packet)}") print(f"write_hex={packet.hex()}") fd = os.open(str(path), os.O_RDWR) try: written = os.write(fd, packet) print(f"written={written}") poller = select.poll() poller.register(fd, select.POLLIN) events = poller.poll(int(args.timeout * 1000)) print(f"events={events}") if not events: print("timeout_waiting_for_response") return 1 response = os.read(fd, 64) print(f"read_len={len(response)}") print(f"read_hex={response.hex()}") if len(response) >= 24: cid, cmd, bc = struct.unpack(">IBH", response[:7]) print(f"resp_cid=0x{cid:08x}") print(f"resp_cmd=0x{cmd:02x}") print(f"resp_bc={bc}") print(f"resp_payload={response[7:7+bc].hex()}") return 0 finally: os.close(fd) if __name__ == "__main__": raise SystemExit(main())