├── .coveragerc ├── .gitignore ├── .gitmodules ├── .travis.yml ├── BLURB ├── COPYING ├── MANIFEST.in ├── NEWS ├── README ├── README.adoc ├── dev-requirements-2.7.txt ├── dev-requirements-3.x.txt ├── dev-requirements.py ├── man ├── u2f-authenticate.1.adoc └── u2f-register.1.adoc ├── setup.py ├── test ├── __init__.py ├── test_exc.py ├── test_hid.py ├── test_hid_transport.py ├── test_reg_auth.py ├── test_soft.py ├── test_u2f.py ├── test_u2f_v2.py └── test_utils.py ├── tox.ini └── u2flib_host ├── __init__.py ├── appid.py ├── authenticate.py ├── constants.py ├── device.py ├── exc.py ├── hid_transport.py ├── register.py ├── soft.py ├── u2f.py ├── u2f_v2.py ├── utils.py └── yubicommon /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = u2flib_host 3 | 4 | [report] 5 | exclude_lines = 6 | pragma: no cover 7 | if __name__ == '__main__': 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg 3 | *.egg-info 4 | build/ 5 | dist/ 6 | .eggs/ 7 | .ropeproject/ 8 | ChangeLog 9 | man/*.1 10 | 11 | # Unit test / coverage reports 12 | htmlcov/ 13 | .tox/ 14 | .coverage 15 | .coverage.* 16 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/yubicommon"] 2 | path = vendor/yubicommon 3 | url = https://github.com/Yubico/python-yubicommon.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | 4 | python: 5 | - 2.7 6 | - 3.4 7 | - 3.5 8 | - 3.6 9 | 10 | cache: 11 | directories: 12 | - $HOME/.cache/pip 13 | 14 | addons: 15 | apt: 16 | packages: 17 | - libudev-dev 18 | - libusb-1.0-0-dev 19 | - libffi-dev 20 | - libssl-dev 21 | - swig 22 | 23 | install: 24 | - pip install "pip>=7.0.2" wheel 25 | - $TRAVIS_BUILD_DIR/dev-requirements.py 26 | - pip install -e . 27 | 28 | script: 29 | - coverage run setup.py test 30 | 31 | after_success: 32 | - coveralls 33 | -------------------------------------------------------------------------------- /BLURB: -------------------------------------------------------------------------------- 1 | Author: Yubico 2 | Basename: python-u2flib-host 3 | Homepage: http://opensource.yubico.com/python-u2flib-host/ 4 | License: GPL-3.0+ 5 | Name: Python U2Flib Host 6 | Project: python-u2flib-host 7 | Summary: Python based U2F host library 8 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Yubico AB 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include COPYING 2 | include NEWS 3 | include ChangeLog 4 | include man/* 5 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | * Version 3.0.3 (released 2018-03-16) 2 | ** Add CTAP HID capability bits to the HIDDevice object. 3 | ** Change the max length of response data to be compatible with more devices. 4 | 5 | * Version 3.0.2 (released 2017-06-16) 6 | ** Bugfix: Continue execution with next device if one device fails to open. 7 | 8 | * Version 3.0.1 (released 2017-06-14) 9 | ** Improved Python 3 compatibility. 10 | 11 | * Version 3.0.0 (released 2016-04-11) 12 | ** Added support for Python 3.3+. 13 | ** utils.rand_bytes() now sources bytes from os.urandom(). 14 | ** utils.websafe_encode() now returns text strings (unicode() on Python 2.x, 15 | str() on Python 3.x). 16 | ** utils.H() has been removed (just use hashlib.sha256() directly instead). 17 | ** soft.SoftU2FDevice() now stores keys, key handles and app params as text 18 | strings. 19 | ** Replace M2Crypto with Cryptography. 20 | 21 | * Version 2.2.1 (released 2015-09-08) 22 | ** Fix setup.py so that installation works properly. 23 | ** Packaging related improvements. 24 | 25 | * Version 2.2.0 (released 2015-07-01) 26 | ** License change: Changed license to BSD 2-clause. 27 | ** Check both usage page and usage. 28 | ** Prefer hidraw backend over libusb, if available. 29 | 30 | * Version 2.1.0 (released 2015-04-13) 31 | ** Use usage page based filtering if possible. 32 | ** Added support for newer YubiKey devices. 33 | ** Fix majorVersion in AppID facet verification. 34 | 35 | * Version 2.0.1 (released 2014-10-28) 36 | ** Ignore failures while listing devices, as other transport might be working. 37 | ** Support for Security Key by Yubico 38 | 39 | * Version 2.0.0 (released 2014-09-26) 40 | ** Updated to U2F_V2 standard. 41 | ** Removed old draft versions. 42 | 43 | * Version 1.1.0 (released 2014-04-15) 44 | ** Added support for the HID transport. 45 | 46 | * Version 1.0.0 (released 2014-02-18) 47 | ** First public release. 48 | ** Added support for U2F_V2. 49 | ** Added soft U2F device for testing purposes. 50 | 51 | * Version 0.0.2 (released 2013-10-07) 52 | ** Better instructions in README 53 | ** Includes executables missing from 0.0.1 54 | 55 | * Version 0.0.1 (released 2013-08-22) 56 | ** Initial internal release! 57 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | == u2flib-host == 2 | Provides library functionality for communicating with a U2F device over USB. 3 | 4 | # NOTE: This project is superseded by https://github.com/Yubico/python-fido2 5 | 6 | Two executables are provided, u2f-register and u2f-authenticate, which support 7 | the register and authenticated commands of U2F as defined in the 8 | http://fidoalliance.org/specifications/download[FIDO specifications]. 9 | 10 | === License === 11 | This project is licensed under the BSD 2-clause license. 12 | See the COPYING file for the full license text. 13 | 14 | === Installation === 15 | u2flib-host is installable by running the following command: 16 | 17 | # pip install python-u2flib-host 18 | 19 | Under Linux you will need to add a Udev rule to be able to access the U2F 20 | device, or run as root. For example, the Udev rule may contain the following: 21 | 22 | ---- 23 | #Udev rule for allowing HID access to Yubico devices for U2F support. 24 | 25 | KERNEL=="hidraw*", SUBSYSTEM=="hidraw", \ 26 | MODE="0664", GROUP="plugdev", ATTRS{idVendor}=="1050" 27 | ---- 28 | 29 | === Dependencies === 30 | u2flib-host is compatible with CPython 2.7, 3.3 onwards. 31 | 32 | To support HID devices, u2flib-host uses hidapi, which has a few dependencies 33 | for building. On a Debian based system, run the following command before 34 | installation: 35 | 36 | # apt-get install build-essential python-dev cython libusb-1.0-0-dev \ 37 | libudev-dev 38 | 39 | The soft U2F device implementation requires link:https://pypi.python.org/pypi/cryptography[cryptography], the build 40 | dependencies can be installed with 41 | 42 | # apt-get install libffi-dev libssl-dev 43 | 44 | === Examples === 45 | ==== Library use ==== 46 | 47 | [source, python] 48 | ---- 49 | from u2flib_host import u2f, exc 50 | 51 | # Enumerate available devices 52 | devices = u2f.list_devices() 53 | 54 | for device in devices: 55 | # The with block ensures that the device is opened and closed. 56 | with device as dev: 57 | # Register the device with some service 58 | registrationResponse = u2f.register(device, registrationRequest, facet) 59 | ---- 60 | 61 | ==== Executable use ==== 62 | The examples below use the soft U2F device to register and authenticate against 63 | the u2f_server example server from the 64 | http://developers.yubico.com/python-u2flib-server[python-u2flib-server] project. 65 | See that project for more details. 66 | The register step will create a new U2F key pair and store the credential in 67 | the soft_device.json file. The authenticate step will use this credential to 68 | sign a challenge given by the server. 69 | 70 | ===== Registration ===== 71 | The u2f-register command takes a RegisterRequest JSON object as input, 72 | registering the attached device and returns the registration response as 73 | output. 74 | 75 | ---- 76 | $ u2f-register -s soft_device.json http://localhost:8081 77 | Enter RegisterRequest JSON data... 78 | {"challenge": "K0aDxsacDNqrzlaGyLZoFYbXvCJcdIhq0SSaMz-lsV4", "version": "U2F_V2", "appId": "http://localhost:8081"} 79 | 80 | Touch the U2F device you wish to register... 81 | [{"clientData": "eyJvcmlnaW4iOiAiaHR0cDovL2xvY2FsaG9zdDo4MDgxIiwgImNoYWxsZW5nZSI6ICJLMGFEeHNhY0ROcXJ6bGFHeUxab0ZZYlh2Q0pjZElocTBTU2FNei1sc1Y0IiwgInR5cCI6ICJuYXZpZ2F0b3IuaWQuZmluaXNoRW5yb2xsbWVudCJ9", "registrationData": "BQTGnJVILHhzuTKg2XClCM5TJjF2WeK4fp9i6fj3VywzOk3d-O1sNaapAUPh-1GxoVCMY6s_jimP-nKqnZT-MGOCQIGD9Hs4qBCXMbfOPfzuB5zhFcOD95ddve67HXV8QeyPDKPZS5zDogvWyl8l4Tv2XRWGo4_6cAPPM4dPZcMreagwggGHMIIBLqADAgECAgkAmb7osQyi7BwwCQYHKoZIzj0EATAhMR8wHQYDVQQDDBZZdWJpY28gVTJGIFNvZnQgRGV2aWNlMB4XDTEzMDcxNzE0MjEwM1oXDTE2MDcxNjE0MjEwM1owITEfMB0GA1UEAwwWWXViaWNvIFUyRiBTb2Z0IERldmljZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDvhl91zfpg9n7DeCedcQ8gGXUnemiXoi-JEAxz-EIhkVsMPAyzhtJZ4V3CqMZ-MOUgICt2aMxacMX9cIa8dgS2jUDBOMB0GA1UdDgQWBBQNqL-TV04iaO6mS5tjGE6ShfexnjAfBgNVHSMEGDAWgBQNqL-TV04iaO6mS5tjGE6ShfexnjAMBgNVHRMEBTADAQH_MAkGByqGSM49BAEDSAAwRQIgXJWZdbvOWdhVaG7IJtn44o21Kmi8EHsDk4cAfnZ0r38CIQD6ZPi3Pl4lXxbY7BXFyrpkiOvCpdyNdLLYbSTbvIBQOTBEAiBk3N3-gH2WPhR7EOq2-vEqrC1EZXgYs7fofhYTNk9jqwIgcAVRCeXfCLfLO7X71vKVeXaRQKCJgvmRZdB8PoPVdjw"}] 82 | ---- 83 | 84 | ===== Authentication ===== 85 | The u2f-authenticate command takes an AuthenticateRequest JSON object as 86 | input, and returns the AuthenticateResponse as output. 87 | 88 | ---- 89 | $ u2f-authenticate -s soft_device.json http://localhost:8081 90 | Enter AuthenticateRequest JSON data... 91 | {"keyHandle": "gYP0ezioEJcxt849_O4HnOEVw4P3l1297rsddXxB7I8Mo9lLnMOiC9bKXyXhO_ZdFYajj_pwA88zh09lwyt5qA", "challenge": "zCfLJtWyaCk86Awi5VFtT7hhLk5yncYppYC0z2Q5xxo", "version": "U2F_V2", "appId": "http://localhost:8081"} 92 | 93 | {"clientData": "eyJvcmlnaW4iOiAiaHR0cDovL2xvY2FsaG9zdDo4MDgxIiwgImNoYWxsZW5nZSI6ICJ6Q2ZMSnRXeWFDazg2QXdpNVZGdFQ3aGhMazV5bmNZcHBZQzB6MlE1eHhvIiwgInR5cCI6ICJuYXZpZ2F0b3IuaWQuZ2V0QXNzZXJ0aW9uIn0", "challenge": "zCfLJtWyaCk86Awi5VFtT7hhLk5yncYppYC0z2Q5xxo", "keyHandle": "gYP0ezioEJcxt849_O4HnOEVw4P3l1297rsddXxB7I8Mo9lLnMOiC9bKXyXhO_ZdFYajj_pwA88zh09lwyt5qA", "signatureData": "AQAAAAEwRAIgK8HLGu8SQNPC3hI1700RsTtyXLlsn9_1sEcIcobhDi0CIFzduJ5IdGus-I-ieHTX1R-1xRCA0e29I9kChKbkkIzF"} 94 | ---- 95 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | README -------------------------------------------------------------------------------- /dev-requirements-2.7.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | coveralls 3 | Cython # For building hidapi in Tox and on Travis 4 | cryptography>=1.0 5 | mock 6 | -------------------------------------------------------------------------------- /dev-requirements-3.x.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | coveralls 3 | Cython # For building hidapi in Tox and on Travis 4 | cryptography>=1.0 5 | -------------------------------------------------------------------------------- /dev-requirements.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | 6 | if sys.version_info.major == 2: 7 | os.system('pip install -r dev-requirements-2.7.txt') 8 | elif sys.version_info.major == 3: 9 | os.system('pip install -r dev-requirements-3.x.txt') 10 | else: 11 | raise Exception('Unsupported python version!') 12 | -------------------------------------------------------------------------------- /man/u2f-authenticate.1.adoc: -------------------------------------------------------------------------------- 1 | u2f\-authenticate(1) 2 | ==================== 3 | :doctype: manpage 4 | :man source: u2f-authenticate 5 | :man manual: u2f-authenticate manual 6 | 7 | == Name 8 | u2f-authenticate - Command-line tool for authentication using a U2F device. 9 | 10 | == Synopsis 11 | *u2f-authenticate* [-h] [-v] [-c] [-i INFILE] [-o OUTFILE] [-s SOFT] facet 12 | 13 | == Description 14 | Signs a U2F challenge using an attached U2F device. Takes a JSON formatted 15 | AuthenticationRequest object on stdin, and returns the result on stdout. 16 | 17 | == Options 18 | u2f-authenticate has the following options: 19 | 20 | *-h, --help*:: 21 | Shows a list of available sub commands and arguments. 22 | 23 | *-v, --version*:: 24 | Shows the program's version number and exits. 25 | 26 | *-i, --infile FILENAME*:: 27 | A file to read registration data from, instead of stdin. 28 | 29 | *-o, --outfile FILENAME*:: 30 | A file to write output to, instead of stdout. 31 | 32 | *-r, --check*:: 33 | Perform the request as a check-only. 34 | 35 | *-s, --soft FILENAME*:: 36 | A file to use as a soft U2F token. 37 | 38 | *facet*:: 39 | The facet of the U2F challenge. 40 | 41 | == Bugs 42 | Report bugs in the issue tracker (https://github.com/Yubico/python-u2flib-host/issues) 43 | 44 | == See also 45 | *u2f-register*(1) 46 | -------------------------------------------------------------------------------- /man/u2f-register.1.adoc: -------------------------------------------------------------------------------- 1 | u2f\-register(1) 2 | =============== 3 | :doctype: manpage 4 | :man source: u2f-register 5 | :man manual: u2f-register manual 6 | 7 | == Name 8 | u2f-register - Command-line tool for registering a U2F device. 9 | 10 | == Synopsis 11 | *u2f-register* [-h] [-v] [-i INFILE] [-o OUTFILE] [-s SOFT] facet 12 | 13 | == Description 14 | Register a U2F device. Takes a JSON formatted RegisterRequest object on stdin, 15 | and returns the result on stdout. 16 | 17 | == Options 18 | u2f-register has the following options: 19 | 20 | *-h, --help*:: 21 | Shows a list of available sub commands and arguments. 22 | 23 | *-v, --version*:: 24 | Shows the program's version number and exits. 25 | 26 | *-i, --infile FILENAME*:: 27 | A file to read registration data from, instead of stdin. 28 | 29 | *-o, --outfile FILENAME*:: 30 | A file to write output to, instead of stdout. 31 | 32 | *-s, --soft FILENAME*:: 33 | A file to use as a soft U2F token. It will be created if it does not exist. 34 | 35 | *facet*:: 36 | The facet of the RegistrationRequest. 37 | 38 | == Bugs 39 | Report bugs in the issue tracker (https://github.com/Yubico/python-u2flib-host/issues) 40 | 41 | == See also 42 | *u2f-authenticate*(1) 43 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Yubico AB 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or 5 | # without modification, are permitted provided that the following 6 | # conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | # POSSIBILITY OF SUCH DAMAGE. 27 | 28 | import sys 29 | from u2flib_host.yubicommon.setup import setup 30 | 31 | tests_require = ['cryptography>=1.0'] 32 | if (sys.version_info < (3, 3)): 33 | tests_require.append('mock') 34 | 35 | setup( 36 | name='python-u2flib-host', 37 | author='Dain Nilsson', 38 | author_email='dain@yubico.com', 39 | description='Python based U2F host library', 40 | maintainer='Yubico Open Source Maintainers', 41 | maintainer_email='ossmaint@yubico.com', 42 | url='https://github.com/Yubico/python-u2flib-host', 43 | install_requires=['requests', 'hidapi>=0.7.99'], 44 | test_suite='test', 45 | entry_points={ 46 | 'console_scripts': [ 47 | 'u2f-register=u2flib_host.register:main', 48 | 'u2f-authenticate=u2flib_host.authenticate:main', 49 | ], 50 | }, 51 | tests_require=tests_require, 52 | extras_require={ 53 | 'soft_device': ['cryptography>=1.0'], 54 | }, 55 | classifiers=[ 56 | 'License :: OSI Approved :: BSD License', 57 | 'Operating System :: OS Independent', 58 | 'Programming Language :: Python', 59 | 'Programming Language :: Python :: 2', 60 | 'Programming Language :: Python :: 2.7', 61 | 'Programming Language :: Python :: 3', 62 | 'Programming Language :: Python :: 3.4', 63 | 'Programming Language :: Python :: 3.5', 64 | 'Programming Language :: Python :: 3.6', 65 | 'Development Status :: 2 - Pre-Alpha', 66 | 'Intended Audience :: Developers', 67 | 'Intended Audience :: System Administrators', 68 | 'Topic :: Internet', 69 | 'Topic :: Security :: Cryptography', 70 | 'Topic :: Software Development :: Libraries :: Python Modules', 71 | ] 72 | ) 73 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yubico/python-u2flib-host/27dc866368f96f071c9edb3ba90bd628f14ad21c/test/__init__.py -------------------------------------------------------------------------------- /test/test_exc.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | 4 | from u2flib_host import exc 5 | 6 | 7 | class APDUErrorTest(unittest.TestCase): 8 | def test_init(self): 9 | error = exc.APDUError(0x3039) 10 | self.assertEqual(error.args[0], '0x3039') 11 | self.assertEqual(error.code, 0x3039) 12 | self.assertEqual(error.sw1, 0x30) 13 | self.assertEqual(error.sw2, 0x39) 14 | -------------------------------------------------------------------------------- /test/test_hid.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Yubico AB 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or 5 | # without modification, are permitted provided that the following 6 | # conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | # POSSIBILITY OF SUCH DAMAGE. 27 | 28 | from u2flib_host.hid_transport import list_devices 29 | import unittest 30 | 31 | 32 | class HidTest(unittest.TestCase): 33 | def get_device(self): 34 | devs = list_devices() 35 | if len(devs) != 1: 36 | self.skipTest("Tests require a single U2F HID device") 37 | return devs[0] 38 | 39 | def test_open_close(self): 40 | dev = self.get_device() 41 | for i in range(0, 10): 42 | dev.open() 43 | dev.close() 44 | 45 | def test_echo(self): 46 | msg1 = b'hello world!' 47 | msg2 = b' ' 48 | msg3 = b'' 49 | with self.get_device() as dev: 50 | self.assertEqual(dev.send_apdu(0x40, 0, 0, msg1), msg1) 51 | self.assertEqual(dev.send_apdu(0x40, 0, 0, msg2), msg2) 52 | self.assertEqual(dev.send_apdu(0x40, 0, 0, msg3), msg3) 53 | -------------------------------------------------------------------------------- /test/test_hid_transport.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from u2flib_host import hid_transport 4 | from u2flib_host import exc 5 | from u2flib_host.yubicommon.compat import byte2int, int2byte 6 | 7 | try: 8 | from unittest.mock import patch 9 | except ImportError: 10 | from mock import patch 11 | 12 | class TestHIDDevice(object): 13 | def write(self, payload): 14 | self.cid = payload[1:5] 15 | self.cmd = payload[5] ^ hid_transport.TYPE_INIT 16 | self.size = (payload[6] << 8) + payload[7] 17 | self.data = list(map(int2byte, payload[8:(8 + self.size)])) 18 | return len(payload) 19 | 20 | def read(self, size): 21 | self.response += [0] * (hid_transport.HID_RPT_SIZE - len(self.response) + 1) 22 | types = list(map(type, self.response)) 23 | return self.response 24 | 25 | def close(self): 26 | return None 27 | 28 | 29 | class HIDDeviceTest(unittest.TestCase): 30 | @classmethod 31 | def build_response(cls, cid, cmd, data): 32 | size = len(data) 33 | size_low = size & 0xff 34 | size_high = (size >> 8) & 0xff 35 | response = list(map(byte2int, cid)) + list(map(byte2int, cmd)) + [size_high, size_low] 36 | response += list(map(byte2int, data)) 37 | return response 38 | 39 | 40 | def test_init(self): 41 | with patch.object(os, 'urandom', return_value=(b'\xab'*8)) as mock_method: 42 | hid_device = TestHIDDevice() 43 | hid_device.response = HIDDeviceTest.build_response( 44 | b'\xff'*4, 45 | b'\x86', 46 | b'\xab'*8 + b'\x01\x02\x03\x04' + b'\x01\x02\x03\x04\x05' 47 | ) 48 | 49 | dev = hid_transport.HIDDevice('/dev/null') 50 | dev.handle = hid_device 51 | dev.init() 52 | self.assertEqual(dev.capabilities, 0x05) 53 | 54 | def test_init_invalid_nonce(self): 55 | with patch.object(os, 'urandom', return_value=(b'\xab'*8)) as mock_method: 56 | hid_device = TestHIDDevice() 57 | hid_device.response = HIDDeviceTest.build_response( 58 | b'\xff'*4, 59 | b'\x86', 60 | b'\x00'*8 + b'\x01\x02\x03\x04' + b'\x01\x02\x03\x04\x05' 61 | ) 62 | 63 | dev = hid_transport.HIDDevice('/dev/null') 64 | dev.handle = hid_device 65 | with self.assertRaises(exc.DeviceError) as context: 66 | dev.init() 67 | self.assertTrue('Wrong INIT response from device' in context.exception) 68 | 69 | def test_init_invalid_length(self): 70 | with patch.object(os, 'urandom', return_value=(b'\xab'*8)) as mock_method: 71 | hid_device = TestHIDDevice() 72 | hid_device.response = HIDDeviceTest.build_response( 73 | b'\xff'*4, 74 | b'\x86', 75 | b'\xab'*8 + b'\x01\x02\x03\x04' + b'\x01\x02\x03\x04' 76 | ) 77 | 78 | dev = hid_transport.HIDDevice('/dev/null') 79 | dev.handle = hid_device 80 | 81 | with self.assertRaises(exc.DeviceError) as context: 82 | dev.init() 83 | self.assertTrue('Wrong INIT response from device' in context.exception) 84 | 85 | def test_ctap2_enabled(self): 86 | dev = hid_transport.HIDDevice('/dev/null') 87 | dev.capabilities = 0x01 88 | self.assertFalse(dev.ctap2_enabled()) 89 | dev.capabilities = 0x04 90 | self.assertTrue(dev.ctap2_enabled()) 91 | -------------------------------------------------------------------------------- /test/test_reg_auth.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 Yubico AB 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or 5 | # without modification, are permitted provided that the following 6 | # conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | # POSSIBILITY OF SUCH DAMAGE. 27 | 28 | 29 | import os 30 | import tempfile 31 | import unittest 32 | import json 33 | 34 | from u2flib_host.utils import websafe_encode 35 | from u2flib_host.soft import SoftU2FDevice 36 | from u2flib_host.register import register 37 | from u2flib_host.authenticate import authenticate 38 | 39 | 40 | VERSION = 'U2F_V2' 41 | FACET = 'https://example.com' 42 | CHALLENGE = 'challenge' 43 | KEY_HANDLE = websafe_encode(b'\0' * 64) 44 | 45 | REG_DATA = json.dumps({ 46 | 'version': VERSION, 47 | 'challenge': CHALLENGE, 48 | 'appId': FACET 49 | }) 50 | 51 | AUTH_DATA = json.dumps({ 52 | 'version': VERSION, 53 | 'challenge': CHALLENGE, 54 | 'appId': FACET, 55 | 'keyHandle': KEY_HANDLE 56 | }) 57 | 58 | 59 | class TestRegister(unittest.TestCase): 60 | def setUp(self): 61 | print('write') 62 | with tempfile.NamedTemporaryFile(delete=False) as f: 63 | f.write(json.dumps({"counter": 0, "keys": {}}).encode('utf8')) 64 | self.device_path = f.name 65 | 66 | def tearDown(self): 67 | os.unlink(self.device_path) 68 | 69 | def test_register(self): 70 | dev = SoftU2FDevice(self.device_path) 71 | 72 | resp = register([dev], REG_DATA, FACET) 73 | self.assertIn('registrationData', resp) 74 | 75 | def test_authenticate(self): 76 | dev = SoftU2FDevice(self.device_path) 77 | 78 | try: 79 | authenticate([dev], AUTH_DATA, FACET, False) 80 | self.fail('Key handle should not match') 81 | except ValueError: 82 | pass 83 | -------------------------------------------------------------------------------- /test/test_soft.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import base64 4 | import struct 5 | import tempfile 6 | import unittest 7 | 8 | from u2flib_host.soft import SoftU2FDevice 9 | from u2flib_host.constants import INS_ENROLL, INS_SIGN 10 | 11 | CLIENT_PARAM = b'clientABCDEFGHIJKLMNOPQRSTUVWXYZ' # 32 bytes 12 | APP_PARAM = b'test_SoftU2FDevice0123456789ABCD' # 32 bytes 13 | 14 | class TestSoftU2FDevice(unittest.TestCase): 15 | def setUp(self): 16 | with tempfile.NamedTemporaryFile(delete=False) as f: 17 | f.write(b'{"counter": 0, "keys": {}}') 18 | self.device_path = f.name 19 | 20 | def tearDown(self): 21 | os.unlink(self.device_path) 22 | 23 | def test_init(self): 24 | dev = SoftU2FDevice(self.device_path) 25 | self.assertEqual(dev.data['counter'], 0) 26 | self.assertEqual(dev.data['keys'], {}) 27 | 28 | def test_get_supported_versions(self): 29 | dev = SoftU2FDevice(self.device_path) 30 | self.assertEqual(dev.get_supported_versions(), ['U2F_V2']) 31 | 32 | def test_registeration(self): 33 | dev = SoftU2FDevice(self.device_path) 34 | request = struct.pack('32s 32s', CLIENT_PARAM, APP_PARAM) 35 | response = dev.send_apdu(INS_ENROLL, data=request) 36 | self.assertEqual(dev.data['counter'], 0) 37 | self.assertTrue(len(dev.data['keys']), 1) 38 | 39 | pub_key, key_handle_len, key_handle, cert, signature = struct.unpack('x 65s B 64s %is 32s' % (len(response)-(1+65+1+64+32),), response) 40 | self.assertEqual(len(key_handle), key_handle_len) 41 | kh_hex = base64.b16encode(key_handle).decode('ascii') 42 | self.assertIn(kh_hex, dev.data['keys']) 43 | self.assertEqual(base64.b16decode(dev.data['keys'][kh_hex]['app_param']), APP_PARAM) 44 | self.assertEqual(dev.data['keys'][kh_hex]['priv_key'].split('\n')[0], 45 | '-----BEGIN PRIVATE KEY-----') 46 | 47 | request = struct.pack('32s 32s B %is' % key_handle_len, 48 | CLIENT_PARAM, APP_PARAM, key_handle_len, key_handle) 49 | response = dev.send_apdu(INS_SIGN, data=request) 50 | self.assertEqual(dev.data['counter'], 1) 51 | 52 | touch, counter, signature = struct.unpack('>? I %is' % (len(response)-(1+4),), response) 53 | self.assertTrue(touch) 54 | self.assertEqual(counter, 1) 55 | 56 | -------------------------------------------------------------------------------- /test/test_u2f.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 Yubico AB 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or 5 | # without modification, are permitted provided that the following 6 | # conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | # POSSIBILITY OF SUCH DAMAGE. 27 | 28 | from u2flib_host import u2f, u2f_v2 29 | import unittest 30 | 31 | 32 | class MockDevice(object): 33 | 34 | def __init__(self, *versions): 35 | self._versions = versions 36 | 37 | def get_supported_versions(self): 38 | return self._versions 39 | 40 | def send_apdu(self, ins, p1, p2, request): 41 | return b'' 42 | 43 | 44 | class TestU2F(unittest.TestCase): 45 | 46 | def test_get_lib_supported(self): 47 | lib = u2f.get_lib(MockDevice('U2F_V2'), {'version': 'U2F_V2'}) 48 | self.assertEqual(lib, u2f_v2) 49 | 50 | lib = u2f.get_lib(MockDevice('foo', 'U2F_V2'), {'version': 'U2F_V2'}) 51 | self.assertEqual(lib, u2f_v2) 52 | 53 | lib = u2f.get_lib(MockDevice('U2F_V2', 'bar'), {'version': 'U2F_V2'}) 54 | self.assertEqual(lib, u2f_v2) 55 | 56 | def test_get_lib_unsupported_by_device(self): 57 | self.assertRaises(ValueError, u2f.get_lib, 58 | MockDevice('unknown', 'versions'), 59 | {'version': 'U2F_V2'}) 60 | 61 | def test_get_lib_unsupported_by_lib(self): 62 | self.assertRaises(ValueError, u2f.get_lib, 63 | MockDevice('U2F_V2'), 64 | {'version': 'invalid_version'}) 65 | -------------------------------------------------------------------------------- /test/test_u2f_v2.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 Yubico AB 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or 5 | # without modification, are permitted provided that the following 6 | # conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | # POSSIBILITY OF SUCH DAMAGE. 27 | 28 | from u2flib_host.constants import INS_ENROLL, INS_SIGN, APDU_USE_NOT_SATISFIED 29 | from u2flib_host.utils import websafe_decode, websafe_encode 30 | from u2flib_host.exc import APDUError 31 | from u2flib_host import u2f_v2 32 | import unittest 33 | import json 34 | 35 | VERSION = 'U2F_V2' 36 | FACET = 'https://example.com' 37 | CHALLENGE = 'challenge' 38 | KEY_HANDLE = websafe_encode(b'\0' * 64) 39 | 40 | REG_DATA = json.dumps({ 41 | 'version': VERSION, 42 | 'challenge': CHALLENGE, 43 | 'appId': FACET 44 | }) 45 | 46 | AUTH_DATA = json.dumps({ 47 | 'version': VERSION, 48 | 'challenge': CHALLENGE, 49 | 'appId': FACET, 50 | 'keyHandle': KEY_HANDLE 51 | }) 52 | 53 | DUMMY_RESP = b'a_dummy_response' 54 | 55 | 56 | class MockDevice(object): 57 | 58 | def __init__(self, response): 59 | self._response = response 60 | 61 | def send_apdu(self, ins, p1, p2, request): 62 | self.ins = ins 63 | self.p1 = p1 64 | self.p2 = p2 65 | self.request = request 66 | 67 | if isinstance(self._response, Exception): 68 | raise self._response 69 | return self._response 70 | 71 | 72 | class TestU2FV2(unittest.TestCase): 73 | 74 | def test_register(self): 75 | device = MockDevice(DUMMY_RESP) 76 | response = u2f_v2.register(device, REG_DATA, FACET) 77 | 78 | self.assertEqual(device.ins, INS_ENROLL) 79 | self.assertEqual(device.p1, 0x03) 80 | self.assertEqual(device.p2, 0x00) 81 | self.assertEqual(len(device.request), 64) 82 | 83 | self.assertEqual(websafe_decode(response['registrationData']), 84 | DUMMY_RESP) 85 | 86 | client_data = json.loads(websafe_decode(response['clientData']) 87 | .decode('utf8')) 88 | self.assertEqual(client_data['typ'], 'navigator.id.finishEnrollment') 89 | self.assertEqual(client_data['origin'], FACET) 90 | self.assertEqual(client_data['challenge'], CHALLENGE) 91 | 92 | def test_authenticate(self): 93 | device = MockDevice(DUMMY_RESP) 94 | response = u2f_v2.authenticate(device, AUTH_DATA, FACET, False) 95 | 96 | self.assertEqual(device.ins, INS_SIGN) 97 | self.assertEqual(device.p1, 0x03) 98 | self.assertEqual(device.p2, 0x00) 99 | self.assertEqual(len(device.request), 64 + 1 + 64) 100 | self.assertEqual(device.request[-64:], websafe_decode(KEY_HANDLE)) 101 | 102 | self.assertEqual(response['keyHandle'], KEY_HANDLE) 103 | self.assertEqual(websafe_decode(response['signatureData']), DUMMY_RESP) 104 | 105 | client_data = json.loads(websafe_decode(response['clientData']) 106 | .decode('utf8')) 107 | self.assertEqual(client_data['typ'], 'navigator.id.getAssertion') 108 | self.assertEqual(client_data['origin'], FACET) 109 | self.assertEqual(client_data['challenge'], CHALLENGE) 110 | 111 | def test_authenticate_check_only(self): 112 | device = MockDevice(APDUError(APDU_USE_NOT_SATISFIED)) 113 | 114 | try: 115 | u2f_v2.authenticate(device, AUTH_DATA, FACET, True) 116 | self.fail('authenticate should throw USE_NOT_SATISIFIED') 117 | except APDUError as e: 118 | self.assertEqual(device.ins, INS_SIGN) 119 | self.assertEqual(device.p1, 0x07) 120 | self.assertEqual(e.code, APDU_USE_NOT_SATISFIED) 121 | -------------------------------------------------------------------------------- /test/test_utils.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import unittest 4 | 5 | from u2flib_host.utils import ( 6 | u2str, 7 | websafe_encode, 8 | websafe_decode 9 | ) 10 | 11 | 12 | class TestU2Str(unittest.TestCase): 13 | def test_u2str(self): 14 | data1 = { 15 | u'greeting_en': u'Hello world', 16 | u'greeting_se': u'Hallå världen', 17 | u'recursive': { 18 | b'plaintext': [u'foo', b'bar', u'BΛZ'], 19 | }, 20 | } 21 | self.assertEqual(u2str(data1), { 22 | b'greeting_en': b'Hello world', 23 | b'greeting_se': b'Hall\xc3\xa5 v\xc3\xa4rlden', # utf-8 encoded 24 | b'recursive': { 25 | b'plaintext': [b'foo', b'bar', b'B\xce\x9bZ'], 26 | }, 27 | }) 28 | 29 | 30 | class TestWebSafe(unittest.TestCase): 31 | # Base64 vectors adapted from https://tools.ietf.org/html/rfc4648#section-10 32 | 33 | def test_websafe_decode(self): 34 | self.assertEqual(websafe_decode(b''), b'') 35 | self.assertEqual(websafe_decode(b'Zg'), b'f') 36 | self.assertEqual(websafe_decode(b'Zm8'), b'fo') 37 | self.assertEqual(websafe_decode(b'Zm9v'), b'foo') 38 | self.assertEqual(websafe_decode(b'Zm9vYg'), b'foob') 39 | self.assertEqual(websafe_decode(b'Zm9vYmE'), b'fooba') 40 | self.assertEqual(websafe_decode(b'Zm9vYmFy'), b'foobar') 41 | 42 | def test_websafe_decode_unicode(self): 43 | self.assertEqual(websafe_decode(u''), b'') 44 | self.assertEqual(websafe_decode(u'Zm9vYmFy'), b'foobar') 45 | 46 | def test_websafe_encode(self): 47 | self.assertEqual(websafe_encode(b''), u'') 48 | self.assertEqual(websafe_encode(b'f'), u'Zg') 49 | self.assertEqual(websafe_encode(b'fo'), u'Zm8') 50 | self.assertEqual(websafe_encode(b'foo'), u'Zm9v') 51 | self.assertEqual(websafe_encode(b'foob'), u'Zm9vYg') 52 | self.assertEqual(websafe_encode(b'fooba'), u'Zm9vYmE') 53 | self.assertEqual(websafe_encode(b'foobar'), u'Zm9vYmFy') 54 | 55 | def test_websafe_encode_unicode(self): 56 | self.assertEqual(websafe_encode(u''), u'') 57 | self.assertEqual(websafe_encode(u'foobar'), u'Zm9vYmFy') 58 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py27, 4 | py34, 5 | py35, 6 | py36, 7 | 8 | [testenv] 9 | develop = True 10 | deps = 11 | -r{toxinidir}/dev-requirements.txt 12 | commands = 13 | coverage run setup.py test 14 | coverage report 15 | coverage html 16 | # Allow any U2F attached HID device time reset itself. Otherwise, if a U2F 17 | # HID device is attached then only the first environment to run succeeds. 18 | python -c "import time; time.sleep(2)" 19 | -------------------------------------------------------------------------------- /u2flib_host/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Yubico AB 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or 5 | # without modification, are permitted provided that the following 6 | # conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | # POSSIBILITY OF SUCH DAMAGE. 27 | 28 | __version__ = "3.0.3" 29 | -------------------------------------------------------------------------------- /u2flib_host/appid.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Yubico AB 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or 5 | # without modification, are permitted provided that the following 6 | # conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | # POSSIBILITY OF SUCH DAMAGE. 27 | 28 | import requests 29 | try: 30 | from urlparse import urlparse 31 | except ImportError: 32 | from urllib.parse import urlparse 33 | 34 | SUFFIX_URL = 'https://publicsuffix.org/list/effective_tld_names.dat' 35 | 36 | 37 | class AppIDVerifier(object): 38 | 39 | def __init__(self): 40 | self._cache = {} 41 | 42 | def get_suffixes(self): 43 | if not hasattr(self, '_suffixes'): 44 | # Obtain the list of public DNS suffixes from 45 | # https://publicsuffix.org/list/effective_tld_names.dat (the client 46 | # may cache such data), or equivalent functionality as available on 47 | # the platform 48 | resp = requests.get(SUFFIX_URL, verify=True) 49 | self._suffixes = [] 50 | for line in resp.text.splitlines(): 51 | if not line.startswith('//') and line: 52 | self._suffixes.append(line.strip()) 53 | return self._suffixes 54 | 55 | def get_json(self, app_id): 56 | if app_id not in self._cache: 57 | self._cache[app_id] = self.fetch_json(app_id) 58 | return self._cache[app_id] 59 | 60 | def fetch_json(self, app_id): 61 | target = app_id 62 | while True: 63 | resp = requests.get(target, allow_redirects=False, verify=True) 64 | 65 | # If the server returns an HTTP redirect (status code 3xx) the 66 | # server must also send the header "FIDO-AppID-Redirect-Authorized: 67 | # true" and the client must verify the presence of such a header 68 | # before following the redirect. This protects against abuse of 69 | # open redirectors within the target domain by unauthorized 70 | # parties. 71 | if 300 <= resp.status_code < 400: 72 | if resp.headers.get('FIDO-AppID-Redirect-Authorized') != \ 73 | 'true': 74 | raise ValueError('Redirect must set ' 75 | 'FIDO-AppID-Redirect-Authorized: true') 76 | target = resp.headers['location'] 77 | else: 78 | # The response must set a MIME Content-Type of 79 | # "application/fido.trusted-apps+json" 80 | if resp.headers['Content-Type'] != \ 81 | 'application/fido.trusted-apps+json': 82 | raise ValueError('Response must have Content-Type: ' 83 | 'application/fido.trusted-apps+json') 84 | return resp.json() 85 | 86 | def least_specific(self, url): 87 | # The least-specific private label is the portion of the host portion 88 | # of the AppID URL that matches a public suffix plus one additional 89 | # label to the left 90 | host = urlparse(url).hostname 91 | for suffix in self.get_suffixes(): 92 | if host.endswith(suffix): 93 | n_parts = len(suffix.split('.')) + 1 94 | return '.'.join(host.split('.')[-n_parts:]).lower() 95 | raise ValueError('Hostname doesn\'t end with a public suffix') 96 | 97 | def valid_facets(self, app_id, facets): 98 | app_id_ls = self.least_specific(app_id) 99 | return [f for f in facets if self.facet_is_valid(app_id_ls, f)] 100 | 101 | def facet_is_valid(self, app_id_ls, facet): 102 | # The scheme of URLs in ids must identify either an application 103 | # identity (e.g. using the apk:, ios: or similar scheme) or an https: 104 | # RFC6454 Web Origin 105 | if facet.startswith('http://'): 106 | return False 107 | 108 | # Entries in ids using the https:// scheme must contain only scheme, 109 | # host and port components, with an optional trailing /. Any path, 110 | # query string, username/password, or fragment information is discarded 111 | if facet.startswith('https://'): 112 | url = urlparse(facet) 113 | facet = '%s://%s' % (url.scheme, url.hostname) 114 | if url.port and url.port != 443: 115 | facet += ':%d' % url.port 116 | 117 | # For each Web Origin in the TrustedFacets list, the calculation of 118 | # the least-specific private label in the DNS must be a 119 | # case-insensitive match of that of the AppID URL itself. Entries 120 | # that do not match must be discarded 121 | if self.least_specific(facet) != app_id_ls: 122 | return False 123 | 124 | return True 125 | 126 | def verify_facet(self, app_id, facet, version=(1, 0)): 127 | url = urlparse(app_id) 128 | 129 | # If the AppID is not an HTTPS URL, and matches the FacetID of the 130 | # caller, no additional processing is necessary and the operation may 131 | # proceed 132 | https = url.scheme == 'https' 133 | if not https and app_id == facet: 134 | return 135 | 136 | # If the caller's FacetID is an https:// Origin sharing the same host 137 | # as the AppID, (e.g. if an application hosted at 138 | # https://fido.example.com/myApp set an AppID of 139 | # https://fido.example.com/myAppId), no additional processing is 140 | # necessary and the operation may proceed 141 | if https and '%s://%s' % (url.scheme, url.netloc) == facet: 142 | return 143 | 144 | # Begin to fetch the Trusted Facet List using the HTTP GET method. The 145 | # location must be identified with an HTTPS URL. 146 | if not https: 147 | raise ValueError('AppID URL must use https.') 148 | 149 | data = self.get_json(app_id) 150 | # From among the objects in the trustedFacet array, select the one with 151 | # the verison matching that of the protocol message. 152 | for entry in data['trustedFacets']: 153 | e_ver = entry['version'] 154 | if (e_ver['major'], e_ver['minor']) == version: 155 | trustedFacets = self.valid_facets(app_id, entry['ids']) 156 | break 157 | else: 158 | raise ValueError( 159 | 'No trusted facets found for version: %r' % 160 | version) 161 | 162 | if facet not in trustedFacets: 163 | raise ValueError('Invalid facet: "%s", expecting one of %r' % 164 | (facet, trustedFacets)) 165 | 166 | 167 | verifier = AppIDVerifier() 168 | verify_facet = verifier.verify_facet 169 | -------------------------------------------------------------------------------- /u2flib_host/authenticate.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Yubico AB 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or 5 | # without modification, are permitted provided that the following 6 | # conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | # POSSIBILITY OF SUCH DAMAGE. 27 | 28 | from __future__ import print_function 29 | 30 | from u2flib_host import u2f, exc, __version__ 31 | from u2flib_host.constants import APDU_USE_NOT_SATISFIED 32 | from u2flib_host.utils import u2str 33 | from u2flib_host.yubicommon.compat import text_type 34 | 35 | import time 36 | import json 37 | import argparse 38 | import sys 39 | 40 | 41 | def authenticate(devices, params, facet, check_only): 42 | """ 43 | Interactively authenticates a AuthenticateRequest using an attached U2F 44 | device. 45 | """ 46 | for device in devices[:]: 47 | try: 48 | device.open() 49 | except: 50 | devices.remove(device) 51 | 52 | try: 53 | prompted = False 54 | while devices: 55 | removed = [] 56 | for device in devices: 57 | try: 58 | return u2f.authenticate(device, params, facet, check_only) 59 | except exc.APDUError as e: 60 | if e.code == APDU_USE_NOT_SATISFIED: 61 | if check_only: 62 | sys.stderr.write('\nCorrect U2F device present!\n') 63 | sys.exit(0) 64 | if not prompted: 65 | sys.stderr.write('\nTouch the flashing U2F device ' 66 | 'to authenticate...\n') 67 | prompted = True 68 | else: 69 | removed.append(device) 70 | except exc.DeviceError: 71 | removed.append(device) 72 | devices = [d for d in devices if d not in removed] 73 | for d in removed: 74 | d.close() 75 | time.sleep(0.25) 76 | finally: 77 | for device in devices: 78 | device.close() 79 | sys.stderr.write('\nThe required U2F device is not present!\n') 80 | sys.exit(1) 81 | 82 | 83 | def parse_args(): 84 | parser = argparse.ArgumentParser( 85 | description="Authenticaties an AuthenticateRequest.\n" 86 | "Takes a JSON formatted AuthenticateRequest object on stdin, and " 87 | "returns the resulting AuthenticateResponse on stdout.", 88 | add_help=True 89 | ) 90 | parser.add_argument('-v', '--version', action='version', 91 | version='%(prog)s ' + __version__) 92 | parser.add_argument('facet', help='the facet for the challenge') 93 | parser.add_argument('-c', '--check-only', action="store_true", help='Check if ' 94 | 'the key handle is correct only, don\'t sign.') 95 | parser.add_argument('-i', '--infile', help='specify a file to read ' 96 | 'AuthenticateRequest from, instead of stdin') 97 | parser.add_argument('-o', '--outfile', help='specify a file to write ' 98 | 'the AuthenticateResponse to, instead of stdout') 99 | parser.add_argument('-s', '--soft', help='Specify a soft U2F token file to use') 100 | return parser.parse_args() 101 | 102 | 103 | def main(): 104 | args = parse_args() 105 | 106 | facet = text_type(args.facet) 107 | if args.infile: 108 | with open(args.infile, 'r') as f: 109 | data = f.read() 110 | else: 111 | if sys.stdin.isatty(): 112 | sys.stderr.write('Enter AuthenticateRequest JSON data...\n') 113 | data = sys.stdin.read() 114 | 115 | params = json.loads(data) 116 | if args.soft: 117 | from u2flib_host.soft import SoftU2FDevice 118 | devices = [SoftU2FDevice(args.soft)] 119 | else: 120 | devices = u2f.list_devices() 121 | result = authenticate(devices, params, facet, args.check_only) 122 | 123 | if args.outfile: 124 | with open(args.outfile, 'w') as f: 125 | json.dump(result, f) 126 | sys.stderr.write('Output written to %s\n' % args.outfile) 127 | else: 128 | sys.stderr.write('\n---Result---\n') 129 | print(json.dumps(result)) 130 | 131 | 132 | if __name__ == '__main__': 133 | main() 134 | -------------------------------------------------------------------------------- /u2flib_host/constants.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Yubico AB 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or 5 | # without modification, are permitted provided that the following 6 | # conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | # POSSIBILITY OF SUCH DAMAGE. 27 | 28 | 29 | #APDU Instructions 30 | INS_ENROLL = 0x01 31 | INS_SIGN = 0x02 32 | INS_GET_VERSION = 0x03 33 | 34 | #APDU Response Codes 35 | APDU_OK = 0x9000 36 | APDU_USE_NOT_SATISFIED = 0x6985 37 | APDU_WRONG_DATA = 0x6a80 38 | -------------------------------------------------------------------------------- /u2flib_host/device.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Yubico AB 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or 5 | # without modification, are permitted provided that the following 6 | # conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | # POSSIBILITY OF SUCH DAMAGE. 27 | 28 | from u2flib_host.constants import APDU_OK, INS_GET_VERSION 29 | from u2flib_host.yubicommon.compat import int2byte 30 | from u2flib_host import exc 31 | import struct 32 | 33 | 34 | class U2FDevice(object): 35 | 36 | """ 37 | A handle to a U2F device. 38 | device.open() needs to be called prior to using the device, and 39 | device.close() should be called when the device is no longer needed, to 40 | ensure that any held resources are released. 41 | As an aternative, the 'with' statement can be used: 42 | 43 | with device as dev: 44 | dev.send_apdu(...) 45 | """ 46 | 47 | def __enter__(self): 48 | self.open() 49 | return self 50 | 51 | def __exit__(self, type, value, traceback): 52 | self.close() 53 | 54 | def __del__(self): 55 | self.close() 56 | 57 | def open(self): 58 | """ 59 | Opens the device for use. 60 | """ 61 | pass 62 | 63 | def close(self): 64 | """ 65 | Closes the device, making it available for use by others. 66 | """ 67 | pass 68 | 69 | def get_supported_versions(self): 70 | """ 71 | Gets a list of supported U2F versions from the device. 72 | """ 73 | if not hasattr(self, '_versions'): 74 | try: 75 | self._versions = [self.send_apdu(INS_GET_VERSION).decode()] 76 | except exc.APDUError as e: 77 | # v0 didn't support the instruction. 78 | self._versions = ['v0'] if e.code == 0x6d00 else [] 79 | 80 | return self._versions 81 | 82 | def _do_send_apdu(self, apdu_data): 83 | """ 84 | Sends an APDU to the device, and returns the response. 85 | """ 86 | # Subclasses should implement this. 87 | raise NotImplementedError('_do_send_apdu not implemented!') 88 | 89 | def send_apdu(self, ins, p1=0, p2=0, data=b''): 90 | """ 91 | Sends an APDU to the device, and waits for a response. 92 | """ 93 | if data is None: 94 | data = b'' 95 | elif isinstance(data, int): 96 | data = int2byte(data) 97 | 98 | size = len(data) 99 | l0 = size >> 16 & 0xff 100 | l1 = size >> 8 & 0xff 101 | l2 = size & 0xff 102 | apdu_data = struct.pack('B B B B B B B %is B B' % size, 103 | 0, ins, p1, p2, l0, l1, l2, data, 0x00, 0x00) 104 | try: 105 | resp = self._do_send_apdu(apdu_data) 106 | except Exception as e: 107 | # TODO Use six.reraise if/when Six becomes an agreed dependency. 108 | raise exc.DeviceError(e) 109 | status = struct.unpack('>H', resp[-2:])[0] 110 | data = resp[:-2] 111 | if status != APDU_OK: 112 | raise exc.APDUError(status) 113 | return data 114 | -------------------------------------------------------------------------------- /u2flib_host/exc.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Yubico AB 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or 5 | # without modification, are permitted provided that the following 6 | # conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | # POSSIBILITY OF SUCH DAMAGE. 27 | 28 | 29 | class APDUError(Exception): 30 | 31 | def __init__(self, code): 32 | super(Exception, self).__init__('0x%X' % code) 33 | self.code = code 34 | self.sw1 = code >> 8 & 0xff 35 | self.sw2 = code & 0xff 36 | 37 | 38 | class DeviceError(Exception): 39 | pass 40 | -------------------------------------------------------------------------------- /u2flib_host/hid_transport.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Yubico AB 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or 5 | # without modification, are permitted provided that the following 6 | # conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | # POSSIBILITY OF SUCH DAMAGE. 27 | 28 | from __future__ import print_function 29 | 30 | import os 31 | try: 32 | import hidraw as hid # Prefer hidraw 33 | except ImportError: 34 | import hid 35 | from time import time, sleep 36 | from u2flib_host.device import U2FDevice 37 | from u2flib_host.yubicommon.compat import byte2int, int2byte 38 | from u2flib_host import exc 39 | 40 | DEVICES = [ 41 | (0x1050, 0x0200), # Gnubby 42 | (0x1050, 0x0113), # YubiKey NEO U2F 43 | (0x1050, 0x0114), # YubiKey NEO OTP+U2F 44 | (0x1050, 0x0115), # YubiKey NEO U2F+CCID 45 | (0x1050, 0x0116), # YubiKey NEO OTP+U2F+CCID 46 | (0x1050, 0x0120), # Security Key by Yubico 47 | (0x1050, 0x0410), # YubiKey Plus 48 | (0x1050, 0x0402), # YubiKey 4 U2F 49 | (0x1050, 0x0403), # YubiKey 4 OTP+U2F 50 | (0x1050, 0x0406), # YubiKey 4 U2F+CCID 51 | (0x1050, 0x0407), # YubiKey 4 OTP+U2F+CCID 52 | (0x2581, 0xf1d0), # Plug-Up U2F Security Key 53 | (0x096e, 0x0850), # Feitian Technologies, Inc. ePass FIDO 54 | (0x096e, 0x0858), # FT U2F 55 | (0x096e, 0x085b), # FS ePass FIDO 56 | (0x24dc, 0x0501), # JaCarta U2F 57 | (0x1ea8, 0xf025), # Thetis U2F 58 | (0x1d50, 0x60fc), # OnlyKey U2F 59 | (0x1209, 0x53c1), # Trezor U2F/FIDO2 60 | ] 61 | HID_RPT_SIZE = 64 62 | 63 | TYPE_INIT = 0x80 64 | U2F_VENDOR_FIRST = 0x40 65 | 66 | # USB Commands 67 | CMD_INIT = 0x06 68 | CMD_WINK = 0x08 69 | CMD_PING = 0x01 70 | CMD_APDU = 0x03 71 | CMD_LOCK = 0x04 72 | U2FHID_YUBIKEY_DEVICE_CONFIG = U2F_VENDOR_FIRST 73 | 74 | STAT_ERR = 0xbf 75 | 76 | 77 | def list_devices(dev_class=None): 78 | dev_class = dev_class or HIDDevice 79 | devices = [] 80 | for d in hid.enumerate(0, 0): 81 | usage_page = d['usage_page'] 82 | if usage_page == 0xf1d0 and d['usage'] == 1: 83 | devices.append(dev_class(d['path'])) 84 | # Usage page doesn't work on Linux 85 | elif (d['vendor_id'], d['product_id']) in DEVICES: 86 | device = HIDDevice(d['path']) 87 | try: 88 | device.open() 89 | device.close() 90 | devices.append(dev_class(d['path'])) 91 | except (exc.DeviceError, IOError, OSError): 92 | pass 93 | return devices 94 | 95 | 96 | def _read_timeout(dev, size, timeout=2.0): 97 | timeout += time() 98 | while time() < timeout: 99 | resp = dev.read(size) 100 | if resp: 101 | return resp 102 | return [] 103 | 104 | 105 | class U2FHIDError(Exception): 106 | def __init__(self, code): 107 | super(Exception, self).__init__("U2FHIDError: 0x%02x" % code) 108 | self.code = code 109 | 110 | 111 | class HIDDevice(U2FDevice): 112 | 113 | """ 114 | U2FDevice implementation using the HID transport. 115 | """ 116 | 117 | def __init__(self, path): 118 | self.path = path 119 | self.cid = b"\xff\xff\xff\xff" 120 | self.capabilities = 0x00 121 | 122 | def open(self): 123 | self.handle = hid.device() 124 | self.handle.open_path(self.path) 125 | self.handle.set_nonblocking(True) 126 | self.init() 127 | 128 | def close(self): 129 | if hasattr(self, 'handle'): 130 | self.handle.close() 131 | del self.handle 132 | 133 | def init(self): 134 | nonce = os.urandom(8) 135 | resp = self.call(CMD_INIT, nonce) 136 | 137 | timeout = time() + 2.0 138 | while (len(resp) != 17 or resp[:8] != nonce): 139 | if timeout < time(): 140 | raise exc.DeviceError('Wrong INIT response from device') 141 | sleep(0.1) 142 | resp = self._read_resp(self.cid, CMD_INIT) 143 | 144 | self.cid = resp[8:12] 145 | self.capabilities = byte2int(resp[16]) 146 | 147 | def ctap2_enabled(self): 148 | return (self.capabilities >> 2) & 0x01 149 | 150 | def set_mode(self, mode): 151 | data = mode + b"\x0f\x00\x00" 152 | self.call(U2FHID_YUBIKEY_DEVICE_CONFIG, data) 153 | 154 | def _do_send_apdu(self, apdu_data): 155 | return self.call(CMD_APDU, apdu_data) 156 | 157 | def wink(self): 158 | self.call(CMD_WINK) 159 | 160 | def ping(self, msg=b'Hello U2F'): 161 | resp = self.call(CMD_PING, msg) 162 | if resp != msg: 163 | raise exc.DeviceError("Incorrect PING readback") 164 | return resp 165 | 166 | def lock(self, lock_time=10): 167 | self.call(CMD_LOCK, lock_time) 168 | 169 | def _write_to_device(self, to_send, timeout=2.0): 170 | expected = len(to_send) 171 | actual = 0 172 | stop_at = time() + timeout 173 | while actual != expected: 174 | if (time() > stop_at): 175 | raise exc.DeviceError("Unable to send data to the device") 176 | 177 | actual = self.handle.write(to_send) 178 | sleep(0.025) 179 | 180 | 181 | def _send_req(self, cid, cmd, data): 182 | size = len(data) 183 | bc_l = int2byte(size & 0xff) 184 | bc_h = int2byte(size >> 8 & 0xff) 185 | payload = cid + int2byte(TYPE_INIT | cmd) + bc_h + bc_l + \ 186 | data[:HID_RPT_SIZE - 7] 187 | payload += b'\0' * (HID_RPT_SIZE - len(payload)) 188 | self._write_to_device([0] + [byte2int(c) for c in payload]) 189 | data = data[HID_RPT_SIZE - 7:] 190 | seq = 0 191 | while len(data) > 0: 192 | payload = cid + int2byte(0x7f & seq) + data[:HID_RPT_SIZE - 5] 193 | payload += b'\0' * (HID_RPT_SIZE - len(payload)) 194 | self._write_to_device([0] + [byte2int(c) for c in payload]) 195 | data = data[HID_RPT_SIZE - 5:] 196 | seq += 1 197 | 198 | def _read_resp(self, cid, cmd): 199 | resp = b'.' 200 | header = cid + int2byte(TYPE_INIT | cmd) 201 | while resp and resp[:5] != header: 202 | resp_vals = _read_timeout(self.handle, HID_RPT_SIZE) 203 | resp = b''.join(int2byte(v) for v in resp_vals) 204 | if resp[:5] == cid + int2byte(STAT_ERR): 205 | raise U2FHIDError(byte2int(resp[7])) 206 | 207 | if not resp: 208 | raise exc.DeviceError("Invalid response from device!") 209 | 210 | data_len = (byte2int(resp[5]) << 8) + byte2int(resp[6]) 211 | data = resp[7:min(7 + data_len, HID_RPT_SIZE)] 212 | data_len -= len(data) 213 | 214 | seq = 0 215 | while data_len > 0: 216 | resp_vals = _read_timeout(self.handle, HID_RPT_SIZE) 217 | resp = b''.join(int2byte(v) for v in resp_vals) 218 | if resp[:4] != cid: 219 | raise exc.DeviceError("Wrong CID from device!") 220 | if byte2int(resp[4]) != seq & 0x7f: 221 | raise exc.DeviceError("Wrong SEQ from device!") 222 | seq += 1 223 | new_data = resp[5:min(5 + data_len, HID_RPT_SIZE)] 224 | data_len -= len(new_data) 225 | data += new_data 226 | return data 227 | 228 | def call(self, cmd, data=b''): 229 | if isinstance(data, int): 230 | data = int2byte(data) 231 | 232 | self._send_req(self.cid, cmd, data) 233 | return self._read_resp(self.cid, cmd) 234 | -------------------------------------------------------------------------------- /u2flib_host/register.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Yubico AB 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or 5 | # without modification, are permitted provided that the following 6 | # conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | # POSSIBILITY OF SUCH DAMAGE. 27 | 28 | from __future__ import print_function 29 | 30 | from u2flib_host import u2f, exc, __version__ 31 | from u2flib_host.constants import APDU_USE_NOT_SATISFIED 32 | from u2flib_host.utils import u2str 33 | from u2flib_host.yubicommon.compat import text_type 34 | 35 | import time 36 | import json 37 | import argparse 38 | import sys 39 | 40 | 41 | def register(devices, params, facet): 42 | """ 43 | Interactively registers a single U2F device, given the RegistrationRequest. 44 | """ 45 | for device in devices[:]: 46 | try: 47 | device.open() 48 | except: 49 | devices.remove(device) 50 | 51 | sys.stderr.write('\nTouch the U2F device you wish to register...\n') 52 | try: 53 | while devices: 54 | removed = [] 55 | for device in devices: 56 | try: 57 | return u2f.register(device, params, facet) 58 | except exc.APDUError as e: 59 | if e.code == APDU_USE_NOT_SATISFIED: 60 | pass 61 | else: 62 | removed.append(device) 63 | except exc.DeviceError: 64 | removed.append(device) 65 | devices = [d for d in devices if d not in removed] 66 | for d in removed: 67 | d.close() 68 | time.sleep(0.25) 69 | finally: 70 | for device in devices: 71 | device.close() 72 | sys.stderr.write('\nUnable to register with any U2F device.\n') 73 | sys.exit(1) 74 | 75 | 76 | def parse_args(): 77 | parser = argparse.ArgumentParser( 78 | description="Registers a U2F device.\n" 79 | "Takes a JSON formatted RegisterRequest object on stdin, and returns " 80 | "the resulting RegistrationResponse on stdout.", 81 | add_help=True 82 | ) 83 | parser.add_argument('-v', '--version', action='version', 84 | version='%(prog)s ' + __version__) 85 | parser.add_argument('facet', help='the facet for registration') 86 | parser.add_argument('-i', '--infile', help='specify a file to read ' 87 | 'RegistrationRequest from, instead of stdin') 88 | parser.add_argument('-o', '--outfile', help='specify a file to write ' 89 | 'the RegistrationResponse to, instead of stdout') 90 | parser.add_argument('-s', '--soft', help='Specify a soft U2F device file ' 91 | 'to use') 92 | return parser.parse_args() 93 | 94 | 95 | def main(): 96 | args = parse_args() 97 | 98 | facet = text_type(args.facet) 99 | if args.infile: 100 | with open(args.infile, 'r') as f: 101 | data = f.read() 102 | else: 103 | if sys.stdin.isatty(): 104 | sys.stderr.write('Enter RegistrationRequest JSON data...\n') 105 | data = sys.stdin.read() 106 | params = json.loads(data) 107 | 108 | if args.soft: 109 | from u2flib_host.soft import SoftU2FDevice 110 | devices = [SoftU2FDevice(args.soft)] 111 | else: 112 | devices = u2f.list_devices() 113 | result = register(devices, params, facet) 114 | 115 | if args.outfile: 116 | with open(args.outfile, 'w') as f: 117 | json.dump(result, f) 118 | sys.stderr.write('Output written to %s\n' % args.outfile) 119 | else: 120 | sys.stderr.write('\n---Result---\n') 121 | print(json.dumps(result)) 122 | 123 | 124 | if __name__ == '__main__': 125 | main() 126 | -------------------------------------------------------------------------------- /u2flib_host/soft.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Yubico AB 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or 5 | # without modification, are permitted provided that the following 6 | # conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | # POSSIBILITY OF SUCH DAMAGE. 27 | 28 | from __future__ import print_function 29 | 30 | try: 31 | from cryptography.hazmat.backends import default_backend 32 | from cryptography.hazmat.primitives import hashes, serialization 33 | from cryptography.hazmat.primitives.asymmetric import ec 34 | except ImportError: 35 | print("The soft U2F token requires cryptography.") 36 | raise 37 | 38 | from u2flib_host.device import U2FDevice 39 | from u2flib_host.constants import INS_ENROLL, INS_SIGN 40 | from u2flib_host.yubicommon.compat import byte2int, int2byte 41 | from u2flib_host import exc 42 | import base64 43 | import json 44 | import os 45 | import struct 46 | 47 | # AKA NID_X9_62_prime256v1 in OpenSSL 48 | CURVE = ec.SECP256R1 49 | 50 | CERT = base64.b64decode(b""" 51 | MIIBhzCCAS6gAwIBAgIJAJm+6LEMouwcMAkGByqGSM49BAEwITEfMB0GA1UEAwwW 52 | WXViaWNvIFUyRiBTb2Z0IERldmljZTAeFw0xMzA3MTcxNDIxMDNaFw0xNjA3MTYx 53 | NDIxMDNaMCExHzAdBgNVBAMMFll1YmljbyBVMkYgU29mdCBEZXZpY2UwWTATBgcq 54 | hkjOPQIBBggqhkjOPQMBBwNCAAQ74Zfdc36YPZ+w3gnnXEPIBl1J3pol6IviRAMc 55 | /hCIZFbDDwMs4bSWeFdwqjGfjDlICArdmjMWnDF/XCGvHYEto1AwTjAdBgNVHQ4E 56 | FgQUDai/k1dOImjupkubYxhOkoX3sZ4wHwYDVR0jBBgwFoAUDai/k1dOImjupkub 57 | YxhOkoX3sZ4wDAYDVR0TBAUwAwEB/zAJBgcqhkjOPQQBA0gAMEUCIFyVmXW7zlnY 58 | VWhuyCbZ+OKNtSpovBB7A5OHAH52dK9/AiEA+mT4tz5eJV8W2OwVxcq6ZIjrwqXc 59 | jXSy2G0k27yAUDk= 60 | """) 61 | CERT_PRIV = b""" 62 | -----BEGIN EC PRIVATE KEY----- 63 | MHcCAQEEIMyk3gKcDg5lsYdl48fZoIFORhAc9cQxmn2Whv/+ya+2oAoGCCqGSM49 64 | AwEHoUQDQgAEO+GX3XN+mD2fsN4J51xDyAZdSd6aJeiL4kQDHP4QiGRWww8DLOG0 65 | lnhXcKoxn4w5SAgK3ZozFpwxf1whrx2BLQ== 66 | -----END EC PRIVATE KEY----- 67 | """ 68 | 69 | 70 | def _b16text(s): 71 | """Encode a byte string s as base16 in a textual (unicode) string.""" 72 | return base64.b16encode(s).decode('ascii') 73 | 74 | 75 | class SoftU2FDevice(U2FDevice): 76 | 77 | """ 78 | This simulates the U2F browser API with a soft U2F device connected. 79 | 80 | It can be used for testing. 81 | 82 | """ 83 | 84 | def __init__(self, filename): 85 | super(SoftU2FDevice, self).__init__() 86 | self.filename = filename 87 | try: 88 | with open(filename, 'r') as fp: 89 | self.data = json.load(fp) 90 | except IOError: 91 | self.data = {'counter': 0, 'keys': {}} 92 | 93 | def _persist(self): 94 | with open(self.filename, 'w') as fp: 95 | json.dump(self.data, fp) 96 | 97 | def get_supported_versions(self): 98 | return ['U2F_V2'] 99 | 100 | def send_apdu(self, ins, p1=0, p2=0, data=b''): 101 | if ins == INS_ENROLL: 102 | return self._register(data) 103 | elif ins == INS_SIGN: 104 | return self._authenticate(data) 105 | raise exc.APDUError(0x6d00) # INS not supported. 106 | 107 | def _register(self, data): 108 | client_param = data[:32] 109 | app_param = data[32:] 110 | 111 | # ECC key generation 112 | privu = ec.generate_private_key(CURVE(), default_backend()) 113 | pubu = privu.public_key() 114 | pub_key_der = pubu.public_bytes( 115 | serialization.Encoding.DER, 116 | serialization.PublicFormat.SubjectPublicKeyInfo, 117 | ) 118 | pub_key = pub_key_der[-65:] 119 | 120 | # Store 121 | key_handle = os.urandom(64) 122 | priv_key_pem = privu.private_bytes( 123 | serialization.Encoding.PEM, 124 | serialization.PrivateFormat.PKCS8, 125 | serialization.NoEncryption(), 126 | ) 127 | self.data['keys'][_b16text(key_handle)] = { 128 | 'priv_key': priv_key_pem.decode('ascii'), 129 | 'app_param': _b16text(app_param), 130 | } 131 | self._persist() 132 | 133 | # Attestation signature 134 | cert = CERT 135 | cert_priv = serialization.load_pem_private_key( 136 | CERT_PRIV, password=None, backend=default_backend(), 137 | ) 138 | signer = cert_priv.signer(ec.ECDSA(hashes.SHA256())) 139 | signer.update( 140 | b'\x00' + app_param + client_param + key_handle + pub_key 141 | ) 142 | signature = signer.finalize() 143 | 144 | raw_response = b'\x05' + pub_key + int2byte(len(key_handle)) + \ 145 | key_handle + cert + signature 146 | 147 | return raw_response 148 | 149 | def _authenticate(self, data): 150 | client_param = data[:32] 151 | app_param = data[32:64] 152 | kh_len = byte2int(data[64]) 153 | key_handle = _b16text(data[65:65+kh_len]) 154 | if key_handle not in self.data['keys']: 155 | raise ValueError("Unknown key handle!") 156 | 157 | # Unwrap: 158 | unwrapped = self.data['keys'][key_handle] 159 | if app_param != base64.b16decode(unwrapped['app_param']): 160 | raise ValueError("Incorrect app param!") 161 | priv_pem = unwrapped['priv_key'].encode('ascii') 162 | privu = serialization.load_pem_private_key( 163 | priv_pem, password=None, backend=default_backend(), 164 | ) 165 | 166 | # Increment counter 167 | self.data['counter'] += 1 168 | self._persist() 169 | 170 | # Create signature 171 | touch = b'\x01' # Always indicate user presence 172 | counter = struct.pack('>I', self.data['counter']) 173 | 174 | signer = privu.signer(ec.ECDSA(hashes.SHA256())) 175 | signer.update(app_param + touch + counter + client_param) 176 | signature = signer.finalize() 177 | raw_response = touch + counter + signature 178 | 179 | return raw_response 180 | -------------------------------------------------------------------------------- /u2flib_host/u2f.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Yubico AB 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or 5 | # without modification, are permitted provided that the following 6 | # conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | # POSSIBILITY OF SUCH DAMAGE. 27 | 28 | from u2flib_host import u2f_v2 29 | from u2flib_host import hid_transport 30 | from u2flib_host.yubicommon.compat import string_types 31 | 32 | import json 33 | 34 | TRANSPORTS = [ 35 | hid_transport 36 | ] 37 | 38 | LIB_VERSIONS = { 39 | 'U2F_V2': u2f_v2 40 | } 41 | 42 | 43 | def list_devices(): 44 | # Combine list_devices for all transports, ignoring exceptions. 45 | devices = [] 46 | for transport in TRANSPORTS: 47 | try: 48 | devices.extend(transport.list_devices()) 49 | except: 50 | pass 51 | return devices 52 | 53 | 54 | def get_lib(device, data): 55 | if isinstance(data, string_types): 56 | data = json.loads(data) 57 | 58 | version = data['version'] 59 | if version not in device.get_supported_versions(): 60 | raise ValueError("Device does not support U2F version: %s" % version) 61 | if version not in LIB_VERSIONS: 62 | raise ValueError("Library does not support U2F version: %s" % version) 63 | 64 | return LIB_VERSIONS[version] 65 | 66 | 67 | def register(device, data, facet): 68 | lib = get_lib(device, data) 69 | return lib.register(device, data, facet) 70 | 71 | 72 | def authenticate(device, data, facet, check_only=False): 73 | lib = get_lib(device, data) 74 | return lib.authenticate(device, data, facet, check_only) 75 | -------------------------------------------------------------------------------- /u2flib_host/u2f_v2.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Yubico AB 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or 5 | # without modification, are permitted provided that the following 6 | # conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | # POSSIBILITY OF SUCH DAMAGE. 27 | 28 | from u2flib_host.constants import INS_ENROLL, INS_SIGN 29 | from u2flib_host.utils import websafe_decode, websafe_encode 30 | from u2flib_host.appid import verify_facet 31 | from u2flib_host.yubicommon.compat import string_types, int2byte 32 | 33 | from hashlib import sha256 34 | import json 35 | 36 | VERSION = 'U2F_V2' 37 | 38 | 39 | def register(device, data, facet): 40 | """ 41 | Register a U2F device 42 | 43 | data = { 44 | "version": "U2F_V2", 45 | "challenge": string, //b64 encoded challenge 46 | "appId": string, //app_id 47 | } 48 | 49 | """ 50 | 51 | if isinstance(data, string_types): 52 | data = json.loads(data) 53 | 54 | if data['version'] != VERSION: 55 | raise ValueError('Unsupported U2F version: %s' % data['version']) 56 | 57 | app_id = data.get('appId', facet) 58 | verify_facet(app_id, facet) 59 | app_param = sha256(app_id.encode('utf8')).digest() 60 | 61 | client_data = { 62 | 'typ': 'navigator.id.finishEnrollment', 63 | 'challenge': data['challenge'], 64 | 'origin': facet 65 | } 66 | client_data = json.dumps(client_data) 67 | client_param = sha256(client_data.encode('utf8')).digest() 68 | 69 | request = client_param + app_param 70 | 71 | p1 = 0x03 72 | p2 = 0 73 | response = device.send_apdu(INS_ENROLL, p1, p2, request) 74 | 75 | return { 76 | 'registrationData': websafe_encode(response), 77 | 'clientData': websafe_encode(client_data) 78 | } 79 | 80 | 81 | def authenticate(device, data, facet, check_only=False): 82 | """ 83 | Signs an authentication challenge 84 | 85 | data = { 86 | 'version': "U2F_V2", 87 | 'challenge': websafe_encode(self.challenge), 88 | 'appId': self.binding.app_id, 89 | 'keyHandle': websafe_encode(self.binding.key_handle) 90 | } 91 | 92 | """ 93 | 94 | if isinstance(data, string_types): 95 | data = json.loads(data) 96 | 97 | if data['version'] != VERSION: 98 | raise ValueError('Unsupported U2F version: %s' % data['version']) 99 | 100 | app_id = data.get('appId', facet) 101 | verify_facet(app_id, facet) 102 | app_param = sha256(app_id.encode('utf8')).digest() 103 | 104 | key_handle = websafe_decode(data['keyHandle']) 105 | 106 | # Client data 107 | client_data = { 108 | 'typ': 'navigator.id.getAssertion', 109 | 'challenge': data['challenge'], 110 | 'origin': facet 111 | } 112 | client_data = json.dumps(client_data) 113 | client_param = sha256(client_data.encode('utf8')).digest() 114 | 115 | request = client_param + app_param + int2byte( 116 | len(key_handle)) + key_handle 117 | 118 | p1 = 0x07 if check_only else 0x03 119 | p2 = 0 120 | response = device.send_apdu(INS_SIGN, p1, p2, request) 121 | 122 | return { 123 | 'clientData': websafe_encode(client_data), 124 | 'signatureData': websafe_encode(response), 125 | 'keyHandle': data['keyHandle'] 126 | } 127 | -------------------------------------------------------------------------------- /u2flib_host/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Yubico AB 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or 5 | # without modification, are permitted provided that the following 6 | # conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | # POSSIBILITY OF SUCH DAMAGE. 27 | 28 | from u2flib_host.yubicommon.compat import text_type 29 | 30 | from base64 import urlsafe_b64decode, urlsafe_b64encode 31 | from hashlib import sha256 32 | 33 | __all__ = [ 34 | 'u2str', 35 | 'websafe_encode', 36 | 'websafe_decode' 37 | ] 38 | 39 | 40 | def u2str(data): 41 | """Recursively converts unicode objects to UTF-8 encoded byte strings.""" 42 | if isinstance(data, dict): 43 | return {u2str(k): u2str(v) for k, v in data.items()} 44 | elif isinstance(data, list): 45 | return [u2str(x) for x in data] 46 | elif isinstance(data, text_type): 47 | return data.encode('utf-8') 48 | else: 49 | return data 50 | 51 | 52 | def websafe_decode(data): 53 | if isinstance(data, text_type): 54 | data = data.encode('ascii') 55 | data += b'=' * (-len(data) % 4) 56 | return urlsafe_b64decode(data) 57 | 58 | 59 | def websafe_encode(data): 60 | if isinstance(data, text_type): 61 | data = data.encode('ascii') 62 | return urlsafe_b64encode(data).replace(b'=', b'').decode('ascii') 63 | -------------------------------------------------------------------------------- /u2flib_host/yubicommon: -------------------------------------------------------------------------------- 1 | ../vendor/yubicommon/yubicommon --------------------------------------------------------------------------------