From 93bba4fb767fdcfd18c19499b32159d29c098ac0 Mon Sep 17 00:00:00 2001 From: Pol Henarejos Date: Tue, 18 Nov 2025 01:04:36 +0100 Subject: [PATCH] Moved to pypicofido. Signed-off-by: Pol Henarejos --- tools/pico-fido-tool.py | 660 ---------------------------------------- 1 file changed, 660 deletions(-) delete mode 100644 tools/pico-fido-tool.py diff --git a/tools/pico-fido-tool.py b/tools/pico-fido-tool.py deleted file mode 100644 index 07c252f..0000000 --- a/tools/pico-fido-tool.py +++ /dev/null @@ -1,660 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -/* - * This file is part of the Pico Fido distribution (https://github.com/polhenarejos/pico-fido). - * Copyright (c) 2022 Pol Henarejos. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -""" - -import sys -import argparse -import platform -from binascii import hexlify -from threading import Event -from typing import List, Mapping, Any, Optional, Callable -import struct -import urllib.request -import json -from enum import IntEnum, unique - -try: - from fido2.ctap2.config import Config - from fido2.ctap2 import Ctap2, ClientPin, PinProtocolV2 - from fido2.hid import CtapHidDevice, CTAPHID - from fido2.utils import bytes2int, int2bytes - from fido2 import cbor - from fido2.ctap import CtapDevice, CtapError - from fido2.ctap2.pin import PinProtocol, _PinUv - from fido2.ctap2.base import args -except: - print('ERROR: fido2 module not found! Install fido2 package.\nTry with `pip install fido2`') - sys.exit(-1) - -try: - from cryptography.hazmat.primitives.asymmetric import ec - from cryptography.hazmat.primitives.kdf.hkdf import HKDF - from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat - from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 - from cryptography.hazmat.primitives import hashes - from cryptography import x509 -except: - print('ERROR: cryptography module not found! Install cryptography package.\nTry with `pip install cryptography`') - sys.exit(-1) - -from enum import IntEnum -from binascii import hexlify - -def get_pki_data(url, data=None, method='GET'): - user_agent = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; ' - 'rv:1.9.0.7) Gecko/2009021910 Firefox/3.0.7' - method = 'GET' - if (data is not None): - method = 'POST' - req = urllib.request.Request(f"https://www.picokeys.com/pico/pico-fido/{url}/", - method=method, - data=data, - headers={'User-Agent': user_agent, }) - response = urllib.request.urlopen(req) - resp = response.read().decode('utf-8') - j = json.loads(resp) - return j - -class VendorConfig(Config): - class PARAM(IntEnum): - VENDOR_COMMAND_ID = 0x01 - VENDOR_PARAM_BYTESTRING = 0x02 - VENDOR_PARAM_INT = 0x03 - VENDOR_PARAM_TEXTSTRING = 0x04 - - class CMD(IntEnum): - CONFIG_AUT_ENABLE = 0x03e43f56b34285e2 - CONFIG_AUT_DISABLE = 0x1831a40f04a25ed9 - CONFIG_EA_UPLOAD = 0x66f2a674c29a8dcf - CONFIG_PHY_VIDPID = 0x6fcb19b0cbe3acfa - CONFIG_PHY_LED_BTNESS = 0x76a85945985d02fd - CONFIG_PHY_LED_GPIO = 0x7b392a394de9f948 - CONFIG_PHY_OPTS = 0x269f3b09eceb805f - CONFIG_PIN_POLICY = 0x6c07d70fe96c3897 - - class RESP(IntEnum): - KEY_AGREEMENT = 0x01 - - def __init__(self, ctap, pin_uv_protocol=None, pin_uv_token=None): - super().__init__(ctap, pin_uv_protocol, pin_uv_token) - - def enable_device_aut(self, ct): - self._call( - Config.CMD.VENDOR_PROTOTYPE, - { - VendorConfig.PARAM.VENDOR_COMMAND_ID: VendorConfig.CMD.CONFIG_AUT_ENABLE, - VendorConfig.PARAM.VENDOR_PARAM_BYTESTRING: ct - }, - ) - - def disable_device_aut(self): - self._call( - Config.CMD.VENDOR_PROTOTYPE, - { - VendorConfig.PARAM.VENDOR_COMMAND_ID: VendorConfig.CMD.CONFIG_AUT_DISABLE - }, - ) - - def vidpid(self, vid, pid): - self._call( - Config.CMD.VENDOR_PROTOTYPE, - { - VendorConfig.PARAM.VENDOR_COMMAND_ID: VendorConfig.CMD.CONFIG_PHY_VIDPID, - VendorConfig.PARAM.VENDOR_PARAM_INT: (vid & 0xFFFF) << 16 | pid - }, - ) - - def led_gpio(self, gpio): - self._call( - Config.CMD.VENDOR_PROTOTYPE, - { - VendorConfig.PARAM.VENDOR_COMMAND_ID: VendorConfig.CMD.CONFIG_PHY_LED_GPIO, - VendorConfig.PARAM.VENDOR_PARAM_INT: gpio - }, - ) - - def led_brightness(self, brightness): - self._call( - Config.CMD.VENDOR_PROTOTYPE, - { - VendorConfig.PARAM.VENDOR_COMMAND_ID: VendorConfig.CMD.CONFIG_PHY_LED_BTNESS, - VendorConfig.PARAM.VENDOR_PARAM_INT: brightness - }, - ) - - def phy_opts(self, opts): - self._call( - Config.CMD.VENDOR_PROTOTYPE, - { - VendorConfig.PARAM.VENDOR_COMMAND_ID: VendorConfig.CMD.CONFIG_PHY_OPTS, - VendorConfig.PARAM.VENDOR_PARAM_INT: opts - }, - ) - - def upload_ea(self, der): - self._call( - Config.CMD.VENDOR_PROTOTYPE, - { - VendorConfig.PARAM.VENDOR_COMMAND_ID: VendorConfig.CMD.CONFIG_EA_UPLOAD, - VendorConfig.PARAM.VENDOR_PARAM_BYTESTRING: der - }, - ) - - def pin_policy(self, url: bytes|str = None, policy: int = None): - if (url is not None or policy is not None): - params = { VendorConfig.PARAM.VENDOR_COMMAND_ID: VendorConfig.CMD.CONFIG_PIN_POLICY } - if (url is not None): - if (isinstance(url, str)): - url = url.encode() - params[VendorConfig.PARAM.VENDOR_PARAM_BYTESTRING] = url - if (policy is not None): - params[VendorConfig.PARAM.VENDOR_PARAM_INT] = policy - self._call( - Config.CMD.VENDOR_PROTOTYPE, - params, - ) - -class Ctap2Vendor(Ctap2): - def __init__(self, device: CtapDevice, strict_cbor: bool = True): - super().__init__(device=device, strict_cbor=strict_cbor) - - - def send_vendor( - self, - cmd: int, - data: Optional[Mapping[int, Any]] = None, - *, - event: Optional[Event] = None, - on_keepalive: Optional[Callable[[int], None]] = None, - ) -> Mapping[int, Any]: - """Sends a VENDOR message to the device, and waits for a response. - - :param cmd: The command byte of the request. - :param data: The payload to send (to be CBOR encoded). - :param event: Optional threading.Event used to cancel the request. - :param on_keepalive: Optional function called when keep-alive is sent by - the authenticator. - """ - request = struct.pack(">B", cmd) - if data is not None: - request += cbor.encode(data) - response = self.device.call(CTAPHID.VENDOR_FIRST + 1, request, event, on_keepalive) - status = response[0] - if status != 0x00: - raise CtapError(status) - enc = response[1:] - if not enc: - return {} - decoded = cbor.decode(enc) - if self._strict_cbor: - expected = cbor.encode(decoded) - if expected != enc: - raise ValueError( - "Non-canonical CBOR from Authenticator.\n" - f"Got: {enc.hex()}\nExpected: {expected.hex()}" - ) - if isinstance(decoded, Mapping): - return decoded - raise TypeError("Decoded value of wrong type") - - def vendor( - self, - cmd: int, - sub_cmd: int, - sub_cmd_params: Optional[Mapping[int, Any]] = None, - pin_uv_protocol: Optional[int] = None, - pin_uv_param: Optional[bytes] = None, - ) -> Mapping[int, Any]: - """CTAP2 authenticator vendor command. - - This command is used to configure various authenticator features through the - use of its subcommands. - - This method is not intended to be called directly. It is intended to be used by - an instance of the Config class. - - :param sub_cmd: A Config sub command. - :param sub_cmd_params: Sub command specific parameters. - :param pin_uv_protocol: PIN/UV auth protocol version used. - :param pin_uv_param: PIN/UV Auth parameter. - """ - return self.send_vendor( - cmd, - args(sub_cmd, sub_cmd_params, pin_uv_protocol, pin_uv_param), - ) - - -class Vendor: - """Implementation of the CTAP2.1 Authenticator Vendor API. It is vendor implementation. - - :param ctap: An instance of a CTAP2Vendor object. - :param pin_uv_protocol: An instance of a PinUvAuthProtocol. - :param pin_uv_token: A valid PIN/UV Auth Token for the current CTAP session. - """ - - @unique - class CMD(IntEnum): - VENDOR_BACKUP = 0x01 - VENDOR_MSE = 0x02 - VENDOR_UNLOCK = 0x03 - VENDOR_EA = 0x04 - VENDOR_PHY = 0x05 - VENDOR_MEMORY = 0x06 - - @unique - class PARAM(IntEnum): - PARAM = 0x01 - COSE_KEY = 0x02 - - class SUBCMD(IntEnum): - ENABLE = 0x01 - DISABLE = 0x02 - KEY_AGREEMENT = 0x01 - EA_CSR = 0x01 - - class RESP(IntEnum): - PARAM = 0x01 - COSE_KEY = 0x02 - - class PHY_OPTS(IntEnum): - PHY_OPT_WCID = 0x1 - PHY_OPT_DIMM = 0x2 - - def __init__( - self, - ctap: Ctap2Vendor, - pin_uv_protocol: Optional[PinProtocol] = None, - pin_uv_token: Optional[bytes] = None, - ): - self.ctap = ctap - self.pin_uv = ( - _PinUv(pin_uv_protocol, pin_uv_token) - if pin_uv_protocol and pin_uv_token - else None - ) - self.__key_enc = None - self.__iv = None - - self.vcfg = VendorConfig(ctap, pin_uv_protocol=pin_uv_protocol, pin_uv_token=pin_uv_token) - - def _call(self, cmd, sub_cmd, params=None): - if params: - params = {k: v for k, v in params.items() if v is not None} - else: - params = None - if self.pin_uv: - msg = ( - b"\xff" * 32 - + b"\x0d" - + struct.pack(" 15): - print('ERROR: Brightness must be between 0 and 15') - return - return self.vcfg.led_brightness(brightness) - - def led_dimmable(self, onoff): - opts = self.phy_opts() - if (onoff): - opts |= Vendor.PHY_OPTS.PHY_OPT_DIMM - else: - opts &= ~Vendor.PHY_OPTS.PHY_OPT_DIMM - print(f'opts: {opts}') - return self.vcfg.phy_opts(opts) - - def wcid(self, onoff): - opts = self.phy_opts() - if (onoff): - opts |= Vendor.PHY_OPTS.PHY_OPT_WCID - else: - opts &= ~Vendor.PHY_OPTS.PHY_OPT_WCID - return self.vcfg.phy_opts(opts) - - def phy_opts(self): - return self._call( - Vendor.CMD.VENDOR_PHY, - Vendor.SUBCMD.ENABLE, - )[Vendor.RESP.PARAM] - - def memory(self): - resp = self._call( - Vendor.CMD.VENDOR_MEMORY, - Vendor.SUBCMD.ENABLE, - ) - return { 'free': resp[1], 'used': resp[2], 'total': resp[3], 'files': resp[4], 'size': resp[5] } - - def enable_enterprise_attestation(self): - self.vcfg.enable_enterprise_attestation() - - def set_min_pin_length(self, length, rpids: list[str] = None, url=None): - params = { - Config.PARAM.NEW_MIN_PIN_LENGTH: length, - Config.PARAM.MIN_PIN_LENGTH_RPIDS: rpids if rpids else None, - } - self.vcfg.set_min_pin_length( - min_pin_length=length, - rp_ids=rpids if rpids else None, - ) - self.vcfg.pin_policy(url=url.encode() if url else None) - - -def parse_args(): - parser = argparse.ArgumentParser() - subparser = parser.add_subparsers(title="commands", dest="command") - parser.add_argument('-p','--pin', help='Specify the PIN of the device.', required=True) - parser_secure = subparser.add_parser('secure', help='Manages security of Pico Fido.') - parser_secure.add_argument('subcommand', choices=['enable', 'disable', 'unlock'], help='Enables, disables or unlocks the security.') - - parser_backup = subparser.add_parser('backup', help='Manages the backup of Pico Fido.') - parser_backup.add_argument('subcommand', choices=['save', 'load'], help='Saves or loads a backup.') - parser_backup.add_argument('filename', help='File to save or load the backup.') - - parser_attestation = subparser.add_parser('attestation', help='Manages Enterprise Attestation') - parser_attestation.add_argument('subcommand', choices=['csr','enable']) - parser_attestation.add_argument('--filename', help='Uploads the certificate filename to the device as enterprise attestation certificate. If not provided, it will generate an enterprise attestation certificate automatically.') - - parser_phy = subparser.add_parser('phy', help='Set PHY options.') - subparser_phy = parser_phy.add_subparsers(title='commands', dest='subcommand', required=True) - parser_phy_vp = subparser_phy.add_parser('vidpid', help='Sets VID/PID. Use VID:PID format (e.g. 1234:5678)') - parser_phy_vp.add_argument('value', help='Value of the PHY option.', metavar='VAL', nargs='?') - parser_phy_ledn = subparser_phy.add_parser('led_gpio', help='Sets LED GPIO number.') - parser_phy_ledn.add_argument('value', help='Value of the PHY option.', metavar='VAL', nargs='?') - parser_phy_optwcid = subparser_phy.add_parser('wcid', help='Enable/Disable Web CCID interface.') - parser_phy_optwcid.add_argument('value', choices=['enable', 'disable'], help='Enable/Disable Web CCID interface.', nargs='?') - parser_phy_ledbtness = subparser_phy.add_parser('led_brightness', help='Sets LED max. brightness.') - parser_phy_ledbtness.add_argument('value', help='Value of the max. brightness.', metavar='VAL', nargs='?') - parser_phy_optdimm = subparser_phy.add_parser('led_dimmable', help='Enable/Disable LED dimming.') - parser_phy_optdimm.add_argument('value', choices=['enable', 'disable'], help='Enable/Disable LED dimming.', nargs='?') - - parser_mem = subparser.add_parser('memory', help='Get current memory usage.') - - parser_pin_policy = subparser.add_parser('pin_policy', help='Manage PIN policy.') - parser_pin_policy.add_argument('length', type=int, help='Minimum PIN length (4-63).') - parser_pin_policy.add_argument('--rpids', help='Comma separated list of Relying Party IDs that have authorization to receive minimum PIN length.') - parser_pin_policy.add_argument('--url', help='URL where the user can consult PIN policy.') - - args = parser.parse_args() - return args - -def secure(vdr: Vendor, args): - if (args.subcommand == 'enable'): - vdr.enable_device_aut() - elif (args.subcommand == 'unlock'): - vdr.unlock_device() - elif (args.subcommand == 'disable'): - vdr.disable_device_aut() - -def backup(vdr: Vendor, args): - if (args.subcommand == 'save'): - vdr.backup_save(args.filename) - elif (args.subcommand == 'load'): - vdr.backup_load(args.filename) - -def attestation(vdr: Vendor, args): - if (args.subcommand == 'csr'): - if (args.filename is None): - csr = x509.load_der_x509_csr(vdr.csr()) - data = urllib.parse.urlencode({'csr': csr.public_bytes(Encoding.PEM)}).encode() - j = get_pki_data('csr', data=data) - cert = x509.load_pem_x509_certificate(j['x509'].encode()) - else: - with open(args.filename, 'rb') as f: - dataf = f.read() - try: - cert = x509.load_der_x509_certificate(dataf) - except ValueError: - cert = x509.load_pem_x509_certificate(dataf) - vdr.upload_ea(cert.public_bytes(Encoding.DER)) - elif (args.subcommand == 'enable'): - vdr.enable_enterprise_attestation() - -def phy(vdr: Vendor, args): - val = args.value if 'value' in args else None - if (val): - if (args.subcommand == 'vidpid'): - sp = val.split(':') - if (len(sp) != 2): - print('ERROR: VID/PID have wrong format. Use VID:PID format (e.g. 1234:5678)') - ret = vdr.vidpid(int(sp[0],16), int(sp[1],16)) - elif (args.subcommand == 'led_gpio'): - val = int(val) - ret = vdr.led_gpio(val) - elif (args.subcommand == 'led_brightness'): - val = int(val) - ret = vdr.led_brightness(val) - elif (args.subcommand == 'led_dimmable'): - ret = vdr.led_dimmable(val == 'enable') - elif (args.subcommand == 'wcid'): - ret = vdr.wcid(val == 'enable') - - if (ret): - print(f'Current value: {hexlify(ret)}') - else: - print('Command executed successfully. Please, restart your Pico Key.') - -def memory(vdr: Vendor, args): - mem = vdr.memory() - print(f'Memory usage:') - print(f'\tFree: {mem["free"]/1024:.2f} kilobytes ({mem["free"]*100/mem["total"]:.2f}%)') - print(f'\tUsed: {mem["used"]/1024:.2f} kilobytes ({mem["used"]*100/mem["total"]:.2f}%)') - print(f'\tTotal: {mem["total"]/1024:.2f} kilobytes') - print(f'\tFlash size: {mem["size"]/1024:.2f} kilobytes') - print(f'\tFiles: {mem["files"]}') - - -def pin_policy(vdr: Vendor, args): - rpids = None - if (args.rpids): - rpids = args.rpids.split(',') - vdr.set_min_pin_length(args.length, rpids, args.url) - - -def main(args): - print('Pico Fido Tool v1.10') - print('Author: Pol Henarejos') - print('Report bugs to https://github.com/polhenarejos/pico-fido/issues') - print('') - print('') - - dev = next(CtapHidDevice.list_devices(), None) - ctap = Ctap2Vendor(dev) - client_pin = ClientPin(ctap) - token = client_pin.get_pin_token(args.pin, permissions=ClientPin.PERMISSION.AUTHENTICATOR_CFG) - vdr = Vendor(ctap, pin_uv_protocol=PinProtocolV2(), pin_uv_token=token) - - if (args.command == 'secure'): - secure(vdr, args) - elif (args.command == 'backup'): - backup(vdr, args) - elif (args.command == 'attestation'): - attestation(vdr, args) - elif (args.command == 'phy'): - phy(vdr, args) - elif (args.command == 'memory'): - memory(vdr, args) - elif (args.command == 'pin_policy'): - pin_policy(vdr, args) - - -def run(): - args = parse_args() - main(args) - -if __name__ == "__main__": - run()