├── .gitignore ├── LICENSE ├── README.md ├── fido2-hid-bridge.service ├── fido2_hid_bridge ├── bridge.py └── ctap_hid_device.py ├── poetry.lock ├── poetry.toml └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | /.venv 2 | /.idea 3 | /*.iml 4 | __pycache__ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Bryan Jacobs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FIDO2 HID Bridge 2 | 3 | This repository contains sources for a Linux virtual USB-HID 4 | FIDO2 device. 5 | 6 | This device will receive FIDO2 CTAP2.1 commands, and forward them 7 | to an attached PC/SC authenticator. 8 | 9 | This allows using authenticators over PC/SC from applications 10 | that only support USB-HID, such as Firefox; with this program running 11 | you can use NFC authenticators or Smartcards. 12 | 13 | Note that this is a very early-stage application, but it does work with 14 | Chrome and Firefox. 15 | 16 | ## Running It 17 | 18 | You'll need to install dependencies: 19 | 20 | ```shell 21 | poetry install 22 | ``` 23 | 24 | And then launch the application in the created virtualenv. You might need to be root 25 | or otherwise get access to raw HID devices (permissions on `/dev/uhid`): 26 | 27 | ```shell 28 | sudo -E ./.venv/bin/fido2-hid-bridge 29 | ``` 30 | 31 | ## Alternative installation 32 | 33 | You can also install the project via pipx 34 | 35 | ```shell 36 | pipx install git+https://github.com/BryanJacobs/fido2-hid-bridge 37 | ``` 38 | 39 | The argument '--system-site-packages' is advised when you already have installed python dependecies system wide (e.g. pyscard). 40 | 41 | Assuming pipx is configured correctly simply lauch: 42 | 43 | ```shell 44 | sudo -E fido2-hid-bridge 45 | ``` 46 | 47 | ## Implementation Details 48 | 49 | This uses the Linux kernel UHID device facility, and the `python-fido2` library. 50 | It relays USB-HID packets to PC/SC. 51 | 52 | Nothing more to it than that. 53 | -------------------------------------------------------------------------------- /fido2-hid-bridge.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Fido2 to HID bridge 3 | After=auditd.service syslog.target network.target local-fs.target pcscd.service 4 | Requires=pcscd.service 5 | 6 | [Service] 7 | WorkingDirectory=/opt/fido2-hid-bridge 8 | ExecStart=/opt/fido2-hid-bridge/.venv/bin/fido2-hid-bridge 9 | Type=simple 10 | Restart=on-failure 11 | RestartSec=60s 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /fido2_hid_bridge/bridge.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import asyncio 4 | import logging 5 | import argparse 6 | 7 | from fido2_hid_bridge.ctap_hid_device import CTAPHIDDevice 8 | 9 | 10 | async def run_device() -> None: 11 | """Asynchronously run the event loop.""" 12 | device = CTAPHIDDevice() 13 | 14 | await device.start() 15 | 16 | 17 | def main(): 18 | parser = argparse.ArgumentParser(description='Relay USB-HID packets to PC/SC', allow_abbrev=False) 19 | parser.add_argument('--debug', action='store_const', const=logging.DEBUG, default=logging.INFO, 20 | help='Enable debug messages') 21 | args = parser.parse_args() 22 | logging.basicConfig(level=args.debug) 23 | loop = asyncio.get_event_loop() 24 | loop.run_until_complete(run_device()) 25 | loop.run_forever() 26 | 27 | -------------------------------------------------------------------------------- /fido2_hid_bridge/ctap_hid_device.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from enum import IntEnum 4 | from random import randint 5 | from typing import Optional, Callable, Dict, Tuple, List 6 | 7 | from uhid import UHIDDevice, _ReportType, AsyncioBlockingUHID, Bus 8 | import fido2 9 | from fido2.pcsc import CtapDevice, CTAPHID, CtapError, CtapPcscDevice 10 | 11 | SECONDS_TO_WAIT_FOR_AUTHENTICATOR = 10 12 | """How long, in seconds, to poll for a USB authenticator before giving up.""" 13 | VID = 0x9999 14 | """USB vendor ID.""" 15 | PID = 0x9999 16 | """USB product ID.""" 17 | 18 | BROADCAST_CHANNEL = bytes([0xFF, 0xFF, 0xFF, 0xFF]) 19 | """Standard CTAP-HID broadcast channel.""" 20 | 21 | 22 | class CommandType(IntEnum): 23 | """Catalog of CTAP-HID command type bytes.""" 24 | 25 | PING = 0x01 26 | MSG = 0x03 27 | INIT = 0x06 28 | WINK = 0x08 29 | CBOR = 0x10 30 | CANCEL = 0x11 31 | KEEPALIVE = 0x3B 32 | ERROR = 0x3F 33 | 34 | 35 | def _wrap_call_with_device_obj( 36 | device: UHIDDevice, call: Callable[[UHIDDevice, List[int], _ReportType], None] 37 | ) -> Callable: 38 | """Pass a UHIDDevice to a given callback.""" 39 | return lambda x, y: call(device, x, y) 40 | 41 | 42 | class CTAPHIDDevice: 43 | device: UHIDDevice 44 | """Underlying UHID device.""" 45 | chosen_device: Optional[CtapDevice] = None 46 | """Mapping from channel strings to CTAP devices.""" 47 | channels_to_state: Dict[str, Tuple[CommandType, int, int, bytes]] = {} 48 | """ 49 | Mapping from channel strings to receive buffer state. 50 | 51 | Each value consists of: 52 | 1. The command type in use on the channel 53 | 2. The total length of the incoming request 54 | 3. The sequence number of the most recently received packet (-1 for initial) 55 | 4. The accumulated data received on the channel 56 | """ 57 | reference_count = 0 58 | """Number of open handles to the device: clear state when it hits zero.""" 59 | 60 | def __init__(self): 61 | self.device = UHIDDevice( 62 | vid=VID, 63 | pid=PID, 64 | name="FIDO2 Virtual USB Device", 65 | report_descriptor=[ 66 | 0x06, 67 | 0xD0, 68 | 0xF1, # Usage Page (FIDO) 69 | 0x09, 70 | 0x01, # Usage (CTAPHID) 71 | 0xA1, 72 | 0x01, # Collection (Application) 73 | 0x09, 74 | 0x20, # Usage (Data In) 75 | 0x15, 76 | 0x00, # Logical min (0) 77 | 0x26, 78 | 0xFF, 79 | 0x00, # Logical max (255) 80 | 0x75, 81 | 0x08, # Report Size (8) 82 | 0x95, 83 | 0x40, # Report count (64 bytes per packet) 84 | 0x81, 85 | 0x02, # Input(HID_Data | HID_Absolute | HID_Variable) 86 | 0x09, 87 | 0x21, # Usage (Data Out) 88 | 0x15, 89 | 0x00, # Logical min (0) 90 | 0x26, 91 | 0xFF, 92 | 0x00, # Logical max (255) 93 | 0x75, 94 | 0x08, # Report Size (8) 95 | 0x95, 96 | 0x40, # Report count (64 bytes per packet) 97 | 0x91, 98 | 0x02, # Output(HID_Data | HID_Absolute | HID_Variable) 99 | 0xC0, # End Collection 100 | ], 101 | backend=AsyncioBlockingUHID, 102 | version=0, 103 | bus=Bus.USB, 104 | ) 105 | 106 | self.device.receive_output = self.process_hid_message 107 | self.device.receive_close = self.process_close 108 | self.device.receive_open = self.process_open 109 | 110 | def process_open(self): 111 | self.reference_count += 1 112 | 113 | def process_close(self): 114 | self.reference_count -= 1 115 | if self.reference_count == 0: 116 | # Clear all state 117 | self.channels_to_state = {} 118 | self.chosen_device = None 119 | 120 | def process_hid_message(self, buffer: List[int], report_type: _ReportType) -> None: 121 | """Core method: handle incoming HID messages.""" 122 | recvd_bytes = bytes(buffer) 123 | logging.debug(f"GOT MESSAGE (type {report_type}): {recvd_bytes.hex()}") 124 | 125 | if self.is_initial_packet(recvd_bytes): 126 | channel, lc, cmd, data = self.parse_initial_packet(recvd_bytes) 127 | channel_key = self.get_channel_key(channel) 128 | logging.debug( 129 | f"CMD {cmd.name} CHANNEL {channel_key} len {lc} (recvd {len(data)}) data {data.hex()}" 130 | ) 131 | self.channels_to_state[channel_key] = cmd, lc, -1, data 132 | if lc == len(data): 133 | # Complete receive 134 | self.finish_receiving(channel) 135 | else: 136 | channel, seq, new_data = self.parse_subsequent_packet(recvd_bytes) 137 | channel_key = self.get_channel_key(channel) 138 | if channel_key not in self.channels_to_state: 139 | self.send_error(channel, 0x0B) 140 | return 141 | cmd, lc, prev_seq, existing_data = self.channels_to_state[channel_key] 142 | if seq != prev_seq + 1: 143 | self.handle_cancel(channel, b"") 144 | self.send_error(channel, 0x04) 145 | return 146 | remaining = lc - len(existing_data) 147 | data = existing_data + new_data[:remaining] 148 | self.channels_to_state[channel_key] = cmd, lc, seq, data 149 | logging.debug(f"After receive, we have {len(data)} bytes out of {lc}") 150 | if lc == len(data): 151 | self.finish_receiving(channel) 152 | 153 | async def start(self): 154 | await self.device.wait_for_start_asyncio() 155 | 156 | def parse_initial_packet( 157 | self, buffer: bytes 158 | ) -> Tuple[bytes, int, CommandType, bytes]: 159 | """Parse an incoming initial packet.""" 160 | logging.debug(f"Initial packet {buffer.hex()}") 161 | channel = buffer[1:5] 162 | cmd_byte = buffer[5] & 0x7F 163 | lc = (int(buffer[6]) << 8) + buffer[7] 164 | data = buffer[8 : 8 + lc] 165 | cmd = CommandType(cmd_byte) 166 | return channel, lc, cmd, data 167 | 168 | def is_initial_packet(self, buffer: bytes) -> bool: 169 | """Return true if packet is the start of a new sequence.""" 170 | if buffer[5] & 0x80 == 0: 171 | return False 172 | return True 173 | 174 | def assign_channel_id(self) -> List[int]: 175 | """Create a new, random, channel ID.""" 176 | return [randint(0, 255), randint(0, 255), randint(0, 255), randint(0, 255)] 177 | 178 | def handle_init(self, channel: bytes, buffer: bytes) -> Optional[bytes]: 179 | """Initialize or re-initialize a channel.""" 180 | logging.debug(f"INIT on channel {channel}") 181 | 182 | if channel == BROADCAST_CHANNEL: 183 | assert len(buffer) == 8 184 | 185 | new_channel = self.assign_channel_id() 186 | 187 | ctap = self.get_pcsc_device(new_channel) 188 | if ctap is None: 189 | return None 190 | 191 | return bytes( 192 | [x for x in buffer] 193 | + [x for x in new_channel] 194 | + [ 195 | 0x02, # protocol version 196 | 0x01, # device version major 197 | 0x00, # device version minor 198 | 0x00, # device version build/point 199 | ctap.capabilities, # capabilities, from the underlying device 200 | ] 201 | ) 202 | else: 203 | self.handle_cancel(channel, b"") 204 | 205 | def get_pcsc_device(self, channel_id: List[int]) -> Optional[CtapDevice]: 206 | """Grab a PC/SC device from python-fido2.""" 207 | if self.chosen_device is None: 208 | start_time = time.time() 209 | while time.time() < start_time + SECONDS_TO_WAIT_FOR_AUTHENTICATOR: 210 | logging.info("WAITING FOR NEW DEVICE") 211 | devices = list(CtapPcscDevice.list_devices()) 212 | if len(devices) == 0: 213 | time.sleep(0.1) 214 | continue 215 | device = devices[0] 216 | self.chosen_device = device 217 | 218 | fido2.pcsc.logger.setLevel(0) 219 | fido2.pcsc.logger.disabled = False 220 | fido2.pcsc.logger.isEnabledFor = lambda x: True 221 | fido2.pcsc.logger.manager.disable = 0 222 | # fido2.pcsc.logger.addHandler(LogPrintHandler()) 223 | fido2.pcsc.logger._cache = {} 224 | 225 | return device 226 | # TODO: send timeout error properly 227 | raise ValueError("Could not connect to a PC/SC device in time!") 228 | # self.send_error(channel_id, 0x05) 229 | # return None 230 | 231 | return self.chosen_device 232 | 233 | def handle_cbor(self, channel: List[int], buffer: bytes) -> Optional[bytes]: 234 | """Handling an incoming CBOR command.""" 235 | ctap = self.get_pcsc_device(channel) 236 | if ctap is None: 237 | return None 238 | logging.debug(f"Sending CBOR to device {ctap}: {buffer.hex()}") 239 | try: 240 | res = ctap.call(cmd=CommandType.CBOR, data=buffer) 241 | return res 242 | except CtapError as e: 243 | logging.info(f"Got CTAP error response from device: {e}") 244 | return bytes([e.code]) 245 | 246 | def handle_cancel(self, channel: List[int], buffer: bytes) -> Optional[bytes]: 247 | channel_key = self.get_channel_key(channel) 248 | if channel_key in self.channels_to_state: 249 | del self.channels_to_state[channel_key] 250 | return bytes() 251 | 252 | def handle_wink(self, channel: List[int], buffer: bytes) -> Optional[bytes]: 253 | """Do nothing; this can't be done over PC/SC.""" 254 | return bytes() 255 | 256 | def handle_msg(self, channel: List[int], buffer: bytes) -> Optional[bytes]: 257 | """Process a U2F/CTAP1 message.""" 258 | device = self.get_pcsc_device(channel) 259 | if device is None: 260 | return None 261 | res = device.call(CTAPHID.MSG, buffer) 262 | return res 263 | 264 | def handle_ping(self, channel: List[int], buffer: bytes) -> Optional[bytes]: 265 | """Handle an echo request.""" 266 | return buffer 267 | 268 | def handle_keepalive(self, channel: List[int], buffer: bytes) -> Optional[bytes]: 269 | """Placeholder: always returns that the device is processing.""" 270 | return bytes([1]) 271 | 272 | def encode_response_packets( 273 | self, 274 | channel: List[int], 275 | cmd: CommandType, 276 | data: bytes, 277 | packet_size: int = 64, 278 | ) -> List[bytes]: 279 | """Chunk response data to be delivered over USB.""" 280 | offset_start = 0 281 | seq = 0 282 | responses = [] 283 | while offset_start < len(data): 284 | if seq == 0: 285 | capacity = packet_size - 7 286 | chunk = data[offset_start : (offset_start + capacity)] 287 | data_len_upper = len(data) >> 8 288 | data_len_lower = len(data) % 256 289 | response = ( 290 | bytes(channel) 291 | + bytes([cmd | 0x80, data_len_upper, data_len_lower]) 292 | + chunk 293 | ) 294 | else: 295 | capacity = packet_size - 5 296 | chunk = data[offset_start : (offset_start + capacity)] 297 | response = bytes(channel) + bytes([seq - 1]) + chunk 298 | 299 | padding_byte_count = packet_size - len(response) 300 | if padding_byte_count > 0: 301 | response = response + bytes([0x00] * padding_byte_count) 302 | 303 | responses.append(bytes(response)) 304 | offset_start += capacity 305 | seq += 1 306 | 307 | return responses 308 | 309 | def get_channel_key(self, channel: List[int]) -> str: 310 | return bytes(channel).hex() 311 | 312 | def send_error(self, channel: List[int], error_type: int) -> None: 313 | responses = self.encode_response_packets( 314 | channel, CommandType.ERROR, bytes([error_type]) 315 | ) 316 | for response in responses: 317 | self.device.send_input(response) 318 | 319 | def finish_receiving(self, channel: List[int]) -> None: 320 | """When finished receiving packets, act on them.""" 321 | channel_key = self.get_channel_key(channel) 322 | cmd, _, _, data = self.channels_to_state[channel_key] 323 | self.handle_cancel(channel, b"") 324 | 325 | try: 326 | handler = getattr(self, f"handle_{cmd.name.lower()}", None) 327 | if handler is not None: 328 | response_body = handler(channel, data) 329 | if response_body is None: 330 | # Already dealt with 331 | return 332 | responses = self.encode_response_packets(channel, cmd, response_body) 333 | else: 334 | self.send_error(channel, 0x01) 335 | return 336 | except Exception as e: 337 | logging.warning(f"Error: {e}") 338 | self.send_error(channel, 0x7F) 339 | self.chosen_device = None 340 | return 341 | 342 | for response in responses: 343 | self.device.send_input(response) 344 | 345 | def parse_subsequent_packet(self, data: bytes) -> Tuple[bytes, int, bytes]: 346 | """Parse a non-initial packet.""" 347 | return data[1:5], data[5], data[6:] 348 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "cffi" 5 | version = "1.15.1" 6 | description = "Foreign Function Interface for Python calling C code." 7 | optional = false 8 | python-versions = "*" 9 | files = [ 10 | {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, 11 | {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, 12 | {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, 13 | {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, 14 | {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, 15 | {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, 16 | {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, 17 | {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, 18 | {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, 19 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, 20 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, 21 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, 22 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, 23 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, 24 | {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, 25 | {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, 26 | {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, 27 | {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, 28 | {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, 29 | {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, 30 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, 31 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, 32 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, 33 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, 34 | {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, 35 | {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, 36 | {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, 37 | {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, 38 | {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, 39 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, 40 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, 41 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, 42 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, 43 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, 44 | {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, 45 | {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, 46 | {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, 47 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, 48 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, 49 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, 50 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, 51 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, 52 | {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, 53 | {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, 54 | {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, 55 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, 56 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, 57 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, 58 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, 59 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, 60 | {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, 61 | {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, 62 | {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, 63 | {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, 64 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, 65 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, 66 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, 67 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, 68 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, 69 | {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, 70 | {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, 71 | {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, 72 | {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, 73 | {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, 74 | ] 75 | 76 | [package.dependencies] 77 | pycparser = "*" 78 | 79 | [[package]] 80 | name = "cryptography" 81 | version = "41.0.3" 82 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 83 | optional = false 84 | python-versions = ">=3.7" 85 | files = [ 86 | {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507"}, 87 | {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922"}, 88 | {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81"}, 89 | {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd"}, 90 | {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47"}, 91 | {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116"}, 92 | {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c"}, 93 | {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae"}, 94 | {file = "cryptography-41.0.3-cp37-abi3-win32.whl", hash = "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306"}, 95 | {file = "cryptography-41.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574"}, 96 | {file = "cryptography-41.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087"}, 97 | {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858"}, 98 | {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906"}, 99 | {file = "cryptography-41.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e"}, 100 | {file = "cryptography-41.0.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd"}, 101 | {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207"}, 102 | {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84"}, 103 | {file = "cryptography-41.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7"}, 104 | {file = "cryptography-41.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d"}, 105 | {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de"}, 106 | {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1"}, 107 | {file = "cryptography-41.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4"}, 108 | {file = "cryptography-41.0.3.tar.gz", hash = "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34"}, 109 | ] 110 | 111 | [package.dependencies] 112 | cffi = ">=1.12" 113 | 114 | [package.extras] 115 | docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] 116 | docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] 117 | nox = ["nox"] 118 | pep8test = ["black", "check-sdist", "mypy", "ruff"] 119 | sdist = ["build"] 120 | ssh = ["bcrypt (>=3.1.5)"] 121 | test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] 122 | test-randomorder = ["pytest-randomly"] 123 | 124 | [[package]] 125 | name = "fido2" 126 | version = "1.1.2" 127 | description = "FIDO2/WebAuthn library for implementing clients and servers." 128 | optional = false 129 | python-versions = ">=3.7,<4.0" 130 | files = [ 131 | {file = "fido2-1.1.2-py3-none-any.whl", hash = "sha256:a3b7d7d233dec3a4fa0d6178fc34d1cce17b820005a824f6ab96917a1e3be8d8"}, 132 | {file = "fido2-1.1.2.tar.gz", hash = "sha256:6110d913106f76199201b32d262b2857562cc46ba1d0b9c51fbce30dc936c573"}, 133 | ] 134 | 135 | [package.dependencies] 136 | cryptography = ">=2.6,<35 || >35,<44" 137 | pyscard = {version = ">=1.9,<3", optional = true, markers = "extra == \"pcsc\""} 138 | 139 | [package.extras] 140 | pcsc = ["pyscard (>=1.9,<3)"] 141 | 142 | [[package]] 143 | name = "pycparser" 144 | version = "2.21" 145 | description = "C parser in Python" 146 | optional = false 147 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 148 | files = [ 149 | {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, 150 | {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, 151 | ] 152 | 153 | [[package]] 154 | name = "pyscard" 155 | version = "2.0.7" 156 | description = "Smartcard module for Python." 157 | optional = false 158 | python-versions = "*" 159 | files = [ 160 | {file = "pyscard-2.0.7-cp310-cp310-win32.whl", hash = "sha256:06666a597e1293421fa90e0d4fc2418add447b10b7dc85f49b3cafc23480f046"}, 161 | {file = "pyscard-2.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:a2266345bd387854298153264bff8b74f494581880a76e3e8679460c1b090fab"}, 162 | {file = "pyscard-2.0.7-cp311-cp311-win32.whl", hash = "sha256:beacdcdc3d1516e195f7a38ec3966c5d4df7390c8f036cb41f6fef72bc5cc646"}, 163 | {file = "pyscard-2.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e37b697327e8dc4848c481428d1cbd10b7ae2ce037bc799e5b8bbd2fc3ab5ed"}, 164 | {file = "pyscard-2.0.7-cp37-cp37m-win32.whl", hash = "sha256:a0c5edbedafba62c68160884f878d9f53996d7219a3fc11b1cea6bab59c7f34a"}, 165 | {file = "pyscard-2.0.7-cp37-cp37m-win_amd64.whl", hash = "sha256:f704ad40dc40306e1c0981941789518ab16aa1f84443b1d52ec0264884092b3b"}, 166 | {file = "pyscard-2.0.7-cp38-cp38-win32.whl", hash = "sha256:59a466ab7ae20188dd197664b9ca1ea9524d115a5aa5b16b575a6b772cdcb73c"}, 167 | {file = "pyscard-2.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:da70aa5b7be5868b88cdb6d4a419d2791b6165beeb90cd01d2748033302a0f43"}, 168 | {file = "pyscard-2.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2d4bdc1f4e0e6c46e417ac1bc9d5990f7cfb24a080e890d453781405f7bd29dc"}, 169 | {file = "pyscard-2.0.7-cp39-cp39-win32.whl", hash = "sha256:39e030c47878b37ae08038a917959357be6468da52e8b144e84ffc659f50e6e2"}, 170 | {file = "pyscard-2.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:5a5865675be294c8d91f22dc91e7d897c4138881e5295fb6b2cd821f7c0389d9"}, 171 | {file = "pyscard-2.0.7.tar.gz", hash = "sha256:278054525fa75fbe8b10460d87edcd03a70ad94d688b11345e4739987f85c1bf"}, 172 | ] 173 | 174 | [package.extras] 175 | gui = ["wxPython"] 176 | pyro = ["Pyro"] 177 | 178 | [[package]] 179 | name = "uhid" 180 | version = "0.0.1" 181 | description = "" 182 | optional = false 183 | python-versions = ">=3.7" 184 | files = [ 185 | {file = "uhid-0.0.1-py3-none-any.whl", hash = "sha256:c4cf5cbe6ab5a57ae4a21e2f79558a7a7af80c518903819fb00b370abc5c6d06"}, 186 | {file = "uhid-0.0.1.tar.gz", hash = "sha256:3c782489890dbf33621fb2c30d1ac81fbc1b3ef1911ae7d4c73907cdc0f59aab"}, 187 | ] 188 | 189 | [package.extras] 190 | docs = ["furo (>=2020.11.19b18)", "sphinx (>=3.0,<4.0)", "sphinx-autodoc-typehints (>=1.10)", "sphinxcontrib-trio (>=1.1.0)"] 191 | test = ["ioctl (>=0.2.1)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-timeout", "pytest-trio"] 192 | trio = ["trio"] 193 | 194 | [metadata] 195 | lock-version = "2.0" 196 | python-versions = "^3.11" 197 | content-hash = "e007fc2fa5d1804c221c7da94cc04ec0afdb4ceb891cd6d33cf1b87be15420d4" 198 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fido2-hid-bridge" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Bryan Jacobs "] 6 | readme = "README.md" 7 | packages = [{include = "fido2_hid_bridge"}] 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.11" 11 | uhid = "^0.0.1" 12 | fido2 = {extras = ["pcsc"], version = "^1.1.2"} 13 | 14 | [tool.poetry.scripts] 15 | fido2-hid-bridge = 'fido2_hid_bridge.bridge:main' 16 | 17 | [build-system] 18 | requires = ["poetry-core"] 19 | build-backend = "poetry.core.masonry.api" 20 | --------------------------------------------------------------------------------