diff -Nru python-fido2-0.6.0~ppa1~disco1/debian/changelog python-fido2-0.7.0~ppa1~disco1/debian/changelog --- python-fido2-0.6.0~ppa1~disco1/debian/changelog 2019-05-10 11:26:22.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/debian/changelog 2019-06-17 13:13:51.000000000 +0000 @@ -1,4 +1,4 @@ -python-fido2 (0.6.0~ppa1~disco1) disco; urgency=low +python-fido2 (0.7.0~ppa1~disco1) disco; urgency=low * Build for ppa - -- Dain Nilsson Fri, 10 May 2019 12:26:22 +0100 + -- Dain Nilsson Mon, 17 Jun 2019 14:28:07 +0100 diff -Nru python-fido2-0.6.0~ppa1~disco1/debian/control python-fido2-0.7.0~ppa1~disco1/debian/control --- python-fido2-0.6.0~ppa1~disco1/debian/control 2018-09-27 10:37:36.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/debian/control 2019-06-17 13:09:04.000000000 +0000 @@ -28,8 +28,9 @@ python-setuptools, python-six, Recommends: libu2f-udev, + python-pyscard, Description: Python library for implementing FIDO 2.0 - A Python library for communicating with a FIDO device over USB HID as + A Python library for communicating with a FIDO device over USB HID as well as verifying attestation and assertion signatures. Supports FIDO U2F and FIDO 2.0. This is the Python 2 version of the package. @@ -44,7 +45,8 @@ python3-setuptools, python3-six, Recommends: libu2f-udev, + python3-pyscard, Description: Python library for implementing FIDO 2.0 - A Python library for communicating with a FIDO device over USB HID as + A Python library for communicating with a FIDO device over USB HID as well as verifying attestation and assertion signatures. This is the Python 3 version of the package. diff -Nru python-fido2-0.6.0~ppa1~disco1/examples/acr122u.py python-fido2-0.7.0~ppa1~disco1/examples/acr122u.py --- python-fido2-0.6.0~ppa1~disco1/examples/acr122u.py 1970-01-01 00:00:00.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/examples/acr122u.py 2019-06-17 13:09:04.000000000 +0000 @@ -0,0 +1,72 @@ + +from fido2.pcsc import CtapPcscDevice +import time + + +class Acr122uPcscDevice(object): + def __init__(self, pcsc_device): + self.pcsc = pcsc_device + + def reader_version(self): + """ + Get reader's version from reader + :return: string. Reader's version + """ + + try: + result, sw1, sw2 = self.pcsc.apdu_exchange(b'\xff\x00\x48\x00\x00') + if len(result) > 0: + str_result = result + bytes([sw1]) + bytes([sw2]) + str_result = str_result.decode('utf-8') + return str_result + except Exception as e: + print('Get version error:', e) + pass + return 'n/a' + + def led_control(self, red=False, green=False, + blink_count=0, red_end_blink=False, green_end_blink=False): + """ + Reader's led control + :param red: boolean. red led on + :param green: boolean. green let on + :param blink_count: int. if needs to blink value > 0. blinks count + :param red_end_blink: boolean. + state of red led at the end of blinking + :param green_end_blink: boolean. + state of green led at the end of blinking + :return: + """ + + try: + if blink_count > 0: + cbyte = 0b00001100 + \ + (0b01 if red_end_blink else 0b00) + \ + (0b10 if green_end_blink else 0b00) + cbyte |= (0b01000000 if red else 0b00000000) + \ + (0b10000000 if green else 0b00000000) + else: + cbyte = 0b00001100 + \ + (0b01 if red else 0b00) + \ + (0b10 if green else 0b00) + + apdu = b'\xff\x00\x40' + \ + bytes([cbyte & 0xff]) + \ + b'\4' + b'\5\3' + \ + bytes([blink_count]) + \ + b'\0' + self.pcsc.apdu_exchange(apdu) + + except Exception as e: + print('LED control error:', e) + + +dev = next(CtapPcscDevice.list_devices()) + +print('CONNECT: %s' % dev) +pcsc_device = Acr122uPcscDevice(dev) +pcsc_device.led_control(False, True, 0) +print('version: %s' % pcsc_device.reader_version()) +pcsc_device.led_control(True, False, 0) +time.sleep(1) +pcsc_device.led_control(False, True, 3) diff -Nru python-fido2-0.6.0~ppa1~disco1/examples/acr1252u.py python-fido2-0.7.0~ppa1~disco1/examples/acr1252u.py --- python-fido2-0.6.0~ppa1~disco1/examples/acr1252u.py 1970-01-01 00:00:00.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/examples/acr1252u.py 2019-06-17 13:09:04.000000000 +0000 @@ -0,0 +1,168 @@ + +from fido2.pcsc import CtapPcscDevice +import time + + +# control codes: +# 3225264 - magic number!!! +# 0x42000000 + 3500 - cross platform way +C_CODE = 3225264 + + +class Acr1252uPcscDevice(object): + def __init__(self, pcsc_device): + self.pcsc = pcsc_device + + def reader_version(self): + try: + res = self.pcsc.control_exchange(C_CODE, b'\xe0\x00\x00\x18\x00') + + if len(res) > 0 and res.find(b'\xe1\x00\x00\x00') == 0: + reslen = res[4] + if reslen == len(res) - 5: + strres = res[5:5+reslen].decode('utf-8') + return strres + except Exception as e: + print('Get version error:', e) + return 'n/a' + + def reader_serial_number(self): + try: + res = self.pcsc.control_exchange(C_CODE, b'\xe0\x00\x00\x33\x00') + + if len(res) > 0 and res.find(b'\xe1\x00\x00\x00') == 0: + reslen = res[4] + if reslen == len(res) - 5: + strres = res[5:5+reslen].decode('utf-8') + return strres + except Exception as e: + print('Get serial number error:', e) + return 'n/a' + + def led_control(self, red=False, green=False): + try: + cbyte = (0b01 if red else 0b00) + (0b10 if green else 0b00) + result = self.pcsc.control_exchange(C_CODE, + b'\xe0\x00\x00\x29\x01' + + bytes([cbyte])) + + if len(result) > 0 and result.find(b'\xe1\x00\x00\x00') == 0: + result_length = result[4] + if result_length == 1: + ex_red = bool(result[5] & 0b01) + ex_green = bool(result[5] & 0b10) + return True, ex_red, ex_green + except Exception as e: + print('LED control error:', e) + + return False, False, False + + def led_status(self): + try: + result = self.pcsc.control_exchange(C_CODE, b'\xe0\x00\x00\x29\x00') + + if len(result) > 0 and result.find(b'\xe1\x00\x00\x00') == 0: + result_length = result[4] + if result_length == 1: + ex_red = bool(result[5] & 0b01) + ex_green = bool(result[5] & 0b10) + return True, ex_red, ex_green + except Exception as e: + print('LED status error:', e) + + return False, False, False + + def get_polling_settings(self): + try: + res = self.pcsc.control_exchange(C_CODE, b'\xe0\x00\x00\x23\x00') + + if len(res) > 0 and res.find(b'\xe1\x00\x00\x00') == 0: + reslen = res[4] + if reslen == 1: + return True, res[5] + except Exception as e: + print('Get polling settings error:', e) + + return False, 0 + + def set_polling_settings(self, settings): + try: + res = self.pcsc.control_exchange(C_CODE, b'\xe0\x00\x00\x23\x01' + + bytes([settings & 0xff])) + + if len(res) > 0 and res.find(b'\xe1\x00\x00\x00') == 0: + reslen = res[4] + if reslen == 1: + return True, res[5] + except Exception as e: + print('Set polling settings error:', e) + + return False, 0 + + def get_picc_operation_parameter(self): + try: + res = self.pcsc.control_exchange(C_CODE, b'\xe0\x00\x00\x20\x00') + + if len(res) > 0 and res.find(b'\xe1\x00\x00\x00') == 0: + reslen = res[4] + if reslen == 1: + return True, res[5] + except Exception as e: + print('Get PICC Operating Parameter error:', e) + + return False, 0 + + def set_picc_operation_parameter(self, param): + try: + res = self.pcsc.control_exchange(C_CODE, b'\xe0\x00\x00\x20\x01' + + bytes([param])) + + if len(res) > 0 and res.find(b'\xe1\x00\x00\x00') == 0: + reslen = res[4] + if reslen == 1: + return True, res[5] + except Exception as e: + print('Set PICC Operating Parameter error:', e) + + return False, 0 + + +dev = next(CtapPcscDevice.list_devices()) + +print('CONNECT: %s' % dev) +pcsc_device = Acr1252uPcscDevice(dev) +if pcsc_device is not None: + print('version: %s' % pcsc_device.reader_version()) + print('serial number: %s' % pcsc_device.reader_serial_number()) + print('') + + result, settings = pcsc_device.set_polling_settings(0x8B) + print('write polling settings: %r 0x%x' % (result, settings)) + + result, settings = pcsc_device.get_polling_settings() + print('polling settings: %r 0x%x' % (result, settings)) + set_desc = [[0, 'Auto PICC Polling'], + [1, 'Turn off Antenna Field if no PICC is found'], + [2, 'Turn off Antenna Field if the PICC is inactive'], + [3, 'Activate the PICC when detected'], + [7, 'Enforce ISO 14443-A Part 4']] + for x in set_desc: + print(x[1], 'on' if settings & (1 << x[0]) else 'off') + interval_desc = [250, 500, 1000, 2500] + print('PICC Poll Interval for PICC', + interval_desc[(settings >> 4) & 0b11], + 'ms') + print('') + + print('PICC operation parameter: %r 0x%x' % + pcsc_device.get_picc_operation_parameter()) + print('') + + result, red, green = pcsc_device.led_control(True, False) + print('led control result:', result, 'red:', red, 'green:', green) + + result, red, green = pcsc_device.led_status() + print('led state result:', result, 'red:', red, 'green:', green) + + time.sleep(1) + pcsc_device.led_control(False, False) diff -Nru python-fido2-0.6.0~ppa1~disco1/examples/credential.py python-fido2-0.7.0~ppa1~disco1/examples/credential.py --- python-fido2-0.6.0~ppa1~disco1/examples/credential.py 2019-05-10 11:21:28.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/examples/credential.py 2019-06-17 13:09:04.000000000 +0000 @@ -26,9 +26,9 @@ # POSSIBILITY OF SUCH DAMAGE. """ -Connects to the first FIDO device found, creates a new credential for it, -and authenticates the credential. This works with both FIDO 2.0 devices as well -as with U2F devices. +Connects to the first FIDO device found (starts from USB, then looks into NFC), +creates a new credential for it, and authenticates the credential. +This works with both FIDO 2.0 devices as well as with U2F devices. """ from __future__ import print_function, absolute_import, unicode_literals @@ -38,9 +38,22 @@ from getpass import getpass import sys +use_nfc = False # Locate a device dev = next(CtapHidDevice.list_devices(), None) +if dev is not None: + print('Use USB HID channel.') +else: + try: + from fido2.pcsc import CtapPcscDevice + + dev = next(CtapPcscDevice.list_devices(), None) + print('Use NFC channel.') + use_nfc = True + except Exception as e: + print('NFC channel search error:', e) + if not dev: print('No FIDO device found') sys.exit(1) @@ -57,9 +70,12 @@ pin = None if client.info.options.get('clientPin'): pin = getpass('Please enter PIN:') +else: + print('no pin') # Create a credential -print('\nTouch your authenticator device now...\n') +if not use_nfc: + print('\nTouch your authenticator device now...\n') attestation_object, client_data = client.make_credential( rp, user, challenge, pin=pin ) @@ -91,7 +107,8 @@ }] # Authenticate the credential -print('\nTouch your authenticator device now...\n') +if not use_nfc: + print('\nTouch your authenticator device now...\n') assertions, client_data = client.get_assertion( rp['id'], challenge, allow_list, pin=pin diff -Nru python-fido2-0.6.0~ppa1~disco1/examples/get_info.py python-fido2-0.7.0~ppa1~disco1/examples/get_info.py --- python-fido2-0.6.0~ppa1~disco1/examples/get_info.py 2018-09-27 10:37:36.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/examples/get_info.py 2019-06-17 13:09:04.000000000 +0000 @@ -34,10 +34,23 @@ from fido2.hid import CtapHidDevice, CAPABILITY from fido2.ctap2 import CTAP2 +try: + from fido2.pcsc import CtapPcscDevice +except ImportError: + CtapPcscDevice = None -for dev in CtapHidDevice.list_devices(): +def enumerate_devices(): + for dev in CtapHidDevice.list_devices(): + yield dev + if CtapPcscDevice: + for dev in CtapPcscDevice.list_devices(): + yield dev + + +for dev in enumerate_devices(): print('CONNECT: %s' % dev) + print('CTAPHID protocol version: %d' % dev.version) if dev.capabilities & CAPABILITY.CBOR: ctap2 = CTAP2(dev) @@ -51,3 +64,5 @@ print('WINK sent!') else: print('Device does not support WINK') + + dev.close() diff -Nru python-fido2-0.6.0~ppa1~disco1/examples/hmac_secret.py python-fido2-0.7.0~ppa1~disco1/examples/hmac_secret.py --- python-fido2-0.6.0~ppa1~disco1/examples/hmac_secret.py 1970-01-01 00:00:00.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/examples/hmac_secret.py 2019-06-17 13:09:04.000000000 +0000 @@ -0,0 +1,136 @@ +# Copyright (c) 2018 Yubico AB +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or +# without modification, are permitted provided that the following +# conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +""" +Connects to the first FIDO device found which supports the HmacSecret extension, +creates a new credential for it with the extension enabled, and uses it to +derive two separate secrets. +""" +from __future__ import print_function, absolute_import, unicode_literals + +from fido2.hid import CtapHidDevice +from fido2.client import Fido2Client +from fido2.extensions import HmacSecretExtension +from getpass import getpass +from binascii import b2a_hex +import sys +import os + +try: + from fido2.pcsc import CtapPcscDevice +except ImportError: + CtapPcscDevice = None + + +def enumerate_devices(): + for dev in CtapHidDevice.list_devices(): + yield dev + if CtapPcscDevice: + for dev in CtapPcscDevice.list_devices(): + yield dev + + +# Locate a device +for dev in enumerate_devices(): + client = Fido2Client(dev, 'https://example.com') + if HmacSecretExtension.NAME in client.info.extensions: + break +else: + print('No Authenticator with the HmacSecret extension found!') + sys.exit(1) + +use_nfc = CtapPcscDevice and isinstance(dev, CtapPcscDevice) + +# Prepare parameters for makeCredential +rp = {'id': 'example.com', 'name': 'Example RP'} +user = {'id': b'user_id', 'name': 'A. User'} +challenge = 'Y2hhbGxlbmdl' + +# Prompt for PIN if needed +pin = None +if client.info.options.get('clientPin'): + pin = getpass('Please enter PIN:') +else: + print('no pin') + +hmac_ext = HmacSecretExtension(client.ctap2) + +# Create a credential +if not use_nfc: + print('\nTouch your authenticator device now...\n') +attestation_object, client_data = client.make_credential( + rp, user, challenge, extensions=hmac_ext.create_dict(), pin=pin +) + +# HmacSecret result: +hmac_result = hmac_ext.results_for(attestation_object.auth_data) + +credential = attestation_object.auth_data.credential_data +print('New credential created, with the HmacSecret extension.') + +# Prepare parameters for getAssertion +challenge = 'Q0hBTExFTkdF' # Use a new challenge for each call. +allow_list = [{ + 'type': 'public-key', + 'id': credential.credential_id +}] + +# Generate a salt for HmacSecret: +salt = os.urandom(32) +print('Authenticate with salt:', b2a_hex(salt)) + +# Authenticate the credential +if not use_nfc: + print('\nTouch your authenticator device now...\n') + +assertions, client_data = client.get_assertion( + rp['id'], challenge, allow_list, extensions=hmac_ext.get_dict(salt), pin=pin +) + +assertion = assertions[0] # Only one cred in allowList, only one response. +hmac_res = hmac_ext.results_for(assertion.auth_data) +print('Authenticated, secret:', b2a_hex(hmac_res[0])) + +# Authenticate again, using two salts to generate two secrets: + +# Generate a second salt for HmacSecret: +salt2 = os.urandom(32) +print('Authenticate with second salt:', b2a_hex(salt2)) + +if not use_nfc: + print('\nTouch your authenticator device now...\n') + +# The first salt is reused, which should result in the same secret. +assertions, client_data = client.get_assertion( + rp['id'], challenge, allow_list, extensions=hmac_ext.get_dict(salt, salt2), + pin=pin +) + +assertion = assertions[0] # Only one cred in allowList, only one response. +hmac_res = hmac_ext.results_for(assertion.auth_data) +print('Old secret:', b2a_hex(hmac_res[0])) +print('New secret:', b2a_hex(hmac_res[1])) diff -Nru python-fido2-0.6.0~ppa1~disco1/examples/u2f_nfc.py python-fido2-0.7.0~ppa1~disco1/examples/u2f_nfc.py --- python-fido2-0.6.0~ppa1~disco1/examples/u2f_nfc.py 1970-01-01 00:00:00.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/examples/u2f_nfc.py 2019-06-17 13:09:04.000000000 +0000 @@ -0,0 +1,31 @@ +from fido2.pcsc import CtapPcscDevice +from fido2.utils import sha256 +from fido2.ctap1 import CTAP1 +import sys + + +dev = next(CtapPcscDevice.list_devices(), None) +if not dev: + print('No NFC u2f device found') + sys.exit(1) + +chal = sha256(b'AAA') +appid = sha256(b'BBB') + +ctap1 = CTAP1(dev) + +print('version:', ctap1.get_version()) + +reg = ctap1.register(chal, appid) +print('register:', reg) + + +reg.verify(appid, chal) +print('Register message verify OK') + + +auth = ctap1.authenticate(chal, appid, reg.key_handle) +print('authenticate result: ', auth) + +res = auth.verify(appid, chal, reg.public_key) +print('Authenticate message verify OK') diff -Nru python-fido2-0.6.0~ppa1~disco1/fido2/client.py python-fido2-0.7.0~ppa1~disco1/fido2/client.py --- python-fido2-0.6.0~ppa1~disco1/fido2/client.py 2019-05-10 11:21:28.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/fido2/client.py 2019-06-17 13:09:04.000000000 +0000 @@ -332,6 +332,16 @@ if uv: options['uv'] = True + # Filter out credential IDs which are too long + max_len = self.info.max_cred_id_length + if max_len and exclude_list: + exclude_list = [e for e in exclude_list if len(e) <= max_len] + + # Reject the request if too many credentials remain. + max_creds = self.info.max_creds_in_list + if max_creds and len(exclude_list or ()) > max_creds: + raise ClientError.ERR.BAD_REQUEST('exclude_list too long') + return self.ctap2.make_credential(client_data.hash, rp, user, key_params, exclude_list, extensions, options, pin_auth, @@ -403,13 +413,22 @@ if len(options) == 0: options = None - assertions = [self.ctap2.get_assertion( + # Filter out credential IDs which are too long + max_len = self.info.max_cred_id_length + if max_len: + allow_list = [e for e in allow_list if len(e) <= max_len] + if not allow_list: + raise CtapError(CtapError.ERR.NO_CREDENTIALS) + + # Reject the request if too many credentials remain. + max_creds = self.info.max_creds_in_list + if max_creds and len(allow_list) > max_creds: + raise ClientError.ERR.BAD_REQUEST('allow_list too long') + + return self.ctap2.get_assertions( rp_id, client_data.hash, allow_list, extensions, options, pin_auth, pin_protocol, timeout, on_keepalive - )] - for _ in range((assertions[0].number_of_credentials or 1) - 1): - assertions.append(self.ctap2.get_next_assertion()) - return assertions + ) def _ctap1_get_assertion(self, client_data, rp_id, allow_list, extensions, up, uv, pin, timeout, on_keepalive): diff -Nru python-fido2-0.6.0~ppa1~disco1/fido2/ctap2.py python-fido2-0.7.0~ppa1~disco1/fido2/ctap2.py --- python-fido2-0.6.0~ppa1~disco1/fido2/ctap2.py 2019-05-10 11:21:28.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/fido2/ctap2.py 2019-06-17 13:09:04.000000000 +0000 @@ -80,12 +80,16 @@ @unique class KEY(IntEnum): - VERSIONS = 1 - EXTENSIONS = 2 - AAGUID = 3 - OPTIONS = 4 - MAX_MSG_SIZE = 5 - PIN_PROTOCOLS = 6 + VERSIONS = 0x01 + EXTENSIONS = 0x02 + AAGUID = 0x03 + OPTIONS = 0x04 + MAX_MSG_SIZE = 0x05 + PIN_PROTOCOLS = 0x06 + MAX_CREDS_IN_LIST = 0x07 + MAX_CRED_ID_LENGTH = 0x08 + TRANSPORTS = 0x09 + ALGORITHMS = 0x0a @classmethod def get(cls, key): @@ -106,6 +110,10 @@ self.max_msg_size = data.get(Info.KEY.MAX_MSG_SIZE, 1024) self.pin_protocols = data.get( Info.KEY.PIN_PROTOCOLS, []) + self.max_creds_in_list = data.get(Info.KEY.MAX_CREDS_IN_LIST) + self.max_cred_id_length = data.get(Info.KEY.MAX_CRED_ID_LENGTH) + self.transports = data.get(Info.KEY.TRANSPORTS, []) + self.algorithms = data.get(Info.KEY.ALGORITHMS) self.data = data def __repr__(self): @@ -118,6 +126,14 @@ r += ', max_message_size: %d' % self.max_msg_size if self.pin_protocols: r += ', pin_protocols: %r' % self.pin_protocols + if self.max_creds_in_list: + r += ', max_credential_count_in_list: %d' % self.max_creds_in_list + if self.max_cred_id_length: + r += ', max_credential_id_length: %d' % self.max_cred_id_length + if self.transports: + r += ', transports: %r' % self.transports + if self.algorithms: + r += ', algorithms: %r' % self.algorithms return r + ')' def __str__(self): @@ -553,6 +569,8 @@ CLIENT_PIN = 0x06 RESET = 0x07 GET_NEXT_ASSERTION = 0x08 + # 0x41 is the command byte for credmgmt preview + CREDENTIAL_MGMT = 0x41 def __init__(self, device): if not device.capabilities & CAPABILITY.CBOR: @@ -561,7 +579,6 @@ def send_cbor(self, cmd, data=None, timeout=None, parse=cbor.decode, on_keepalive=None): - """Sends a CBOR message to the device, and waits for a response. The optional parameter 'timeout' can either be a numeric time in seconds @@ -595,7 +612,7 @@ exclude_list=None, extensions=None, options=None, pin_auth=None, pin_protocol=None, timeout=None, on_keepalive=None): - """CTAP2 makeCredential operation, + """CTAP2 makeCredential operation. :param client_data_hash: SHA256 hash of the ClientData. :param rp: PublicKeyCredentialRpEntity parameters. @@ -629,7 +646,7 @@ pin_protocol=None, timeout=None, on_keepalive=None): """CTAP2 getAssertion command. - :param rp_id: SHA256 hash of the RP ID of the credential. + :param rp_id: The RP ID of the credential. :param client_data_hash: SHA256 hash of the ClientData used. :param allow_list: Optional list of PublicKeyCredentialDescriptors. :param extensions: Optional dict of extensions. @@ -699,6 +716,33 @@ return self.send_cbor(CTAP2.CMD.GET_NEXT_ASSERTION, parse=AssertionResponse) + def credential_mgmt(self, sub_cmd, sub_cmd_params=None, pin_protocol=None, + pin_auth=None): + """CTAP2 credentialManagement command, used to manage resident + credentials. + + :param sub_cmd: A credentialManagement sub command. + :param sub_cmd_params: Sub command specific parameters. + :param pin_protocol: PIN protocol version used. + :pin_auth: + """ + return self.send_cbor(CTAP2.CMD.CREDENTIAL_MGMT, args( + sub_cmd, + sub_cmd_params, + pin_protocol, + pin_auth + )) + + def get_assertions(self, *args, **kwargs): + """Convenience method to get list of assertions. + + See get_assertion and get_assertion_next for details. + """ + first = self.get_assertion(*args, **kwargs) + rest = [self.get_assertion_next() + for _ in range(1, first.number_of_credentials or 1)] + return [first] + rest + def _pad_pin(pin): if not isinstance(pin, six.string_types): @@ -713,7 +757,7 @@ class PinProtocolV1(object): - """Implementation of the CTAP1 PIN protocol v1. + """Implementation of the CTAP2 PIN protocol v1. :param ctap: An instance of a CTAP2 object. :cvar VERSION: The version number of the PIV protocol. @@ -739,7 +783,7 @@ def __init__(self, ctap): self.ctap = ctap - def _init_shared_secret(self): + def get_shared_secret(self): be = default_backend() sk = ec.generate_private_key(ec.SECP256R1(), be) pn = sk.public_key().public_numbers() @@ -759,17 +803,19 @@ shared_secret = sha256(sk.exchange(ec.ECDH(), pk)) # x-coordinate, 32b return key_agreement, shared_secret + def _get_cipher(self, secret): + be = default_backend() + return Cipher(algorithms.AES(secret), modes.CBC(PinProtocolV1.IV), be) + def get_pin_token(self, pin): """Get a PIN token from the authenticator. :param pin: The PIN of the authenticator. :return: A PIN token. """ - key_agreement, shared_secret = self._init_shared_secret() + key_agreement, shared_secret = self.get_shared_secret() - be = default_backend() - cipher = Cipher(algorithms.AES(shared_secret), - modes.CBC(PinProtocolV1.IV), be) + cipher = self._get_cipher(shared_secret) pin_hash = sha256(pin.encode())[:16] enc = cipher.encryptor() pin_hash_enc = enc.update(pin_hash) + enc.finalize() @@ -792,17 +838,16 @@ def set_pin(self, pin): """Set the PIN of the autenticator. + This only works when no PIN is set. To change the PIN when set, use change_pin. :param pin: A PIN to set. """ pin = _pad_pin(pin) - key_agreement, shared_secret = self._init_shared_secret() + key_agreement, shared_secret = self.get_shared_secret() - be = default_backend() - cipher = Cipher(algorithms.AES(shared_secret), - modes.CBC(PinProtocolV1.IV), be) + cipher = self._get_cipher(shared_secret) enc = cipher.encryptor() pin_enc = enc.update(pin) + enc.finalize() pin_auth = hmac_sha256(shared_secret, pin_enc)[:16] @@ -813,6 +858,7 @@ def change_pin(self, old_pin, new_pin): """Change the PIN of the authenticator. + This only works when a PIN is already set. If no PIN is set, use set_pin. @@ -820,11 +866,9 @@ :param new_pin: The new PIN to set. """ new_pin = _pad_pin(new_pin) - key_agreement, shared_secret = self._init_shared_secret() + key_agreement, shared_secret = self.get_shared_secret() - be = default_backend() - cipher = Cipher(algorithms.AES(shared_secret), - modes.CBC(PinProtocolV1.IV), be) + cipher = self._get_cipher(shared_secret) pin_hash = sha256(old_pin.encode())[:16] enc = cipher.encryptor() pin_hash_enc = enc.update(pin_hash) + enc.finalize() @@ -837,3 +881,165 @@ pin_hash_enc=pin_hash_enc, new_pin_enc=new_pin_enc, pin_auth=pin_auth) + + +class CredentialManagement(object): + """Implementation of a draft specification of the Credential Management API. + WARNING: This specification is not final and this class is likely to change. + + :param ctap: An instance of a CTAP2 object. + :param pin_protocol: The PIN protocol version used. + :param pin_token: A valid pin_token for the current CTAP session. + """ + + @unique + class CMD(IntEnum): + GET_CREDS_METADATA = 0x01 + ENUMERATE_RPS_BEGIN = 0x02 + ENUMERATE_RPS_NEXT = 0x03 + ENUMERATE_CREDS_BEGIN = 0x04 + ENUMERATE_CREDS_NEXT = 0x05 + DELETE_CREDENTIAL = 0x06 + + @unique + class SUB_PARAMETER(IntEnum): + RP_ID_HASH = 0x01 + CREDENTIAL_ID = 0x02 + + @unique + class RESULT(IntEnum): + EXISTING_CRED_COUNT = 0x01 + MAX_REMAINING_COUNT = 0x02 + RP = 0x03 + RP_ID_HASH = 0x04 + TOTAL_RPS = 0x05 + USER = 0x06 + CREDENTIAL_ID = 0x07 + PUBLIC_KEY = 0x08 + TOTAL_CREDENTIALS = 0x09 + CRED_PROTECT = 0x0a + + def __init__(self, ctap, pin_protocol, pin_token): + self.ctap = ctap + self.pin_protocol = pin_protocol + self.pin_token = pin_token + + def _call(self, sub_cmd, params=None, auth=True): + kwargs = { + 'sub_cmd': sub_cmd, + 'sub_cmd_params': params + } + if auth: + msg = struct.pack('>B', sub_cmd) + if params is not None: + msg += cbor.encode(params) + kwargs['pin_protocol'] = self.pin_protocol + kwargs['pin_auth'] = hmac_sha256(self.pin_token, msg)[:16] + return self.ctap.credential_mgmt(**kwargs) + + def get_metadata(self): + """Get credentials metadata. + + This returns the existing resident credentials count, and the max + possible number of remaining resident credentials (the actual number of + remaining credentials may depend on algorithm choice, etc). + + :return: A dict containing EXISTING_CRED_COUNT, and MAX_REMAINING_COUNT. + """ + return self._call(CredentialManagement.CMD.GET_CREDS_METADATA) + + def enumerate_rps_begin(self): + """Start enumeration of RP entities of resident credentials. + + This will begin enumeration of stored RP entities, returning the first + entity, as well as a count of the total number of entities stored. + + :return: A dict containing RP, RP_ID_HASH, and TOTAL_RPS. + """ + return self._call(CredentialManagement.CMD.ENUMERATE_RPS_BEGIN) + + def enumerate_rps_next(self): + """Get the next RP entity stored. + + This continues enumeration of stored RP entities, returning the next + entity. + + :return: A dict containing RP, and RP_ID_HASH. + """ + return self._call( + CredentialManagement.CMD.ENUMERATE_RPS_NEXT, + auth=False + ) + + def enumerate_rps(self): + """Convenience method to enumerate all RPs. + + See enumerate_rps_begin and enumerate_rps_next for details. + """ + first = self.enumerate_rps_begin() + n_rps = first[CredentialManagement.RESULT.TOTAL_RPS] + if n_rps == 0: + return [] + rest = [self.enumerate_rps_next() + for _ in range( + 1, + n_rps + )] + return [first] + rest + + def enumerate_creds_begin(self, rp_id_hash): + """Start enumeration of resident credentials. + + This will begin enumeration of resident credentials for a given RP, + returning the first credential, as well as a count of the total number + of resident credentials stored for the given RP. + + :param rp_id_hash: SHA256 hash of the RP ID. + :return: A dict containing USER, CREDENTIAL_ID, PUBLIC_KEY, and + TOTAL_CREDENTIALS. + """ + return self._call( + CredentialManagement.CMD.ENUMERATE_CREDS_BEGIN, + {CredentialManagement.SUB_PARAMETER.RP_ID_HASH: rp_id_hash} + ) + + def enumerate_creds_next(self): + """Get the next resident credential stored. + + This continues enumeration of resident credentials, returning the next + credential. + + :return: A dict containing USER, CREDENTIAL_ID, and PUBLIC_KEY. + """ + return self._call( + CredentialManagement.CMD.ENUMERATE_CREDS_NEXT, + auth=False + ) + + def enumerate_creds(self, *args, **kwargs): + """Convenience method to enumerate all resident credentials for an RP. + + See enumerate_creds_begin and enumerate_creds_next for details. + """ + try: + first = self.enumerate_creds_begin(*args, **kwargs) + except CtapError as e: + if e.code == CtapError.ERR.NO_CREDENTIALS: + return [] + raise # Other error + rest = [self.enumerate_creds_next() + for _ in range( + 1, + first.get(CredentialManagement.RESULT.TOTAL_CREDENTIALS, 1) + )] + return [first] + rest + + def delete_cred(self, cred_id): + """Delete a resident credential. + + :param cred_id: The ID of the credential to delete. + """ + return self._call( + CredentialManagement.CMD.DELETE_CREDENTIAL, + {CredentialManagement.SUB_PARAMETER.CREDENTIAL_ID: cred_id} + ) diff -Nru python-fido2-0.6.0~ppa1~disco1/fido2/ctap.py python-fido2-0.7.0~ppa1~disco1/fido2/ctap.py --- python-fido2-0.6.0~ppa1~disco1/fido2/ctap.py 2018-09-27 10:37:36.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/fido2/ctap.py 2019-06-17 13:09:04.000000000 +0000 @@ -31,6 +31,12 @@ import abc +@unique +class STATUS(IntEnum): + PROCESSING = 1 + UPNEEDED = 2 + + class CtapDevice(abc.ABC): """ CTAP-capable device. Subclasses of this should implement call, as well as @@ -51,6 +57,9 @@ :return: The response from the authenticator. """ + def close(self): + """Close the device, releasing any held resources.""" + @classmethod @abc.abstractmethod def list_devices(cls): diff -Nru python-fido2-0.6.0~ppa1~disco1/fido2/extensions.py python-fido2-0.7.0~ppa1~disco1/fido2/extensions.py --- python-fido2-0.6.0~ppa1~disco1/fido2/extensions.py 1970-01-01 00:00:00.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/fido2/extensions.py 2019-06-17 13:09:04.000000000 +0000 @@ -0,0 +1,132 @@ +# Copyright (c) 2018 Yubico AB +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or +# without modification, are permitted provided that the following +# conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, unicode_literals + +from .ctap2 import PinProtocolV1 +from .utils import hmac_sha256 +import abc + + +class Extension(abc.ABC): + """ + Base class for CTAP2 extensions. + """ + + NAME = None + + def results_for(self, auth_data): + """ + Get the parsed extension results from an AuthenticatorData object. + """ + data = auth_data.extensions.get(self.NAME) + if auth_data.is_attested(): + return self.create_result(data) + else: + return self.get_result(data) + + def create_dict(self, *args, **kwargs): + """ + Return extension dict for use with calls to make_credential. + """ + return {self.NAME: self.create_data(*args, **kwargs)} + + def get_dict(self, *args, **kwargs): + """ + Return extension dict for use with calls to get_assertion. + """ + return {self.NAME: self.get_data(*args, **kwargs)} + + @abc.abstractmethod + def create_data(self, *args, **kwargs): + """ + Return extension data value for use with calls to make_credential. + """ + + @abc.abstractmethod + def create_result(self, data): + """ + Process and return extension result from call to make_credential. + """ + + @abc.abstractmethod + def get_data(self, *args, **kwargs): + """ + Return extension data value for use with calls to get_assertion. + """ + + @abc.abstractmethod + def get_result(self, data): + """ + Process and return extension result from call to get_assertion. + """ + + +class HmacSecretExtension(Extension): + """ + Implements the hmac-secret CTAP2 extension. + """ + + NAME = 'hmac-secret' + SALT_LEN = 32 + + def __init__(self, ctap): + self._pin_protocol = PinProtocolV1(ctap) + + def create_data(self): + return True + + def create_result(self, data): + if data is not True: + raise ValueError('hmac-secret extension not supported') + + def get_data(self, salt1, salt2=b''): + if len(salt1) != self.SALT_LEN: + raise ValueError('Wrong length for salt1') + if salt2 and len(salt2) != self.SALT_LEN: + raise ValueError('Wrong length for salt2') + + key_agreement, shared_secret = self._pin_protocol.get_shared_secret() + self._agreement = key_agreement + self._secret = shared_secret + + enc = self._pin_protocol._get_cipher(shared_secret).encryptor() + salt_enc = enc.update(salt1) + enc.update(salt2) + enc.finalize() + + return { + 1: key_agreement, + 2: salt_enc, + 3: hmac_sha256(shared_secret, salt_enc)[:16] + } + + def get_result(self, data): + dec = self._pin_protocol._get_cipher(self._secret).decryptor() + salt = dec.update(data) + dec.finalize() + return ( + salt[:HmacSecretExtension.SALT_LEN], + salt[HmacSecretExtension.SALT_LEN:] + ) diff -Nru python-fido2-0.6.0~ppa1~disco1/fido2/hid.py python-fido2-0.7.0~ppa1~disco1/fido2/hid.py --- python-fido2-0.6.0~ppa1~disco1/fido2/hid.py 2018-09-27 10:37:36.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/fido2/hid.py 2019-06-17 13:09:04.000000000 +0000 @@ -1,7 +1,7 @@ from __future__ import absolute_import -from .ctap import CtapDevice, CtapError +from .ctap import CtapDevice, CtapError, STATUS from ._pyu2f import hidtransport from enum import IntEnum, unique @@ -26,12 +26,6 @@ @unique -class STATUS(IntEnum): - PROCESSING = 1 - UPNEEDED = 2 - - -@unique class CAPABILITY(IntEnum): WINK = 0x01 LOCK = 0x02 # Not used @@ -74,7 +68,7 @@ def version(self): """CTAP HID protocol version. - :rtype: Tuple[int, int, int] + :rtype: int """ return self._dev.u2fhid_version @@ -131,6 +125,10 @@ """Locks the channel.""" self.call(CTAPHID.LOCK, struct.pack('>B', lock_time)) + def close(self): + del self._dev + del self.descriptor + @classmethod def list_devices(cls, selector=hidtransport.HidUsageSelector): for d in hidtransport.hid.Enumerate(): diff -Nru python-fido2-0.6.0~ppa1~disco1/fido2/__init__.py python-fido2-0.7.0~ppa1~disco1/fido2/__init__.py --- python-fido2-0.6.0~ppa1~disco1/fido2/__init__.py 2019-05-10 11:21:28.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/fido2/__init__.py 2019-06-17 13:09:04.000000000 +0000 @@ -36,4 +36,4 @@ abc.ABC = ABC -__version__ = '0.6.0' +__version__ = '0.7.0' diff -Nru python-fido2-0.6.0~ppa1~disco1/fido2/nfc.py python-fido2-0.7.0~ppa1~disco1/fido2/nfc.py --- python-fido2-0.6.0~ppa1~disco1/fido2/nfc.py 1970-01-01 00:00:00.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/fido2/nfc.py 2019-06-17 13:09:04.000000000 +0000 @@ -0,0 +1,170 @@ +# Copyright (c) 2019 Yubico AB +# Copyright (c) 2019 Oleg Moiseenko +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or +# without modification, are permitted provided that the following +# conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, unicode_literals + +from .ctap import CtapDevice, CtapError, STATUS +from .hid import CAPABILITY, CTAPHID +from .pcsc import PCSCDevice +from smartcard.Exceptions import CardConnectionException +from threading import Event +import struct +import six + + +AID_FIDO = b'\xa0\x00\x00\x06\x47\x2f\x00\x01' +SW_SUCCESS = (0x90, 0x00) +SW_UPDATE = (0x91, 0x00) +SW1_MORE_DATA = 0x61 + + +class CardSelectException(Exception): + """can't select u2f/fido2 application on the card""" + pass + + +class CtapNfcDevice(CtapDevice): + """ + CtapDevice implementation using the pcsc NFC transport. + """ + + def __init__(self, dev): + self._dev = dev + self._dev.connect() + self._capabilities = 0 + + result, sw1, sw2 = self._dev.select_applet(AID_FIDO) + if (sw1, sw2) != SW_SUCCESS: + raise CardSelectException('Select error') + + if result == b'U2F_V2': + self._capabilities |= CAPABILITY.NMSG + try: # Probe for CTAP2 by calling GET_INFO + self.call(CTAPHID.CBOR, b'\x04') + self._capabilities |= CAPABILITY.CBOR + except CtapError: + pass + + @property + def pcsc_device(self): + return self._dev + + def __repr__(self): + return 'CtapNfcDevice(%s)' % self._dev.reader.name + + @property + def version(self): + """CTAP NFC protocol version. + :rtype: int + """ + return 2 if self._capabilities & CAPABILITY.CBOR else 1 + + @property + def capabilities(self): + """Capabilities supported by the device.""" + return self._capabilities + + def _chain_apdus(self, cla, ins, p1, p2, data=b''): + while len(data) > 250: + to_send, data = data[:250], data[250:] + header = struct.pack('!BBBBB', 0x90, ins, p1, p2, len(to_send)) + resp, sw1, sw2 = self._dev.apdu_exchange(header + to_send) + if (sw1, sw2) != SW_SUCCESS: + return resp, sw1, sw2 + apdu = struct.pack('!BBBB', cla, ins, p1, p2) + if data: + apdu += struct.pack('!B', len(data)) + data + resp, sw1, sw2 = self._dev.apdu_exchange(apdu + b'\x00') + while sw1 == SW1_MORE_DATA: + apdu = b'\x00\xc0\x00\x00' + struct.pack('!B', sw2) # sw2 == le + lres, sw1, sw2 = self._dev.apdu_exchange(apdu) + resp += lres + return resp, sw1, sw2 + + def _call_apdu(self, apdu): + if len(apdu) >= 7 and six.indexbytes(apdu, 4) == 0: + # Extended APDU + data_len = struct.unpack('!H', apdu[5:7])[0] + data = apdu[7:7+data_len] + else: + # Short APDU + data_len = six.indexbytes(apdu, 4) + data = apdu[5:5+data_len] + (cla, ins, p1, p2) = six.iterbytes(apdu[:4]) + + resp, sw1, sw2 = self._chain_apdus(cla, ins, p1, p2, data) + return resp + struct.pack('!BB', sw1, sw2) + + def _call_cbor(self, data=b'', event=None, on_keepalive=None): + event = event or Event() + # NFCCTAP_MSG + resp, sw1, sw2 = self._chain_apdus(0x80, 0x10, 0x80, 0x00, data) + last_ka = None + + while not event.is_set(): + while (sw1, sw2) == SW_UPDATE: + ka_status = six.indexbytes(resp, 0) + if on_keepalive and last_ka != ka_status: + try: + ka_status = STATUS(ka_status) + except ValueError: + pass # Unknown status value + last_ka = ka_status + on_keepalive(ka_status) + + # NFCCTAP_GETRESPONSE + resp, sw1, sw2 = self._chain_apdus(0x80, 0x11, 0x00, 0x00, b'') + + if (sw1, sw2) != SW_SUCCESS: + raise CtapError(CtapError.ERR.OTHER) # TODO: Map from SW error + + return resp + + raise CtapError(CtapError.ERR.KEEPALIVE_CANCEL) + + def call(self, cmd, data=b'', event=None, on_keepalive=None): + if cmd == CTAPHID.MSG: + return self._call_apdu(data) + elif cmd == CTAPHID.CBOR: + return self._call_cbor(data, event, on_keepalive) + else: + raise CtapError(CtapError.ERR.INVALID_COMMAND) + + @classmethod # selector='CL' + def list_devices(cls, selector='', pcsc_device=PCSCDevice): + """ + Returns list of readers in the system. Iterator. + :param selector: + :param pcsc_device: device to work with. PCSCDevice by default. + :return: iterator. next reader + """ + for d in pcsc_device.list_devices(selector): + try: + yield cls(d) + except CardConnectionException: + pass diff -Nru python-fido2-0.6.0~ppa1~disco1/fido2/pcsc.py python-fido2-0.7.0~ppa1~disco1/fido2/pcsc.py --- python-fido2-0.6.0~ppa1~disco1/fido2/pcsc.py 1970-01-01 00:00:00.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/fido2/pcsc.py 2019-06-17 13:09:04.000000000 +0000 @@ -0,0 +1,223 @@ +# Copyright (c) 2019 Yubico AB +# Copyright (c) 2019 Oleg Moiseenko +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or +# without modification, are permitted provided that the following +# conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, unicode_literals + +from .ctap import CtapDevice, CtapError, STATUS +from .hid import CAPABILITY, CTAPHID +from smartcard import System +from smartcard.pcsc.PCSCExceptions import ListReadersException +from smartcard.pcsc.PCSCContext import PCSCContext + +from binascii import b2a_hex +from threading import Event +import struct +import six +import logging + + +AID_FIDO = b'\xa0\x00\x00\x06\x47\x2f\x00\x01' +SW_SUCCESS = (0x90, 0x00) +SW_UPDATE = (0x91, 0x00) +SW1_MORE_DATA = 0x61 + + +logger = logging.getLogger(__name__) + + +class CtapPcscDevice(CtapDevice): + """ + CtapDevice implementation using pyscard (PCSC). + + This class is intended for use with NFC readers. + """ + + def __init__(self, connection, name): + self._capabilities = 0 + self._conn = connection + self._conn.connect() + self._name = name + self._select() + + try: # Probe for CTAP2 by calling GET_INFO + self.call(CTAPHID.CBOR, b'\x04') + self._capabilities |= CAPABILITY.CBOR + except CtapError: + if self._capabilities == 0: + raise ValueError('Unsupported device') + + def __repr__(self): + return 'CtapPcscDevice(%s)' % self._name + + @property + def version(self): + """CTAPHID protocol version. + :rtype: int + """ + return 2 if self._capabilities & CAPABILITY.CBOR else 1 + + @property + def capabilities(self): + """Capabilities supported by the device.""" + return self._capabilities + + def get_atr(self): + """Get the ATR/ATS of the connected card.""" + return self._conn.getATR() + + def apdu_exchange(self, apdu, protocol=None): + """Exchange data with smart card. + + :param apdu: byte string. data to exchange with card + :return: byte string. response from card + """ + + logger.debug('apdu %s', b2a_hex(apdu)) + resp, sw1, sw2 = self._conn.transmit( + list(six.iterbytes(apdu)), + protocol + ) + response = bytes(bytearray(resp)) + logger.debug('response [0x%04X] %s', sw1 << 8 + sw2, b2a_hex(response)) + + return response, sw1, sw2 + + def control_exchange(self, control_code, control_data=b''): + """Sends control sequence to reader's driver. + + :param control_code: int. code to send to reader driver. + :param control_data: byte string. data to send to driver + :return: byte string. response + """ + + logger.debug('control %s', b2a_hex(control_data)) + response = self._conn.control( + control_code, + list(six.iterbytes(control_data)) + ) + response = bytes(bytearray(response)) + logger.debug('response %s', b2a_hex(response)) + + return response + + def _select(self): + apdu = b'\x00\xa4\x04\x00' + struct.pack('!B', len(AID_FIDO)) + AID_FIDO + resp, sw1, sw2 = self.apdu_exchange(apdu) + if (sw1, sw2) != SW_SUCCESS: + raise ValueError('FIDO applet selection failure.') + if resp == b'U2F_V2': + self._capabilities |= 0x08 + + def _chain_apdus(self, cla, ins, p1, p2, data=b''): + while len(data) > 250: + to_send, data = data[:250], data[250:] + header = struct.pack('!BBBBB', 0x90, ins, p1, p2, len(to_send)) + resp, sw1, sw2 = self.apdu_exchange(header + to_send) + if (sw1, sw2) != SW_SUCCESS: + return resp, sw1, sw2 + apdu = struct.pack('!BBBB', cla, ins, p1, p2) + if data: + apdu += struct.pack('!B', len(data)) + data + resp, sw1, sw2 = self.apdu_exchange(apdu + b'\x00') + while sw1 == SW1_MORE_DATA: + apdu = b'\x00\xc0\x00\x00' + struct.pack('!B', sw2) # sw2 == le + lres, sw1, sw2 = self.apdu_exchange(apdu) + resp += lres + return resp, sw1, sw2 + + def _call_apdu(self, apdu): + if len(apdu) >= 7 and six.indexbytes(apdu, 4) == 0: + # Extended APDU + data_len = struct.unpack('!H', apdu[5:7])[0] + data = apdu[7:7+data_len] + else: + # Short APDU + data_len = six.indexbytes(apdu, 4) + data = apdu[5:5+data_len] + (cla, ins, p1, p2) = six.iterbytes(apdu[:4]) + + resp, sw1, sw2 = self._chain_apdus(cla, ins, p1, p2, data) + return resp + struct.pack('!BB', sw1, sw2) + + def _call_cbor(self, data=b'', event=None, on_keepalive=None): + event = event or Event() + # NFCCTAP_MSG + resp, sw1, sw2 = self._chain_apdus(0x80, 0x10, 0x80, 0x00, data) + last_ka = None + + while not event.is_set(): + while (sw1, sw2) == SW_UPDATE: + ka_status = six.indexbytes(resp, 0) + if on_keepalive and last_ka != ka_status: + try: + ka_status = STATUS(ka_status) + except ValueError: + pass # Unknown status value + last_ka = ka_status + on_keepalive(ka_status) + + # NFCCTAP_GETRESPONSE + resp, sw1, sw2 = self._chain_apdus(0x80, 0x11, 0x00, 0x00) + + if (sw1, sw2) != SW_SUCCESS: + raise CtapError(CtapError.ERR.OTHER) # TODO: Map from SW error + + return resp + + raise CtapError(CtapError.ERR.KEEPALIVE_CANCEL) + + def call(self, cmd, data=b'', event=None, on_keepalive=None): + if cmd == CTAPHID.CBOR: + return self._call_cbor(data, event, on_keepalive) + elif cmd == CTAPHID.MSG: + return self._call_apdu(data) + else: + raise CtapError(CtapError.ERR.INVALID_COMMAND) + + def close(self): + self._conn.disconnect() + + @classmethod + def list_devices(cls, name=''): + for reader in _list_readers(): + if name in reader.name: + try: + yield cls(reader.createConnection(), reader.name) + except Exception as e: + logger.debug('Error %r', e) + + +def _list_readers(): + try: + return System.readers() + except ListReadersException: + # If the PCSC system has restarted the context might be stale, try + # forcing a new context (This happens on Windows if the last reader is + # removed): + PCSCContext.instance = None + return System.readers() diff -Nru python-fido2-0.6.0~ppa1~disco1/NEWS python-fido2-0.7.0~ppa1~disco1/NEWS --- python-fido2-0.6.0~ppa1~disco1/NEWS 2019-05-10 11:21:28.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/NEWS 2019-06-17 13:09:04.000000000 +0000 @@ -1,3 +1,9 @@ +* Version 0.7.0 (released 2019-06-17) + ** Add support for NFC devices using PCSC. + ** Add support for the hmac-secret Authenticator extension. + ** Honor max credential ID length and number of credentials to Authenticator. + ** Add close() method to CTAP devices to explicitly release their resources. + * Version 0.6.0 (released 2019-05-10) ** Don't fail if CTAP2 Info contains unknown fields. ** Replace cbor loads/dumps functions with encode/decode/decode_from. diff -Nru python-fido2-0.6.0~ppa1~disco1/Pipfile python-fido2-0.7.0~ppa1~disco1/Pipfile --- python-fido2-0.6.0~ppa1~disco1/Pipfile 2019-05-10 11:21:28.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/Pipfile 2019-06-17 13:09:04.000000000 +0000 @@ -6,9 +6,10 @@ [dev-packages] "mock" = ">=1.0.1" "pyfakefs" = ">=3.4" +pyscard = "*" [packages] -"fido2" = {editable = true, path = "."} +fido2 = {editable = true,path = "."} [scripts] test = "python setup.py test" diff -Nru python-fido2-0.6.0~ppa1~disco1/Pipfile.lock python-fido2-0.7.0~ppa1~disco1/Pipfile.lock --- python-fido2-0.6.0~ppa1~disco1/Pipfile.lock 2019-05-10 11:21:28.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/Pipfile.lock 2019-06-17 13:09:04.000000000 +0000 @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "7dc184b9dfca959d29b1ce8958d8b4e3b8cde3b9c308562e834bc10d988ae174" + "sha256": "9844396d64ad7e8b7e1f75f6f43844cb62bfc303c27ee57862dbda4dcb8086d5" }, "pipfile-spec": 6, "requires": {}, @@ -56,27 +56,24 @@ }, "cryptography": { "hashes": [ - "sha256:066f815f1fe46020877c5983a7e747ae140f517f1b09030ec098503575265ce1", - "sha256:210210d9df0afba9e000636e97810117dc55b7157c903a55716bb73e3ae07705", - "sha256:26c821cbeb683facb966045e2064303029d572a87ee69ca5a1bf54bf55f93ca6", - "sha256:2afb83308dc5c5255149ff7d3fb9964f7c9ee3d59b603ec18ccf5b0a8852e2b1", - "sha256:2db34e5c45988f36f7a08a7ab2b69638994a8923853dec2d4af121f689c66dc8", - "sha256:409c4653e0f719fa78febcb71ac417076ae5e20160aec7270c91d009837b9151", - "sha256:45a4f4cf4f4e6a55c8128f8b76b4c057027b27d4c67e3fe157fa02f27e37830d", - "sha256:48eab46ef38faf1031e58dfcc9c3e71756a1108f4c9c966150b605d4a1a7f659", - "sha256:6b9e0ae298ab20d371fc26e2129fd683cfc0cfde4d157c6341722de645146537", - "sha256:6c4778afe50f413707f604828c1ad1ff81fadf6c110cb669579dea7e2e98a75e", - "sha256:8c33fb99025d353c9520141f8bc989c2134a1f76bac6369cea060812f5b5c2bb", - "sha256:9873a1760a274b620a135054b756f9f218fa61ca030e42df31b409f0fb738b6c", - "sha256:9b069768c627f3f5623b1cbd3248c5e7e92aec62f4c98827059eed7053138cc9", - "sha256:9e4ce27a507e4886efbd3c32d120db5089b906979a4debf1d5939ec01b9dd6c5", - "sha256:acb424eaca214cb08735f1a744eceb97d014de6530c1ea23beb86d9c6f13c2ad", - "sha256:c8181c7d77388fe26ab8418bb088b1a1ef5fde058c6926790c8a0a3d94075a4a", - "sha256:d4afbb0840f489b60f5a580a41a1b9c3622e08ecb5eec8614d4fb4cd914c4460", - "sha256:d9ed28030797c00f4bc43c86bf819266c76a5ea61d006cd4078a93ebf7da6bfd", - "sha256:e603aa7bb52e4e8ed4119a58a03b60323918467ef209e6ff9db3ac382e5cf2c6" + "sha256:24b61e5fcb506424d3ec4e18bca995833839bf13c59fc43e530e488f28d46b8c", + "sha256:25dd1581a183e9e7a806fe0543f485103232f940fcfc301db65e630512cce643", + "sha256:3452bba7c21c69f2df772762be0066c7ed5dc65df494a1d53a58b683a83e1216", + "sha256:41a0be220dd1ed9e998f5891948306eb8c812b512dc398e5a01846d855050799", + "sha256:5751d8a11b956fbfa314f6553d186b94aa70fdb03d8a4d4f1c82dcacf0cbe28a", + "sha256:5f61c7d749048fa6e3322258b4263463bfccefecb0dd731b6561cb617a1d9bb9", + "sha256:72e24c521fa2106f19623a3851e9f89ddfdeb9ac63871c7643790f872a305dfc", + "sha256:7b97ae6ef5cba2e3bb14256625423413d5ce8d1abb91d4f29b6d1a081da765f8", + "sha256:961e886d8a3590fd2c723cf07be14e2a91cf53c25f02435c04d39e90780e3b53", + "sha256:96d8473848e984184b6728e2c9d391482008646276c3ff084a1bd89e15ff53a1", + "sha256:ae536da50c7ad1e002c3eee101871d93abdc90d9c5f651818450a0d3af718609", + "sha256:b0db0cecf396033abb4a93c95d1602f268b3a68bb0a9cc06a7cff587bb9a7292", + "sha256:cfee9164954c186b191b91d4193989ca994703b2fff406f71cf454a2d3c7327e", + "sha256:e6347742ac8f35ded4a46ff835c60e68c22a536a8ae5c4422966d06946b6d4c6", + "sha256:f27d93f0139a3c056172ebb5d4f9056e770fdf0206c2f422ff2ebbad142e09ed", + "sha256:f57b76e46a58b63d1c6375017f4564a28f19a5ca912691fd2e4261b3414b618d" ], - "version": "==2.6.1" + "version": "==2.7" }, "enum34": { "hashes": [ @@ -92,6 +89,14 @@ "editable": true, "path": "." }, + "funcsigs": { + "hashes": [ + "sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca", + "sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50" + ], + "markers": "python_version < '3.3'", + "version": "==1.0.2" + }, "ipaddress": { "hashes": [ "sha256:64b28eec5e78e7510698f6d4da08800a5c575caa4a286c93d651c5d3ff7b6794", @@ -100,6 +105,14 @@ "markers": "python_version < '3'", "version": "==1.0.22" }, + "mock": { + "hashes": [ + "sha256:83657d894c90d5681d62155c82bda9c1187827525880eda8ff5df4ec813437c3", + "sha256:d157e52d4e5b938c550f39eb2fd15610db062441a9c2747d3dbfa9298211d0f8" + ], + "index": "pypi", + "version": "==3.0.5" + }, "pycparser": { "hashes": [ "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" @@ -139,6 +152,16 @@ "index": "pypi", "version": "==3.5.8" }, + "pyscard": { + "hashes": [ + "sha256:3786b06cf41a24cf22879016d009a8cf8d1664cd93919d54e6903f7e385c9873", + "sha256:61250b43425d30395ed290a0a44f10f5d933026bb406e1953554d47138776ef3", + "sha256:a8ab0b20b781e01c4ac6e1121c47a92096a64fa243dd26ab4f6a9356c8c95263", + "sha256:f59dc7ee467b210094e64c923e1c7f5e8e9501a672fc0c8f2cd958153e00d095" + ], + "index": "pypi", + "version": "==1.9.8" + }, "six": { "hashes": [ "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", diff -Nru python-fido2-0.6.0~ppa1~disco1/README.adoc python-fido2-0.7.0~ppa1~disco1/README.adoc --- python-fido2-0.6.0~ppa1~disco1/README.adoc 2019-05-10 11:21:28.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/README.adoc 2019-06-17 13:09:04.000000000 +0000 @@ -49,6 +49,11 @@ # pip install fido2 +To install the dependencies required for communication with NFC Authenticators, +instead use: + + # pip install fido2[pcsc] + Under Linux you will need to add a Udev rule to be able to access the FIDO device, or run as root. For example, the Udev rule may contain the following: @@ -67,6 +72,10 @@ This project depends on Cryptography. For instructions on installing this dependency, see https://cryptography.io/en/latest/installation/. +NFC support is optionally available via PCSC, using the pyscard library. For +instructions on installing this dependency, see +https://github.com/LudovicRousseau/pyscard/blob/master/INSTALL.md. + === Development For development of the library, we recommend using `pipenv`. To set up the dev diff -Nru python-fido2-0.6.0~ppa1~disco1/setup.py python-fido2-0.7.0~ppa1~disco1/setup.py --- python-fido2-0.6.0~ppa1~disco1/setup.py 2019-05-10 11:21:28.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/setup.py 2019-06-17 13:09:04.000000000 +0000 @@ -50,7 +50,8 @@ 'cryptography>=1.5', ], extras_require={ - ':python_version < "3.4"': ['enum34'] + ':python_version < "3.4"': ['enum34'], + 'pcsc': ['pyscard'] }, test_suite='test', tests_require=['mock>=1.0.1', 'pyfakefs>=3.4'], diff -Nru python-fido2-0.6.0~ppa1~disco1/test/test_cose.py python-fido2-0.7.0~ppa1~disco1/test/test_cose.py --- python-fido2-0.6.0~ppa1~disco1/test/test_cose.py 2019-05-10 11:21:28.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/test/test_cose.py 2019-06-17 13:09:04.000000000 +0000 @@ -30,16 +30,11 @@ from fido2 import cbor from fido2.cose import CoseKey, ES256, RS256, EdDSA, UnsupportedKey +from cryptography.exceptions import UnsupportedAlgorithm from binascii import a2b_hex import unittest -try: - import __pypy__ # noqa - PYPY = True -except ImportError: - PYPY = False - _ES256_KEY = a2b_hex(b'A5010203262001215820A5FD5CE1B1C458C530A54FA61B31BF6B04BE8B97AFDE54DD8CBB69275A8A1BE1225820FA3A3231DD9DEED9D1897BE5A6228C59501E4BCD12975D3DFF730F01278EA61C') # noqa _RS256_KEY = a2b_hex(b'A401030339010020590100B610DCE84B65029FAE24F7BF8A1730D37BC91435642A628E691E9B030BF3F7CEC59FF91CBE82C54DE16C136FA4FA8A58939B5A950B32E03073592FEC8D8B33601C04F70E5E2D5CF7B4E805E1990EA5A86928A1B390EB9026527933ACC03E6E41DC0BE40AA5EB7B9B460743E4DD80895A758FB3F3F794E5E9B8310D3A60C28F2410D95CF6E732749A243A30475267628B456DE770BC2185BBED1D451ECB0062A3D132C0E4D842E0DDF93A444A3EE33A85C2E913156361713155F1F1DC64E8E68ED176466553BBDE669EB82810B104CB4407D32AE6316C3BD6F382EC3AE2C5FD49304986D64D92ED11C25B6C5CF1287233545A987E9A3E169F99790603DBA5C8AD2143010001') # noqa @@ -78,7 +73,6 @@ a2b_hex(b'071B707D11F0E7F62861DFACA89C4E674321AD8A6E329FDD40C7D6971348FBB0514E7B2B0EFE215BAAC0365C4124A808F8180D6575B710E7C01DAE8F052D0C5A2CE82F487C656E7AD824F3D699BE389ADDDE2CBF39E87A8955E93202BAE8830AB4139A7688DFDAD849F1BB689F3852BA05BED70897553CC44704F6941FD1467AD6A46B4DAB503716D386FE7B398E78E0A5A8C4040539D2C9BFA37E4D94F96091FFD1D194DE2CA58E9124A39757F013801421E09BD261ADA31992A8B0386A80AF51A87BD0CEE8FDAB0D4651477670D4C7B245489BED30A57B83964DB79418D5A4F5F2E5ABCA274426C9F90B007A962AE15DFF7343AF9E110746E2DB9226D785C6') # noqa ) - @unittest.skipIf(PYPY, 'EdDSA not supported under pypy') def test_EdDSA_parse_verify(self): key = CoseKey.parse(cbor.decode(_EdDSA_KEY)) self.assertIsInstance(key, EdDSA) @@ -88,10 +82,13 @@ -1: 6, -2: a2b_hex('EE9B21803405D3CF45601E58B6F4C06EA93862DE87D3AF903C5870A5016E86F5') # noqa }) - key.verify( - a2b_hex(b'a379a6f6eeafb9a55e378c118034e2751e682fab9f2d30ab13d2125586ce1947010000000500a11a323057d1103784ddff99a354ddd42348c2f00e88d8977b916cabf92268'), # noqa - a2b_hex(b'e8c927ef1a57c738ff4ba8d6f90e06d837a5219eee47991f96b126b0685d512520c9c2eedebe4b88ff2de2b19cb5f8686efc7c4261e9ed1cb3ac5de50869be0a') # noqa - ) + try: + key.verify( + a2b_hex(b'a379a6f6eeafb9a55e378c118034e2751e682fab9f2d30ab13d2125586ce1947010000000500a11a323057d1103784ddff99a354ddd42348c2f00e88d8977b916cabf92268'), # noqa + a2b_hex(b'e8c927ef1a57c738ff4ba8d6f90e06d837a5219eee47991f96b126b0685d512520c9c2eedebe4b88ff2de2b19cb5f8686efc7c4261e9ed1cb3ac5de50869be0a') # noqa + ) + except UnsupportedAlgorithm: + self.skipTest('EdDSA support missing') def test_unsupported_key(self): key = CoseKey.parse({1: 4711, 3: 4712, -1: b'123', -2: b'456'}) diff -Nru python-fido2-0.6.0~ppa1~disco1/test/test_ctap2.py python-fido2-0.7.0~ppa1~disco1/test/test_ctap2.py --- python-fido2-0.6.0~ppa1~disco1/test/test_ctap2.py 2019-05-10 11:21:28.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/test/test_ctap2.py 2019-06-17 13:09:04.000000000 +0000 @@ -253,7 +253,7 @@ } } - key_agreement, shared = prot._init_shared_secret() + key_agreement, shared = prot.get_shared_secret() self.assertEqual(shared, SHARED) self.assertEqual(key_agreement[-2], EC_PUB_X) @@ -261,7 +261,7 @@ def test_get_pin_token(self): prot = PinProtocolV1(mock.MagicMock()) - prot._init_shared_secret = mock.Mock(return_value=({}, SHARED)) + prot.get_shared_secret = mock.Mock(return_value=({}, SHARED)) prot.ctap.client_pin.return_value = { 2: TOKEN_ENC } @@ -273,7 +273,7 @@ def test_set_pin(self): prot = PinProtocolV1(mock.MagicMock()) - prot._init_shared_secret = mock.Mock(return_value=({}, SHARED)) + prot.get_shared_secret = mock.Mock(return_value=({}, SHARED)) prot.set_pin('1234') prot.ctap.client_pin.assert_called_with( @@ -286,7 +286,7 @@ def test_change_pin(self): prot = PinProtocolV1(mock.MagicMock()) - prot._init_shared_secret = mock.Mock(return_value=({}, SHARED)) + prot.get_shared_secret = mock.Mock(return_value=({}, SHARED)) prot.change_pin('1234', '4321') prot.ctap.client_pin.assert_called_with( diff -Nru python-fido2-0.6.0~ppa1~disco1/test/test_pcsc.py python-fido2-0.7.0~ppa1~disco1/test/test_pcsc.py --- python-fido2-0.6.0~ppa1~disco1/test/test_pcsc.py 1970-01-01 00:00:00.000000000 +0000 +++ python-fido2-0.7.0~ppa1~disco1/test/test_pcsc.py 2019-06-17 13:09:04.000000000 +0000 @@ -0,0 +1,100 @@ +# Copyright (c) 2019 Yubico AB +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or +# without modification, are permitted provided that the following +# conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, unicode_literals + +import unittest +import mock +import sys +from fido2.hid import CTAPHID + +sys.modules['smartcard'] = mock.Mock() +sys.modules['smartcard.Exceptions'] = mock.Mock() +sys.modules['smartcard.System'] = mock.Mock() +sys.modules['smartcard.pcsc'] = mock.Mock() +sys.modules['smartcard.pcsc.PCSCExceptions'] = mock.Mock() +sys.modules['smartcard.pcsc.PCSCContext'] = mock.Mock() +from fido2.pcsc import CtapPcscDevice # noqa + + +class PcscTest(unittest.TestCase): + + def test_pcsc_call_cbor(self): + connection = mock.Mock() + connection.transmit.side_effect = [ + (b'U2F_V2', 0x90, 0x00), + (b'', 0x90, 0x00) + ] + + CtapPcscDevice(connection, 'Mock') + + connection.transmit.assert_called_with( + [0x80, 0x10, 0x80, 0x00, 0x01, 0x04, 0x00], + None + ) + + def test_pcsc_call_u2f(self): + connection = mock.Mock() + connection.transmit.side_effect = [ + (b'U2F_V2', 0x90, 0x00), + (b'', 0x90, 0x00), + (b'u2f_resp', 0x90, 0x00) + ] + + dev = CtapPcscDevice(connection, 'Mock') + res = dev.call(CTAPHID.MSG, + b'\x00\x01\x00\x00\x05' + + b'\x01' * 5 + + b'\x00') + + connection.transmit.assert_called_with( + [0x00, 0x01, 0x00, 0x00, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00], + None + ) + self.assertEqual(res, b'u2f_resp\x90\x00') + + def test_pcsc_call_version_2(self): + connection = mock.Mock() + connection.transmit.side_effect = [ + (b'U2F_V2', 0x90, 0x00), + (b'', 0x90, 0x00), + ] + + dev = CtapPcscDevice(connection, 'Mock') + + self.assertEqual(dev.version, 2) + + def test_pcsc_call_version_1(self): + connection = mock.Mock() + connection.transmit.side_effect = [ + (b'U2F_V2', 0x90, 0x00), + (b'', 0x63, 0x85), + ] + + dev = CtapPcscDevice(connection, 'Mock') + + self.assertEqual(dev.version, 1)