#!/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 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_AUT_CT = 0x02 class CMD(IntEnum): CONFIG_AUT_ENABLE = 0x03e43f56b34285e2 CONFIG_AUT_DISABLE = 0x1831a40f04a25ed9 CONFIG_VENDOR_PROTOTYPE = 0x7f 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( VendorConfig.CMD.CONFIG_VENDOR_PROTOTYPE, { VendorConfig.PARAM.VENDOR_COMMAND_ID: VendorConfig.CMD.CONFIG_AUT_ENABLE, VendorConfig.PARAM.VENDOR_AUT_CT: ct }, ) def disable_device_aut(self): self._call( VendorConfig.CMD.CONFIG_VENDOR_PROTOTYPE, { VendorConfig.PARAM.VENDOR_COMMAND_ID: VendorConfig.CMD.CONFIG_AUT_DISABLE }, ) 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 @unique class PARAM(IntEnum): PARAM = 0x01 COSE_KEY = 0x02 class SUBCMD(IntEnum): ENABLE = 0x01 DISABLE = 0x02 KEY_AGREEMENT = 0x01 EA_CSR = 0x01 EA_UPLOAD = 0x02 class RESP(IntEnum): PARAM = 0x01 COSE_KEY = 0x02 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("