k_card/ctaphid_init_probe.py

75 lines
2.1 KiB
Python

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