diff --git a/CMakeLists.txt b/CMakeLists.txt
index 93402f2..5591a2c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -113,7 +113,7 @@ set(SOURCES ${SOURCES}
)
endif()
-SET_VERSION(ver_major ver_minor "${CMAKE_CURRENT_LIST_DIR}/src/fido/version.h" 1)
+SET_VERSION(ver_major ver_minor "${CMAKE_CURRENT_LIST_DIR}/src/fido/version.h" 2)
if(ESP_PLATFORM)
project(pico_fido)
endif()
diff --git a/build_pico_fido.sh b/build_pico_fido.sh
index 6a85586..46266fc 100755
--- a/build_pico_fido.sh
+++ b/build_pico_fido.sh
@@ -1,7 +1,7 @@
#!/bin/bash
-VERSION_MAJOR="6"
-VERSION_MINOR="6"
+VERSION_MAJOR="7"
+VERSION_MINOR="0"
NO_EDDSA=0
SUFFIX="${VERSION_MAJOR}.${VERSION_MINOR}"
#if ! [[ -z "${GITHUB_SHA}" ]]; then
diff --git a/pico-keys-sdk b/pico-keys-sdk
index d410a4c..2438356 160000
--- a/pico-keys-sdk
+++ b/pico-keys-sdk
@@ -1 +1 @@
-Subproject commit d410a4cfc21fd5e97131b8d79869939e3f717a31
+Subproject commit 2438356d83cec557fbf861d23fa3ce423006e417
diff --git a/src/fido/defs.c b/src/fido/defs.c
index 74713be..9401d37 100644
--- a/src/fido/defs.c
+++ b/src/fido/defs.c
@@ -17,5 +17,8 @@
#include "pico_keys.h"
#include "fido.h"
+#include "version.h"
uint8_t PICO_PRODUCT = 2; // Pico FIDO
+uint8_t PICO_VERSION_MAJOR = PICO_FIDO_VERSION_MAJOR;
+uint8_t PICO_VERSION_MINOR = PICO_FIDO_VERSION_MINOR;
diff --git a/src/fido/version.h b/src/fido/version.h
index b02293c..7417630 100644
--- a/src/fido/version.h
+++ b/src/fido/version.h
@@ -18,7 +18,7 @@
#ifndef __VERSION_H_
#define __VERSION_H_
-#define PICO_FIDO_VERSION 0x0606
+#define PICO_FIDO_VERSION 0x0700
#define PICO_FIDO_VERSION_MAJOR ((PICO_FIDO_VERSION >> 8) & 0xff)
#define PICO_FIDO_VERSION_MINOR (PICO_FIDO_VERSION & 0xff)
diff --git a/tests/conftest.py b/tests/conftest.py
index dce2795..db2ccab 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -466,7 +466,7 @@ def ccid_card():
@pytest.fixture(scope="class")
def select_oath(ccid_card):
- aid = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01, 0x01]
+ aid = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01]
resp = send_apdu(ccid_card, 0xA4, 0x04, 0x00, aid)
return ccid_card
diff --git a/tests/pico-fido/test_070_oath.py b/tests/pico-fido/test_070_oath.py
index df79242..719e86b 100644
--- a/tests/pico-fido/test_070_oath.py
+++ b/tests/pico-fido/test_070_oath.py
@@ -123,7 +123,7 @@ def test_auth(reset_oath):
resp = list_apdu(reset_oath)
assert([e.value.sw1, e.value.sw2] == [0x69, 0x82])
- aid = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01, 0x01]
+ aid = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01]
resp = send_apdu(reset_oath, 0xA4, 0x04, 0x00, aid)
assert(resp[15] == TAG_CHALLENGE)
assert(resp[16] == 8)
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()