yubikey-manager-3.1.1/0000755000175100001630000000000013614233377015416 5ustar runnerdocker00000000000000yubikey-manager-3.1.1/COPYING0000644000175100001630000000245213614233340016442 0ustar runnerdocker00000000000000Copyright (c) 2015 Yubico AB All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. yubikey-manager-3.1.1/MANIFEST.in0000644000175100001630000000033413614233340017142 0ustar runnerdocker00000000000000include COPYING include NEWS include ChangeLog include resources/* include doc/*.adoc recursive-include test * include README.adoc include man/* include ykman/VERSION recursive-exclude *.pyc recursive-exclude test *.pyc yubikey-manager-3.1.1/NEWS0000644000175100001630000002470413614233340016112 0ustar runnerdocker00000000000000* Version 3.1.1 (released 2020-01-29) ** Add support for YubiKey 5C NFC ** OpenPGP: set-touch now performs compatibility checks before prompting for PIN ** OpenPGP: Improve error messages and documentation for set-touch ** PIV: read-object command no longer adds a trailing newline ** CLI: Hint at missing permissions when opening a device fails ** Linux: Improve error handling when pcscd is not running ** Windows: Improve how .DLL files are loaded, thanks to Marius Gabriel Mihai for reporting this! ** Bugfix: set-touch now accepts the cached-fixed option ** Bugfix: Fix crash in OtpController.prepare_upload_key() error parsing ** Bugfix: Fix crash in piv info command when a certificate slot contains an invalid certificate ** Library: PivController.read_certificate(slot) now wraps certificate parsing exceptions in new exception type `InvalidCertificate` ** Library: PivController.list_certificates() now returns `None` for slots containing invalid certificate, instead of raising an exception * Version 3.1.0 (released 2019-08-20) ** Add support for YubiKey 5Ci ** OpenPGP: the info command now prints OpenPGP specification version as well ** OpenPGP: Update support for attestation to match OpenPGP v3.4 ** PIV: Use UTC time for self-signed certificates ** OTP: Static password now supports the Norman keyboard layout * Version 3.0.0 (released 2019-06-24) ** Add support for new YubiKey Preview and lightning form factor ** FIDO: Support for credential management ** OpenPGP: Support for OpenPGP attestation, cardholder certificates and cached touch policies ** OTP: Add flag for using numeric keypad when sending digits * Version 2.1.1 (released 2019-05-28) ** OTP: Add initial support for uploading Yubico OTP credentials to YubiCloud ** Don't automatically select the U2F applet on YubiKey NEO, it might be blocked by the OS ** ChalResp: Always pad challenge correctly ** Bugfix: Don't crash with older versions of cryptography ** Bugfix: Password was always prompted in OATH command, even if sent as argument * Version 2.1.0 (released 2019-03-11) ** Add --reader flag to ykman list, to list available smart card readers ** FIPS: Checking if a YubiKey FIPS is in FIPS mode is now opt-in, with the --check-fips flag ** PIV: Add commands for writing and reading arbitrary PIV objects ** PIV: Verify that the PIN must be between 6 - 8 characters long ** PIV: In import-certificate, make the verification that the certificate and private key matches opt-in, with the --verify flag ** PIV: The piv info command now shows the serial number of the certificates ** PIV: The piv info command now shows the full Distinguished Name (DN) of the certificate subject and issuer, if possible ** PIV: Malformed certificates are now handled better ** OpenPGP: The openpgp touch command now shows current touch policies ** The ykman usb/nfc config command now accepts openpgp as well as opgp as an argument ** Bugfix: Fix support for german (DE) keyboard layout for static passwords * Version 2.0.0 (released 2019-01-09) ** Add support for Security Key NFC ** Add experimental support for external smart card reader. See --reader flag ** Add a minimal manpage ** Add examples in help texts ** PIV: update CHUID when importing a certificate ** PIV: Optionally validate that private key and certificate match when importing a certificate (on by default in CLI) ** PIV: Improve support for importing certificate chains and .PEM files with comments ** Breaking API changes: *** Merge CCID status word constants into a single SW enum in ykman.driver_ccid *** Throw custom exception types instead of raw APDUErrors from many methods of PivController *** Write CLI prompts to standard error instead of standard output *** Replace function `ykman.util.parse_certificate` with `parse_certificates` which returns a list * Version 1.0.1 (released 2018-10-10) ** Support for YubiKey 5A ** OATH: Ignore extra parameters in URI parsing ** Bugfix: Never say that NFC is supported for YubiKeys without NFC * Version 1.0.0 (released 2018-09-24) ** Add support for YubiKey 5 Series ** Config: Add flag to generate a random configuration lock ** OATH: Give a proper error message when a touch credential times out ** NDEF: Allow setting the NDEF prefix from the CLI ** FIDO: Block reset when multiple YubiKeys are connected * Version 0.7.1 (released 2018-07-09) ** Support for YubiKey FIPS. ** OTP: Allow setting and removing access codes on the slots. ** Interfaces: set-lock-code now only accepts hexadecimal inputs. ** Bugfix: Don't fail to open the YubiKey when the serial is not visible. * Version 0.7.0 (released 2018-05-07) ** Support for YubiKey Preview. ** Add command to configure enabled applications over USB and NFC. See ykman config -h. ** Add command for selecting which slot to use for NDEF. See ykman otp ndef -h. * Version 0.6.1 (released 2018-04-16) ** Support for YubiKeys with FIDO2. See ykman fido -h ** Report the form factor for YubiKeys that support it. ** OTP: slot command is now called otp. See ykman otp -h for all changes. ** Static password: Add support for different keyboard layouts. See ykman otp static -h ** PIV: Signatures for CSRs are now correct. ** PIV: Commands on slots with PIN policy ALWAYS no longer fail if the YubiKey has a management key protected by PIN. ** Mode: The U2F mode is now called FIDO. ** Dependencies: libu2f-host is no longer used for FIDO communication over USB, instead the python library fido2 is used. * Version 0.6.0 (released 2018-02-09) ** OpenPGP: Expose remaining PIN retries in info command and API. ** CCID: Only try YubiKey smart card readers by default. ** Handle NEO issues with challenge-response credentials better. ** Improve logging. ** Improve error handling when opening device over OTP. ** Bugfix: Fix adding OTP data through the interactive prompt. * Version 0.5.0 (released 2017-12-15) ** API breaking changes: *** OATH: New API more similar to yubioath-android ** CLI breaking changes: *** OATH: Touch prompt now written to stderr instead of stdout *** OATH: `-a|--algorithm` option to `list` command removed *** OATH: Columns in `code` command are now dynamically spaced depending on contents *** OATH: `delete` command now requires confirmation or `-f|--force` argument *** OATH: IDs printed by `list` command now include TOTP period if not 30 *** Changed outputs: **** INFO: "Device name" output changed to "Device type" **** PIV: "Management key is stored on device" output changed to "Management key is stored on the YubiKey" **** PIV: "All PIV data have been cleared from the device" output changed to "All PIV data have been cleared from your YubiKey" **** PIV: "The current management key is stored on the device" prompt changed to "The current management key is stored on the YubiKey" **** SLOT: "blank to use device serial" prompt changed to "blank to use YubiKey serial number" **** SLOT: "Using device serial" output changed to "Using YubiKey device serial" **** Lots of failure case outputs changed ** New features: *** Support for multiple devices via new top-level option `-d|--device` *** New top-level option `-l|--log-level` to enable logging *** OATH: Support for remembering passwords locally. *** OATH: New option `-s|--single` for `code` command *** PIV: `set-pin-retries` command now warns that PIN and PUK will be reset to factory defaults, and prints those defaults after resetting ** API bug fixes: *** OATH: `valid_from` and `valid_to` for `Code` are now absolute instead of relative to the credential period *** OATH: `period` for non-TOTP `Code` is now `None` * Version 0.4.6 (released 2017-10-17) ** Will now attempt to open device 3 times before failing ** OpenPGP: Don't say data is removed when not ** OpenPGP: Don't swallow APDU errors ** PIV: Block on-chip RSA key generation for firmware versions 4.2.0 to 4.3.4 (inclusive) since these chips are vulnerable to http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15361[CVE-2017-15631]. * Version 0.4.5 (released 2017-09-14) ** OATH: Don't print issuer if there is no issuer. * Version 0.4.4 (released 2017-09-06) ** OATH: Fix yet another issue with backwards compatibility, for adding new credentials. * Version 0.4.3 (released 2017-09-06) ** OATH: Fix issue with backwards compatibility, when used as a library. * Version 0.4.2 (released 2017-09-05) ** OATH: Support 7 digit credentials. ** OATH: Support credentials with a period other than 30 seconds. ** OATH: The remove command is now called delete. * Version 0.4.1 (released 2017-08-10) ** PIV: Dropped support for deriving a management key from PIN. ** PIV: Added support for generating a random management key and storing it on the device protected by the PIN. ** OpenPGP: The reset command now handles a device in terminated state. ** OATH: Credential filtering is now working properly on Python 2. * Version 0.4.0 (released 2017-06-19) ** Added PIV support. The tool and library now supports most of the PIV functionality found on the YubiKey 4 and NEO. To list the available commands, run ykman piv -h. ** Mode command now supports adding and removing modes incrementally. * Version 0.3.3 (released 2017-05-08) ** Bugfix: Fix issue with OATH credentials from Steam on YubiKey 4. * Version 0.3.2 (released 2017-04-24) ** Allow access code input through an interactive prompt. ** Bugfix: Some versions of YubiKey NEO occasionally failed calculating challenge-response credentials with touch. * Version 0.3.1 (released 2017-03-13) ** Allow programming of TOTP credentials in YubiKey Slots using the chalresp command. ** Add a calculate command (and library support) to perform a challenge-response operation. Can also be used to generate TOTP codes for credentials stored in a slot. ** OATH: Remove whitespace in secret keys provided by the user. ** OATH: Prompt the user to touch the YubiKey for HOTP touch credentials. ** Bugfix: The flag for showing hidden credentials was not working correctly for the oath code command. * Version 0.3.0 (released 2017-01-23) ** OATH functionality added. The tool now exposes the OATH functionality found on the YubiKey 4 and NEO. To list the available commands, run ykman oath -h. ** Added support for randomly generated static passwords. * Version 0.2.0 (released 2016-11-23) ** Removed all GUI code. This project is now only for the python library and CLI tool. The GUI will be re-released separately in a different project. ** Added command to update settings for YubiKey Slots. * Version 0.1.0 (released 2016-07-07) ** Initial release for beta testing. yubikey-manager-3.1.1/PKG-INFO0000644000175100001630000000133713614233377016517 0ustar runnerdocker00000000000000Metadata-Version: 1.2 Name: yubikey-manager Version: 3.1.1 Summary: Tool for managing your YubiKey configuration. Home-page: https://github.com/Yubico/yubikey-manager Author: Dain Nilsson Author-email: dain@yubico.com Maintainer: Yubico Open Source Maintainers Maintainer-email: ossmaint@yubico.com License: BSD 2 clause Description: UNKNOWN Platform: UNKNOWN Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: X11 Applications :: Qt Classifier: Intended Audience :: End Users/Desktop Classifier: Topic :: Security :: Cryptography Classifier: Topic :: Utilities yubikey-manager-3.1.1/README.adoc0000644000175100001630000000542113614233340017173 0ustar runnerdocker00000000000000== YubiKey Manager CLI image:https://github.com/Yubico/yubikey-manager/workflows/build/badge.svg["Build Status", link="https://github.com/Yubico/yubikey-manager/actions"] Python library and command line tool for configuring a YubiKey. If you're looking for the full graphical application, which also includes the command line tool, it's https://developers.yubico.com/yubikey-manager-qt/[here]. === Usage For more usage information and examples, see the https://support.yubico.com/support/solutions/articles/15000012643-yubikey-manager-cli-ykman-user-guide[YubiKey Manager CLI User Manual]. .... Usage: ykman [OPTIONS] COMMAND [ARGS]... Configure your YubiKey via the command line. Examples: List connected YubiKeys, only output serial number: $ ykman list --serials Show information about YubiKey with serial number 0123456: $ ykman --device 0123456 info Options: -v, --version -d, --device SERIAL -l, --log-level [DEBUG|INFO|WARNING|ERROR|CRITICAL] Enable logging at given verbosity level. --log-file FILE Write logs to the given FILE instead of standard error; ignored unless --log-level is also set. -r, --reader NAME Use an external smart card reader. Conflicts with --device and list. -h, --help Show this message and exit. Commands: config Enable/Disable applications. fido Manage FIDO applications. info Show general information. list List connected YubiKeys. mode Manage connection modes (USB Interfaces). oath Manage OATH Application. openpgp Manage OpenPGP Application. otp Manage OTP Application. piv Manage PIV Application. .... === Installation ==== Ubuntu $ sudo apt-add-repository ppa:yubico/stable $ sudo apt update $ sudo apt install yubikey-manager ==== macOS $ brew install ykman ==== Windows The command line tool is installed together with the GUI version of https://developers.yubico.com/yubikey-manager-qt/[YubiKey Manager]. ==== Pip $ pip install yubikey-manager In order for the pip package to work, https://developers.yubico.com/yubikey-personalization/[ykpers] and http://libusb.info/[libusb] need to be installed on your system as well. https://pyscard.sourceforge.io/[Pyscard] is also needed in some form, and if it's not installed pip builds it using http://www.swig.org/[swig] and potentially https://pcsclite.alioth.debian.org/pcsclite.html[PCSC lite]. ==== Source To install from source, see the link:doc/development.adoc[development] instructions. === Bash completion Experimental Bash completion for the command line tool is available, but not enabled by default. To enable it, run this command once: $ source <(_YKMAN_COMPLETE=source ykman | sudo tee /etc/bash_completion.d/ykman) yubikey-manager-3.1.1/doc/0000755000175100001630000000000013614233377016163 5ustar runnerdocker00000000000000yubikey-manager-3.1.1/doc/development.adoc0000644000175100001630000000361313614233340021326 0ustar runnerdocker00000000000000== Working with the code === Install dependencies It's assumed a Python environment with pip is installed. ==== Windows Make sure the http://www.swig.org/[swig] executable is in your PATH. Add http://libusb.info/[libusb] and https://developers.yubico.com/yubikey-personalization/[ykpers] DLLs to the root of the repository. ==== macOS $ brew install swig ykpers libusb ==== Linux (Debian-based distributions) $ sudo apt install swig libykpers-1-1 libu2f-udev pcscd libpcsclite-dev === Install yubikey-manager from source Clone the repository: $ git clone https://github.com/Yubico/yubikey-manager.git $ cd yubikey-manager Install in editable mode with pip: $ pip install -e . Show available commands: $ ykman --help Show information about inserted YubiKey: $ ykman info Run ykman in DEBUG mode: $ ykman --log-level DEBUG info To uninstall, run: $ pip uninstall yubikey-manager === Code Style This project uses http://flake8.pycqa.org/[Flake8] for code style with a http://pre-commit.com/[pre-commit] hook. To use these: $ pip install pre-commit flake8 $ pre-commit install === Unit tests To run unit tests: $ python setup.py test === Integration tests WARNING: ONLY run these on dedicated developer keys, as it will permanently delete data on the device(s)! To run integration tests, indicate the serial numbers (given by `ykman list`) of the YubiKeys to test with: $ DESTRUCTIVE_TEST_YUBIKEY_SERIALs=123456,234567 python setup.py test The integration test suite will automatically identify which test cases can be run with the available YubiKeys, and run those test cases for each eligible YubiKey. See link:integration-tests.adoc[integration-tests.adoc] for a deep dive into how this works. === Packaging For third-party packaging, use the source releases and signatures available https://developers.yubico.com/yubikey-manager/Releases/[here]. yubikey-manager-3.1.1/doc/integration-tests.adoc0000644000175100001630000001736213614233340022475 0ustar runnerdocker00000000000000The integration tests use an internal mini-framework to abstract access to YubiKeys to test with. This document describes how that framework works. == API for writing tests The framework is activated by defining a function `def additional_tests(arg)` decorated with either `@device_test_suite(transports)` or `@cli_test_suite`. This function is expected to return a list of `unittest.TestCase` subclasses - note: the classes themselves, not instances of the classes. The `additional_tests` function receives one argument. If decorated with `@device_test_suite(transports)`, the argument is a specialized variant of `descriptor.open_device` which opens a particular YubiKey, so name the argument `open_device`. If decorated with `@cli_test_suite`, the argument is a specialized variant of `test.util.ykman_cli` which runs the CLI with a particular YubiKey, so name the argument `ykman_cli`. Use this argument function to interact with YubiKeys within the `TestCase` classes defined in the `additional_tests` body. The module `test.on_yubikey.framework.yubikey_conditions` provides decorators which restrict test methods to running only on YubiKeys matching those conditions. Test methods with no condition decorators are run for all YubiKeys. You can also define new conditions by defining a function that takes a `ykman.device.YubiKey` as a parameter and returns `True` if tests with this condition should be run with that YubiKey, and decorating the function with `@yubikey_conditions.yubikey_condition`. See the `yubikey_conditions` module for examples. == How it works The system utilizes the https://setuptools.readthedocs.io/en/latest/setuptools.html#test-build-package-and-run-a-unittest-suite[`additional_tests()`] hook of setuptools's test discovery, and conceptually transforms this: ```python @device_test_suite(TRANSPORT.CCID) def additional_tests(open_device): class PivKeyManagement(unittest.TestCase): def setUp(self): self.dev = open_device() self.controller = PivController(self.dev.driver) def tearDown(self): self.dev.driver.close() def generate_key(self, slot, alg=ALGO.ECCP256, pin_policy=None): self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) public_key = self.controller.generate_key( slot, alg, pin_policy=pin_policy, touch_policy=TOUCH_POLICY.NEVER) self.reconnect() return public_key @yubikey_conditions.supports_piv_touch_policies def test_delete_certificate_requires_authentication(self): self.generate_key(SLOT.AUTHENTICATION) with self.assertRaises(APDUError): self.controller.delete_certificate(SLOT.AUTHENTICATION) self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) self.controller.delete_certificate(SLOT.AUTHENTICATION) @yubikey_conditions.is_fips def test_pin_policy_never_blocked_on_fips(self): with self.assertRaises(APDUError): self.generate_key(pin_policy=PIN_POLICY.NEVER) return [PivKeyManagement] ``` into this: ```python class PivKeyManagement_CCID_5.0.2_7652135(unittest.TestCase): def setUp(self): self.dev = open_device(transports=TRANSPORT.CCID, serial=7652135) self.controller = PivController(self.dev.driver) def tearDown(self): self.dev.driver.close() def generate_key(self, slot, alg=ALGO.ECCP256, pin_policy=None): self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) public_key = self.controller.generate_key( slot, alg, pin_policy=pin_policy, touch_policy=TOUCH_POLICY.NEVER) self.reconnect() return public_key def test_delete_certificate_requires_authentication(self): self.generate_key(SLOT.AUTHENTICATION) with self.assertRaises(APDUError): self.controller.delete_certificate(SLOT.AUTHENTICATION) self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) self.controller.delete_certificate(SLOT.AUTHENTICATION) class PivKeyManagement_CCID_3.5.0_6513273(unittest.TestCase): def setUp(self): self.dev = open_device(transports=TRANSPORT.CCID, serial=6513273) self.controller = PivController(self.dev.driver) def tearDown(self): self.dev.driver.close() def generate_key(self, slot, alg=ALGO.ECCP256, pin_policy=None): self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) public_key = self.controller.generate_key( slot, alg, pin_policy=pin_policy, touch_policy=TOUCH_POLICY.NEVER) self.reconnect() return public_key class PivKeyManagement(unittest.TestCase): def test_pin_policy_never_blocked_on_fips(self): self.skipTest('No YubiKey available for test.') ``` So in this case the test suite is run with two YubiKeys: one of firmware version 3.5.0 and one 5.0.2. Note that - Each YubiKey gets its own variant of the test class, where the class name contains the firmware version and serial number of the YubiKey and the transport used to communicate with the YubiKey. - Each variant of the test class gets its own arguments to `open_device` - One test is missing from the 3.5.0 test class, because the method was decorated with `@yubikey_conditions.supports_piv_touch_policies` which is a condition the YubiKey NEO does not satisfy. - One test was not satisfied by either YubiKey because it was decorated with `@yubikey_conditions.is_fips`, so it remains in the original test class which now marks all its tests as skipped. This isn't actually how it's implemented under the hood, but it's the mental model for how the design is intended to work. In order to not break how the `setUp` and `tearDown` hooks work, we need to transform each original class definition into multiple variants of the class, one for each YubiKey and transport. That means we can't do this with a simple class decorator, which is why the `additional_tests()` hook is used. Functions can define and return classes, which is what happens inside the `@device_test_suite` decorator. A real `additional_tests()` function should take no parameters and return a `unittest.TestSuite` instance; the `@device_test_suite` decorator expects the decorated function to take one parameter and return a list of `unittest.TestCase` subclasses. `@device_test_suite` then creates a specialized `open_device` function for each YubiKey and transport, and calls the decorated function repeatedly once for each function specialization. Each call to the decorated `additional_tests(open_device)` creates a new set of test classes; the `@device_test_suite` decorator removes any unsupported test methods from each of them and finally assembles and returns a `TestSuite` instance to setuptools's test discovery mechanism. The `@cli_test_suite` decorator works in much the same way, except it doesn't take a `transports` parameter and it instead provides the decorated `additional_tests(ykman_cli)` with a specialized function for invoking the CLI. The condition decorators work by attaching a new attribute `_yubikey_conditions` to the test method. The attribute is a set of predicate functions, and each condition decorator adds a predicate to the set. The `@*_test_suite` decorators call each of the predicates in the set, passing as an argument the `ykman.device.YubiKey` handle for the YubiKey for the test method's parent test class, and deletes the method if any predicate returns a falsy value. This means that each test method can be decorated with multiple conditions, but the condition decorators themselves cannot reference each other (for "not" variants, for example) because the decorator functions themselves are not predicates. The condition decorators can also decorate test classes, in which case they are forwarded to all test methods. yubikey-manager-3.1.1/man/0000755000175100001630000000000013614233377016171 5ustar runnerdocker00000000000000yubikey-manager-3.1.1/man/ykman.10000644000175100001630000000241313614233340017360 0ustar runnerdocker00000000000000.TH YKMAN "1" "January 2020" "ykman 3.1.1" "User Commands" .SH NAME ykman \- YubiKey Manager (ykman) .SH SYNOPSIS .B ykman [\fI\,OPTIONS\/\fR] \fI\,COMMAND \/\fR[\fI\,ARGS\/\fR]... .SH DESCRIPTION .PP Configure your YubiKey via the command line. .SH OPTIONS .HP \fB\-v\fR, \fB\-\-version\fR .HP \fB\-d\fR, \fB\-\-device\fR SERIAL .TP \fB\-l\fR, \fB\-\-log\-level\fR [DEBUG|INFO|WARNING|ERROR|CRITICAL] Enable logging at given verbosity level. .TP \fB\-\-log\-file\fR FILE Write logs to the given FILE instead of standard error; ignored unless \fB\-\-log\-level\fR is also set. .TP \fB\-r\fR, \fB\-\-reader\fR NAME Use an external smart card reader. Conflicts with \fB\-\-device\fR and list. .TP \fB\-h\fR, \fB\-\-help\fR Show this message and exit. .SS "Commands:" .TP config Enable/Disable applications. .TP fido Manage FIDO applications. .TP info Show general information. .TP list List connected YubiKeys. .TP mode Manage connection modes (USB Interfaces). .TP oath Manage OATH Application. .TP openpgp Manage OpenPGP Application. .TP otp Manage OTP Application. .TP piv Manage PIV Application. .SH EXAMPLES .PP List connected YubiKeys, only output serial number: .PP $ ykman list --serials .PP Show information about YubiKey with serial number 0123456: .PP $ ykman --device 0123456 info yubikey-manager-3.1.1/setup.cfg0000644000175100001630000000012313614233377017233 0ustar runnerdocker00000000000000[flake8] max-line-length = 80 exclude = .*/ [egg_info] tag_build = tag_date = 0 yubikey-manager-3.1.1/setup.py0000755000175100001630000000564613614233340017134 0ustar runnerdocker00000000000000# Copyright (c) 2015 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. import sys import os from setuptools import setup install_requires = [ 'six', 'pyscard', 'pyusb', 'click', 'cryptography', 'pyopenssl', 'fido2 >= 0.7' ] tests_require = [] if sys.version_info < (3, 3): tests_require.append('mock') if sys.version_info < (3, 4): install_requires.append('enum34') if sys.platform == 'win32': install_requires.append('pypiwin32') with open( os.path.join( os.path.dirname(__file__), 'ykman/VERSION')) as version_file: version = version_file.read().strip() setup( name='yubikey-manager', version=version, author='Dain Nilsson', author_email='dain@yubico.com', maintainer='Yubico Open Source Maintainers', maintainer_email='ossmaint@yubico.com', url='https://github.com/Yubico/yubikey-manager', description='Tool for managing your YubiKey configuration.', license='BSD 2 clause', entry_points={ 'console_scripts': ['ykman=ykman.cli.__main__:main'], }, packages=[ 'ykman', 'ykman.native', 'ykman.scancodes', 'ykman.cli'], install_requires=install_requires, package_data={'ykman': ['VERSION']}, include_package_data=True, test_suite='test', tests_require=tests_require, classifiers=[ 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Development Status :: 5 - Production/Stable', 'Environment :: X11 Applications :: Qt', 'Intended Audience :: End Users/Desktop', 'Topic :: Security :: Cryptography', 'Topic :: Utilities' ] ) yubikey-manager-3.1.1/test/0000755000175100001630000000000013614233377016375 5ustar runnerdocker00000000000000yubikey-manager-3.1.1/test/__init__.py0000644000175100001630000000000013614233340020462 0ustar runnerdocker00000000000000yubikey-manager-3.1.1/test/files/0000755000175100001630000000000013614233377017477 5ustar runnerdocker00000000000000yubikey-manager-3.1.1/test/files/rsa_1024_key.pem0000644000175100001630000000156713614233340022304 0ustar runnerdocker00000000000000-----BEGIN RSA PRIVATE KEY----- MIICWwIBAAKBgQDGAQa8eT5T9FjSP2Xcw/uj5LZrq5/hdpEGG7XO10IfPy0wbwqP j+omaCxJlAPXuxFy0cYFNQlangIu0HAJ/TMAZXPJLBSRwdK7X/aZn/Ds2vRNAcp5 av+Pym9cfnfgMoS+6CvbUMAduLhzrnh4tQv4lb/AkliomHuoczdbcWHvKwIDAQAB AoGAXzxrIwgmBHeIqUe5FOBnDsOZQlyAQA+pXYjCf8Rll2XptFwUdkzAUMzWUGWT G5ZspA9l8Wc7IozRe/bhjMxuVK5yZhPDKbjqRdWICA95Jd7fxlIirHOVMQRdzI7x NKqMNQN05MLJfsEHUYtOLhZE+tfhJTJnnmB7TMwnJgc4O5ECQQD8oOJ45tyr46zc OAt6ao7PefVLiW5Qu+PxfoHmZmDV2UQqeM5XtZg4O97VBSugOs3+quIdAC6LotYl /6N+E4y3AkEAyKWD2JNCrAgtjk2bfF1HYt24tq8+q7x2ek3/cUhqwInkrZqOFoke x3+yBB879TuUOadvBXndgMHHcJQKSAJlLQJAXRuGnHyptAhTe06EnHeNbtZKG67p I4Q8PJMdmSb+ZZKP1v9zPUxGb+NQ+z3OmF1T8ppUf8/DV9+KAbM4NI1L/QJAdGBs BKYFObrUkYE5+fwwd4uao3sponqBTZcH3jDemiZg2MCYQUHu9E+AdRuYrziLVJVk s4xniVLb1tRG0lVxUQJASfjdGT81HDJSzTseigrM+JnBKPPrzpeEp0RbTP52Lm23 YARjLCwmPMMdAwYZsvqeTuHEDQcOHxLHWuyN/zgP2A== -----END RSA PRIVATE KEY----- yubikey-manager-3.1.1/test/files/rsa_2048_cert.der0000644000175100001630000000133713614233340022444 0ustar runnerdocker0000000000000000C 0  *H 01 0 UJP10 UTokyo10UChuo-ku10U Frank4DD10U WebCert Support10UFrank4DD Web CA1#0! *H  support@frank4dd.com0 120822052741Z 170821052741Z0J1 0 UJP10 U Tokyo10U Frank4DD10U www.example.com0"0  *H 0 ^3) Ϯv-șxy ԺמR LpB)sPw,lզZV&QK%4*)AUk۝UB%I52i<~O%?W$yI'׉Kj. Q#=VX1xhnD~ڌZ{$@)Hܮ*]jŰY؜xZuVAdy45zsxr$%eu0Y0  *H @[tss o:D/Ss2?yd9xbIj摅O8i.I#hKWZQ1lUBh";='P2bz}+)歲iM( yubikey-manager-3.1.1/test/files/rsa_2048_cert.pem0000644000175100001630000000203213614233340022444 0ustar runnerdocker00000000000000-----BEGIN CERTIFICATE----- MIIC2jCCAkMCAg38MA0GCSqGSIb3DQEBBQUAMIGbMQswCQYDVQQGEwJKUDEOMAwG A1UECBMFVG9reW8xEDAOBgNVBAcTB0NodW8ta3UxETAPBgNVBAoTCEZyYW5rNERE MRgwFgYDVQQLEw9XZWJDZXJ0IFN1cHBvcnQxGDAWBgNVBAMTD0ZyYW5rNEREIFdl YiBDQTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEBmcmFuazRkZC5jb20wHhcNMTIw ODIyMDUyNzQxWhcNMTcwODIxMDUyNzQxWjBKMQswCQYDVQQGEwJKUDEOMAwGA1UE CAwFVG9reW8xETAPBgNVBAoMCEZyYW5rNEREMRgwFgYDVQQDDA93d3cuZXhhbXBs ZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0z9FeMynsC8+u dvX+LciZxnh5uRj4C9S6tNeeAlIGCfQYk0zUcNFCoCkTknNQd/YEiawDLNbxBqut bMDZ1aarys1a0lYmUeVLCIqvzBkPJTSQsCopQQ9V8WuT252zzNzs68dVGNdCJd5J NRQykpwexmnjPPv0mvj7i8XgG379TyW6P+WWV5okeUkXJ9eJS2ouDYdR2SM9BoVW +FgxDu6BmXhozW5EfsnajFp7HL8kQClI0QOc79yuKl3492rH6bzFsFn2lfwWy9ic 7cP8EpCTeFp1tFaD+vxBhPZkeTQ1HKx6hQ5zeHIB5ySJJZ7af2W8r4eTGYzbdRW2 4DDHCPhZAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAQMv+BFvGdMVzkQaQ3/+2noVz /uAKbzpEL8xTcxYyP3lkOeh4FoxiSWqy5pGFALdPONoDuYFpLhjJSZaEwuvjI/Tr rGhLV1pRG9frwDFshqD2Vaj4ENBCBh6UpeBop5+285zQ4SI7q4U9oSebUDJiuOx6 +tZ9KynmrbJpTSi0+BM= -----END CERTIFICATE----- yubikey-manager-3.1.1/test/files/rsa_2048_cert_metadata.pem0000644000175100001630000000210013614233340024300 0ustar runnerdocker00000000000000Subject: Subject Name Another comment -----BEGIN CERTIFICATE----- MIIC2jCCAkMCAg38MA0GCSqGSIb3DQEBBQUAMIGbMQswCQYDVQQGEwJKUDEOMAwG A1UECBMFVG9reW8xEDAOBgNVBAcTB0NodW8ta3UxETAPBgNVBAoTCEZyYW5rNERE MRgwFgYDVQQLEw9XZWJDZXJ0IFN1cHBvcnQxGDAWBgNVBAMTD0ZyYW5rNEREIFdl YiBDQTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEBmcmFuazRkZC5jb20wHhcNMTIw ODIyMDUyNzQxWhcNMTcwODIxMDUyNzQxWjBKMQswCQYDVQQGEwJKUDEOMAwGA1UE CAwFVG9reW8xETAPBgNVBAoMCEZyYW5rNEREMRgwFgYDVQQDDA93d3cuZXhhbXBs ZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0z9FeMynsC8+u dvX+LciZxnh5uRj4C9S6tNeeAlIGCfQYk0zUcNFCoCkTknNQd/YEiawDLNbxBqut bMDZ1aarys1a0lYmUeVLCIqvzBkPJTSQsCopQQ9V8WuT252zzNzs68dVGNdCJd5J NRQykpwexmnjPPv0mvj7i8XgG379TyW6P+WWV5okeUkXJ9eJS2ouDYdR2SM9BoVW +FgxDu6BmXhozW5EfsnajFp7HL8kQClI0QOc79yuKl3492rH6bzFsFn2lfwWy9ic 7cP8EpCTeFp1tFaD+vxBhPZkeTQ1HKx6hQ5zeHIB5ySJJZ7af2W8r4eTGYzbdRW2 4DDHCPhZAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAQMv+BFvGdMVzkQaQ3/+2noVz /uAKbzpEL8xTcxYyP3lkOeh4FoxiSWqy5pGFALdPONoDuYFpLhjJSZaEwuvjI/Tr rGhLV1pRG9frwDFshqD2Vaj4ENBCBh6UpeBop5+285zQ4SI7q4U9oSebUDJiuOx6 +tZ9KynmrbJpTSi0+BM= -----END CERTIFICATE----- yubikey-manager-3.1.1/test/files/rsa_2048_key.pem0000644000175100001630000000321713614233340022305 0ustar runnerdocker00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA5ltzQUrGUklMtSXFHG7bb4BcQ4UPCpV09X9oUCvOt/IqU6GB bGHb4K6IZcFi3Jd/3LKknNmV3wq16Jjvc38/vtPlsI+hw733I4d73feyR/+NHt/e dL9pmSAjSyui6HY/Cdu+Gacd8lDekAe/Hndn9Z4E9eitTkCjQwfJulZg1AqFNUxi yeIxJPTF8xTHxgLiXEuK+x8gmu1kpT1aZo++MqFsV+PHuh6wSrO6K1LGJAMdOYWc IrAfI6z8geKrE7XdXdyUwofJNN8qdtxwl4z33Af2f/0uLX9wNNasmh8gA/LSXYXJ VEp8vF5PNkF1WF/4PqouEyvGvPJBcaxW7sf6hQIDAQABAoIBADud1VFDidoH8Fs9 YCsAobfUr4wl5oOltHRIufVtsP04Ji4osTcciGw4n0I+b1iJuOSkMygIw9nKitOc qPPqLdQ0QNCWC5Z+FnTSfoMutKwffiVMaOUsGKcxgxDURUAGQkBJ54P6FSz+Mutx pcu7uWL+t2fxBNEot1gErveTnVGiva0lX+xTQM8c+Ghce5IXHrjFD9Em7Mu+1mms OYxlinwfUQvgvaJArHmKKOGmG9/nwNIrRuvQCWHfVSj0feEubFaZIxHRRU3k21OB 51knwBPh6vDwQ2ksqyWvRyshq0EsWNuLT5l2+Zb1C+NXwqh8/SlxzHUqi3LbK4Gg MRPEhO0CgYEA9SPEwe2MCOv3onpFWJOtsBulyrLed+MzVbDqRC29Y3PjHFftb6mi 2Dh0LpkjoFeGA+L29JZJnqfkhVLbu+5z8LB6Z/zem4EzxXMxQ2b8h9MnbJQYqDHg Xq1XeVmxPJMKN67O8u5ku19av5vZHGq0Q65nln77UiKwEND54DoOBucCgYEA8JAG +INMMYPAAJIjhWAV8UbiY0IRcpE+eIpb4gjWmPciskzxLKAjedrQxJywebu12r8r IH2redvfTSyeDWH1dAj2n71W14flZThbPmkSlb5gO2R0wEhDw2pUdTBTtegP88+Z rL5WIA+Hle/AU7uFQKOn4r6php474Bt06sZzwbMCgYEAktjxdeZyO6n3NyqdvfkB U/zL7UgHQrQkvVF0lJD94cS7KPB3OKva9EGlP4DXOacUjeF5ZH1e7p7OoxtGrCak 52sgeIifZXIZbE+cFC9uWYMhG8b/mkn+iVi3jOcw6AOBXGfoathqGWB+wUd/4Kj/ AYhJX3sD3GkRJZG6DhtY6cMCgYEAp7RAp88gtwQaPkui58Bsi5/XA0tzzmLjIjWS iKmQsWLYlWR+XZXmJXUeRXLWtIbf6HeNIUF64aEeszZ/mOTJsPLuu73LZMYgbcg0 E/Y8NphZjg4iNkoqs3jVGD1wnkgBlv8LKxomAIPTCfvyIG2CH+X3jGNO28JEC6AY ifN/j3ECgYBkWWh8gQEHIEFkce4GvjDI0TQiT1HAO01MNVl1Si5BjBL9rPLlbAPO sMkiGjKCbAn/w7xhln9IcsTm8EhsKrpsNkAon2sNNGg5uoCsMwO9CqGIvVTBsX84 lSqIz1Vex5yCZAySKgPUFw89Llvu+WLcy6ZXf9ZDiH6oqR+rdmd6EQ== -----END RSA PRIVATE KEY----- yubikey-manager-3.1.1/test/files/rsa_2048_key_cert.pfx0000644000175100001630000000464513614233340023344 0ustar runnerdocker000000000000000 0 g *H  X T0 P0 *H 00 *H 0 *H  0|o@SzYIGl޲QY|5Υ|إP],[POWI,IXǤ t"To7T^ G%2Bd6D{-YkyM +;0:2bA'L8xq"C\S}P;4.5_2`;W%1H qc輌\@L!dA*iDzgІgvjz$eNB|NsQ[8#'W;>#*70A *H 2.0*0& *H  00 *H  0 Ȫ+24ȯ:s4Bjr+aM}B2UDR\A\l, FAF^KK?ޖiD-O*SɁI ?l8]EB0gSC@E)eO(Ae=R[]3w嗑^9Γ׵ ɽ Pm4HˣOjMS˫b0BIuۿ|Y2 S06}35]= ;D_/F*@poq; 5^6v(g뒚y^fWi qkȊ$B7Ι4olqW</X[޺i\d1w簌Lޯiڐj]L@p )LmX`v殨NJ<#6s9c R]; ѼBBji] S\aI\(~þ~%_威%cJܒ#LZk0.sV܇y;6L<.<$.3a4} ңaҨH\I#bܶ1Z_Fs r*`Ma\lxpbvC|x 6o0}gJ!=ATx^q^d]3/]JV/2OA_8V/N?}:DcECnlwž_c ]Ds;Z~-rJ͜2Gu̖wfc-=?6?~ƍ3J%XSI5'ď=;SkjeR΀H m=2ef͹MtĒSяqkeT0+D}IeqvجcD^#^>2"Lst?|7 1%0# *H  1g (eȦ3d010!0 + g!o"gLmCooCyubikey-manager-3.1.1/test/files/rsa_2048_key_cert_encrypted.pfx0000644000175100001630000000464513614233340025421 0ustar runnerdocker000000000000000 0 g *H  X T0 P0 *H 00 *H 0 *H  0.[ NT6w1u0JsyS7*ɫkVS/dPu1}/K . ?>wPەb/\Ҧ]:p2~QvFzLˑ t%\;%A;)U;H ,{+4wMN ޟn,Cbdzgطkדm1t6B]9q;.8%$BB폍N&0 튽,E0хF/"peF;(ʦ|uˑ-{Le")=*{ "TRׅ`? l33$1:ݼ2 }nкY\.J!"FWJS ׏*)ὒK?^L`ܢG;< (a&:]pyxV & ,1Keep]!_s1N`Pθu'gk8rW:w[[@H ?hT@f5%'մ[@22 gC;s>SkR6anG(\*5XLs10YiWz*@1rCҧ7|-EJ$Q/~!BX@"Sr}۸EAyjْ+{){:cڐtBSxsx\_Z& [ҋ ?_f oX@ǹ Boolean`. This makes the decorated function usable as a condition decorator for test methods. The decorated function should return `True` if the `YubiKey` argument matches the condition - and tests with this condition should run with that YubiKey - and `False` otherwise. ''' def decorate_method(method): method_conditions = ( getattr(method, '_yubikey_conditions') if '_yubikey_conditions' in dir(method) else set()) method_conditions.add(condition) setattr(method, '_yubikey_conditions', method_conditions) return method def decorate_class(cls): for method_name in _get_test_method_names(cls): setattr( cls, method_name, decorate_method(getattr(cls, method_name))) return cls def decorate(method_or_class): if type(method_or_class) is type: return decorate_class(method_or_class) else: return decorate_method(method_or_class) return decorate @yubikey_condition def is_fips(dev): return dev.is_fips @yubikey_condition def is_not_fips(dev): return not dev.is_fips @yubikey_condition def is_neo(dev): return dev.version < (4, 0, 0) @yubikey_condition def is_not_neo(dev): return dev.version >= (4, 0, 0) @yubikey_condition def supports_piv_attestation(dev): return dev.version >= (4, 3, 0) @yubikey_condition def not_supports_piv_attestation(dev): return dev.version < (4, 3, 0) @yubikey_condition def supports_piv_pin_policies(dev): return dev.version >= (4, 0, 0) @yubikey_condition def supports_piv_touch_policies(dev): return dev.version >= (4, 0, 0) @yubikey_condition def is_roca(dev): return is_cve201715361_vulnerable_firmware_version(dev.version) @yubikey_condition def is_not_roca(dev): return not is_cve201715361_vulnerable_firmware_version(dev.version) @yubikey_condition def can_write_config(dev): return dev.can_write_config def version_min(min_version): return yubikey_condition(lambda dev: dev.version >= min_version) def version_in_range(min_inclusive, max_inclusive): return yubikey_condition( lambda dev: min_inclusive <= dev.version <= max_inclusive ) def version_not_in_range(min_inclusive, max_inclusive): return yubikey_condition( lambda dev: dev.version < min_inclusive or dev.version > max_inclusive ) yubikey-manager-3.1.1/test/on_yubikey/test_cli_config.py0000644000175100001630000001570013614233340024250 0ustar runnerdocker00000000000000import time import unittest from .framework import cli_test_suite, yubikey_conditions VALID_LOCK_CODE = 'a' * 32 INVALID_LOCK_CODE_NON_HEX = 'z' * 32 @cli_test_suite def additional_tests(ykman_cli): @yubikey_conditions.can_write_config class TestConfigUSB(unittest.TestCase): def setUp(self): ykman_cli('config', 'usb', '--enable-all', '-f') def tearDown(self): ykman_cli('config', 'usb', '--enable-all', '-f') def test_disable_otp(self): ykman_cli('config', 'usb', '--disable', 'OTP', '-f') output = ykman_cli('config', 'usb', '--list') self.assertNotIn('OTP', output) def test_disable_u2f(self): ykman_cli('config', 'usb', '--disable', 'U2F', '-f') output = ykman_cli('config', 'usb', '--list') self.assertNotIn('FIDO U2F', output) def test_disable_openpgp(self): ykman_cli('config', 'usb', '--disable', 'OPGP', '-f') output = ykman_cli('config', 'usb', '--list') self.assertNotIn('OpenPGP', output) def test_disable_openpgp_alternative_syntax(self): ykman_cli('config', 'usb', '--disable', 'openpgp', '-f') output = ykman_cli('config', 'usb', '--list') self.assertNotIn('OpenPGP', output) def test_disable_piv(self): ykman_cli('config', 'usb', '--disable', 'PIV', '-f') output = ykman_cli('config', 'usb', '--list') self.assertNotIn('PIV', output) def test_disable_oath(self): ykman_cli('config', 'usb', '--disable', 'OATH', '-f') output = ykman_cli('config', 'usb', '--list') self.assertNotIn('OATH', output) def test_disable_fido2(self): ykman_cli('config', 'usb', '--disable', 'FIDO2', '-f') output = ykman_cli('config', 'usb', '--list') self.assertNotIn('FIDO2', output) def test_disable_and_enable(self): with self.assertRaises(SystemExit): ykman_cli( 'config', 'usb', '--disable', 'FIDO2', '--enable', 'FIDO2', '-f') with self.assertRaises(SystemExit): ykman_cli( 'config', 'usb', '--enable-all', '--disable', 'FIDO2', '-f') def test_disable_all(self): with self.assertRaises(SystemExit): ykman_cli( 'config', 'usb', '-d', 'FIDO2', '-d', 'U2F', '-d', 'OATH', '-d', 'OPGP', 'PIV', '-d', 'OTP') def test_mode_command(self): ykman_cli('mode', 'ccid', '-f') output = ykman_cli('config', 'usb', '--list') self.assertNotIn('FIDO U2F', output) self.assertNotIn('FIDO2', output) self.assertNotIn('OTP', output) ykman_cli('mode', 'otp', '-f') output = ykman_cli('config', 'usb', '--list') self.assertNotIn('FIDO U2F', output) self.assertNotIn('FIDO2', output) self.assertNotIn('OpenPGP', output) self.assertNotIn('PIV', output) self.assertNotIn('OATH', output) ykman_cli('mode', 'fido', '-f') output = ykman_cli('config', 'usb', '--list') self.assertNotIn('OTP', output) self.assertNotIn('OATH', output) self.assertNotIn('PIV', output) self.assertNotIn('OpenPGP', output) # Prevent communication errors in other tests time.sleep(1) @yubikey_conditions.can_write_config class TestConfigNFC(unittest.TestCase): def setUp(self): ykman_cli('config', 'nfc', '--enable-all', '-f') def tearDown(self): ykman_cli('config', 'nfc', '--enable-all', '-f') def test_disable_otp(self): ykman_cli('config', 'nfc', '--disable', 'OTP', '-f') output = ykman_cli('config', 'nfc', '--list') self.assertNotIn('OTP', output) def test_disable_u2f(self): ykman_cli('config', 'nfc', '--disable', 'U2F', '-f') output = ykman_cli('config', 'nfc', '--list') self.assertNotIn('FIDO U2F', output) def test_disable_openpgp(self): ykman_cli('config', 'nfc', '--disable', 'OPGP', '-f') output = ykman_cli('config', 'nfc', '--list') self.assertNotIn('OpenPGP', output) def test_disable_piv(self): ykman_cli('config', 'nfc', '--disable', 'PIV', '-f') output = ykman_cli('config', 'nfc', '--list') self.assertNotIn('PIV', output) def test_disable_oath(self): ykman_cli('config', 'nfc', '--disable', 'OATH', '-f') output = ykman_cli('config', 'nfc', '--list') self.assertNotIn('OATH', output) def test_disable_fido2(self): ykman_cli('config', 'nfc', '--disable', 'FIDO2', '-f') output = ykman_cli('config', 'nfc', '--list') self.assertNotIn('FIDO2', output) def test_disable_all(self): ykman_cli('config', 'nfc', '--disable-all', '-f') output = ykman_cli('config', 'nfc', '--list') self.assertFalse(output) def test_disable_and_enable(self): with self.assertRaises(SystemExit): ykman_cli( 'config', 'nfc', '--disable', 'FIDO2', '--enable', 'FIDO2', '-f') with self.assertRaises(SystemExit): ykman_cli( 'config', 'nfc', '--disable-all', '--enable', 'FIDO2', '-f') with self.assertRaises(SystemExit): ykman_cli( 'config', 'nfc', '--enable-all', '--disable', 'FIDO2', '-f') with self.assertRaises(SystemExit): ykman_cli( 'config', 'nfc', '--enable-all', '--disable-all', 'FIDO2', '-f') @yubikey_conditions.can_write_config class TestConfigLockCode(unittest.TestCase): def test_set_lock_code(self): ykman_cli( 'config', 'set-lock-code', '--new-lock-code', VALID_LOCK_CODE) output = ykman_cli('info') self.assertIn( 'Configured applications are protected by a lock code', output) ykman_cli( 'config', 'set-lock-code', '-l', VALID_LOCK_CODE, '--clear') output = ykman_cli('info') self.assertNotIn( 'Configured applications are protected by a lock code', output) def test_set_invalid_lock_code(self): with self.assertRaises(SystemExit): ykman_cli( 'config', 'set-lock-code', '--new-lock-code', 'aaaa') with self.assertRaises(SystemExit): ykman_cli( 'config', 'set-lock-code', '--new-lock-code', INVALID_LOCK_CODE_NON_HEX) return [ TestConfigUSB, TestConfigNFC, TestConfigLockCode, ] yubikey-manager-3.1.1/test/on_yubikey/test_cli_misc.py0000644000175100001630000000170713614233340023740 0ustar runnerdocker00000000000000import unittest from .framework import cli_test_suite, yubikey_conditions @cli_test_suite def additional_tests(ykman_cli): class TestYkmanInfo(unittest.TestCase): def test_ykman_info(self): info = ykman_cli('info') self.assertIn('Device type:', info) self.assertIn('Serial number:', info) self.assertIn('Firmware version:', info) @yubikey_conditions.is_not_fips def test_ykman_info_does_not_report_fips_for_non_fips_device(self): info = ykman_cli('info', '--check-fips') self.assertNotIn('FIPS', info) @yubikey_conditions.is_fips def test_ykman_info_reports_fips_status(self): info = ykman_cli('info', '--check-fips') self.assertIn('FIPS Approved Mode:', info) self.assertIn(' FIDO U2F:', info) self.assertIn(' OATH:', info) self.assertIn(' OTP:', info) return [TestYkmanInfo] yubikey-manager-3.1.1/test/on_yubikey/test_cli_oath.py0000644000175100001630000001315613614233340023741 0ustar runnerdocker00000000000000# -*- coding: utf-8 -*- import unittest from .framework import cli_test_suite, yubikey_conditions URI_HOTP_EXAMPLE = 'otpauth://hotp/Example:demo@example.com?' \ 'secret=JBSWY3DPK5XXE3DEJ5TE6QKUJA======&issuer=Example&counter=1' URI_TOTP_EXAMPLE = ( 'otpauth://totp/ACME%20Co:john.doe@email.com?' 'secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co' '&algorithm=SHA1&digits=6&period=30') URI_TOTP_EXAMPLE_B = ( 'otpauth://totp/ACME%20Co:john.doe.b@email.com?' 'secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co' '&algorithm=SHA1&digits=6&period=30') URI_TOTP_EXAMPLE_EXTRA_PARAMETER = ( 'otpauth://totp/ACME%20Co:john.doe.extra@email.com?' 'secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co' '&algorithm=SHA1&digits=6&period=30&skid=JKS3424d') PASSWORD = 'aaaa' @cli_test_suite def additional_tests(ykman_cli): class TestOATH(unittest.TestCase): def setUp(cls): ykman_cli('oath', 'reset', '-f') def test_oath_info(self): output = ykman_cli('oath', 'info') self.assertIn('version:', output) @yubikey_conditions.is_not_fips def test_info_does_not_indicate_fips_mode_for_non_fips_key(self): info = ykman_cli('oath', 'info') self.assertNotIn('FIPS:', info) def test_oath_add_credential(self): ykman_cli('oath', 'add', 'test-name', 'abba') creds = ykman_cli('oath', 'list') self.assertIn('test-name', creds) def test_oath_add_credential_prompt(self): ykman_cli('oath', 'add', 'test-name-2', input='abba') creds = ykman_cli('oath', 'list') self.assertIn('test-name-2', creds) def test_oath_add_credential_with_space(self): ykman_cli('oath', 'add', 'test-name-space', 'ab ba') creds = ykman_cli('oath', 'list') self.assertIn('test-name-space', creds) def test_oath_hidden_cred(self): ykman_cli('oath', 'add', '_hidden:name', 'abba') creds = ykman_cli('oath', 'code') self.assertNotIn('_hidden:name', creds) creds = ykman_cli('oath', 'code', '-H') self.assertIn('_hidden:name', creds) def test_oath_add_uri_hotp(self): ykman_cli('oath', 'uri', URI_HOTP_EXAMPLE) creds = ykman_cli('oath', 'list') self.assertIn('Example:demo', creds) def test_oath_add_uri_totp(self): ykman_cli('oath', 'uri', URI_TOTP_EXAMPLE) creds = ykman_cli('oath', 'list') self.assertIn('john.doe', creds) def test_oath_add_uri_totp_extra_parameter(self): ykman_cli('oath', 'uri', URI_TOTP_EXAMPLE_EXTRA_PARAMETER) creds = ykman_cli('oath', 'list') self.assertIn('john.doe.extra', creds) def test_oath_add_uri_totp_prompt(self): ykman_cli('oath', 'uri', input=URI_TOTP_EXAMPLE_B) creds = ykman_cli('oath', 'list') self.assertIn('john.doe', creds) def test_oath_code(self): ykman_cli('oath', 'add', 'test-name2', 'abba') creds = ykman_cli('oath', 'code') self.assertIn('test-name2', creds) def test_oath_code_query(self): ykman_cli('oath', 'add', 'query-me', 'abba') creds = ykman_cli('oath', 'code', 'query-me') self.assertIn('query-me', creds) def test_oath_reset(self): output = ykman_cli('oath', 'reset', '-f') self.assertIn('Success! All OATH credentials have been cleared ' 'from your YubiKey', output) def test_oath_hotp_code(self): ykman_cli('oath', 'add', '-o', 'HOTP', 'hotp-cred', 'abba') cred = ykman_cli('oath', 'code', 'hotp-cred') self.assertIn('659165', cred) def test_oath_hotp_steam_code(self): ykman_cli('oath', 'add', '-o', 'HOTP', 'Steam:steam-cred', 'abba') cred = ykman_cli('oath', 'code', 'steam-cred') self.assertIn('CGC3K', cred) def test_oath_delete(self): ykman_cli('oath', 'add', 'delete-me', 'abba') ykman_cli('oath', 'delete', 'delete-me', '-f') self.assertNotIn('delete-me', ykman_cli('oath', 'list')) def test_oath_unicode(self): ykman_cli('oath', 'add', '😃', 'abba') ykman_cli('oath', 'code') ykman_cli('oath', 'list') ykman_cli('oath', 'delete', '😃', '-f') @yubikey_conditions.is_not_fips @yubikey_conditions.version_min((4, 3, 1)) def test_oath_sha512(self): ykman_cli('oath', 'add', 'abba', 'abba', '--algorithm', 'SHA512') ykman_cli('oath', 'delete', 'abba', '-f') @yubikey_conditions.is_fips class TestOathFips(unittest.TestCase): def setUp(self): ykman_cli('oath', 'reset', '-f') @classmethod def tearDownClass(cls): ykman_cli('oath', 'reset', '-f') def test_no_fips_mode_without_password(self): output = ykman_cli('oath', 'info') self.assertIn('FIPS Approved Mode: No', output) def test_fips_mode_with_password(self): ykman_cli('oath', 'set-password', '-n', PASSWORD) output = ykman_cli('oath', 'info') self.assertIn('FIPS Approved Mode: Yes', output) def test_sha512_not_supported(self): with self.assertRaises(SystemExit): ykman_cli('oath', 'add', 'abba', 'abba', '--algorithm', 'SHA512') return [ TestOATH, TestOathFips, ] yubikey-manager-3.1.1/test/on_yubikey/test_cli_openpgp.py0000644000175100001630000000115013614233340024445 0ustar runnerdocker00000000000000import unittest from .framework import cli_test_suite @cli_test_suite def additional_tests(ykman_cli): class TestOpenPGP(unittest.TestCase): def setUp(self): ykman_cli('openpgp', 'reset', '-f') def test_openpgp_info(self): output = ykman_cli('openpgp', 'info') self.assertIn('OpenPGP version:', output) def test_openpgp_reset(self): output = ykman_cli('openpgp', 'reset', '-f') self.assertIn( 'Success! All data has been cleared and default PINs are set.', output) return [TestOpenPGP] yubikey-manager-3.1.1/test/on_yubikey/test_cli_otp.py0000644000175100001630000005155713614233340023617 0ustar runnerdocker00000000000000# vim: set fileencoding=utf-8 : # Copyright (c) 2018 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. import unittest from .framework import cli_test_suite, yubikey_conditions @cli_test_suite def additional_tests(ykman_cli): class TestSlotStatus(unittest.TestCase): def test_ykman_otp_info(self): info = ykman_cli('otp', 'info') self.assertIn('Slot 1:', info) self.assertIn('Slot 2:', info) def test_ykman_swap_slots(self): output = ykman_cli('otp', 'swap', '-f') self.assertIn('Swapping slots...', output) output = ykman_cli('otp', 'swap', '-f') self.assertIn('Swapping slots...', output) @yubikey_conditions.is_not_fips def test_ykman_otp_info_does_not_indicate_fips_mode_for_non_fips_key(self): # noqa: E501 info = ykman_cli('otp', 'info') self.assertNotIn('FIPS Approved Mode:', info) class TestSlotStaticPassword(unittest.TestCase): def setUp(self): ykman_cli('otp', 'delete', '2', '-f') def tearDown(self): ykman_cli('otp', 'delete', '2', '-f') def test_too_long(self): with self.assertRaises(SystemExit): ykman_cli( 'otp', 'static', '2', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') def test_unsupported_chars(self): with self.assertRaises(ValueError): ykman_cli('otp', 'static', '2', 'ö') with self.assertRaises(ValueError): ykman_cli('otp', 'static', '2', '@') def test_provide_valid_pw(self): ykman_cli( 'otp', 'static', '2', 'higngdukgerjktbbikrhirngtlkkttkb') self.assertIn('Slot 2: programmed', ykman_cli('otp', 'info')) def test_provide_valid_pw_prompt(self): ykman_cli( 'otp', 'static', '2', input='higngdukgerjktbbikrhirngtlkkttkb\ny\n') self.assertIn('Slot 2: programmed', ykman_cli('otp', 'info')) def test_generate_pw_too_long(self): with self.assertRaises(SystemExit): ykman_cli('otp', 'static', '2', '--generate', '--length', '39') def test_generate_pw_no_length(self): with self.assertRaises(SystemExit): ykman_cli('otp', 'static', '2', '--generate', '--length') with self.assertRaises(SystemExit): ykman_cli('otp', 'static', '2', '--generate') def test_generate_zero_length(self): with self.assertRaises(SystemExit): ykman_cli('otp', 'static', '2', '--generate', '--length', '0') def test_generate_pw(self): ykman_cli('otp', 'static', '2', '--generate', '--length', '38') self.assertIn('Slot 2: programmed', ykman_cli('otp', 'info')) def test_us_scancodes(self): ykman_cli('otp', 'static', '2', 'abcABC123', '--keyboard-layout', 'US') ykman_cli('otp', 'static', '2', '@!)', '-f', '--keyboard-layout', 'US') def test_de_scancodes(self): ykman_cli('otp', 'static', '2', 'abcABC123', '--keyboard-layout', 'DE') ykman_cli('otp', 'static', '2', 'Üßö', '-f', '--keyboard-layout', 'DE') def test_overwrite_prompt(self): ykman_cli('otp', 'static', '2', 'bbb') with self.assertRaises(SystemExit): ykman_cli('otp', 'static', '2', 'ccc') ykman_cli('otp', 'static', '2', 'ddd', '-f') self.assertIn('Slot 2: programmed', ykman_cli('otp', 'info')) class TestSlotProgramming(unittest.TestCase): def setUp(self): ykman_cli('otp', 'delete', '2', '-f') def tearDown(self): ykman_cli('otp', 'delete', '2', '-f') def test_ykman_program_otp_slot_2(self): ykman_cli( 'otp', 'yubiotp', '2', '--public-id', 'vvccccfiluij', '--private-id', '267e0a88949b', '--key', 'b8e31ab90bb8830e3c1fe1b483a8e0d4', '-f') self._check_slot_2_programmed() def test_ykman_program_otp_slot_2_prompt(self): ykman_cli( 'otp', 'yubiotp', '2', input='vvccccfiluij\n' '267e0a88949b\n' 'b8e31ab90bb8830e3c1fe1b483a8e0d4\n' 'n\n' 'y\n') self._check_slot_2_programmed() def test_ykman_program_otp_slot_2_options(self): output = ykman_cli( 'otp', 'yubiotp', '2', '--public-id', 'vvccccfiluij', '--private-id', '267e0a88949b', '--key', 'b8e31ab90bb8830e3c1fe1b483a8e0d4', '-f') self.assertEqual('', output) self._check_slot_2_programmed() def test_ykman_program_otp_slot_2_generated_all(self): output = ykman_cli( 'otp', 'yubiotp', '2', '-f', '--serial-public-id', '--generate-private-id', '--generate-key') self.assertIn('Using YubiKey serial as public ID', output) self.assertIn('Using a randomly generated private ID', output) self.assertIn('Using a randomly generated secret key', output) self._check_slot_2_programmed() def test_ykman_program_otp_slot_2_serial_public_id(self): output = ykman_cli( 'otp', 'yubiotp', '2', '--serial-public-id', '--private-id', '267e0a88949b', '--key', 'b8e31ab90bb8830e3c1fe1b483a8e0d4', '-f') self.assertIn('Using YubiKey serial as public ID', output) self.assertNotIn('generated private ID', output) self.assertNotIn('generated secret key', output) self._check_slot_2_programmed() def test_invalid_public_id(self): with self.assertRaises(SystemExit): ykman_cli('otp', 'yubiotp', '-P', 'imnotmodhex!') def test_ykman_program_otp_slot_2_generated_private_id(self): output = ykman_cli( 'otp', 'yubiotp', '2', '--public-id', 'vvccccfiluij', '--generate-private-id', '--key', 'b8e31ab90bb8830e3c1fe1b483a8e0d4', '-f') self.assertNotIn('serial as public ID', output) self.assertIn('Using a randomly generated private ID', output) self.assertNotIn('generated secret key', output) self._check_slot_2_programmed() def test_ykman_program_otp_slot_2_generated_secret_key(self): output = ykman_cli( 'otp', 'yubiotp', '2', '--public-id', 'vvccccfiluij', '--private-id', '267e0a88949b', '--generate-key', '-f') self.assertNotIn('serial as public ID', output) self.assertNotIn('generated private ID', output) self.assertIn('Using a randomly generated secret key', output) self._check_slot_2_programmed() def test_ykman_program_otp_slot_2_serial_id_conflicts_public_id(self): with self.assertRaises(SystemExit): ykman_cli('otp', 'yubiotp', '2', '-f', '--serial-public-id', '--public-id', 'vvccccfiluij', '--generate-private-id', '--generate-key') self._check_slot_2_not_programmed() def test_ykman_program_otp_slot_2_generate_id_conflicts_private_id(self): # noqa: E501 with self.assertRaises(SystemExit): ykman_cli( 'otp', 'yubiotp', '2', '-f', '--serial-public-id', '--generate-private-id', '--private-id', '267e0a88949b', '--generate-key') self._check_slot_2_not_programmed() def test_ykman_program_otp_slot_2_generate_key_conflicts_key(self): with self.assertRaises(SystemExit): ykman_cli('otp', 'yubiotp', '2', '-f', '--serial-public-id', '--generate-private-id', '--generate-key', '--key', 'b8e31ab90bb8830e3c1fe1b483a8e0d4') self._check_slot_2_not_programmed() def test_ykman_program_chalresp_slot_2(self): ykman_cli('otp', 'chalresp', '2', 'abba', '-f') self._check_slot_2_programmed() ykman_cli('otp', 'chalresp', '2', '--totp', 'abba', '-f') self._check_slot_2_programmed() ykman_cli('otp', 'chalresp', '2', '--touch', 'abba', '-f') self._check_slot_2_programmed() def test_ykman_program_chalresp_slot_2_force_fails_without_key(self): with self.assertRaises(SystemExit): ykman_cli('otp', 'chalresp', '2', '-f') self._check_slot_2_not_programmed() def test_ykman_program_chalresp_slot_2_generated(self): output = ykman_cli('otp', 'chalresp', '2', '-f', '-g') self.assertRegex(output, 'Using a randomly generated key: [0-9a-f]{40}$') self._check_slot_2_programmed() def test_ykman_program_chalresp_slot_2_generated_fails_if_also_given(self): # noqa: E501 with self.assertRaises(SystemExit): ykman_cli('otp', 'chalresp', '2', '-f', '-g', 'abababab') def test_ykman_program_chalresp_slot_2_prompt(self): ykman_cli('otp', 'chalresp', '2', input='abba\ny\n') self._check_slot_2_programmed() def test_ykman_program_hotp_slot_2(self): ykman_cli( 'otp', 'hotp', '2', '27KIZZE3SD7GE2FVJJBAXEI3I6RRTPGM', '-f') self._check_slot_2_programmed() def test_ykman_program_hotp_slot_2_prompt(self): ykman_cli('otp', 'hotp', '2', input='abba\ny\n') self._check_slot_2_programmed() def test_update_settings_enter_slot_2(self): ykman_cli('otp', 'static', '2', '-f', '-g', '-l', '20') output = ykman_cli('otp', 'settings', '2', '-f', '--no-enter') self.assertIn('Updating settings for slot', output) def test_delete_slot_2(self): ykman_cli('otp', 'static', '2', '-f', '-g', '-l', '20') output = ykman_cli('otp', 'delete', '2', '-f') self.assertIn('Deleting the configuration', output) status = ykman_cli('otp', 'info') self.assertIn('Slot 2: empty', status) def test_access_code_slot_2(self): ykman_cli( 'otp', '--access-code', '111111111111', 'static', '2', '--generate', '--length', '10') self._check_slot_2_programmed() self._check_slot_2_has_access_code() ykman_cli('otp', '--access-code', '111111111111', 'delete', '2', '-f') status = ykman_cli('otp', 'info') self.assertIn('Slot 2: empty', status) @yubikey_conditions.version_in_range((4, 3, 2), (4, 3, 5)) def test_update_access_code_fails_on_yk_432_to_435(self): ykman_cli('otp', 'static', '2', '--generate', '--length', '10') self._check_slot_2_programmed() with self.assertRaises(SystemExit): ykman_cli( 'otp', 'settings', '--new-access-code', '111111111111', '2', '-f') ykman_cli( 'otp', '--access-code', '111111111111', 'static', '2', '-f', '--generate', '--length', '10') with self.assertRaises(SystemExit): ykman_cli('otp', 'delete', '2', '-f') with self.assertRaises(SystemExit): ykman_cli( 'otp', '--access-code', '111111111111', 'settings', '--new-access-code', '222222222222', '2', '-f') ykman_cli('otp', '--access-code', '111111111111', 'delete', '2', '-f') @yubikey_conditions.version_in_range((4, 3, 2), (4, 3, 5)) def test_delete_access_code_fails_on_yk_432_to_435(self): ykman_cli('otp', '--access-code', '111111111111', 'static', '2', '--generate', '--length', '10') self._check_slot_2_programmed() with self.assertRaises(SystemExit): ykman_cli('otp', '--access-code', '111111111111', 'settings', '--delete-access-code', '2', '-f') with self.assertRaises(SystemExit): ykman_cli('otp', 'delete', '2', '-f') ykman_cli('otp', '--access-code', '111111111111', 'delete', '2', '-f') @yubikey_conditions.version_not_in_range((4, 3, 2), (4, 3, 5)) def test_update_access_code_slot_2(self): ykman_cli('otp', 'static', '2', '--generate', '--length', '10') self._check_slot_2_programmed() self._check_slot_2_does_not_have_access_code() ykman_cli('otp', 'settings', '--new-access-code', '111111111111', '2', '-f') self._check_slot_2_has_access_code() ykman_cli('otp', '--access-code', '111111111111', 'settings', '--delete-access-code', '2', '-f') self._check_slot_2_does_not_have_access_code() ykman_cli('otp', 'delete', '2', '-f') @yubikey_conditions.version_not_in_range((4, 3, 2), (4, 3, 5)) def test_update_access_code_prompt_slot_2(self): ykman_cli('otp', 'static', '2', '--generate', '--length', '10') self._check_slot_2_programmed() self._check_slot_2_does_not_have_access_code() ykman_cli('otp', 'settings', '--new-access-code', '', '2', '-f', input='111111111111') self._check_slot_2_has_access_code() ykman_cli('otp', '--access-code', '', 'settings', '--delete-access-code', '2', '-f', input='111111111111') self._check_slot_2_does_not_have_access_code() ykman_cli('otp', 'delete', '2', '-f') @yubikey_conditions.version_not_in_range((4, 3, 2), (4, 3, 5)) def test_new_access_code_conflicts_with_delete_access_code(self): ykman_cli('otp', 'static', '2', '--generate', '--length', '10') self._check_slot_2_programmed() self._check_slot_2_does_not_have_access_code() with self.assertRaises(SystemExit): ykman_cli('otp', 'settings', '--delete-access-code', '--new-access-code', '111111111111', '2', '-f') self._check_slot_2_does_not_have_access_code() ykman_cli('otp', 'settings', '--new-access-code', '111111111111', '2', '-f') with self.assertRaises(SystemExit): ykman_cli('otp', 'settings', '--delete-access-code', '--new-access-code', '111111111111', '2', '-f') self._check_slot_2_has_access_code() ykman_cli('otp', '--access-code', '111111111111', 'delete', '2', '-f') def _check_slot_2_programmed(self): status = ykman_cli('otp', 'info') self.assertIn('Slot 2: programmed', status) def _check_slot_2_not_programmed(self): status = ykman_cli('otp', 'info') self.assertIn('Slot 2: empty', status) def _check_slot_2_has_access_code(self): with self.assertRaises(SystemExit): ykman_cli('otp', 'settings', '--pacing', '0', '2', '-f') ykman_cli('otp', '--access-code', '111111111111', 'settings', '--pacing', '0', '2', '-f') def _check_slot_2_does_not_have_access_code(self): ykman_cli('otp', 'settings', '--pacing', '0', '2', '-f') class TestSlotCalculate(unittest.TestCase): def test_calculate_hex(self): ykman_cli('otp', 'delete', '2', '-f') ykman_cli('otp', 'chalresp', '2', 'abba', '-f') output = ykman_cli('otp', 'calculate', '2', 'abba') self.assertIn('f8de2586056d89d8b961a072d1245a495d2155e1', output) def test_calculate_totp(self): ykman_cli('otp', 'delete', '2', '-f') ykman_cli('otp', 'chalresp', '2', 'abba', '-f') output = ykman_cli('otp', 'calculate', '2', '999', '-T') self.assertEqual('533486', output.strip()) output = ykman_cli('otp', 'calculate', '2', '999', '-T', '-d', '8') self.assertEqual('04533486', output.strip()) output = ykman_cli('otp', 'calculate', '2', '-T') self.assertEqual(6, len(output.strip())) output = ykman_cli('otp', 'calculate', '2', '-T', '-d', '8') self.assertEqual(8, len(output.strip())) @yubikey_conditions.is_fips class TestFipsMode(unittest.TestCase): def tearDown(self): ykman_cli('otp', '--access-code', '111111111111', 'delete', '1', '-f') ykman_cli('otp', '--access-code', '111111111111', 'delete', '2', '-f') def test_not_fips_mode_if_no_slot_programmed(self): ykman_cli('otp', 'delete', '1', '-f') ykman_cli('otp', 'delete', '2', '-f') info = ykman_cli('otp', 'info') self.assertIn('FIPS Approved Mode: No', info) def test_not_fips_mode_if_slot_1_not_programmed(self): ykman_cli('otp', 'delete', '1', '-f') ykman_cli('otp', 'static', '2', '--generate', '--length', '10') info = ykman_cli('otp', 'info') self.assertIn('FIPS Approved Mode: No', info) def test_not_fips_mode_if_slot_2_not_programmed(self): ykman_cli('otp', 'static', '1', '--generate', '--length', '10') ykman_cli('otp', 'delete', '2', '-f') info = ykman_cli('otp', 'info') self.assertIn('FIPS Approved Mode: No', info) def test_not_fips_mode_if_no_slot_has_access_code(self): ykman_cli('otp', 'static', '1', '--generate', '--length', '10') ykman_cli('otp', 'static', '2', '--generate', '--length', '10') info = ykman_cli('otp', 'info') self.assertIn('FIPS Approved Mode: No', info) def test_not_fips_mode_if_only_slot_1_has_access_code(self): ykman_cli('otp', 'static', '1', '--generate', '--length', '10') ykman_cli('otp', 'static', '2', '--generate', '--length', '10') ykman_cli('otp', 'settings', '--new-access-code', '111111111111', '1', '-f') info = ykman_cli('otp', 'info') self.assertIn('FIPS Approved Mode: No', info) def test_not_fips_mode_if_only_slot_2_has_access_code(self): ykman_cli('otp', 'static', '1', '--generate', '--length', '10') ykman_cli('otp', 'static', '2', '--generate', '--length', '10') ykman_cli('otp', 'settings', '--new-access-code', '111111111111', '2', '-f') info = ykman_cli('otp', 'info') self.assertIn('FIPS Approved Mode: No', info) def test_fips_mode_if_both_slots_have_access_code(self): ykman_cli('otp', 'static', '--generate', '--length', '10', '1', '-f') ykman_cli('otp', 'static', '--generate', '--length', '10', '2', '-f') ykman_cli('otp', 'settings', '--new-access-code', '111111111111', '1', '-f') ykman_cli('otp', 'settings', '--new-access-code', '111111111111', '2', '-f') info = ykman_cli('otp', 'info') self.assertIn('FIPS Approved Mode: Yes', info) return [ TestSlotStatus, TestSlotStaticPassword, TestSlotProgramming, TestSlotCalculate, TestFipsMode, ] yubikey-manager-3.1.1/test/on_yubikey/test_fips_u2f_commands.py0000644000175100001630000000767413614233340025565 0ustar runnerdocker00000000000000import struct import unittest from fido2.hid import (CTAPHID) from ykman.util import (TRANSPORT) from ykman.driver_fido import (FIPS_U2F_CMD) from .framework import device_test_suite, yubikey_conditions HID_CMD = 0x03 P1 = 0 P2 = 0 @device_test_suite(TRANSPORT.FIDO) def additional_tests(open_device): @yubikey_conditions.is_fips class TestFipsU2fCommands(unittest.TestCase): def test_echo_command(self): with open_device(transports=TRANSPORT.FIDO) as dev: res = dev.driver._dev.call( CTAPHID.MSG, struct.pack( '>HBBBH6s', FIPS_U2F_CMD.ECHO, P1, P2, 0, 6, b'012345' )) self.assertEqual(res, b'012345\x90\x00') def test_pin_commands(self): # Assumes PIN is 012345 or not set at beginning of test # Sets PIN to 012345 with open_device(transports=TRANSPORT.FIDO) as dev: verify_res1 = dev.driver._dev.call( CTAPHID.MSG, struct.pack( '>HBBBH6s', FIPS_U2F_CMD.VERIFY_PIN, P1, P2, 0, 6, b'012345' )) if verify_res1 == b'\x63\xc0': self.skipTest('PIN set to something other than 012345') if verify_res1 == b'\x69\x83': self.skipTest('PIN blocked') if verify_res1 == b'\x90\x00': res = dev.driver._dev.call( CTAPHID.MSG, struct.pack( '>HBBBHB6s6s', FIPS_U2F_CMD.SET_PIN, P1, P2, 0, 13, 6, b'012345', b'012345' )) else: res = dev.driver._dev.call( CTAPHID.MSG, struct.pack( '>HBBBHB6s', FIPS_U2F_CMD.SET_PIN, P1, P2, 0, 7, 6, b'012345' )) verify_res2 = dev.driver._dev.call( CTAPHID.MSG, struct.pack( '>HBBBH6s', FIPS_U2F_CMD.VERIFY_PIN, P1, P2, 0, 6, b'543210' )) verify_res3 = dev.driver._dev.call( CTAPHID.MSG, struct.pack( '>HBBBH6s', FIPS_U2F_CMD.VERIFY_PIN, P1, P2, 0, 6, b'012345' )) # OK/not set self.assertIn(verify_res1, [b'\x90\x00', b'\x69\x86']) self.assertEqual(res, b'\x90\x00') # Success self.assertEqual(verify_res2, b'\x63\xc0') # Incorrect PIN self.assertEqual(verify_res3, b'\x90\x00') # Success def test_reset_command(self): with open_device(transports=TRANSPORT.FIDO) as dev: res = dev.driver._dev.call( CTAPHID.MSG, struct.pack( '>HBB', FIPS_U2F_CMD.RESET, P1, P2 )) # 0x6985: Touch required # 0x6986: Power cycle required # 0x9000: Success self.assertIn(res, [b'\x69\x85', b'\x69\x86', b'\x90\x00']) def test_verify_fips_mode_command(self): with open_device(transports=TRANSPORT.FIDO) as dev: res = dev.driver._dev.call( CTAPHID.MSG, struct.pack( '>HBB', FIPS_U2F_CMD.VERIFY_FIPS_MODE, P1, P2 )) # 0x6a81: Function not supported (PIN not set - not FIPS Mode) # 0x9000: Success (PIN set - FIPS Approved Mode) self.assertIn(res, [b'\x6a\x81', b'\x90\x00']) return [TestFipsU2fCommands] yubikey-manager-3.1.1/test/on_yubikey/test_interfaces.py0000644000175100001630000000175713614233340024306 0ustar runnerdocker00000000000000import unittest from .framework import DestructiveYubikeyTestCase, exactly_one_yubikey_present from ykman import driver_fido, driver_otp, driver_ccid @unittest.skipIf(not exactly_one_yubikey_present(), 'Exactly one YubiKey must be present.') class TestInterfaces(DestructiveYubikeyTestCase): def test_switch_interfaces(self): next(driver_fido.open_devices()).read_config() next(driver_otp.open_devices()).read_config() next(driver_fido.open_devices()).read_config() next(driver_ccid.open_devices()).read_config() next(driver_otp.open_devices()).read_config() next(driver_ccid.open_devices()).read_config() next(driver_otp.open_devices()).read_config() next(driver_fido.open_devices()).read_config() next(driver_ccid.open_devices()).read_config() next(driver_fido.open_devices()).read_config() next(driver_ccid.open_devices()).read_config() next(driver_otp.open_devices()).read_config() yubikey-manager-3.1.1/test/on_yubikey/test_opgp.py0000644000175100001630000001121313614233340023114 0ustar runnerdocker00000000000000from __future__ import unicode_literals import unittest from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from cryptography.hazmat.primitives.asymmetric import ec, rsa from ykman.driver_ccid import APDUError from ykman.opgp import OpgpController, KEY_SLOT from ykman.util import TRANSPORT from .framework import device_test_suite, yubikey_conditions E = 65537 DEFAULT_PIN = '123456' NON_DEFAULT_PIN = '654321' DEFAULT_ADMIN_PIN = '12345678' NON_DEFAULT_ADMIN_PIN = '87654321' @device_test_suite(TRANSPORT.CCID) def additional_tests(open_device): class OpgpTestCase(unittest.TestCase): def setUp(self): self.dev = open_device() self.controller = OpgpController(self.dev.driver) def tearDown(self): self.dev.driver.close() def reconnect(self): self.dev.driver.close() self.dev = open_device() self.controller = OpgpController(self.dev.driver) class KeyManagement(OpgpTestCase): @classmethod def setUpClass(cls): with open_device() as dev: controller = OpgpController(dev.driver) controller.reset() def test_generate_requires_admin(self): with self.assertRaises(APDUError): self.controller.generate_rsa_key(KEY_SLOT.SIG, 2048) @yubikey_conditions.is_not_roca def test_generate_rsa2048(self): self.controller.verify_admin(DEFAULT_ADMIN_PIN) pub = self.controller.generate_rsa_key(KEY_SLOT.SIG, 2048) self.assertEqual(pub.key_size, 2048) self.controller.delete_key(KEY_SLOT.SIG) @yubikey_conditions.is_not_roca @yubikey_conditions.version_min((4, 0, 0)) def test_generate_rsa4096(self): self.controller.verify_admin(DEFAULT_ADMIN_PIN) pub = self.controller.generate_rsa_key(KEY_SLOT.SIG, 4096) self.assertEqual(pub.key_size, 4096) @yubikey_conditions.version_min((5, 2, 0)) def test_generate_secp256r1(self): self.controller.verify_admin(DEFAULT_ADMIN_PIN) pub = self.controller.generate_ec_key(KEY_SLOT.SIG, 'secp256r1') self.assertEqual(pub.key_size, 256) self.assertEqual(pub.curve.name, 'secp256r1') @yubikey_conditions.version_min((5, 2, 0)) def test_generate_ed25519(self): self.controller.verify_admin(DEFAULT_ADMIN_PIN) pub = self.controller.generate_ec_key(KEY_SLOT.SIG, 'ed25519') self.assertEqual( len(pub.public_bytes( Encoding.Raw, PublicFormat.Raw )), 32 ) @yubikey_conditions.version_min((5, 2, 0)) def test_generate_x25519(self): self.controller.verify_admin(DEFAULT_ADMIN_PIN) pub = self.controller.generate_ec_key(KEY_SLOT.ENC, 'x25519') self.assertEqual( len(pub.public_bytes( Encoding.Raw, PublicFormat.Raw )), 32 ) def test_import_rsa2048(self): priv = rsa.generate_private_key(E, 2048, default_backend()) self.controller.verify_admin(DEFAULT_ADMIN_PIN) self.controller.import_key(KEY_SLOT.SIG, priv) @yubikey_conditions.version_min((4, 0, 0)) def test_import_rsa4096(self): priv = rsa.generate_private_key(E, 4096, default_backend()) self.controller.verify_admin(DEFAULT_ADMIN_PIN) self.controller.import_key(KEY_SLOT.SIG, priv) @yubikey_conditions.version_min((5, 2, 0)) def test_import_secp256r1(self): priv = ec.generate_private_key(ec.SECP256R1(), default_backend()) self.controller.verify_admin(DEFAULT_ADMIN_PIN) self.controller.import_key(KEY_SLOT.SIG, priv) @yubikey_conditions.version_min((5, 2, 0)) def test_import_ed25519(self): from cryptography.hazmat.primitives.asymmetric import ed25519 priv = ed25519.Ed25519PrivateKey.generate() self.controller.verify_admin(DEFAULT_ADMIN_PIN) self.controller.import_key(KEY_SLOT.SIG, priv) @yubikey_conditions.version_min((5, 2, 0)) def test_import_x25519(self): from cryptography.hazmat.primitives.asymmetric import x25519 priv = x25519.X25519PrivateKey.generate() self.controller.verify_admin(DEFAULT_ADMIN_PIN) self.controller.import_key(KEY_SLOT.ENC, priv) return [OpgpTestCase, KeyManagement] yubikey-manager-3.1.1/test/on_yubikey/test_piv.py0000644000175100001630000004773213614233340022764 0ustar runnerdocker00000000000000from __future__ import unicode_literals import datetime import random import unittest from binascii import a2b_hex from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec from ykman.driver_ccid import APDUError from ykman.piv import (ALGO, PIN_POLICY, PivController, SLOT, TOUCH_POLICY) from ykman.piv import ( AuthenticationBlocked, AuthenticationFailed, WrongPuk, KeypairMismatch) from ykman.util import TRANSPORT, parse_certificates, parse_private_key from .framework import device_test_suite, yubikey_conditions from ..util import open_file DEFAULT_PIN = '123456' NON_DEFAULT_PIN = '654321' DEFAULT_PUK = '12345678' NON_DEFAULT_PUK = '87654321' DEFAULT_MANAGEMENT_KEY = a2b_hex('010203040506070801020304050607080102030405060708') # noqa: E501 NON_DEFAULT_MANAGEMENT_KEY = a2b_hex('010103040506070801020304050607080102030405060708') # noqa: E501 now = datetime.datetime.now def get_test_cert(): with open_file('rsa_2048_cert.pem') as f: return parse_certificates(f.read(), None)[0] def get_test_key(): with open_file('rsa_2048_key.pem') as f: return parse_private_key(f.read(), None) @device_test_suite(TRANSPORT.CCID) def additional_tests(open_device): class PivTestCase(unittest.TestCase): def setUp(self): self.dev = open_device() self.controller = PivController(self.dev.driver) def tearDown(self): self.dev.driver.close() def assertMgmKeyIs(self, key): self.controller.authenticate(key) def assertMgmKeyIsNot(self, key): with self.assertRaises(AuthenticationFailed): self.controller.authenticate(key) def assertStoredMgmKeyEquals(self, key): self.assertEqual(self.controller._pivman_protected_data.key, key) def assertStoredMgmKeyNotEquals(self, key): self.assertNotEqual(self.controller._pivman_protected_data.key, key) def reconnect(self): self.dev.driver.close() self.dev = open_device() self.controller = PivController(self.dev.driver) class KeyManagement(PivTestCase): @classmethod def setUpClass(cls): with open_device() as dev: controller = PivController(dev.driver) controller.reset() def generate_key(self, slot, alg=ALGO.ECCP256, pin_policy=None): self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) public_key = self.controller.generate_key( slot, alg, pin_policy=pin_policy, touch_policy=TOUCH_POLICY.NEVER) self.reconnect() return public_key @yubikey_conditions.supports_piv_touch_policies def test_delete_certificate_requires_authentication(self): self.generate_key(SLOT.AUTHENTICATION) with self.assertRaises(APDUError): self.controller.delete_certificate(SLOT.AUTHENTICATION) self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) self.controller.delete_certificate(SLOT.AUTHENTICATION) def test_generate_csr_works(self): public_key = self.generate_key(SLOT.AUTHENTICATION) if self.dev.version < (4, 0, 0): # NEO always has PIN policy "ONCE" self.controller.verify(DEFAULT_PIN) self.controller.verify(DEFAULT_PIN) csr = self.controller.generate_certificate_signing_request( SLOT.AUTHENTICATION, public_key, 'alice') self.assertEqual( csr.public_key().public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo), public_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo), ) self.assertEqual( csr.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0].value, # noqa: E501 'alice' ) def test_generate_self_signed_certificate_requires_authentication(self): public_key = self.generate_key(SLOT.AUTHENTICATION) if self.dev.version < (4, 0, 0): # NEO always has PIN policy "ONCE" self.controller.verify(DEFAULT_PIN) with self.assertRaises(APDUError): self.controller.generate_self_signed_certificate( SLOT.AUTHENTICATION, public_key, 'alice', now(), now()) self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) self.controller.verify(DEFAULT_PIN) self.controller.generate_self_signed_certificate( SLOT.AUTHENTICATION, public_key, 'alice', now(), now()) def _test_generate_self_signed_certificate(self, slot): public_key = self.generate_key(slot) self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) self.controller.verify(DEFAULT_PIN) self.controller.generate_self_signed_certificate( slot, public_key, 'alice', now(), now()) cert = self.controller.read_certificate(slot) self.assertEqual( cert.public_key().public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo), public_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo), ) self.assertEqual( cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0].value, # noqa: E501 'alice' ) def test_generate_self_signed_certificate_slot_9a_works(self): self._test_generate_self_signed_certificate(SLOT.AUTHENTICATION) def test_generate_self_signed_certificate_slot_9c_works(self): self._test_generate_self_signed_certificate(SLOT.SIGNATURE) def test_generate_key_requires_authentication(self): with self.assertRaises(APDUError): self.controller.generate_key(SLOT.AUTHENTICATION, ALGO.ECCP256, touch_policy=TOUCH_POLICY.NEVER) self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) self.controller.generate_key(SLOT.AUTHENTICATION, ALGO.ECCP256, touch_policy=TOUCH_POLICY.NEVER) def test_import_certificate_requires_authentication(self): cert = get_test_cert() with self.assertRaises(APDUError): self.controller.import_certificate(SLOT.AUTHENTICATION, cert, verify=False) self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) self.controller.import_certificate(SLOT.AUTHENTICATION, cert, verify=False) def _test_import_key_pairing(self, alg1, alg2): # Set up a key in the slot and create a certificate for it public_key = self.generate_key( SLOT.AUTHENTICATION, alg=alg1, pin_policy=PIN_POLICY.NEVER) self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) self.controller.generate_self_signed_certificate( SLOT.AUTHENTICATION, public_key, 'test', datetime.datetime.now(), datetime.datetime.now()) cert = self.controller.read_certificate(SLOT.AUTHENTICATION) self.controller.delete_certificate(SLOT.AUTHENTICATION) # Importing the correct certificate should work self.controller.import_certificate(SLOT.AUTHENTICATION, cert, verify=True) # Overwrite the key with one of the same type self.generate_key( SLOT.AUTHENTICATION, alg=alg1, pin_policy=PIN_POLICY.NEVER) # Importing the same certificate should not work with the new key self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) with self.assertRaises(KeypairMismatch): self.controller.import_certificate(SLOT.AUTHENTICATION, cert, verify=True) # Overwrite the key with one of a different type self.generate_key( SLOT.AUTHENTICATION, alg=alg2, pin_policy=PIN_POLICY.NEVER) # Importing the same certificate should not work with the new key self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) with self.assertRaises(KeypairMismatch): self.controller.import_certificate(SLOT.AUTHENTICATION, cert, verify=True) @yubikey_conditions.is_not_fips @yubikey_conditions.is_not_roca def test_import_certificate_verifies_key_pairing_rsa1024(self): self._test_import_key_pairing(ALGO.RSA1024, ALGO.ECCP256) @yubikey_conditions.is_not_roca def test_import_certificate_verifies_key_pairing_rsa2048(self): self._test_import_key_pairing(ALGO.RSA2048, ALGO.ECCP256) def test_import_certificate_verifies_key_pairing_eccp256(self): self._test_import_key_pairing(ALGO.ECCP256, ALGO.ECCP384) def test_import_certificate_verifies_key_pairing_eccp384(self): self._test_import_key_pairing(ALGO.ECCP384, ALGO.ECCP256) def test_import_key_requires_authentication(self): private_key = get_test_key() with self.assertRaises(APDUError): self.controller.import_key(SLOT.AUTHENTICATION, private_key) self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) self.controller.import_key(SLOT.AUTHENTICATION, private_key) def test_read_certificate_does_not_require_authentication(self): cert = get_test_cert() self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) self.controller.import_certificate(SLOT.AUTHENTICATION, cert, verify=False) self.reconnect() cert = self.controller.read_certificate(SLOT.AUTHENTICATION) self.assertIsNotNone(cert) class ManagementKeyReadOnly(PivTestCase): """ Tests after which the management key is always the default management key. Placing compatible tests here reduces the amount of slow reset calls needed. """ @classmethod def setUpClass(cls): with open_device() as dev: PivController(dev.driver).reset() def test_authenticate_twice_does_not_throw(self): self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) def test_reset_resets_has_stored_key_flag(self): self.assertFalse(self.controller.has_stored_key) self.controller.verify(DEFAULT_PIN) self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) self.controller.set_mgm_key(None, store_on_device=True) self.assertTrue(self.controller.has_stored_key) self.reconnect() self.controller.reset() self.assertFalse(self.controller.has_stored_key) def test_reset_while_verified_throws_nice_ValueError(self): self.controller.verify(DEFAULT_PIN) with self.assertRaises(ValueError) as cm: self.controller.reset() self.assertTrue( 'Cannot read remaining tries from status word: 9000' in str(cm.exception)) def test_set_mgm_key_does_not_change_key_if_not_authenticated(self): with self.assertRaises(APDUError): self.controller.set_mgm_key(NON_DEFAULT_MANAGEMENT_KEY) self.assertMgmKeyIs(DEFAULT_MANAGEMENT_KEY) @yubikey_conditions.version_min((3, 5, 0)) def test_set_stored_mgm_key_does_not_destroy_key_if_pin_not_verified(self): # noqa: E501 self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) with self.assertRaises(APDUError): self.controller.set_mgm_key(None, store_on_device=True) self.assertMgmKeyIs(DEFAULT_MANAGEMENT_KEY) class ManagementKeyReadWrite(PivTestCase): """ Tests after which the management key may not be the default management key. """ def setUp(self): PivTestCase.setUp(self) self.controller.reset() def test_set_mgm_key_changes_mgm_key(self): self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) self.controller.set_mgm_key(NON_DEFAULT_MANAGEMENT_KEY) self.assertMgmKeyIsNot(DEFAULT_MANAGEMENT_KEY) self.assertMgmKeyIs(NON_DEFAULT_MANAGEMENT_KEY) def test_set_stored_mgm_key_succeeds_if_pin_is_verified(self): self.controller.verify(DEFAULT_PIN) self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) self.controller.set_mgm_key(NON_DEFAULT_MANAGEMENT_KEY, store_on_device=True) self.assertMgmKeyIsNot(DEFAULT_MANAGEMENT_KEY) self.assertMgmKeyIs(NON_DEFAULT_MANAGEMENT_KEY) self.assertStoredMgmKeyEquals(NON_DEFAULT_MANAGEMENT_KEY) self.assertMgmKeyIs(self.controller._pivman_protected_data.key) def test_set_stored_random_mgm_key_succeeds_if_pin_is_verified(self): self.controller.verify(DEFAULT_PIN) self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) self.controller.set_mgm_key(None, store_on_device=True) self.assertMgmKeyIsNot(DEFAULT_MANAGEMENT_KEY) self.assertMgmKeyIsNot(NON_DEFAULT_MANAGEMENT_KEY) self.assertMgmKeyIs(self.controller._pivman_protected_data.key) self.assertStoredMgmKeyNotEquals(DEFAULT_MANAGEMENT_KEY) self.assertStoredMgmKeyNotEquals(NON_DEFAULT_MANAGEMENT_KEY) class Operations(PivTestCase): def setUp(self): PivTestCase.setUp(self) self.controller.reset() def generate_key(self, pin_policy=None): self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) public_key = self.controller.generate_key( SLOT.AUTHENTICATION, ALGO.ECCP256, pin_policy=pin_policy, touch_policy=TOUCH_POLICY.NEVER) self.reconnect() return public_key @yubikey_conditions.supports_piv_pin_policies def test_sign_with_pin_policy_always_requires_pin_every_time(self): self.generate_key(pin_policy=PIN_POLICY.ALWAYS) with self.assertRaises(APDUError): self.controller.sign(SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo') self.controller.verify(DEFAULT_PIN) sig = self.controller.sign( SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo') self.assertIsNotNone(sig) with self.assertRaises(APDUError): self.controller.sign(SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo') self.controller.verify(DEFAULT_PIN) sig = self.controller.sign( SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo') self.assertIsNotNone(sig) @yubikey_conditions.is_not_fips @yubikey_conditions.supports_piv_pin_policies def test_sign_with_pin_policy_never_does_not_require_pin(self): self.generate_key(pin_policy=PIN_POLICY.NEVER) sig = self.controller.sign( SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo') self.assertIsNotNone(sig) @yubikey_conditions.is_fips def test_pin_policy_never_blocked_on_fips(self): with self.assertRaises(APDUError): self.generate_key(pin_policy=PIN_POLICY.NEVER) def test_sign_with_pin_policy_once_requires_pin_once_per_session(self): self.generate_key(pin_policy=PIN_POLICY.ONCE) with self.assertRaises(APDUError): self.controller.sign(SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo') self.controller.verify(DEFAULT_PIN) sig = self.controller.sign( SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo') self.assertIsNotNone(sig) sig = self.controller.sign( SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo') self.assertIsNotNone(sig) self.reconnect() with self.assertRaises(APDUError): self.controller.sign(SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo') self.controller.verify(DEFAULT_PIN) sig = self.controller.sign( SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo') self.assertIsNotNone(sig) sig = self.controller.sign( SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo') self.assertIsNotNone(sig) def test_signature_can_be_verified_by_public_key(self): public_key = self.generate_key(pin_policy=PIN_POLICY.ONCE) signed_data = bytes(random.randint(0, 255) for i in range(32)) self.controller.verify(DEFAULT_PIN) sig = self.controller.sign( SLOT.AUTHENTICATION, ALGO.ECCP256, signed_data) self.assertIsNotNone(sig) public_key.verify( sig, signed_data, ec.ECDSA(hashes.SHA256())) class UnblockPin(PivTestCase): def setUp(self): super().setUp() self.controller.reset() def block_pin(self): while self.controller.get_pin_tries() > 0: try: self.controller.verify(NON_DEFAULT_PIN) except Exception: pass def test_unblock_pin_requires_no_previous_authentication(self): self.controller.unblock_pin(DEFAULT_PUK, NON_DEFAULT_PIN) def test_unblock_pin_with_wrong_puk_throws_WrongPuk(self): with self.assertRaises(WrongPuk): self.controller.unblock_pin(NON_DEFAULT_PUK, NON_DEFAULT_PIN) def test_unblock_pin_resets_pin_and_retries(self): self.controller.reset() self.reconnect() self.controller.verify(DEFAULT_PIN, NON_DEFAULT_PIN) self.reconnect() self.block_pin() with self.assertRaises(AuthenticationBlocked): self.controller.verify(DEFAULT_PIN) self.controller.unblock_pin(DEFAULT_PUK, NON_DEFAULT_PIN) self.assertEqual(self.controller.get_pin_tries(), 3) self.controller.verify(NON_DEFAULT_PIN) def test_set_pin_retries_requires_pin_and_mgm_key(self): # Fails with no authentication with self.assertRaises(APDUError): self.controller.set_pin_retries(4, 4) # Fails with only PIN self.controller.verify(DEFAULT_PIN) with self.assertRaises(APDUError): self.controller.set_pin_retries(4, 4) self.reconnect() # Fails with only management key self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) with self.assertRaises(APDUError): self.controller.set_pin_retries(4, 4) # Succeeds with both PIN and management key self.controller.verify(DEFAULT_PIN) self.controller.set_pin_retries(4, 4) def test_set_pin_retries_sets_pin_and_puk_tries(self): pin_tries = 9 puk_tries = 7 self.controller.verify(DEFAULT_PIN) self.controller.authenticate(DEFAULT_MANAGEMENT_KEY) self.controller.set_pin_retries(pin_tries, puk_tries) self.reconnect() self.assertEqual(self.controller.get_pin_tries(), pin_tries) self.assertEqual(self.controller._get_puk_tries(), puk_tries - 1) return [ KeyManagement, ManagementKeyReadOnly, ManagementKeyReadWrite, Operations, UnblockPin, ] yubikey-manager-3.1.1/test/test_device.py0000644000175100001630000000301113614233340021226 0ustar runnerdocker00000000000000import unittest try: from unittest.mock import Mock except ImportError: from mock import Mock from ykman.device import YubiKey from ykman.util import TRANSPORT, YUBIKEY, Mode class TestSpecificError(Exception): pass def make_mocks(): descriptor = Mock() descriptor.version = (4, 0, 0) descriptor.mode.transports = TRANSPORT.CCID driver = Mock() driver.key_type = YUBIKEY.YK4 driver.mode = Mode.from_code(1) driver.read_config.return_value = b'\5\5\3\0\0\0' return descriptor, driver class TestDevice(unittest.TestCase): def test_with_as_closes_driver(self): descriptor, driver = make_mocks() with YubiKey(descriptor, driver) as dev: # noqa: F841 pass driver.close.assert_called_once_with() def test_with_as_reraises_exception(self): descriptor, driver = make_mocks() with self.assertRaises(TestSpecificError): with YubiKey(descriptor, driver) as dev: # noqa: F841 raise TestSpecificError() driver.close.assert_called_once_with() def test_with_closes_driver(self): descriptor, driver = make_mocks() with YubiKey(descriptor, driver): pass driver.close.assert_called_once_with() def test_with_reraises_exception(self): descriptor, driver = make_mocks() with self.assertRaises(TestSpecificError): with YubiKey(descriptor, driver): raise TestSpecificError() driver.close.assert_called_once_with() yubikey-manager-3.1.1/test/test_external_libs.py0000644000175100001630000000075113614233340022632 0ustar runnerdocker00000000000000import unittest from test.util import ykman_cli class TestExternalLibraries(unittest.TestCase): def test_ykman_version(self): output = ykman_cli('-v') # Test that major version is 1 on all libs self.assertIn('libykpers 1', output) self.assertIn('libusb 1', output) def test_ykman_version_not_found(self): output = ykman_cli('-v') self.assertNotIn('not found!', output) self.assertNotIn('', output) yubikey-manager-3.1.1/test/test_oath.py0000644000175100001630000001006013614233340020724 0ustar runnerdocker00000000000000# vim: set fileencoding=utf-8 : from ykman.oath import Credential, CredentialData, _derive_key, OATH_TYPE, ALGO import unittest class TestOathFunctions(unittest.TestCase): def test_credential_parse_period_and_issuer_and_name(self): issuer, name, period = Credential.parse_key(b'20/Issuer:name') self.assertEqual(20, period) self.assertEqual('Issuer', issuer) self.assertEqual('name', name) def test_credential_parse_weird_issuer_and_name(self): issuer, name, period = Credential.parse_key(b'weird/Issuer:name') self.assertEqual(30, period) self.assertEqual('weird/Issuer', issuer) self.assertEqual('name', name) def test_credential_parse_issuer_and_name(self): issuer, name, period = Credential.parse_key(b'Issuer:name') self.assertEqual(30, period) self.assertEqual('Issuer', issuer) self.assertEqual('name', name) def test_credential_parse_period_and_name(self): issuer, name, period = Credential.parse_key(b'20/name') self.assertEqual(20, period) self.assertIsNone(issuer) self.assertEqual('name', name) def test_credential_parse_only_name(self): issuer, name, period = Credential.parse_key(b'name') self.assertEqual(30, period) self.assertIsNone(issuer) self.assertEqual('name', name) def test_credential_data_make_key(self): self.assertEqual(b'name', CredentialData(b'', None, 'name').make_key()) self.assertEqual(b'Issuer:name', CredentialData(b'', 'Issuer', 'name').make_key()) self.assertEqual(b'20/Issuer:name', CredentialData(b'', 'Issuer', 'name', period=20 ).make_key()) self.assertEqual(b'Issuer:name', CredentialData(b'', 'Issuer', 'name', period=30 ).make_key()) self.assertEqual(b'20/name', CredentialData(b'', None, 'name', period=20 ).make_key()) def test_derive_key(self): self.assertEqual( b'\xb0}\xa1\xe7\xde\x87\xf8\x9a\x87\xa2\xb5\x98\xea\xa2\x18\x8c', _derive_key(b'\0\0\0\0\0\0\0\0', u'foobar')) self.assertEqual( b'\xda\x81\x8ek,\xf0\xa2\xd0\xbf\x19\xb3\xdd\xd3K\x83\xf5', _derive_key(b'12345678', u'Hallå världen!')) self.assertEqual( b'\xf3\xdf\xa7\x81T\xc8\x102\x99E\xfb\xc4\xb55\xe57', _derive_key(b'saltsalt', u'Ťᶒśƫ ᵽĥřӓşḛ')) def test_parse_uri_issuer(self): no_issuer = CredentialData.from_uri('otpauth://totp/account' '?secret=abba') self.assertIsNone(no_issuer.issuer) from_param = CredentialData.from_uri('otpauth://totp/account' '?secret=abba&issuer=Test') self.assertEqual('Test', from_param.issuer) from_name = CredentialData.from_uri('otpauth://totp/Test:account' '?secret=abba') self.assertEqual('Test', from_name.issuer) with_both = CredentialData.from_uri('otpauth://totp/TestA:account' '?secret=abba&issuer=TestB') self.assertEqual('TestB', with_both.issuer) def test_parse_uri(self): data = CredentialData.from_uri('otpauth://totp/Issuer:account' '?secret=abba&issuer=Issuer' '&algorithm=SHA256&digits=7' '&period=20&counter=5') self.assertEqual(b'\0B', data.secret) self.assertEqual('Issuer', data.issuer) self.assertEqual('account', data.name) self.assertEqual(OATH_TYPE.TOTP, data.oath_type) self.assertEqual(ALGO.SHA256, data.algorithm) self.assertEqual(7, data.digits) self.assertEqual(20, data.period) self.assertEqual(5, data.counter) self.assertEqual(False, data.touch) yubikey-manager-3.1.1/test/test_piv.py0000644000175100001630000000311013614233340020565 0ustar runnerdocker00000000000000# vim: set fileencoding=utf-8 : import ykman.piv as piv import unittest from ykman.piv import ALGO class FakeController(object): def __init__(self, version): self.version = version @property def is_fips(self): return piv.PivController.is_fips.fget(self) class TestPivFunctions(unittest.TestCase): def test_generate_random_management_key(self): output1 = piv.generate_random_management_key() output2 = piv.generate_random_management_key() self.assertIsInstance(output1, bytes) self.assertIsInstance(output2, bytes) self.assertNotEqual(output1, output2) def test_supported_algorithms(self): neo_supported = piv.PivController.supported_algorithms.fget( FakeController((3, 1, 1))) self.assertNotIn(ALGO.TDES, neo_supported) self.assertNotIn(ALGO.ECCP384, neo_supported) fips_supported = piv.PivController.supported_algorithms.fget( FakeController((4, 4, 1))) self.assertNotIn(ALGO.TDES, fips_supported) self.assertNotIn(ALGO.RSA1024, fips_supported) roca_supported = piv.PivController.supported_algorithms.fget( FakeController((4, 3, 4))) self.assertNotIn(ALGO.TDES, roca_supported) self.assertNotIn(ALGO.RSA1024, roca_supported) self.assertNotIn(ALGO.RSA2048, roca_supported) yk5_supported = piv.PivController.supported_algorithms.fget( FakeController((5, 1, 0))) self.assertEqual( set(yk5_supported), set([a for a in ALGO if a != ALGO.TDES]), ) yubikey-manager-3.1.1/test/test_scancodes.py0000644000175100001630000005151613614233340021746 0ustar runnerdocker00000000000000# vim: set fileencoding=utf-8 : # Copyright (c) 2018 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import unicode_literals import unittest from ykman.scancodes import encode, KEYBOARD_LAYOUT class TestScanMap(unittest.TestCase): def test_us_layout(self): self.assertEqual(b'\x04', encode('a', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x05', encode('b', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x06', encode('c', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x07', encode('d', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x08', encode('e', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x09', encode('f', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x0a', encode('g', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x0b', encode('h', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x0c', encode('i', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x0d', encode('j', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x0e', encode('k', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x0f', encode('l', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x10', encode('m', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x11', encode('n', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x12', encode('o', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x13', encode('p', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x14', encode('q', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x15', encode('r', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x16', encode('s', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x17', encode('t', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x18', encode('u', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x19', encode('v', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x1a', encode('w', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x1b', encode('x', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x1c', encode('y', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x1d', encode('z', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x84', encode('A', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x85', encode('B', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x86', encode('C', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x87', encode('D', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x88', encode('E', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x89', encode('F', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x8a', encode('G', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x8b', encode('H', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x8c', encode('I', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x8d', encode('J', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x8e', encode('K', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x8f', encode('L', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x90', encode('M', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x91', encode('N', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x92', encode('O', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x93', encode('P', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x94', encode('Q', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x95', encode('R', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x96', encode('S', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x97', encode('T', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x98', encode('U', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x99', encode('V', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x9a', encode('W', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x9b', encode('X', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x9c', encode('Y', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x9d', encode('Z', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x27', encode('0', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x1e', encode('1', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x1f', encode('2', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x20', encode('3', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x21', encode('4', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x22', encode('5', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x23', encode('6', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x24', encode('7', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x25', encode('8', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x26', encode('9', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x2b', encode('\t', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x28', encode('\n', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x9e', encode('!', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\xb4', encode('"', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\xa0', encode('#', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\xa1', encode('$', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\xa2', encode('%', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\xa4', encode('&', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x34', encode("'", KEYBOARD_LAYOUT.US)) self.assertEqual(b'\xa6', encode('(', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\xa7', encode(')', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\xa5', encode('*', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\xae', encode('+', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x35', encode('`', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x36', encode(',', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x2d', encode('-', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x37', encode('.', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x38', encode('/', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\xb3', encode(':', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x33', encode(';', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\xb6', encode('<', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x2e', encode('=', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\xb7', encode('>', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\xb8', encode('?', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x9f', encode('@', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x2f', encode('[', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x32', encode('\\', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x30', encode(']', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\xa3', encode('^', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\xad', encode('_', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\xaf', encode('{', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\xb0', encode('}', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\xb2', encode('|', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\xb5', encode('~', KEYBOARD_LAYOUT.US)) self.assertEqual(b'\x04\x05\x06', encode('abc', KEYBOARD_LAYOUT.US)) with self.assertRaises(ValueError): encode('ö') def test_de_layout(self): self.assertEqual(b'\x04', encode('a', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x05', encode('b', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x06', encode('c', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x07', encode('d', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x08', encode('e', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x09', encode('f', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x0a', encode('g', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x0b', encode('h', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x0c', encode('i', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x0d', encode('j', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x0e', encode('k', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x0f', encode('l', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x10', encode('m', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x11', encode('n', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x12', encode('o', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x13', encode('p', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x14', encode('q', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x15', encode('r', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x16', encode('s', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x17', encode('t', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x18', encode('u', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x19', encode('v', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x1a', encode('w', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x1b', encode('x', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x1d', encode('y', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x1c', encode('z', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x84', encode('A', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x85', encode('B', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x86', encode('C', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x87', encode('D', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x88', encode('E', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x89', encode('F', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x8a', encode('G', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x8b', encode('H', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x8c', encode('I', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x8d', encode('J', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x8e', encode('K', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x8f', encode('L', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x90', encode('M', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x91', encode('N', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x92', encode('O', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x93', encode('P', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x94', encode('Q', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x95', encode('R', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x96', encode('S', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x97', encode('T', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x98', encode('U', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x99', encode('V', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x9a', encode('W', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x9b', encode('X', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x9d', encode('Y', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x9c', encode('Z', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x27', encode('0', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x1e', encode('1', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x1f', encode('2', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x20', encode('3', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x21', encode('4', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x22', encode('5', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x23', encode('6', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x24', encode('7', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x25', encode('8', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x26', encode('9', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x2b', encode('\t', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x28', encode('\n', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x32', encode('#', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x30', encode('+', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x36', encode(',', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x38', encode('-', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x64', encode('<', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x35', encode('^', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x2c', encode(' ', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x2e', encode('´', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x2d', encode('ß', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x34', encode('ä', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x33', encode('ö', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x2f', encode('ü', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x9e', encode('!', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\x9f', encode('"', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\xa1', encode('$', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\xa2', encode('%', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\xa3', encode('&', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\xb2', encode("'", KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\xa5', encode('(', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\xa6', encode(')', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\xb0', encode('*', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\xa4', encode('/', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\xb7', encode(':', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\xb6', encode(';', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\xa7', encode('=', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\xe4', encode('>', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\xad', encode('?', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\xb8', encode('_', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\xad', encode('`', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\xa0', encode('§', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\xb4', encode('Ä', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\xb3', encode('Ö', KEYBOARD_LAYOUT.DE)) self.assertEqual(b'\xaf', encode('Ü', KEYBOARD_LAYOUT.DE)) self.assertEqual( b'\xb4\xb3\xaf', encode('ÄÖÜ', KEYBOARD_LAYOUT.DE)) with self.assertRaises(ValueError): encode('@', KEYBOARD_LAYOUT.DE) def test_norman_layout(self): self.assertEqual(b'\x04', encode('a', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x05', encode('b', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x06', encode('c', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x08', encode('d', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x07', encode('e', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x15', encode('f', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x0a', encode('g', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x33', encode('h', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x0e', encode('i', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x1c', encode('j', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x17', encode('k', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x12', encode('l', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x10', encode('m', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x0d', encode('n', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x0f', encode('o', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x11', encode('p', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x14', encode('q', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x0c', encode('r', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x16', encode('s', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x09', encode('t', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x18', encode('u', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x19', encode('v', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x1a', encode('w', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x1b', encode('x', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x0b', encode('y', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x1d', encode('z', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x84', encode('A', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x85', encode('B', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x86', encode('C', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x88', encode('D', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x87', encode('E', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x95', encode('F', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x8a', encode('G', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\xb3', encode('H', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x8e', encode('I', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x9c', encode('J', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x97', encode('K', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x92', encode('L', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x90', encode('M', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x8d', encode('N', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x8f', encode('O', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x91', encode('P', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x94', encode('Q', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x8c', encode('R', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x96', encode('S', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x89', encode('T', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x98', encode('U', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x99', encode('V', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x9a', encode('W', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x9b', encode('X', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x8b', encode('Y', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x9d', encode('Z', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x27', encode('0', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x1e', encode('1', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x1f', encode('2', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x20', encode('3', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x21', encode('4', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x22', encode('5', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x23', encode('6', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x24', encode('7', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x25', encode('8', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x26', encode('9', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x2b', encode('\t', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x28', encode('\n', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\xa0', encode('#', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\xae', encode('+', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x36', encode(',', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'-', encode('-', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\xb6', encode('<', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\xa3', encode('^', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x2c', encode(' ', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x9e', encode('!', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\xb4', encode('"', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\xa1', encode('$', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\xa2', encode('%', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\xa4', encode('&', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'4', encode("'", KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\xa6', encode('(', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\xa7', encode(')', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\xa5', encode('*', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'8', encode('/', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\xb3', encode(':', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x13', encode(';', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'.', encode('=', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\xb7', encode('>', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\xb8', encode('?', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\xad', encode('_', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'5', encode('`', KEYBOARD_LAYOUT.NORMAN)) self.assertEqual(b'\x04\x05\x06', encode('abc', KEYBOARD_LAYOUT.NORMAN)) with self.assertRaises(ValueError): encode('ö') yubikey-manager-3.1.1/test/test_util.py0000644000175100001630000001544713614233340020764 0ustar runnerdocker00000000000000# vim: set fileencoding=utf-8 : from ykman.util import (bytes2int, format_code, generate_static_pw, hmac_shorten_key, modhex_decode, modhex_encode, parse_tlvs, parse_truncated, time_challenge, Tlv, is_pkcs12, is_pem, FORM_FACTOR) from .util import open_file import unittest if not getattr(unittest.TestCase, 'assertRegex', None): # Python 2.7 can use assertRegexpMatches unittest.TestCase.assertRegex = unittest.TestCase.assertRegexpMatches class TestUtilityFunctions(unittest.TestCase): def test_bytes2int(self): self.assertEqual(0x57, bytes2int(b'\x57')) self.assertEqual(0x1234, bytes2int(b'\x12\x34')) self.assertEqual(0xcafed00d, bytes2int(b'\xca\xfe\xd0\x0d')) def test_format_code(self): self.assertEqual('000000', format_code(0)) self.assertEqual('00000000', format_code(0, 8)) self.assertEqual('345678', format_code(12345678)) self.assertEqual('34567890', format_code(1234567890, 8)) self.assertEqual('22222', format_code(0, steam=True)) self.assertEqual('DVNKW', format_code(1234567890, steam=True)) self.assertEqual('KDNYM', format_code(9999999999, steam=True)) def test_generate_static_pw(self): for l in range(0, 38): self.assertRegex( generate_static_pw(l), '^[cbdefghijklnrtuvCBDEFGHIJKLNRTUV]{%d}$' % l) def test_hmac_shorten_key(self): self.assertEqual(b'short', hmac_shorten_key(b'short', 'sha1')) self.assertEqual(b'x'*64, hmac_shorten_key(b'x'*64, 'sha1')) self.assertEqual( b'0\xec\xd3\xf4\xb5\xcej\x1a\xc6x' b'\x15\xdb\xa1\xfb\x7f\x9f\xff\x00`\x14', hmac_shorten_key(b'l'*65, 'sha1') ) self.assertEqual(b'x'*64, hmac_shorten_key(b'x'*64, 'sha256')) self.assertEqual( b'l\xf9\x08}"vi\xbcj\xa9\nlkQ\x81\xd9`' b'\xbb\x88\xe9L4\x0b\xbd?\x07s/K\xae\xb9L', hmac_shorten_key(b'l'*65, 'sha256') ) def test_modhex_decode(self): self.assertEqual(b'', modhex_decode('')) self.assertEqual(b'\x2d\x34\x4e\x83', modhex_decode('dteffuje')) self.assertEqual( b'\x69\xb6\x48\x1c\x8b\xab\xa2\xb6\x0e\x8f\x22\x17\x9b\x58\xcd\x56', modhex_decode('hknhfjbrjnlnldnhcujvddbikngjrtgh') ) def test_modhex_encode(self): self.assertEqual('', modhex_encode(b'')) self.assertEqual('dteffuje', modhex_encode(b'\x2d\x34\x4e\x83')) self.assertEqual( 'hknhfjbrjnlnldnhcujvddbikngjrtgh', modhex_encode(b'\x69\xb6\x48\x1c\x8b\xab\xa2\xb6' b'\x0e\x8f\x22\x17\x9b\x58\xcd\x56') ) def test_parse_tlvs(self): tlvs = parse_tlvs(b'\x00\x02\xd0\x0d\xa1\x00\xfe\x04\xfe\xed\xfa\xce') self.assertEqual(3, len(tlvs)) self.assertEqual(0, tlvs[0].tag) self.assertEqual(2, tlvs[0].length) self.assertEqual(b'\xd0\x0d', tlvs[0].value) self.assertEqual(0xa1, tlvs[1].tag) self.assertEqual(0, tlvs[1].length) self.assertEqual(b'', tlvs[1].value) self.assertEqual(0xfe, tlvs[2].tag) self.assertEqual(4, tlvs[2].length) self.assertEqual(b'\xfe\xed\xfa\xce', tlvs[2].value) def test_parse_truncated(self): self.assertEqual(0x01020304, parse_truncated(b'\1\2\3\4')) self.assertEqual(0xdeadbeef & 0x7fffffff, parse_truncated(b'\xde\xad\xbe\xef')) def test_time_challenge(self): self.assertEqual(b'\0'*8, time_challenge(0)) self.assertEqual(b'\x00\x00\x00\x00\x00\x06G\x82', time_challenge(12345678)) self.assertEqual(b'\x00\x00\x00\x00\x02\xf2\xeaC', time_challenge(1484223461.2644958)) def test_tlv(self): self.assertEqual(Tlv(b'\xfe\6foobar'), Tlv(0xfe, b'foobar')) tlv1 = Tlv(b'\0\5hello') tlv2 = Tlv(0xff, b'') tlv3 = Tlv(0x12, b'hi'*200) self.assertEqual(b'\0\5hello', tlv1) self.assertEqual(b'\xff\0', tlv2) self.assertEqual(b'\x12\x82\x01\x90' + b'hi'*200, tlv3) self.assertEqual(b'\0\5hello\xff\0\x12\x82\x01\x90' + b'hi'*200, tlv1 + tlv2 + tlv3) def test_is_pkcs12(self): self.assertFalse(is_pkcs12('just a string')) self.assertFalse(is_pkcs12(None)) with open_file('rsa_2048_key.pem') as rsa_2048_key_pem: self.assertFalse(is_pkcs12(rsa_2048_key_pem.read())) with open_file('rsa_2048_key_encrypted.pem') as f: self.assertFalse(is_pkcs12(f.read())) with open_file('rsa_2048_cert.pem') as rsa_2048_cert_pem: self.assertFalse(is_pkcs12(rsa_2048_cert_pem.read())) with open_file('rsa_2048_key_cert.pfx') as rsa_2048_key_cert_pfx: self.assertTrue(is_pkcs12(rsa_2048_key_cert_pfx.read())) with open_file( 'rsa_2048_key_cert_encrypted.pfx') as \ rsa_2048_key_cert_encrypted_pfx: self.assertTrue(is_pkcs12(rsa_2048_key_cert_encrypted_pfx.read())) def test_is_pem(self): self.assertFalse(is_pem(b'just a byte string')) self.assertFalse(is_pem(None)) with open_file('rsa_2048_key.pem') as rsa_2048_key_pem: self.assertTrue(is_pem(rsa_2048_key_pem.read())) with open_file('rsa_2048_key_encrypted.pem') as f: self.assertTrue(is_pem(f.read())) with open_file('rsa_2048_cert.pem') as rsa_2048_cert_pem: self.assertTrue(is_pem(rsa_2048_cert_pem.read())) with open_file('rsa_2048_key_cert.pfx') as rsa_2048_key_cert_pfx: self.assertFalse(is_pem(rsa_2048_key_cert_pfx.read())) with open_file('rsa_2048_cert_metadata.pem') as f: self.assertTrue(is_pem(f.read())) with open_file( 'rsa_2048_key_cert_encrypted.pfx') as \ rsa_2048_key_cert_encrypted_pfx: self.assertFalse(is_pem(rsa_2048_key_cert_encrypted_pfx.read())) def test_form_factor_from_code(self): self.assertEqual(FORM_FACTOR.UNKNOWN, FORM_FACTOR.from_code(None)) with self.assertRaises(ValueError): FORM_FACTOR.from_code('im a string') self.assertEqual(FORM_FACTOR.UNKNOWN, FORM_FACTOR.from_code(0x00)) self.assertEqual( FORM_FACTOR.USB_A_KEYCHAIN, FORM_FACTOR.from_code(0x01)) self.assertEqual(FORM_FACTOR.USB_A_NANO, FORM_FACTOR.from_code(0x02)) self.assertEqual( FORM_FACTOR.USB_C_KEYCHAIN, FORM_FACTOR.from_code(0x03)) self.assertEqual(FORM_FACTOR.USB_C_NANO, FORM_FACTOR.from_code(0x04)) self.assertEqual(FORM_FACTOR.USB_C_LIGHTNING, FORM_FACTOR.from_code(0x05)) self.assertEqual(FORM_FACTOR.UNKNOWN, FORM_FACTOR.from_code(0x06)) yubikey-manager-3.1.1/test/util.py0000644000175100001630000000526613614233340017723 0ustar runnerdocker00000000000000import datetime import logging import os from click.testing import CliRunner from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.serialization import Encoding from cryptography.utils import int_from_bytes from cryptography.x509.oid import NameOID from ykman.cli.__main__ import cli from ykman.util import Tlv logger = logging.getLogger(__name__) PKG_DIR = os.path.dirname(os.path.abspath(__file__)) def open_file(*relative_path): return open(os.path.join(PKG_DIR, 'files', *relative_path), 'rb') def ykman_cli(*argv, **kwargs): result = ykman_cli_raw(*argv, **kwargs) if result.exit_code != 0: raise result.exception return result.output def ykman_cli_bytes(*argv, **kwargs): result = ykman_cli_raw(*argv, **kwargs) if result.exit_code != 0: raise result.exception return result.stdout_bytes def ykman_cli_raw(*argv, **kwargs): runner = CliRunner() result = runner.invoke(cli, list(argv), obj={}, **kwargs) return result def _generate_private_key(): return ec.generate_private_key(ec.SECP256R1(), default_backend()) def _sign_cert(key, builder): cert = builder.sign(key, hashes.SHA256(), default_backend()) sig = key.sign(cert.tbs_certificate_bytes, ec.ECDSA(hashes.SHA256())) seq = Tlv.parse_list(Tlv.unpack(0x30, cert.public_bytes(Encoding.DER))) # Replace signature, add unused bits = 0 seq[2] = Tlv(seq[2].tag, b'\0' + sig) # Re-assemble sequence der = Tlv(0x30, b''.join(seq)) return x509.load_der_x509_certificate(der, default_backend()) def generate_self_signed_certificate( common_name='Test', valid_from=None, valid_to=None): valid_from = valid_from if valid_from else datetime.datetime.utcnow() valid_to = valid_to if valid_to else valid_from + datetime.timedelta(days=1) private_key = _generate_private_key() public_key = private_key.public_key() builder = x509.CertificateBuilder() builder = builder.public_key(public_key) builder = builder.subject_name( x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, common_name), ])) # Same as subject on self-signed certificates. builder = builder.issuer_name( x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, common_name), ])) # x509.random_serial_number added in cryptography 1.6 serial = int_from_bytes(os.urandom(20), 'big') >> 1 builder = builder.serial_number(serial) builder = builder.not_valid_before(valid_from) builder = builder.not_valid_after(valid_to) return _sign_cert(private_key, builder) yubikey-manager-3.1.1/ykman/0000755000175100001630000000000013614233377016535 5ustar runnerdocker00000000000000yubikey-manager-3.1.1/ykman/VERSION0000644000175100001630000000000613614233340017567 0ustar runnerdocker000000000000003.1.1 yubikey-manager-3.1.1/ykman/__init__.py0000644000175100001630000000300713614233340020634 0ustar runnerdocker00000000000000# Copyright (c) 2015 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. import os with open( os.path.join( os.path.dirname(__file__), 'VERSION')) as version_file: version = version_file.read().strip() __version__ = version yubikey-manager-3.1.1/ykman/cli/0000755000175100001630000000000013614233377017304 5ustar runnerdocker00000000000000yubikey-manager-3.1.1/ykman/cli/__init__.py0000644000175100001630000000253313614233340021406 0ustar runnerdocker00000000000000# Copyright (c) 2015 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. yubikey-manager-3.1.1/ykman/cli/__main__.py0000644000175100001630000002414613614233340021373 0ustar runnerdocker00000000000000# Copyright (c) 2015 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import, print_function import ykman.logging_setup import smartcard.pcsc.PCSCExceptions from ykman import __version__ from ..util import TRANSPORT, Cve201715361VulnerableError, YUBIKEY from ..native.pyusb import get_usb_backend_version from ..driver_otp import libversion as ykpers_version from ..driver_ccid import open_devices as open_ccid, list_readers from ..device import YubiKey from ..descriptor import (get_descriptors, list_devices, open_device, FailedOpeningDeviceException, Descriptor) from .util import UpperCaseChoice, YkmanContextObject from .info import info from .mode import mode from .otp import otp from .opgp import openpgp from .oath import oath from .piv import piv from .fido import fido from .config import config import usb.core import click import logging import sys logger = logging.getLogger(__name__) CLICK_CONTEXT_SETTINGS = dict( help_option_names=['-h', '--help'], max_content_width=999 ) def print_version(ctx, param, value): if not value or ctx.resilient_parsing: return click.echo('YubiKey Manager (ykman) version: {}'.format(__version__)) libs = [] libs.append('libykpers ' + ('.'.join('%d' % d for d in ykpers_version) if ykpers_version is not None else 'not found!')) usb_lib = get_usb_backend_version() libs.append(usb_lib or '') click.echo('Libraries:') for lib in libs: click.echo(' {}'.format(lib)) ctx.exit() def _disabled_transport(ctx, transports, cmd_name): req = ', '.join((t.name for t in TRANSPORT if t & transports)) click.echo("Command '{}' requires one of the following USB interfaces " "to be enabled: '{}'.".format(cmd_name, req)) ctx.fail("Use 'ykman mode' to set the enabled USB interfaces.") def _run_cmd_for_serial(ctx, cmd, transports, serial): try: return open_device(transports, serial=serial) except FailedOpeningDeviceException: try: # Retry, any transport dev = open_device(serial=serial) if not dev.mode.transports & transports: if dev.config.usb_supported & transports: _disabled_transport(ctx, transports, cmd) else: ctx.fail("Command '{}' is not supported by this device." .format(cmd)) except FailedOpeningDeviceException: ctx.fail( 'Failed connecting to a YubiKey with serial: {}. \ Make sure the application have the required \ permissions.'.format(serial)) def _run_cmd_for_single(ctx, cmd, transports, reader=None): if reader: if TRANSPORT.has(transports, TRANSPORT.CCID): readers = list(open_ccid(reader)) if len(readers) == 1: return YubiKey(Descriptor.from_driver(readers[0]), readers[0]) elif len(readers) > 1: ctx.fail('Multiple YubiKeys on external readers detected.') else: ctx.fail('No YubiKey found on external reader.') else: ctx.fail('Not a CCID command.') try: descriptors = get_descriptors() except usb.core.NoBackendError: ctx.fail('No PyUSB backend detected!') n_keys = len(descriptors) if n_keys == 0: ctx.fail('No YubiKey detected!') if n_keys > 1: ctx.fail('Multiple YubiKeys detected. Use --device SERIAL to specify ' 'which one to use.') descriptor = descriptors[0] if descriptor.mode.transports & transports: try: return descriptor.open_device(transports) except FailedOpeningDeviceException: ctx.fail('Failed connecting to {} [{}]. Make sure the application have \ the required permissions.'.format( descriptor.name, descriptor.mode)) else: _disabled_transport(ctx, transports, cmd) @click.group(context_settings=CLICK_CONTEXT_SETTINGS) @click.option('-v', '--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True) @click.option('-d', '--device', type=int, metavar='SERIAL') @click.option('-l', '--log-level', default=None, type=UpperCaseChoice(ykman.logging_setup.LOG_LEVEL_NAMES), help='Enable logging at given verbosity level.', ) @click.option('--log-file', default=None, type=str, metavar='FILE', help='Write logs to the given FILE instead of standard error; ' 'ignored unless --log-level is also set.', ) @click.option( '-r', '--reader', help='Use an external smart card reader. Conflicts with --device and ' 'list.', metavar='NAME', default=None) @click.pass_context def cli(ctx, device, log_level, log_file, reader): """ Configure your YubiKey via the command line. Examples: \b List connected YubiKeys, only output serial number: $ ykman list --serials \b Show information about YubiKey with serial number 0123456: $ ykman --device 0123456 info """ ctx.obj = YkmanContextObject() if log_level: ykman.logging_setup.setup(log_level, log_file=log_file) if reader and device: ctx.fail('--reader and --device options can\'t be combined.') subcmd = next(c for c in COMMANDS if c.name == ctx.invoked_subcommand) if subcmd == list_keys: if reader: ctx.fail('--reader and list command can\'t be combined.') return transports = getattr(subcmd, 'transports', TRANSPORT.usb_transports()) if transports: def resolve_device(): if device is not None: dev = _run_cmd_for_serial(ctx, subcmd.name, transports, device) else: dev = _run_cmd_for_single(ctx, subcmd.name, transports, reader) ctx.call_on_close(dev.close) return dev ctx.obj.add_resolver('dev', resolve_device) @cli.command('list') @click.option('-s', '--serials', is_flag=True, help='Output only serial ' 'numbers, one per line (devices without serial will be omitted).') @click.option( '-r', '--readers', is_flag=True, help='List available smart card readers.') @click.pass_context def list_keys(ctx, serials, readers): """ List connected YubiKeys. """ def _print_device(dev, serial): if serials: if serial: click.echo(serial) else: click.echo('{} [{}]{}'.format( dev.device_name, dev.mode, ' Serial: {}'.format(serial) if serial else '') ) if readers: for reader in list_readers(): click.echo(reader.name) ctx.exit() descriptors = get_descriptors() handled_serials = set() try: for dev in list_devices(transports=TRANSPORT.CCID): if dev.key_type == YUBIKEY.SKY: # We have nothing to match on, so just drop a SKY descriptor d = next(x for x in descriptors if x.key_type == YUBIKEY.SKY) descriptors.remove(d) _print_device(dev, None) else: serial = dev.serial if serial not in handled_serials: # Drop a descriptor with a matching serial and mode handled_serials.add(serial) matches = [d for d in descriptors if (d.key_type, d.mode) == (dev.driver.key_type, dev.driver.mode)] if len(matches) > 0: d = matches[0] descriptors.remove(d) _print_device(dev, serial) dev.close() if not descriptors: break except smartcard.pcsc.PCSCExceptions.EstablishContextException as e: logger.error('Failed to list devices', exc_info=e) ctx.fail( 'Failed to establish CCID context. Is the pcscd service running?') # List descriptors that failed to open. if len(descriptors) > 0: logger.debug( 'Failed to open some devices, listing based on descriptors') for desc in descriptors: click.echo('{} [{}]'.format(desc.name, desc.mode)) COMMANDS = (list_keys, info, mode, otp, openpgp, oath, piv, fido, config) for cmd in COMMANDS: cli.add_command(cmd) def main(): try: cli(obj={}) except ValueError as e: logger.error('Error', exc_info=e) click.echo('Error: ' + str(e)) return 1 except Cve201715361VulnerableError as err: logger.error('Error', exc_info=err) click.echo('Error: ' + str(err)) return 2 if __name__ == '__main__': sys.exit(main()) yubikey-manager-3.1.1/ykman/cli/config.py0000644000175100001630000003426313614233340021121 0ustar runnerdocker00000000000000# Copyright (c) 2018 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import from .util import click_postpone_execution, click_force_option, EnumChoice from ..device import device_config, FLAGS from ..util import APPLICATION from binascii import a2b_hex, b2a_hex import os import logging import click logger = logging.getLogger(__name__) CLEAR_LOCK_CODE = '0' * 32 class ApplicationsChoice(EnumChoice): """ Special version of EnumChoice that accepts openpgp as OPGP """ def convert(self, value, param, ctx): if value.lower() == 'openpgp': return super(ApplicationsChoice, self).convert('OPGP', param, ctx) else: return super(ApplicationsChoice, self).convert(value, param, ctx) def prompt_lock_code(prompt='Enter your lock code'): return click.prompt( prompt, default='', hide_input=True, show_default=False, err=True) @click.group() @click.pass_context @click_postpone_execution def config(ctx): """ Enable/Disable applications. The applications may be enabled and disabled independently over different interfaces (USB and NFC). The configuration may also be protected by a lock code. Examples: \b Disable PIV over the NFC interface: $ ykman config nfc --disable PIV \b Enable all applications over USB: $ ykman config usb --enable-all \b Generate and set a random application lock code: $ ykman config set-lock-code --generate """ dev = ctx.obj['dev'] if not dev.can_write_config: ctx.fail('Configuring applications is not supported on this YubiKey. ' 'Use the `mode` command to configure USB interfaces.') @config.command('set-lock-code') @click.pass_context @click_force_option @click.option('-l', '--lock-code', metavar='HEX', help='Current lock code.') @click.option( '-n', '--new-lock-code', metavar='HEX', help='New lock code. Conflicts with --generate.') @click.option('-c', '--clear', is_flag=True, help='Clear the lock code.') @click.option( '-g', '--generate', is_flag=True, help='Generate a random lock code. Conflicts with --new-lock-code.') def set_lock_code(ctx, lock_code, new_lock_code, clear, generate, force): """ Set or change the configuration lock code. A lock code may be used to protect the application configuration. The lock code must be a 32 characters (16 bytes) hex value. """ dev = ctx.obj['dev'] def prompt_new_lock_code(): return prompt_lock_code(prompt='Enter your new lock code') def prompt_current_lock_code(): return prompt_lock_code(prompt='Enter your current lock code') def change_lock_code(lock_code, new_lock_code): lock_code = _parse_lock_code(ctx, lock_code) new_lock_code = _parse_lock_code(ctx, new_lock_code) try: dev.write_config( device_config( config_lock=new_lock_code), reboot=True, lock_key=lock_code) except Exception as e: logger.error('Changing the lock code failed', exc_info=e) ctx.fail('Failed to change the lock code. Wrong current code?') def set_lock_code(new_lock_code): new_lock_code = _parse_lock_code(ctx, new_lock_code) try: dev.write_config( device_config( config_lock=new_lock_code), reboot=True) except Exception as e: logger.error('Setting the lock code failed', exc_info=e) ctx.fail('Failed to set the lock code.') if generate and new_lock_code: ctx.fail('Invalid options: --new-lock-code conflicts with --generate.') if clear: new_lock_code = CLEAR_LOCK_CODE if generate: new_lock_code = b2a_hex(os.urandom(16)).decode('utf-8') click.echo( 'Using a randomly generated lock code: {}'.format(new_lock_code)) force or click.confirm( 'Lock configuration with this lock code?', abort=True, err=True) if dev.config.configuration_locked: if lock_code: if new_lock_code: change_lock_code(lock_code, new_lock_code) else: new_lock_code = prompt_new_lock_code() change_lock_code(lock_code, new_lock_code) else: if new_lock_code: lock_code = prompt_current_lock_code() change_lock_code(lock_code, new_lock_code) else: lock_code = prompt_current_lock_code() new_lock_code = prompt_new_lock_code() change_lock_code(lock_code, new_lock_code) else: if lock_code: ctx.fail( 'There is no current lock code set. ' 'Use --new-lock-code to set one.') else: if new_lock_code: set_lock_code(new_lock_code) else: new_lock_code = prompt_new_lock_code() set_lock_code(new_lock_code) @config.command() @click.pass_context @click_force_option @click.option( '-e', '--enable', multiple=True, type=ApplicationsChoice(APPLICATION), help='Enable applications.') @click.option( '-d', '--disable', multiple=True, type=ApplicationsChoice(APPLICATION), help='Disable applications.') @click.option('-l', '--list', 'list_enabled', is_flag=True, help='List enabled applications.') @click.option( '-a', '--enable-all', is_flag=True, help='Enable all applications.') @click.option( '-L', '--lock-code', metavar='HEX', help='Current application configuration lock code.') @click.option( '--touch-eject', is_flag=True, help='When set, the button toggles the state' ' of the smartcard between ejected and inserted. (CCID only).') @click.option( '--no-touch-eject', is_flag=True, help='Disable touch eject (CCID only).') @click.option( '--autoeject-timeout', required=False, type=int, default=0, metavar='SECONDS', help='When set, the smartcard will automatically eject' ' after the given time. Implies --touch-eject.') @click.option( '--chalresp-timeout', required=False, type=int, default=0, metavar='SECONDS', help='Sets the timeout when waiting for touch' ' for challenge-response in the OTP application.') def usb( ctx, enable, disable, list_enabled, enable_all, touch_eject, no_touch_eject, autoeject_timeout, chalresp_timeout, lock_code, force): """ Enable or disable applications over USB. """ def ensure_not_all_disabled(ctx, usb_enabled): for app in APPLICATION: if app & usb_enabled: return ctx.fail('Can not disable all applications over USB.') if not (list_enabled or enable_all or enable or disable or touch_eject or no_touch_eject or autoeject_timeout or chalresp_timeout): ctx.fail('No configuration options chosen.') enable = list(APPLICATION) if enable_all else enable _ensure_not_invalid_options(ctx, enable, disable) if touch_eject and no_touch_eject: ctx.fail('Invalid options.') dev = ctx.obj['dev'] usb_supported = dev.config.usb_supported usb_enabled = dev.config.usb_enabled flags = dev.config.device_flags if not usb_supported: ctx.fail('USB interface not supported.') if list_enabled: _list_apps(ctx, usb_enabled) if touch_eject: flags |= FLAGS.MODE_FLAG_EJECT if no_touch_eject: flags &= ~FLAGS.MODE_FLAG_EJECT for app in enable: if app & usb_supported: usb_enabled |= app else: ctx.fail('{} not supported over USB.'.format(app.name)) for app in disable: if app & usb_supported: usb_enabled &= ~app else: ctx.fail('{} not supported over USB.'.format(app.name)) ensure_not_all_disabled(ctx, usb_enabled) f_confirm = '{}{}{}{}{}{}Configure USB interface?'.format( 'Enable {}.\n'.format( ', '.join( [str(app) for app in enable])) if enable else '', 'Disable {}.\n'.format( ', '.join( [str(app) for app in disable])) if disable else '', 'Set touch eject.\n' if touch_eject else '', 'Disable touch eject.\n' if no_touch_eject else '', 'Set autoeject timeout to {}.\n'.format( autoeject_timeout) if autoeject_timeout else '', 'Set challenge-response timeout to {}.\n'.format( chalresp_timeout) if chalresp_timeout else '') is_locked = dev.config.configuration_locked if force and is_locked and not lock_code: ctx.fail('Configuration is locked - please supply the --lock-code ' 'option.') if lock_code and not is_locked: ctx.fail('Configuration is not locked - please remove the ' '--lock-code option.') force or click.confirm(f_confirm, abort=True, err=True) if is_locked and not lock_code: lock_code = prompt_lock_code() if lock_code: lock_code = _parse_lock_code(ctx, lock_code) try: dev.write_config( device_config( usb_enabled=usb_enabled, flags=flags, auto_eject_timeout=autoeject_timeout, chalresp_timeout=chalresp_timeout), reboot=True, lock_key=lock_code) except Exception as e: logger.error('Failed to write config', exc_info=e) ctx.fail('Failed to configure USB applications.') @config.command() @click.pass_context @click_force_option @click.option( '-e', '--enable', multiple=True, type=ApplicationsChoice(APPLICATION), help='Enable applications.') @click.option( '-d', '--disable', multiple=True, type=ApplicationsChoice(APPLICATION), help='Disable applications.') @click.option( '-a', '--enable-all', is_flag=True, help='Enable all applications.') @click.option( '-D', '--disable-all', is_flag=True, help='Disable all applications') @click.option('-l', '--list', 'list_enabled', is_flag=True, help='List enabled applications') @click.option( '-L', '--lock-code', metavar='HEX', help='Current application configuration lock code.') def nfc(ctx, enable, disable, enable_all, disable_all, list_enabled, lock_code, force): """ Enable or disable applications over NFC. """ if not (list_enabled or enable_all or enable or disable_all or disable): ctx.fail('No configuration options chosen.') if enable_all: enable = list(APPLICATION) if disable_all: disable = list(APPLICATION) _ensure_not_invalid_options(ctx, enable, disable) dev = ctx.obj['dev'] nfc_supported = dev.config.nfc_supported nfc_enabled = dev.config.nfc_enabled if not nfc_supported: ctx.fail('NFC interface not available.') if list_enabled: _list_apps(ctx, nfc_enabled) for app in enable: if app & nfc_supported: nfc_enabled |= app else: ctx.fail('{} not supported over NFC.'.format(app.name)) for app in disable: if app & nfc_supported: nfc_enabled &= ~app else: ctx.fail('{} not supported over NFC.'.format(app.name)) f_confirm = '{}{}Configure NFC interface?'.format( 'Enable {}.\n'.format( ', '.join( [str(app) for app in enable])) if enable else '', 'Disable {}.\n'.format( ', '.join( [str(app) for app in disable])) if disable else '') is_locked = dev.config.configuration_locked if force and is_locked and not lock_code: ctx.fail('Configuration is locked - please supply the --lock-code ' 'option.') if lock_code and not is_locked: ctx.fail('Configuration is not locked - please remove the ' '--lock-code option.') force or click.confirm(f_confirm, abort=True, err=True) if is_locked and not lock_code: lock_code = prompt_lock_code() if lock_code: lock_code = _parse_lock_code(ctx, lock_code) try: dev.write_config( device_config( nfc_enabled=nfc_enabled), reboot=True, lock_key=lock_code) except Exception as e: logger.error('Failed to write config', exc_info=e) ctx.fail('Failed to configure NFC applications.') def _list_apps(ctx, enabled): for app in APPLICATION: if app & enabled: click.echo(str(app)) ctx.exit() def _ensure_not_invalid_options(ctx, enable, disable): if any(a in enable for a in disable): ctx.fail('Invalid options.') def _parse_lock_code(ctx, lock_code): try: lock_code = a2b_hex(lock_code) if lock_code and len(lock_code) != 16: ctx.fail('Lock code must be exactly 16 bytes ' '(32 hexadecimal digits) long.') return lock_code except Exception: ctx.fail('Lock code has the wrong format.') yubikey-manager-3.1.1/ykman/cli/fido.py0000644000175100001630000003343413614233340020574 0ustar runnerdocker00000000000000# Copyright (c) 2018 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import import click import logging from fido2.ctap1 import ApduError from fido2.ctap import CtapError from time import sleep from .util import click_postpone_execution, prompt_for_touch, click_force_option from ..driver_ccid import SW from ..util import TRANSPORT from ..fido import Fido2Controller, FipsU2fController from ..descriptor import get_descriptors logger = logging.getLogger(__name__) FIPS_PIN_MIN_LENGTH = 6 PIN_MIN_LENGTH = 4 @click.group() @click.pass_context @click_postpone_execution def fido(ctx): """ Manage FIDO applications. Examples: \b Reset the FIDO (FIDO2 and U2F) applications: $ ykman fido reset \b Change the FIDO2 PIN from 123456 to 654321: $ ykman fido set-pin --pin 123456 --new-pin 654321 """ dev = ctx.obj['dev'] if dev.is_fips: try: ctx.obj['controller'] = FipsU2fController(dev.driver) except Exception as e: logger.debug('Failed to load FipsU2fController', exc_info=e) ctx.fail('Failed to load FIDO Application.') else: try: ctx.obj['controller'] = Fido2Controller(dev.driver) except Exception as e: logger.debug('Failed to load Fido2Controller', exc_info=e) ctx.fail('Failed to load FIDO 2 Application.') @fido.command() @click.pass_context def info(ctx): """ Display status of FIDO2 application. """ controller = ctx.obj['controller'] if controller.is_fips: click.echo('FIPS Approved Mode: {}'.format( 'Yes' if controller.is_in_fips_mode else 'No')) else: if controller.has_pin: try: click.echo( 'PIN is set, with {} tries left.'.format( controller.get_pin_retries())) except CtapError as e: if e.code == CtapError.ERR.PIN_BLOCKED: click.echo('PIN is blocked.') else: click.echo('PIN is not set.') @fido.command('list') @click.pass_context @click.option('-P', '--pin', help='PIN code.') def list_creds(ctx, pin): """ List resident credentials. """ controller = ctx.obj['controller'] if not controller.has_pin: ctx.fail('No PIN set.') if controller.has_pin and pin is None: pin = _prompt_current_pin(prompt='Enter your PIN') try: for cred in controller.get_resident_credentials(pin): click.echo('{} ({})'.format(cred.user_name, cred.rp_id)) except CtapError as e: if e.code == CtapError.ERR.PIN_INVALID: ctx.fail('Wrong PIN.') except Exception as e: logger.debug('Failed to list resident credentials', exc_info=e) ctx.fail('Failed to list resident credentials.') @fido.command() @click.pass_context @click.argument('query') @click.option('-P', '--pin', help='PIN code.') @click.option('-f', '--force', is_flag=True, help='Confirm deletion without prompting') def delete(ctx, query, pin, force): """ Delete a resident credential. """ controller = ctx.obj['controller'] if not controller.has_pin: ctx.fail('No PIN set.') if controller.has_pin and pin is None: pin = _prompt_current_pin(prompt='Enter your PIN') try: hits = [ cred for cred in controller.get_resident_credentials(pin) if query.lower() in cred.user_name or query.lower() in cred.rp_id ] if len(hits) == 0: ctx.fail('No matches, nothing to be done.') elif len(hits) == 1: cred = hits[0] if force or click.confirm( 'Delete credential {} ({})?'.format( cred.user_name, cred.rp_id)): controller.delete_resident_credential( cred.credential_id, pin) else: ctx.fail('Multiple matches, make the query more specific.') except CtapError as e: if e.code == CtapError.ERR.PIN_INVALID: ctx.fail('Wrong PIN.') except Exception as e: logger.debug('Failed to delete resident credential', exc_info=e) ctx.fail('Failed to delete resident credential.') @fido.command('set-pin') @click.pass_context @click.option('-P', '--pin', help='Current PIN code.') @click.option('-n', '--new-pin', help='A new PIN.') @click.option('-u', '--u2f', is_flag=True, help='Set FIDO U2F PIN instead of FIDO2 PIN.') def set_pin(ctx, pin, new_pin, u2f): """ Set or change the PIN code. The FIDO2 PIN must be at least 4 characters long, and supports any type of alphanumeric characters. On YubiKey FIPS, a PIN can be set for FIDO U2F. That PIN must be at least 6 characters long. """ controller = ctx.obj['controller'] is_fips = controller.is_fips if is_fips and not u2f: ctx.fail('This is a YubiKey FIPS. To set the U2F PIN, pass the --u2f ' 'option.') if u2f and not is_fips: ctx.fail('This is not a YubiKey FIPS, and therefore does not support a ' 'U2F PIN. To set the FIDO2 PIN, remove the --u2f option.') def prompt_new_pin(): return click.prompt( 'Enter your new PIN', default='', hide_input=True, show_default=False, confirmation_prompt=True, err=True) def change_pin(pin, new_pin): if pin is not None: _fail_if_not_valid_pin(ctx, pin, is_fips) _fail_if_not_valid_pin(ctx, new_pin, is_fips) try: if is_fips: try: # Failing this with empty current PIN does not cost a retry controller.change_pin(old_pin=pin or '', new_pin=new_pin) except ApduError as e: if e.code == SW.WRONG_LENGTH: pin = _prompt_current_pin() _fail_if_not_valid_pin(ctx, pin, is_fips) controller.change_pin(old_pin=pin, new_pin=new_pin) else: raise else: controller.change_pin(old_pin=pin, new_pin=new_pin) except CtapError as e: if e.code == CtapError.ERR.PIN_INVALID: ctx.fail('Wrong PIN.') if e.code == CtapError.ERR.PIN_AUTH_BLOCKED: ctx.fail( 'PIN authentication is currently blocked. ' 'Remove and re-insert the YubiKey.') if e.code == CtapError.ERR.PIN_BLOCKED: ctx.fail('PIN is blocked.') if e.code == CtapError.ERR.PIN_POLICY_VIOLATION: ctx.fail('New PIN is too long.') logger.error('Failed to change PIN', exc_info=e) ctx.fail('Failed to change PIN.') except ApduError as e: if e.code == SW.VERIFY_FAIL_NO_RETRY: ctx.fail('Wrong PIN.') if e.code == SW.AUTH_METHOD_BLOCKED: ctx.fail('PIN is blocked.') logger.error('Failed to change PIN', exc_info=e) ctx.fail('Failed to change PIN.') def set_pin(new_pin): _fail_if_not_valid_pin(ctx, new_pin, is_fips) try: controller.set_pin(new_pin) except CtapError as e: if e.code == CtapError.ERR.PIN_POLICY_VIOLATION: ctx.fail('PIN is too long.') logger.error('Failed to set PIN', exc_info=e) ctx.fail('Failed to set PIN') if pin and not controller.has_pin: ctx.fail('There is no current PIN set. Use --new-pin to set one.') if controller.has_pin and pin is None and not is_fips: pin = _prompt_current_pin() if not new_pin: new_pin = prompt_new_pin() if controller.has_pin: change_pin(pin, new_pin) else: set_pin(new_pin) @fido.command('reset') @click_force_option @click.pass_context def reset(ctx, force): """ Reset all FIDO applications. This action will wipe all FIDO credentials, including FIDO U2F credentials, on the YubiKey and remove the PIN code. The reset must be triggered immediately after the YubiKey is inserted, and requires a touch on the YubiKey. """ n_keys = len(list(get_descriptors())) if n_keys > 1: ctx.fail('Only one YubiKey can be connected to perform a reset.') if not force: if not click.confirm('WARNING! This will delete all FIDO credentials, ' 'including FIDO U2F credentials, and restore ' 'factory settings. Proceed?', err=True): ctx.abort() def prompt_re_insert_key(): click.echo('Remove and re-insert your YubiKey to perform the reset...') removed = False while True: sleep(0.1) n_keys = len(list(get_descriptors())) if not n_keys: removed = True if removed and n_keys == 1: return def try_reset(controller_type): if not force: prompt_re_insert_key() dev = list(get_descriptors())[0].open_device(TRANSPORT.FIDO) controller = controller_type(dev.driver) controller.reset(touch_callback=prompt_for_touch) else: controller = ctx.obj['controller'] controller.reset(touch_callback=prompt_for_touch) if ctx.obj['dev'].is_fips: if not force: destroy_input = click.prompt( 'WARNING! This is a YubiKey FIPS device. This command will ' 'also overwrite the U2F attestation key; this action cannot be ' 'undone and this YubiKey will no longer be a FIPS compliant ' 'device.\n' 'To proceed, please enter the text "OVERWRITE"', default='', show_default=False, err=True ) if destroy_input != 'OVERWRITE': ctx.fail('Reset aborted by user.') try: try_reset(FipsU2fController) except ApduError as e: if e.code == SW.COMMAND_NOT_ALLOWED: ctx.fail( 'Reset failed. Reset must be triggered within 5 seconds' ' after the YubiKey is inserted.') else: logger.error('Reset failed', exc_info=e) ctx.fail('Reset failed.') except Exception as e: logger.error('Reset failed', exc_info=e) ctx.fail('Reset failed.') else: try: try_reset(Fido2Controller) except CtapError as e: if e.code == CtapError.ERR.ACTION_TIMEOUT: ctx.fail( 'Reset failed. You need to touch your' ' YubiKey to confirm the reset.') elif e.code == CtapError.ERR.NOT_ALLOWED: ctx.fail( 'Reset failed. Reset must be triggered within 5 seconds' ' after the YubiKey is inserted.') else: logger.error(e) ctx.fail('Reset failed.') except Exception as e: logger.error(e) ctx.fail('Reset failed.') @fido.command('unlock') @click.pass_context @click.option('-P', '--pin', help='Current PIN code.') def unlock(ctx, pin): """ Verify U2F PIN for YubiKey FIPS. Unlock the YubiKey FIPS and allow U2F registration. """ controller = ctx.obj['controller'] if not controller.is_fips: ctx.fail('This is not a YubiKey FIPS, and therefore' ' does not support a U2F PIN.') if pin is None: pin = _prompt_current_pin('Enter your PIN') _fail_if_not_valid_pin(ctx, pin, True) try: controller.verify_pin(pin) except ApduError as e: if e.code == SW.VERIFY_FAIL_NO_RETRY: ctx.fail('Wrong PIN.') if e.code == SW.AUTH_METHOD_BLOCKED: ctx.fail('PIN is blocked.') if e.code == SW.COMMAND_NOT_ALLOWED: ctx.fail('PIN is not set.') logger.error('PIN verification failed', exc_info=e) ctx.fail('PIN verification failed.') def _prompt_current_pin(prompt='Enter your current PIN'): return click.prompt( prompt, default='', hide_input=True, show_default=False, err=True) def _fail_if_not_valid_pin(ctx, pin=None, is_fips=False): min_length = FIPS_PIN_MIN_LENGTH \ if is_fips else PIN_MIN_LENGTH if not pin or len(pin) < min_length: ctx.fail('PIN must be over {} characters long'.format(min_length)) fido.transports = TRANSPORT.FIDO yubikey-manager-3.1.1/ykman/cli/info.py0000644000175100001630000001356213614233340020606 0ustar runnerdocker00000000000000# Copyright (c) 2016 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import from ..descriptor import open_device, FailedOpeningDeviceException from ..fido import FipsU2fController from ..oath import OathController from ..otp import OtpController from ..util import APPLICATION, TRANSPORT import click import logging logger = logging.getLogger(__name__) def print_app_status_table(config): rows = [] for app in APPLICATION: if app & config.usb_supported: if app & config.usb_enabled: usb_status = 'Enabled' else: usb_status = 'Disabled' else: usb_status = 'Not available' if config.nfc_supported: if app & config.nfc_supported: if app & config.nfc_enabled: nfc_status = 'Enabled' else: nfc_status = 'Disabled' else: nfc_status = 'Not available' rows.append([str(app), usb_status, nfc_status]) else: rows.append([str(app), usb_status]) column_l = [] for row in rows: for idx, c in enumerate(row): if len(column_l) > idx: if len(c) > column_l[idx]: column_l[idx] = len(c) else: column_l.append(len(c)) f_apps = 'Applications'.ljust(column_l[0]) if config.nfc_supported: f_USB = 'USB'.ljust(column_l[1]) f_NFC = 'NFC'.ljust(column_l[2]) f_table = '' for row in rows: for idx, c in enumerate(row): f_table += '{}\t'.format(c.ljust(column_l[idx])) f_table += '\n' if config.nfc_supported: click.echo('{}\t{}\t{}'.format(f_apps, f_USB, f_NFC)) else: click.echo('{}'.format(f_apps)) click.echo(f_table, nl=False) def get_fips_status_over_transport(serial, transport, controller_constructor): try: with open_device(transports=transport, serial=serial) as dev: return controller_constructor(dev._driver).is_in_fips_mode except FailedOpeningDeviceException as e: logger.debug('Failed to open device', exc_info=e) return False def get_overall_fips_status(serial, config): statuses = {} if config.usb_enabled & APPLICATION.OTP: statuses['OTP'] = get_fips_status_over_transport( serial, TRANSPORT.OTP, OtpController) else: statuses['OTP'] = False if config.usb_enabled & APPLICATION.OATH: statuses['OATH'] = get_fips_status_over_transport( serial, TRANSPORT.CCID, OathController) else: statuses['OATH'] = False if config.usb_enabled & APPLICATION.U2F: statuses['FIDO U2F'] = get_fips_status_over_transport( serial, TRANSPORT.FIDO, FipsU2fController) else: statuses['FIDO U2F'] = False return statuses @click.option( '-c', '--check-fips', help='Check if YubiKey is in FIPS Approved mode.', is_flag=True) @click.command() @click.pass_context def info(ctx, check_fips): """ Show general information. Displays information about the attached YubiKey such as serial number, firmware version, applications, etc. """ dev = ctx.obj['dev'] if dev.is_fips and check_fips: fips_status = get_overall_fips_status(dev.serial, dev.config) click.echo('Device type: {}'.format(dev.device_name)) click.echo('Serial number: {}'.format( dev.serial or 'Not set or unreadable')) if dev.version: f_version = '.'.join(str(x) for x in dev.version) click.echo('Firmware version: {}'.format(f_version)) else: click.echo('Firmware version: Uncertain, re-run with only one ' 'YubiKey connected') config = dev.config if config.form_factor: click.echo('Form factor: {!s}'.format(config.form_factor)) click.echo('Enabled USB interfaces: {}'.format(dev.mode)) if config.nfc_supported: f_nfc = 'enabled' if config.nfc_enabled else 'disabled' click.echo('NFC interface is {}.'.format(f_nfc)) if config.configuration_locked: click.echo('Configured applications are protected by a lock code.') click.echo() print_app_status_table(config) if dev.is_fips and check_fips: click.echo() click.echo('FIPS Approved Mode: {}'.format( 'Yes' if all(fips_status.values()) else 'No')) status_keys = list(fips_status.keys()) status_keys.sort() for status_key in status_keys: click.echo(' {}: {}'.format( status_key, 'Yes' if fips_status[status_key] else 'No')) yubikey-manager-3.1.1/ykman/cli/mode.py0000644000175100001630000001321213614233340020567 0ustar runnerdocker00000000000000# Copyright (c) 2015 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import from .util import click_force_option from ..util import Mode, TRANSPORT from ..driver import ModeSwitchError import logging import re import click logger = logging.getLogger(__name__) def _parse_transport_string(transport): for t in TRANSPORT: if t.name.startswith(transport): return t raise ValueError() def _parse_mode_string(ctx, param, mode): if mode is None: return None try: mode_int = int(mode) return Mode.from_code(mode_int) except IndexError: ctx.fail('Invalid mode: {}'.format(mode_int)) except ValueError: pass # Not a numeric mode, parse string try: transports = set() if mode[0] in ['+', '-']: transports.update(TRANSPORT.split(ctx.obj['dev'].mode.transports)) for mod in re.findall(r'[+-][A-Z]+', mode.upper()): transport = _parse_transport_string(mod[1:]) if mod.startswith('+'): transports.add(transport) else: transports.discard(transport) else: for t in filter(None, re.split(r'[+]+', mode.upper())): transports.add(_parse_transport_string(t)) except ValueError: ctx.fail('Invalid mode string: {}'.format(mode)) return Mode(sum(transports)) @click.command() @click.argument('mode', required=False, callback=_parse_mode_string) @click.option('--touch-eject', is_flag=True, help='When set, the button ' 'toggles the state of the smartcard between ejected and inserted ' '(CCID mode only).') @click.option('--autoeject-timeout', required=False, type=int, default=0, metavar='SECONDS', help='When set, the smartcard will automatically eject after the ' 'given time. Implies --touch-eject (CCID mode only).' ) @click.option('--chalresp-timeout', required=False, type=int, default=0, metavar='SECONDS', help='Sets the timeout when waiting for touch for challenge ' 'response.') @click_force_option @click.pass_context def mode(ctx, mode, touch_eject, autoeject_timeout, chalresp_timeout, force): """ Manage connection modes (USB Interfaces). Get the current connection mode of the YubiKey, or set it to MODE. MODE can be a string, such as "OTP+FIDO+CCID", or a shortened form: "o+f+c". It can also be a mode number. Examples: \b Set the OTP and FIDO mode: $ ykman mode OTP+FIDO \b Set the CCID only mode and use touch to eject the smart card: $ ykman mode CCID --touch-eject """ dev = ctx.obj['dev'] if autoeject_timeout: touch_eject = True autoeject = autoeject_timeout if touch_eject else None if mode is not None: if mode.transports != TRANSPORT.CCID: autoeject = None if touch_eject: ctx.fail('--touch-eject can only be used when setting' ' CCID-only mode') if not force: if mode == dev.mode: click.echo('Mode is already {}, nothing to do...'.format(mode)) ctx.exit() elif not dev.has_mode(mode): click.echo('Mode {} is not supported on this YubiKey!' .format(mode)) ctx.fail('Use --force to attempt to set it anyway.') force or click.confirm('Set mode of YubiKey to {}?'.format(mode), abort=True, err=True) try: dev.set_mode(mode, chalresp_timeout, autoeject) if not dev.can_write_config: click.echo( 'Mode set! You must remove and re-insert your YubiKey ' 'for this change to take effect.') except ModeSwitchError as e: logger.debug('Failed to switch mode', exc_info=e) click.echo('Failed to switch mode on the YubiKey. Make sure your ' 'YubiKey does not have an access code set.') else: click.echo('Current connection mode is: {}'.format(dev.mode)) supported = ', '.join(t.name for t in TRANSPORT .split(dev.config.usb_supported)) click.echo('Supported USB interfaces are: {}'.format(supported)) yubikey-manager-3.1.1/ykman/cli/oath.py0000644000175100001630000004233713614233340020610 0ustar runnerdocker00000000000000# Copyright (c) 2015 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import import click import logging from threading import Timer from binascii import b2a_hex, a2b_hex from .util import ( click_force_option, click_postpone_execution, click_callback, click_parse_b32_key, prompt_for_touch, EnumChoice ) from ..driver_ccid import ( APDUError, SW ) from ..util import TRANSPORT, parse_b32_key from ..oath import OathController, CredentialData, OATH_TYPE, ALGO from ..settings import Settings logger = logging.getLogger(__name__) click_touch_option = click.option( '-t', '--touch', is_flag=True, help='Require touch on YubiKey to generate code.') click_show_hidden_option = click.option( '-H', '--show-hidden', is_flag=True, help='Include hidden credentials.') @click_callback() def _clear_callback(ctx, param, clear): if clear: ensure_validated(ctx) controller = ctx.obj['controller'] settings = ctx.obj['settings'] controller.clear_password() keys = settings.setdefault('keys', {}) if controller.id in keys: del keys[controller.id] settings.write() click.echo('Password cleared.') ctx.exit() @click_callback() def click_parse_uri(ctx, param, val): try: return CredentialData.from_uri(val) except ValueError: raise click.BadParameter('URI seems to have the wrong format.') @click.group() @click.pass_context @click_postpone_execution @click.option('-p', '--password', help='Provide a password to unlock the ' 'YubiKey.') def oath(ctx, password): """ Manage OATH Application. Examples: \b Generate codes for credentials starting with 'yubi': $ ykman oath code yubi \b Add a touch credential with the secret key f5up4ub3dw and the name yubico: $ ykman oath add yubico f5up4ub3dw --touch \b Set a password for the OATH application: $ ykman oath set-password """ try: controller = OathController(ctx.obj['dev'].driver) ctx.obj['controller'] = controller ctx.obj['settings'] = Settings('oath') except APDUError as e: if e.sw == SW.NOT_FOUND: ctx.fail("The OATH application can't be found on this YubiKey.") raise if password: ctx.obj['key'] = controller.derive_key(password) @oath.command() @click.pass_context def info(ctx): """ Display status of OATH application. """ controller = ctx.obj['controller'] version = controller.version click.echo( 'OATH version: {}.{}.{}'.format(version[0], version[1], version[2])) click.echo('Password protection ' + ('enabled' if controller.locked else 'disabled')) keys = ctx.obj['settings'].get('keys', {}) if controller.locked and controller.id in keys: click.echo('The password for this YubiKey is remembered by ykman.') if ctx.obj['dev'].is_fips: click.echo('FIPS Approved Mode: {}'.format( 'Yes' if controller.is_in_fips_mode else 'No')) @oath.command() @click.pass_context @click.confirmation_option( '-f', '--force', prompt='WARNING! This will delete ' 'all stored OATH credentials and restore factory settings?') def reset(ctx): """ Reset all OATH data. This action will wipe all credentials and reset factory settings for the OATH application on the YubiKey. """ controller = ctx.obj['controller'] click.echo('Resetting OATH data...') old_id = controller.id controller.reset() settings = ctx.obj['settings'] keys = settings.setdefault('keys', {}) if old_id in keys: del keys[old_id] settings.write() click.echo( 'Success! All OATH credentials have been cleared from your YubiKey.') @oath.command() @click.argument('name') @click.argument('secret', callback=click_parse_b32_key, required=False) @click.option( '-o', '--oath-type', type=EnumChoice(OATH_TYPE), default=OATH_TYPE.TOTP.name, help='Time-based (TOTP) or counter-based (HOTP) credential.', show_default=True) @click.option( '-d', '--digits', type=click.Choice(['6', '7', '8']), default='6', help='Number of digits in generated code.', show_default=True) @click.option( '-a', '--algorithm', type=EnumChoice(ALGO), default=ALGO.SHA1.name, show_default=True, help='Algorithm to use for code generation.') @click.option( '-c', '--counter', type=click.INT, default=0, help='Initial counter value for HOTP credentials.') @click.option('-i', '--issuer', help='Issuer of the credential.') @click.option( '-p', '--period', help='Number of seconds a TOTP code is valid.', default=30, show_default=True) @click_touch_option @click_force_option @click.pass_context def add(ctx, secret, name, issuer, period, oath_type, digits, touch, algorithm, counter, force): """ Add a new credential. This will add a new credential to your YubiKey. """ digits = int(digits) if not secret: while True: secret = click.prompt('Enter a secret key (base32)', err=True) try: secret = parse_b32_key(secret) break except Exception as e: click.echo(e) ensure_validated(ctx) _add_cred(ctx, CredentialData(secret, issuer, name, oath_type, algorithm, digits, period, counter, touch), force) @oath.command() @click.argument('uri', callback=click_parse_uri, required=False) @click_touch_option @click_force_option @click.pass_context def uri(ctx, uri, touch, force): """ Add a new credential from URI. Use a URI to add a new credential to your YubiKey. """ if not uri: while True: uri = click.prompt('Enter an OATH URI', err=True) try: uri = CredentialData.from_uri(uri) break except Exception as e: click.echo(e) ensure_validated(ctx) data = uri # Steam is a special case where we allow the otpauth # URI to contain a 'digits' value of '5'. if data.digits == 5 and data.issuer == 'Steam': data.digits = 6 data.touch = touch _add_cred(ctx, data, force=force) def _add_cred(ctx, data, force): controller = ctx.obj['controller'] if not (0 < len(data.name) <= 64): ctx.fail('Name must be between 1 and 64 bytes.') if len(data.secret) < 2: ctx.fail('Secret must be at least 2 bytes.') if data.touch and controller.version < (4, 2, 6): ctx.fail('Touch-required credentials not supported on this key.') if data.counter and data.oath_type != OATH_TYPE.HOTP: ctx.fail('Counter only supported for HOTP credentials.') if data.algorithm == ALGO.SHA512 and ( controller.version < (4, 3, 1) or ctx.obj['dev'].is_fips): ctx.fail('Algorithm SHA512 not supported on this YubiKey.') key = data.make_key() if not force and any(cred.key == key for cred in controller.list()): click.confirm( 'A credential called {} already exists on this YubiKey.' ' Do you want to overwrite it?'.format(data.name), abort=True, err=True) firmware_overwrite_issue = (4, 0, 0) < controller.version < (4, 3, 5) cred_is_subset = any( (cred.key.startswith(key) and cred.key != key) for cred in controller.list()) # YK4 has an issue with credential overwrite in firmware versions < 4.3.5 if firmware_overwrite_issue and cred_is_subset: ctx.fail( 'Choose a name that is not a subset of an existing credential.') try: controller.put(data) except APDUError as e: if e.sw == SW.NO_SPACE: ctx.fail('No space left on your YubiKey for OATH credentials.') elif e.sw == SW.COMMAND_ABORTED: # Some NEOs do not use the NO_SPACE error. ctx.fail( 'The command failed. Is there enough space on your YubiKey?') else: raise @oath.command() @click_show_hidden_option @click.pass_context @click.option('-o', '--oath-type', is_flag=True, help='Display the OATH type.') @click.option('-p', '--period', is_flag=True, help='Display the period.') def list(ctx, show_hidden, oath_type, period): """ List all credentials. List all credentials stored on your YubiKey. """ ensure_validated(ctx) controller = ctx.obj['controller'] creds = [cred for cred in controller.list() if show_hidden or not cred.is_hidden ] creds.sort() for cred in creds: click.echo(cred.printable_key, nl=False) if oath_type: click.echo(u', {}'.format(cred.oath_type.name), nl=False) if period: click.echo(', {}'.format(cred.period), nl=False) click.echo() @oath.command() @click_show_hidden_option @click.pass_context @click.argument('query', required=False, default='') @click.option('-s', '--single', is_flag=True, help='Ensure only a single ' 'match, and output only the code.') def code(ctx, show_hidden, query, single): """ Generate codes. Generate codes from credentials stored on your YubiKey. Provide a query string to match one or more specific credentials. Touch and HOTP credentials require a single match to be triggered. """ ensure_validated(ctx) controller = ctx.obj['controller'] creds = [(cr, c) for (cr, c) in controller.calculate_all() if show_hidden or not cr.is_hidden ] creds = _search(creds, query) if len(creds) == 1: cred, code = creds[0] if cred.touch: prompt_for_touch() try: if cred.oath_type == OATH_TYPE.HOTP: # HOTP might require touch, we don't know. # Assume yes after 500ms. hotp_touch_timer = Timer(0.500, prompt_for_touch) hotp_touch_timer.start() creds = [(cred, controller.calculate(cred))] hotp_touch_timer.cancel() elif code is None: creds = [(cred, controller.calculate(cred))] except APDUError as e: if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED: ctx.fail('Touch credential timed out!') elif single: _error_multiple_hits(ctx, [cr for cr, c in creds]) if single: click.echo(creds[0][1].value) else: creds.sort() outputs = [ ( cr.printable_key, c.value if c else '[Touch Credential]' if cr.touch else '[HOTP Credential]' if cr.oath_type == OATH_TYPE.HOTP else '' ) for (cr, c) in creds ] longest_name = max(len(n) for (n, c) in outputs) if outputs else 0 longest_code = max(len(c) for (n, c) in outputs) if outputs else 0 format_str = u'{:<%d} {:>%d}' % (longest_name, longest_code) for name, result in outputs: click.echo(format_str.format(name, result)) @oath.command() @click.pass_context @click.argument('query') @click.option('-f', '--force', is_flag=True, help='Confirm deletion without prompting') def delete(ctx, query, force): """ Delete a credential. Delete a credential from your YubiKey. Provide a query string to match the credential to delete. """ ensure_validated(ctx) controller = ctx.obj['controller'] creds = controller.list() hits = _search(creds, query) if len(hits) == 0: click.echo('No matches, nothing to be done.') elif len(hits) == 1: cred = hits[0] if force or (click.confirm( u'Delete credential: {} ?'.format(cred.printable_key), default=False, err=True )): controller.delete(cred) click.echo(u'Deleted {}.'.format(cred.printable_key)) else: click.echo('Deletion aborted by user.') else: _error_multiple_hits(ctx, hits) @oath.command('set-password') @click.pass_context @click.option( '-c', '--clear', is_flag=True, expose_value=False, callback=_clear_callback, is_eager=True, help='Clear the current password.') @click.option( '-n', '--new-password', help='Provide a new password as an argument.') @click.option('-r', '--remember', is_flag=True, help='Remember the new ' 'password on this machine.') def set_password(ctx, new_password, remember): """ Password protect the OATH credentials. Allows you to set a password that will be required to access the OATH credentials stored on your YubiKey. """ ensure_validated(ctx, prompt='Enter your current password') if not new_password: new_password = click.prompt( 'Enter your new password', hide_input=True, confirmation_prompt=True, err=True) controller = ctx.obj['controller'] settings = ctx.obj['settings'] keys = settings.setdefault('keys', {}) key = controller.set_password(new_password) click.echo('Password updated.') if remember: keys[controller.id] = b2a_hex(key).decode() settings.write() click.echo('Password remembered') elif controller.id in keys: del keys[controller.id] settings.write() @oath.command('remember-password') @click.pass_context @click.option('-F', '--forget', is_flag=True, help='Forget a password.') @click.option('-c', '--clear-all', is_flag=True, help='Remove all stored ' 'passwords from this computer.') def remember_password(ctx, forget, clear_all): """ Manage local password storage. Store your YubiKeys password on this computer to avoid having to enter it on each use, or delete stored passwords. """ controller = ctx.obj['controller'] settings = ctx.obj['settings'] keys = settings.setdefault('keys', {}) if clear_all: del settings['keys'] settings.write() click.echo('All passwords have been cleared.') elif forget: if controller.id in keys: del keys[controller.id] settings.write() click.echo('Password forgotten.') else: ensure_validated(ctx, remember=True) def ensure_validated(ctx, prompt='Enter your password', remember=False): controller = ctx.obj['controller'] if controller.locked: # If password given as arg, use it if 'key' in ctx.obj: _validate(ctx, ctx.obj['key'], remember) return # Use stored key if available keys = ctx.obj['settings'].setdefault('keys', {}) if controller.id in keys: try: controller.validate(a2b_hex(keys[controller.id])) return except Exception as e: logger.debug('Error', exc_info=e) del keys[controller.id] # Prompt for password password = click.prompt(prompt, hide_input=True, err=True) key = controller.derive_key(password) _validate(ctx, key, remember) def _validate(ctx, key, remember): try: controller = ctx.obj['controller'] controller.validate(key) if remember: settings = ctx.obj['settings'] keys = settings.setdefault('keys', {}) keys[controller.id] = b2a_hex(key).decode() settings.write() click.echo('Password remembered.') except Exception: ctx.fail('Authentication to the YubiKey failed. Wrong password?') def _search(creds, query): hits = [] for entry in creds: c = entry[0] if isinstance(entry, tuple) else entry if c.printable_key == query: return [entry] if query.lower() in c.printable_key.lower(): hits.append(entry) return hits def _error_multiple_hits(ctx, hits): click.echo( 'Error: Multiple matches, please make the query more specific.', err=True ) click.echo('', err=True) for cred in hits: click.echo(cred.printable_key, err=True) ctx.exit(1) oath.transports = TRANSPORT.CCID yubikey-manager-3.1.1/ykman/cli/opgp.py0000644000175100001630000003321713614233340020617 0ustar runnerdocker00000000000000# Copyright (c) 2015 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import import logging import click from ..util import TRANSPORT, parse_certificates, parse_private_key from ..opgp import OpgpController, KEY_SLOT, TOUCH_MODE from ..driver_ccid import APDUError, SW from .util import ( click_force_option, click_format_option, click_postpone_execution, EnumChoice) logger = logging.getLogger(__name__) def one_of(data): def inner(ctx, param, key): if key is not None: return data[key] return inner def get_or_fail(data): def inner(key): if key in data: return data[key] raise ValueError('Invalid value: {}. Must be one of: {}'.format( key, ', '.join(data.keys()))) return inner def int_in_range(minval, maxval): def inner(val): intval = int(val) if minval <= intval <= maxval: return intval raise ValueError('Invalid value: {}. Must be in range {}-{}'.format( intval, minval, maxval)) return inner @click.group() @click.pass_context @click_postpone_execution def openpgp(ctx): """ Manage OpenPGP Application. Examples: \b Set the retries for PIN, Reset Code and Admin PIN to 10: $ ykman openpgp set-retries 10 10 10 \b Require touch to use the authentication key: $ ykman openpgp set-touch aut on """ try: ctx.obj['controller'] = OpgpController(ctx.obj['dev'].driver) except APDUError as e: if e.sw == SW.NOT_FOUND: ctx.fail("The OpenPGP application can't be found on this " 'YubiKey.') logger.debug('Failed to load OpenPGP Application', exc_info=e) ctx.fail('Failed to load OpenPGP Application') @openpgp.command() @click.pass_context def info(ctx): """ Display status of OpenPGP application. """ controller = ctx.obj['controller'] click.echo('OpenPGP version: %d.%d' % controller.get_openpgp_version()) click.echo('Application version: %d.%d.%d' % controller.version) click.echo() retries = controller.get_remaining_pin_tries() click.echo('PIN tries remaining: {}'.format(retries.pin)) click.echo('Reset code tries remaining: {}'.format(retries.reset)) click.echo('Admin PIN tries remaining: {}'.format(retries.admin)) # Touch only available on YK4 and later if controller.version >= (4, 2, 6): click.echo() click.echo('Touch policies') click.echo( 'Signature key {!s}'.format( controller.get_touch(KEY_SLOT.SIG))) click.echo( 'Encryption key {!s}'.format( controller.get_touch(KEY_SLOT.ENC))) click.echo( 'Authentication key {!s}'.format( controller.get_touch(KEY_SLOT.AUT))) if controller.supports_attestation: click.echo( 'Attestation key {!s}'.format( controller.get_touch(KEY_SLOT.ATT))) @openpgp.command() @click.confirmation_option('-f', '--force', prompt='WARNING! This will delete ' 'all stored OpenPGP keys and data and restore ' 'factory settings?') @click.pass_context def reset(ctx): """ Reset OpenPGP application. This action will wipe all OpenPGP data, and set all PINs to their default values. """ click.echo("Resetting OpenPGP data, don't remove your YubiKey...") ctx.obj['controller'].reset() click.echo('Success! All data has been cleared and default PINs are set.') echo_default_pins() def echo_default_pins(): click.echo('PIN: 123456') click.echo('Reset code: NOT SET') click.echo('Admin PIN: 12345678') @openpgp.command('set-touch') @click.argument('key', metavar='KEY', type=EnumChoice(KEY_SLOT)) @click.argument('policy', metavar='POLICY', type=EnumChoice(TOUCH_MODE)) @click.option('-a', '--admin-pin', help='Admin PIN for OpenPGP.') @click_force_option @click.pass_context def set_touch(ctx, key, policy, admin_pin, force): """ Set touch policy for OpenPGP keys. \b KEY Key slot to set (sig, enc, aut or att). POLICY Touch policy to set (on, off, fixed, cached or cached-fixed). The touch policy is used to require user interaction for all operations using the private key on the YubiKey. The touch policy is set indivdually for each key slot. To see the current touch policy, run \b $ ykman openpgp info Touch policies: \b Off (default) No touch required On Touch required Fixed Touch required, can't be disabled without a full reset Cached Touch required, cached for 15s after use Cached-Fixed Touch required, cached for 15s after use, can't be disabled without a full reset """ controller = ctx.obj['controller'] policy_name = policy.name.lower().replace('_', '-') if policy not in controller.supported_touch_policies: ctx.fail('Touch policy {} not supported by this YubiKey.' .format(policy_name)) if key == KEY_SLOT.ATT and not controller.supports_attestation: ctx.fail('Attestation is not supported by this YubiKey.') if admin_pin is None: admin_pin = click.prompt('Enter admin PIN', hide_input=True, err=True) if force or click.confirm( 'Set touch policy of {} key to {}?'.format( key.value.lower(), policy_name), abort=True, err=True): try: controller.verify_admin(admin_pin) controller.set_touch(key, policy) except APDUError as e: if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED: ctx.fail('Touch policy not allowed.') logger.debug('Failed to set touch policy', exc_info=e) ctx.fail('Failed to set touch policy.') @openpgp.command('set-pin-retries') @click.argument( 'pin-retries', type=click.IntRange(1, 99), metavar='PIN-RETRIES') @click.argument( 'reset-code-retries', type=click.IntRange(1, 99), metavar='RESET-CODE-RETRIES') @click.argument( 'admin-pin-retries', type=click.IntRange(1, 99), metavar='ADMIN-PIN-RETRIES') @click.option('-a', '--admin-pin', help='Admin PIN for OpenPGP.') @click_force_option @click.pass_context def set_pin_retries( ctx, admin_pin, pin_retries, reset_code_retries, admin_pin_retries, force): """ Set PIN, Reset Code and Admin PIN retries. """ controller = ctx.obj['controller'] if admin_pin is None: admin_pin = click.prompt('Enter admin PIN', hide_input=True, err=True) resets_pins = controller.version < (4, 0, 0) if resets_pins: click.echo('WARNING: Setting PIN retries will reset the values for all ' '3 PINs!') if force or click.confirm( 'Set PIN retry counters to: {} {} {}?'.format( pin_retries, reset_code_retries, admin_pin_retries), abort=True, err=True): controller.verify_admin(admin_pin) controller.set_pin_retries( pin_retries, reset_code_retries, admin_pin_retries) if resets_pins: click.echo('Default PINs are set.') echo_default_pins() @openpgp.command() @click.pass_context @click.option('-P', '--pin', help='PIN code.') @click_format_option @click.argument('key', metavar='KEY', type=EnumChoice(KEY_SLOT)) @click.argument('certificate', type=click.File('wb'), metavar='CERTIFICATE') def attest(ctx, key, certificate, pin, format): """ Generate a attestation certificate for a key. Attestation is used to show that an asymmetric key was generated on the YubiKey and therefore doesn't exist outside the device. \b KEY Key slot to attest (sig, enc, aut). CERTIFICATE File to write attestation certificate to. Use '-' to use stdout. """ controller = ctx.obj['controller'] if not pin: pin = click.prompt( 'Enter PIN', default='', hide_input=True, show_default=False, err=True) try: cert = controller.read_certificate(key) except ValueError: cert = None if not cert or click.confirm( 'There is already data stored in the certificate slot for {}, ' 'do you want to overwrite it?'.format(key.value)): touch_policy = controller.get_touch(KEY_SLOT.ATT) if touch_policy in [TOUCH_MODE.ON, TOUCH_MODE.FIXED]: click.echo('Touch your YubiKey...') try: controller.verify_pin(pin) cert = controller.attest(key) certificate.write(cert.public_bytes(encoding=format)) except Exception as e: logger.debug('Failed to attest', exc_info=e) ctx.fail('Attestation failed') @openpgp.command('export-certificate') @click.pass_context @click.argument('key', metavar='KEY', type=EnumChoice(KEY_SLOT)) @click_format_option @click.argument('certificate', type=click.File('wb'), metavar='CERTIFICATE') def export_certificate(ctx, key, format, certificate): """ Export an OpenPGP certificate. \b KEY Key slot to read from (sig, enc, aut, or att). CERTIFICATE File to write certificate to. Use '-' to use stdout. """ controller = ctx.obj['controller'] try: cert = controller.read_certificate(key) except ValueError: ctx.fail('Failed to read certificate from {}'.format(key.name)) certificate.write(cert.public_bytes(encoding=format)) @openpgp.command('delete-certificate') @click.option('-a', '--admin-pin', help='Admin PIN for OpenPGP.') @click.pass_context @click.argument('key', metavar='KEY', type=EnumChoice(KEY_SLOT)) def delete_certificate(ctx, key, admin_pin): """ Delete an OpenPGP certificate. \b KEY Key slot to delete certificate from (sig, enc, aut, or att). """ controller = ctx.obj['controller'] if admin_pin is None: admin_pin = click.prompt('Enter admin PIN', hide_input=True, err=True) try: controller.verify_admin(admin_pin) controller.delete_certificate(key) except Exception as e: logger.debug('Failed to delete ', exc_info=e) ctx.fail('Failed to delete certificate.') @openpgp.command('import-certificate') @click.option('-a', '--admin-pin', help='Admin PIN for OpenPGP.') @click.pass_context @click.argument('key', metavar='KEY', type=EnumChoice(KEY_SLOT)) @click.argument('cert', type=click.File('rb'), metavar='CERTIFICATE') def import_certificate(ctx, key, cert, admin_pin): """ Import an OpenPGP certificate. \b KEY Key slot to import certificate to (sig, enc, aut, or att). CERTIFICATE File containing the certificate. Use '-' to use stdin. """ controller = ctx.obj['controller'] if admin_pin is None: admin_pin = click.prompt('Enter admin PIN', hide_input=True, err=True) try: certs = parse_certificates(cert.read(), password=None) except Exception as e: logger.debug('Failed to parse', exc_info=e) ctx.fail('Failed to parse certificate.') if len(certs) != 1: ctx.fail('Can only import one certificate.') try: controller.verify_admin(admin_pin) controller.import_certificate(key, certs[0]) except Exception as e: logger.debug('Failed to import', exc_info=e) ctx.fail('Failed to import certificate') @openpgp.command('import-attestation-key') @click.option('-a', '--admin-pin', help='Admin PIN for OpenPGP.') @click.pass_context @click.argument('private-key', type=click.File('rb'), metavar='PRIVATE-KEY') def import_attestation_key(ctx, private_key, admin_pin): """ Import a private attestation key. Import a private key for OpenPGP attestation. \b PRIVATE-KEY File containing the private key. Use '-' to use stdin. """ controller = ctx.obj['controller'] if admin_pin is None: admin_pin = click.prompt('Enter admin PIN', hide_input=True, err=True) try: private_key = parse_private_key(private_key.read(), password=None) except Exception as e: logger.debug('Failed to parse', exc_info=e) ctx.fail('Failed to parse private key.') try: controller.verify_admin(admin_pin) controller.import_key(KEY_SLOT.ATT, private_key) except Exception as e: logger.debug('Failed to import', exc_info=e) ctx.fail('Failed to import attestation key.') openpgp.transports = TRANSPORT.CCID yubikey-manager-3.1.1/ykman/cli/otp.py0000644000175100001630000005275713614233340020466 0ustar runnerdocker00000000000000# Copyright (c) 2015 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import from .util import ( click_force_option, click_callback, click_parse_b32_key, click_postpone_execution, prompt_for_touch, EnumChoice) from ..util import ( TRANSPORT, generate_static_pw, modhex_decode, modhex_encode, parse_key, parse_b32_key) from binascii import a2b_hex, b2a_hex from .. import __version__ from ..driver_otp import YkpersError from ..otp import OtpController, PrepareUploadFailed, SlotConfig from ..scancodes import KEYBOARD_LAYOUT import logging import os import struct import click import webbrowser logger = logging.getLogger(__name__) def parse_hex(length): @click_callback() def inner(ctx, param, val): val = a2b_hex(val) if len(val) != length: raise ValueError('Must be exactly {} bytes.'.format(length)) return val return inner def parse_access_code_hex(access_code_hex): try: access_code = a2b_hex(access_code_hex) except TypeError as e: raise ValueError(e) if len(access_code) != 6: raise ValueError('Must be exactly 6 bytes.') return access_code click_slot_argument = click.argument('slot', type=click.Choice(['1', '2']), callback=lambda c, p, v: int(v)) def _failed_to_write_msg(ctx, exc_info): logger.error('Failed to write to device', exc_info=exc_info) ctx.fail('Failed to write to the YubiKey. Make sure the device does not ' 'have restricted access.') def _confirm_slot_overwrite(controller, slot): slot1, slot2 = controller.slot_status if slot == 1 and slot1: click.confirm( 'Slot 1 is already configured. Overwrite configuration?', abort=True, err=True) if slot == 2 and slot2: click.confirm( 'Slot 2 is already configured. Overwrite configuration?', abort=True, err=True) @click.group() @click.pass_context @click_postpone_execution @click.option( '--access-code', required=False, metavar='HEX', help='A 6 byte access code. Set to empty to use a prompt for input.') def otp(ctx, access_code): """ Manage OTP Application. The YubiKey provides two keyboard-based slots which can each be configured with a credential. Several credential types are supported. A slot configuration may be write-protected with an access code. This prevents the configuration to be overwritten without the access code provided. Mode switching the YubiKey is not possible when a slot is configured with an access code. Examples: \b Swap the configurations between the two slots: $ ykman otp swap \b Program a random challenge-response credential to slot 2: $ ykman otp chalresp --generate 2 \b Program a Yubico OTP credential to slot 1, using the serial as public id: $ ykman otp yubiotp 1 --serial-public-id \b Program a random 38 characters long static password to slot 2: $ ykman otp static --generate 2 --length 38 """ ctx.obj['controller'] = OtpController(ctx.obj['dev'].driver) if access_code is not None: if access_code == '': access_code = click.prompt( 'Enter access code', show_default=False, err=True) try: access_code = parse_access_code_hex(access_code) except Exception as e: ctx.fail('Failed to parse access code: ' + str(e)) ctx.obj['controller'].access_code = access_code @otp.command() @click.pass_context def info(ctx): """ Display status of YubiKey Slots. """ dev = ctx.obj['dev'] controller = ctx.obj['controller'] slot1, slot2 = controller.slot_status click.echo('Slot 1: {}'.format(slot1 and 'programmed' or 'empty')) click.echo('Slot 2: {}'.format(slot2 and 'programmed' or 'empty')) if dev.is_fips: click.echo('FIPS Approved Mode: {}'.format( 'Yes' if controller.is_in_fips_mode else 'No')) @otp.command() @click.confirmation_option('-f', '--force', prompt='Swap the two slots of the ' 'YubiKey?') @click.pass_context def swap(ctx): """ Swaps the two slot configurations. """ controller = ctx.obj['controller'] click.echo('Swapping slots...') try: controller.swap_slots() except YkpersError as e: _failed_to_write_msg(ctx, e) @otp.command() @click_slot_argument @click.pass_context @click.option( '-p', '--prefix', help='Added before the NDEF payload. Typically a URI.') def ndef(ctx, slot, prefix): """ Select slot configuration to use for NDEF. The default prefix will be used if no prefix is specified. """ dev = ctx.obj['dev'] controller = ctx.obj['controller'] if not dev.config.nfc_supported: ctx.fail('NFC interface not available.') if not controller.slot_status[slot - 1]: ctx.fail('Slot {} is empty.'.format(slot)) try: if prefix: controller.configure_ndef_slot(slot, prefix) else: controller.configure_ndef_slot(slot) except YkpersError as e: _failed_to_write_msg(ctx, e) @otp.command() @click_slot_argument @click_force_option @click.pass_context def delete(ctx, slot, force): """ Deletes the configuration of a slot. """ controller = ctx.obj['controller'] if not force and not controller.slot_status[slot - 1]: ctx.fail('Not possible to delete an empty slot.') force or click.confirm( 'Do you really want to delete' ' the configuration of slot {}?'.format(slot), abort=True, err=True) click.echo('Deleting the configuration of slot {}...'.format(slot)) try: controller.zap_slot(slot) except YkpersError as e: _failed_to_write_msg(ctx, e) @otp.command() @click_slot_argument @click.option('-P', '--public-id', required=False, help='Public identifier prefix.', metavar='MODHEX') @click.option('-p', '--private-id', required=False, metavar='HEX', callback=parse_hex(6), help='6 byte private identifier.') @click.option('-k', '--key', required=False, metavar='HEX', callback=parse_hex(16), help='16 byte secret key.') @click.option('--no-enter', is_flag=True, help="Don't send an Enter " 'keystroke after emitting the OTP.') @click.option( '-S', '--serial-public-id', is_flag=True, required=False, help='Use YubiKey serial number as public ID. Conflicts with --public-id.') @click.option( '-g', '--generate-private-id', is_flag=True, required=False, help='Generate a random private ID. Conflicts with --private-id.') @click.option( '-G', '--generate-key', is_flag=True, required=False, help='Generate a random secret key. Conflicts with --key.') @click.option( '-u', '--upload', is_flag=True, required=False, help='Upload credential to YubiCloud (opens in browser). ' 'Conflicts with --force.') @click_force_option @click.pass_context def yubiotp(ctx, slot, public_id, private_id, key, no_enter, force, serial_public_id, generate_private_id, generate_key, upload): """ Program a Yubico OTP credential. """ dev = ctx.obj['dev'] controller = ctx.obj['controller'] if public_id and serial_public_id: ctx.fail('Invalid options: --public-id conflicts with ' '--serial-public-id.') if private_id and generate_private_id: ctx.fail('Invalid options: --private-id conflicts with ' '--generate-public-id.') if upload and force: ctx.fail('Invalid options: --upload conflicts with --force.') if key and generate_key: ctx.fail('Invalid options: --key conflicts with --generate-key.') if not public_id: if serial_public_id: if dev.serial is None: ctx.fail('Serial number not set, public ID must be provided') public_id = modhex_encode( b'\xff\x00' + struct.pack(b'>I', dev.serial)) click.echo( 'Using YubiKey serial as public ID: {}'.format(public_id)) elif force: ctx.fail( 'Public ID not given. Please remove the --force flag, or ' 'add the --serial-public-id flag or --public-id option.') else: public_id = click.prompt('Enter public ID', err=True) try: public_id = modhex_decode(public_id) except KeyError: ctx.fail('Invalid public ID, must be modhex.') if not private_id: if generate_private_id: private_id = os.urandom(6) click.echo( 'Using a randomly generated private ID: {}'.format( b2a_hex(private_id).decode('ascii'))) elif force: ctx.fail( 'Private ID not given. Please remove the --force flag, or ' 'add the --generate-private-id flag or --private-id option.') else: private_id = click.prompt('Enter private ID', err=True) private_id = a2b_hex(private_id) if not key: if generate_key: key = os.urandom(16) click.echo( 'Using a randomly generated secret key: {}'.format( b2a_hex(key).decode('ascii'))) elif force: ctx.fail('Secret key not given. Please remove the --force flag, or ' 'add the --generate-key flag or --key option.') else: key = click.prompt('Enter secret key', err=True) key = a2b_hex(key) if not upload and not force: upload = click.confirm('Upload credential to YubiCloud?', abort=False, err=True) if upload: try: upload_url = controller.prepare_upload_key( key, public_id, private_id, serial=dev.serial, user_agent='ykman/' + __version__) click.echo('Upload to YubiCloud initiated successfully.') except PrepareUploadFailed as e: error_msg = '\n'.join(e.messages()) ctx.fail('Upload to YubiCloud failed.\n' + error_msg) force or click.confirm('Program an OTP credential in slot {}?'.format(slot), abort=True, err=True) try: controller.program_otp(slot, key, public_id, private_id, SlotConfig( append_cr=not no_enter )) except YkpersError as e: _failed_to_write_msg(ctx, e) if upload: click.echo('Opening upload form in browser: ' + upload_url) webbrowser.open_new_tab(upload_url) @otp.command() @click_slot_argument @click.argument('password', required=False) @click.option( '-g', '--generate', is_flag=True, help='Generate a random password.') @click.option( '-l', '--length', type=click.IntRange(1, 38), help='Length of generated password.') @click.option( '-k', '--keyboard-layout', type=EnumChoice(KEYBOARD_LAYOUT), default='MODHEX', show_default=True, help='Keyboard layout to use for the static password.') @click.option('--no-enter', is_flag=True, help="Don't send an Enter " 'keystroke after outputting the password.') @click_force_option @click.pass_context def static( ctx, slot, password, generate, length, keyboard_layout, no_enter, force): """ Configure a static password. To avoid problems with different keyboard layouts, the following characters are allowed by default: cbdefghijklnrtuv Use the --keyboard-layout option to allow more characters based on preferred keyboard layout. """ controller = ctx.obj['controller'] if password and len(password) > 38: ctx.fail('Password too long (maximum length is 38 characters).') if generate and not length: ctx.fail('Provide a length for the generated password.') if not password and not generate: password = click.prompt('Enter a static password', err=True) elif not password and generate: password = generate_static_pw(length, keyboard_layout) if not force: _confirm_slot_overwrite(controller, slot) try: controller.program_static(slot, password, keyboard_layout, SlotConfig( append_cr=not no_enter )) except YkpersError as e: _failed_to_write_msg(ctx, e) @otp.command() @click_slot_argument @click.argument('key', required=False) @click.option( '-t', '--touch', is_flag=True, help='Require touch' ' on YubiKey to generate response.') @click.option( '-T', '--totp', is_flag=True, required=False, help='Use a base32 encoded key for TOTP credentials.') @click.option( '-g', '--generate', is_flag=True, required=False, help='Generate a random secret key. Conflicts with KEY argument.') @click_force_option @click.pass_context def chalresp(ctx, slot, key, totp, touch, force, generate): """ Program a challenge-response credential. If KEY is not given, an interactive prompt will ask for it. """ controller = ctx.obj['controller'] if key: if generate: ctx.fail('Invalid options: --generate conflicts with KEY argument.') elif totp: key = parse_b32_key(key) else: key = parse_key(key) else: if force and not generate: ctx.fail('No secret key given. Please remove the --force flag, ' 'set the KEY argument or set the --generate flag.') elif totp: while True: key = click.prompt('Enter a secret key (base32)', err=True) try: key = parse_b32_key(key) break except Exception as e: click.echo(e) else: if generate: key = os.urandom(20) click.echo('Using a randomly generated key: {}'.format( b2a_hex(key).decode('ascii'))) else: key = click.prompt('Enter a secret key', err=True) key = parse_key(key) cred_type = 'TOTP' if totp else 'challenge-response' force or click.confirm('Program a {} credential in slot {}?' .format(cred_type, slot), abort=True, err=True) try: controller.program_chalresp(slot, key, touch) except YkpersError as e: _failed_to_write_msg(ctx, e) @otp.command() @click_slot_argument @click.argument('challenge', required=False) @click.option( '-T', '--totp', is_flag=True, help='Generate a TOTP code, ' 'use the current time as challenge.') @click.option( '-d', '--digits', type=click.Choice(['6', '8']), default='6', help='Number of digits in generated TOTP code (default is 6).') @click.pass_context def calculate(ctx, slot, challenge, totp, digits): """ Perform a challenge-response operation. Send a challenge (in hex) to a YubiKey slot with a challenge-response credential, and read the response. Supports output as a OATH-TOTP code. """ controller = ctx.obj['controller'] if not challenge and not totp: ctx.fail('No challenge provided.') # Check that slot is not empty slot1, slot2 = controller.slot_status if (slot == 1 and not slot1) or (slot == 2 and not slot2): ctx.fail('Cannot perform challenge-response on an empty slot.') # Timestamp challenge should be int if challenge and totp: try: challenge = int(challenge) except Exception as e: logger.error('Error', exc_info=e) ctx.fail('Timestamp challenge for TOTP must be an integer.') try: res = controller.calculate( slot, challenge, totp=totp, digits=int(digits), wait_for_touch=False) except YkpersError as e: # Touch is set if e.errno == 11: prompt_for_touch() try: res = controller.calculate( slot, challenge, totp=totp, digits=int(digits), wait_for_touch=True) except YkpersError as e: # Touch timed out if e.errno == 4: ctx.fail('The YubiKey timed out.') else: ctx.fail(e) else: ctx.fail('Failed to calculate challenge.') click.echo(res) @otp.command() @click_slot_argument @click.argument('key', callback=click_parse_b32_key, required=False) @click.option('-d', '--digits', type=click.Choice(['6', '8']), default='6', help='Number of digits in generated code (default is 6).') @click.option('-c', '--counter', type=int, default=0, help='Initial counter value.') @click.option('--no-enter', is_flag=True, help="Don't send an Enter " 'keystroke after outputting the code.') @click_force_option @click.pass_context def hotp(ctx, slot, key, digits, counter, no_enter, force): """ Program an HMAC-SHA1 OATH-HOTP credential. """ controller = ctx.obj['controller'] if not key: while True: key = click.prompt('Enter a secret key (base32)', err=True) try: key = parse_b32_key(key) break except Exception as e: click.echo(e) force or click.confirm( 'Program a HOTP credential in slot {}?'.format(slot), abort=True, err=True) try: controller.program_hotp( slot, key, counter, int(digits) == 8, SlotConfig( append_cr=not no_enter )) except YkpersError as e: _failed_to_write_msg(ctx, e) @otp.command() @click_slot_argument @click_force_option @click.pass_context @click.option( '-A', '--new-access-code', metavar='HEX', required=False, help='Set a new 6 byte access code for the slot. Set to empty to use a ' 'prompt for input.') @click.option( '--delete-access-code', is_flag=True, help='Remove access code from the slot.') @click.option( '--enter/--no-enter', default=True, show_default=True, help="Should send 'Enter' keystroke after slot output.") @click.option( '-p', '--pacing', type=click.Choice(['0', '20', '40', '60']), default='0', show_default=True, help='Throttle output speed by ' 'adding a delay (in ms) between characters emitted.') @click.option('--use-numeric-keypad', is_flag=True, show_default=True, help='Use scancodes for numeric keypad when sending digits.' ' Helps with some keyboard layouts. ') def settings(ctx, slot, new_access_code, delete_access_code, enter, pacing, use_numeric_keypad, force): """ Update the settings for a slot. Change the settings for a slot without changing the stored secret. All settings not specified will be written with default values. """ controller = ctx.obj['controller'] if (new_access_code is not None) and delete_access_code: ctx.fail('--new-access-code conflicts with --delete-access-code.') if not controller.slot_status[slot - 1]: ctx.fail('Not possible to update settings on an empty slot.') if new_access_code is not None: if new_access_code == '': new_access_code = click.prompt( 'Enter new access code', show_default=False, err=True) try: new_access_code = parse_access_code_hex(new_access_code) except Exception as e: ctx.fail('Failed to parse access code: ' + str(e)) force or click.confirm( 'Update the settings for slot {}? ' 'All existing settings will be overwritten.'.format(slot), abort=True, err=True) click.echo('Updating settings for slot {}...'.format(slot)) if pacing is not None: pacing = int(pacing) try: controller.update_settings(slot, SlotConfig( append_cr=enter, pacing=pacing, numeric_keypad=use_numeric_keypad )) except YkpersError as e: _failed_to_write_msg(ctx, e) if new_access_code: try: controller.set_access_code(slot, new_access_code) except Exception as e: logger.error('Failed to set access code', exc_info=e) ctx.fail('Failed to set access code: ' + str(e)) if delete_access_code: try: controller.delete_access_code(slot) except Exception as e: logger.error('Failed to delete access code', exc_info=e) ctx.fail('Failed to delete access code: ' + str(e)) otp.transports = TRANSPORT.OTP yubikey-manager-3.1.1/ykman/cli/piv.py0000644000175100001630000010140613614233340020444 0ustar runnerdocker00000000000000# Copyright (c) 2017 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import from ..util import ( TRANSPORT, get_leaf_certificates, parse_private_key, parse_certificates) from ..piv import ( PivController, ALGO, OBJ, SLOT, PIN_POLICY, TOUCH_POLICY, DEFAULT_MANAGEMENT_KEY, generate_random_management_key) from ..piv import ( AuthenticationBlocked, AuthenticationFailed, KeypairMismatch, UnsupportedAlgorithm, WrongPin, WrongPuk) from ..driver_ccid import APDUError, SW from .util import ( click_force_option, click_format_option, click_postpone_execution, click_callback, prompt_for_touch, EnumChoice) from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.backends import default_backend from binascii import b2a_hex, a2b_hex import click import datetime import logging logger = logging.getLogger(__name__) @click_callback() def click_parse_piv_slot(ctx, param, val): try: return SLOT(int(val, 16)) except Exception: raise ValueError(val) @click_callback() def click_parse_management_key(ctx, param, val): try: key = a2b_hex(val) if key and len(key) != 24: raise ValueError('Management key must be exactly 24 bytes ' '(48 hexadecimal digits) long.') return key except Exception: raise ValueError(val) click_slot_argument = click.argument('slot', callback=click_parse_piv_slot) click_management_key_option = click.option( '-m', '--management-key', help='The management key.', callback=click_parse_management_key) click_pin_option = click.option( '-P', '--pin', help='PIN code.') click_pin_policy_option = click.option( '--pin-policy', type=EnumChoice(PIN_POLICY), help='PIN policy for slot.') click_touch_policy_option = click.option( '--touch-policy', type=EnumChoice(TOUCH_POLICY), help='Touch policy for slot.') @click.group() @click.pass_context @click_postpone_execution def piv(ctx): """ Manage PIV Application. Examples: \b Generate an ECC P-256 private key and a self-signed certificate in slot 9a: $ ykman piv generate-key --algorithm ECCP256 9a pubkey.pem $ ykman piv generate-certificate --subject "yubico" 9a pubkey.pem \b Change the PIN from 123456 to 654321: $ ykman piv change-pin --pin 123456 --new-pin 654321 \b Reset all PIV data and restore default settings: $ ykman piv reset """ try: ctx.obj['controller'] = PivController(ctx.obj['dev'].driver) except APDUError as e: if e.sw == SW.NOT_FOUND: ctx.fail("The PIV application can't be found on this YubiKey.") raise @piv.command() @click.pass_context def info(ctx): """ Display status of PIV application. """ controller = ctx.obj['controller'] click.echo('PIV version: %d.%d.%d' % controller.version) # Largest possible number of PIN tries to get back is 15 tries = controller.get_pin_tries() tries = '15 or more.' if tries == 15 else tries click.echo('PIN tries remaining: %s' % tries) if controller.puk_blocked: click.echo('PUK blocked.') if controller.has_derived_key: click.echo('Management key is derived from PIN.') if controller.has_stored_key: click.echo('Management key is stored on the YubiKey, protected by PIN.') try: chuid = b2a_hex(controller.get_data(OBJ.CHUID)).decode() except APDUError as e: if e.sw == SW.NOT_FOUND: chuid = 'No data available.' click.echo('CHUID:\t' + chuid) try: ccc = b2a_hex(controller.get_data(OBJ.CAPABILITY)).decode() except APDUError as e: if e.sw == SW.NOT_FOUND: ccc = 'No data available.' click.echo('CCC: \t' + ccc) for (slot, cert) in controller.list_certificates().items(): click.echo('Slot %02x:' % slot) if isinstance(cert, x509.Certificate): try: # Try to read out full DN, fallback to only CN. # Support for DN was added in crytography 2.5 subject_dn = cert.subject.rfc4514_string() issuer_dn = cert.issuer.rfc4514_string() print_dn = True except AttributeError: print_dn = False logger.debug('Failed to read DN, falling back to only CNs') subject_cn = cert.subject.get_attributes_for_oid( x509.NameOID.COMMON_NAME) subject_cn = subject_cn[0].value if subject_cn else 'None' issuer_cn = cert.issuer.get_attributes_for_oid( x509.NameOID.COMMON_NAME) issuer_cn = issuer_cn[0].value if issuer_cn else 'None' except ValueError as e: # Malformed certificates may throw ValueError logger.debug('Failed parsing certificate', exc_info=e) click.echo('\tMalformed certificate: {}'.format(e)) continue fingerprint = b2a_hex( cert.fingerprint(hashes.SHA256())).decode('ascii') algo = ALGO.from_public_key(cert.public_key()) serial = cert.serial_number try: not_before = cert.not_valid_before except ValueError as e: logger.debug('Failed reading not_valid_before', exc_info=e) not_before = None try: not_after = cert.not_valid_after except ValueError as e: logger.debug('Failed reading not_valid_after', exc_info=e) not_after = None # Print out everything click.echo('\tAlgorithm:\t%s' % algo.name) if print_dn: click.echo('\tSubject DN:\t%s' % subject_dn) click.echo('\tIssuer DN:\t%s' % issuer_dn) else: click.echo('\tSubject CN:\t%s' % subject_cn) click.echo('\tIssuer CN:\t%s' % issuer_cn) click.echo('\tSerial:\t\t%s' % serial) click.echo('\tFingerprint:\t%s' % fingerprint) if not_before: click.echo('\tNot before:\t%s' % not_before) if not_after: click.echo('\tNot after:\t%s' % not_after) else: click.echo('\tError: Failed to parse certificate.') @piv.command() @click.pass_context @click.confirmation_option( '-f', '--force', prompt='WARNING! This will delete ' 'all stored PIV data and restore factory settings. Proceed?') def reset(ctx): """ Reset all PIV data. This action will wipe all data and restore factory settings for the PIV application on your YubiKey. """ click.echo('Resetting PIV data...') ctx.obj['controller'].reset() click.echo( 'Success! All PIV data have been cleared from your YubiKey.') click.echo('Your YubiKey now has the default PIN, PUK and Management Key:') click.echo('\tPIN:\t123456') click.echo('\tPUK:\t12345678') click.echo( '\tManagement Key:\t010203040506070801020304050607080102030405060708') @piv.command('generate-key') @click.pass_context @click_slot_argument @click_management_key_option @click_pin_option @click.option( '-a', '--algorithm', help='Algorithm to use in key generation.', type=EnumChoice(ALGO), default=ALGO.RSA2048.name, show_default=True) @click_format_option @click_pin_policy_option @click_touch_policy_option @click.argument( 'public-key-output', type=click.File('wb'), metavar='PUBLIC-KEY') def generate_key( ctx, slot, public_key_output, management_key, pin, algorithm, format, pin_policy, touch_policy): """ Generate an asymmetric key pair. The private key is generated on the YubiKey, and written to one of the slots. \b SLOT PIV slot where private key should be stored. PUBLIC-KEY File containing the generated public key. Use '-' to use stdout. """ dev = ctx.obj['dev'] controller = ctx.obj['controller'] _ensure_authenticated(ctx, controller, pin, management_key) _check_pin_policy(ctx, dev, controller, pin_policy) _check_touch_policy(ctx, controller, touch_policy) try: public_key = controller.generate_key( slot, algorithm, pin_policy, touch_policy) except UnsupportedAlgorithm: ctx.fail('Algorithm {} is not supported by this ' 'YubiKey.'.format(algorithm.name)) key_encoding = format public_key_output.write(public_key.public_bytes( encoding=key_encoding, format=serialization.PublicFormat.SubjectPublicKeyInfo)) @piv.command('import-certificate') @click.pass_context @click_slot_argument @click_management_key_option @click_pin_option @click.option( '-p', '--password', help='A password may be needed to decrypt the data.') @click.option( '-v', '--verify', is_flag=True, help='Verify that the certificate matches the private key in the slot.') @click.argument('cert', type=click.File('rb'), metavar='CERTIFICATE') def import_certificate( ctx, slot, management_key, pin, cert, password, verify): """ Import a X.509 certificate. Write a certificate to one of the slots on the YubiKey. \b SLOT PIV slot to import the certificate to. CERTIFICATE File containing the certificate. Use '-' to use stdin. """ controller = ctx.obj['controller'] _ensure_authenticated(ctx, controller, pin, management_key) data = cert.read() while True: if password is not None: password = password.encode() try: certs = parse_certificates(data, password) except (ValueError, TypeError): if password is None: password = click.prompt( 'Enter password to decrypt certificate', default='', hide_input=True, show_default=False, err=True) continue else: password = None click.echo('Wrong password.') continue break if len(certs) > 1: # If multiple certs, only import leaf. # Leaf is the cert with a subject that is not an issuer in the chain. leafs = get_leaf_certificates(certs) cert_to_import = leafs[0] else: cert_to_import = certs[0] def do_import(retry=True): try: controller.import_certificate( slot, cert_to_import, verify=verify, touch_callback=prompt_for_touch) except KeypairMismatch: ctx.fail('This certificate is not tied to the private key in the ' '{} slot.'.format(slot.name)) except APDUError as e: if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED and retry: _verify_pin(ctx, controller, pin) do_import(retry=False) else: raise do_import() @piv.command('import-key') @click.pass_context @click_slot_argument @click_pin_option @click_management_key_option @click_pin_policy_option @click_touch_policy_option @click.argument('private-key', type=click.File('rb'), metavar='PRIVATE-KEY') @click.option( '-p', '--password', help='Password used to decrypt the private key.') def import_key( ctx, slot, management_key, pin, private_key, pin_policy, touch_policy, password): """ Import a private key. Write a private key to one of the slots on the YubiKey. \b SLOT PIV slot to import the private key to. PRIVATE-KEY File containing the private key. Use '-' to use stdin. """ dev = ctx.obj['dev'] controller = ctx.obj['controller'] _ensure_authenticated(ctx, controller, pin, management_key) data = private_key.read() while True: if password is not None: password = password.encode() try: private_key = parse_private_key(data, password) except (ValueError, TypeError): if password is None: password = click.prompt( 'Enter password to decrypt key', default='', hide_input=True, show_default=False, err=True) continue else: password = None click.echo('Wrong password.') continue break _check_pin_policy(ctx, dev, controller, pin_policy) _check_touch_policy(ctx, controller, touch_policy) _check_key_size(ctx, controller, private_key) controller.import_key( slot, private_key, pin_policy, touch_policy) @piv.command() @click.pass_context @click_slot_argument @click_format_option @click.argument('certificate', type=click.File('wb'), metavar='CERTIFICATE') def attest(ctx, slot, certificate, format): """ Generate a attestation certificate for a key. Attestation is used to show that an asymmetric key was generated on the YubiKey and therefore doesn't exist outside the device. \b SLOT PIV slot with a private key to attest. CERTIFICATE File to write attestation certificate to. Use '-' to use stdout. """ controller = ctx.obj['controller'] try: cert = controller.attest(slot) except APDUError as e: logger.error('Attestation failed', exc_info=e) ctx.fail('Attestation failed.') certificate.write(cert.public_bytes(encoding=format)) @piv.command('export-certificate') @click.pass_context @click_slot_argument @click_format_option @click.argument('certificate', type=click.File('wb'), metavar='CERTIFICATE') def export_certificate(ctx, slot, format, certificate): """ Export a X.509 certificate. Reads a certificate from one of the slots on the YubiKey. \b SLOT PIV slot to read certificate from. CERTIFICATE File to write certificate to. Use '-' to use stdout. """ controller = ctx.obj['controller'] try: cert = controller.read_certificate(slot) except APDUError as e: if e.sw == SW.NOT_FOUND: ctx.fail('No certificate found.') else: logger.error('Failed to read certificate from slot %s', slot, exc_info=e) certificate.write(cert.public_bytes(encoding=format)) @piv.command('set-chuid') @click.pass_context @click_pin_option @click_management_key_option def set_chuid(ctx, management_key, pin): """ Generate and set a CHUID on the YubiKey. """ controller = ctx.obj['controller'] _ensure_authenticated(ctx, controller, pin, management_key) controller.update_chuid() @piv.command('set-ccc') @click.pass_context @click_pin_option @click_management_key_option def set_ccc(ctx, management_key, pin): """ Generate and set a CCC on the YubiKey. """ controller = ctx.obj['controller'] _ensure_authenticated(ctx, controller, pin, management_key) controller.update_ccc() @piv.command('set-pin-retries') @click.pass_context @click.argument( 'pin-retries', type=click.IntRange(1, 255), metavar='PIN-RETRIES') @click.argument( 'puk-retries', type=click.IntRange(1, 255), metavar='PUK-RETRIES') @click_management_key_option @click_pin_option @click_force_option def set_pin_retries(ctx, management_key, pin, pin_retries, puk_retries, force): """ Set the number of PIN and PUK retries. NOTE: This will reset the PIN and PUK to their factory defaults. """ controller = ctx.obj['controller'] _ensure_authenticated( ctx, controller, pin, management_key, require_pin_and_key=True, no_prompt=force) click.echo('WARNING: This will reset the PIN and PUK to the factory ' 'defaults!') force or click.confirm('Set PIN and PUK retry counters to: {} {}?'.format( pin_retries, puk_retries), abort=True, err=True) try: controller.set_pin_retries(pin_retries, puk_retries) click.echo('Default PINs are set.') click.echo('PIN: 123456') click.echo('PUK: 12345678') except Exception as e: logger.error('Failed to set PIN retries', exc_info=e) ctx.fail('Setting pin retries failed.') @piv.command('generate-certificate') @click.pass_context @click_slot_argument @click_management_key_option @click_pin_option @click.argument('public-key', type=click.File('rb'), metavar='PUBLIC-KEY') @click.option( '-s', '--subject', help='Subject common name (CN) for the certificate.', required=True) @click.option( '-d', '--valid-days', help='Number of days until the certificate expires.', type=click.INT, default=365, show_default=True) def generate_certificate( ctx, slot, management_key, pin, public_key, subject, valid_days): """ Generate a self-signed X.509 certificate. A self-signed certificate is generated and written to one of the slots on the YubiKey. A private key need to exist in the slot. \b SLOT PIV slot where private key is stored. PUBLIC-KEY File containing a public key. Use '-' to use stdin. """ controller = ctx.obj['controller'] _ensure_authenticated( ctx, controller, pin, management_key, require_pin_and_key=True) data = public_key.read() public_key = serialization.load_pem_public_key( data, default_backend()) now = datetime.datetime.utcnow() valid_to = now + datetime.timedelta(days=valid_days) try: controller.generate_self_signed_certificate( slot, public_key, subject, now, valid_to, touch_callback=prompt_for_touch) except APDUError as e: logger.error('Failed to generate certificate for slot %s', slot, exc_info=e) ctx.fail('Certificate generation failed.') @piv.command('generate-csr') @click.pass_context @click_slot_argument @click_pin_option @click.argument('public-key', type=click.File('rb'), metavar='PUBLIC-KEY') @click.argument('csr-output', type=click.File('wb'), metavar='CSR') @click.option( '-s', '--subject', help='Subject common name (CN) for the requested certificate.', required=True) def generate_certificate_signing_request( ctx, slot, pin, public_key, csr_output, subject): """ Generate a Certificate Signing Request (CSR). A private key need to exist in the slot. \b SLOT PIV slot where the private key is stored. PUBLIC-KEY File containing a public key. Use '-' to use stdin. CSR File to write CSR to. Use '-' to use stdout. """ controller = ctx.obj['controller'] _verify_pin(ctx, controller, pin) data = public_key.read() public_key = serialization.load_pem_public_key( data, default_backend()) try: csr = controller.generate_certificate_signing_request( slot, public_key, subject, touch_callback=prompt_for_touch) except APDUError: ctx.fail('Certificate Signing Request generation failed.') csr_output.write(csr.public_bytes(encoding=serialization.Encoding.PEM)) @piv.command('delete-certificate') @click.pass_context @click_slot_argument @click_management_key_option @click_pin_option def delete_certificate(ctx, slot, management_key, pin): """ Delete a certificate. Delete a certificate from a slot on the YubiKey. """ controller = ctx.obj['controller'] _ensure_authenticated(ctx, controller, pin, management_key) controller.delete_certificate(slot) @piv.command('change-pin') @click.pass_context @click.option( '-P', '--pin', help='Current PIN code.') @click.option('-n', '--new-pin', help='A new PIN.') def change_pin(ctx, pin, new_pin): """ Change the PIN code. The PIN must be between 6 and 8 characters long, and supports any type of alphanumeric characters. For cross-platform compatibility, numeric digits are recommended. """ controller = ctx.obj['controller'] if not pin: pin = _prompt_pin(ctx, prompt='Enter your current PIN') if not new_pin: new_pin = click.prompt( 'Enter your new PIN', default='', hide_input=True, show_default=False, confirmation_prompt=True, err=True) if not _valid_pin_length(pin): ctx.fail('Current PIN must be between 6 and 8 characters long.') if not _valid_pin_length(new_pin): ctx.fail('New PIN must be between 6 and 8 characters long.') try: controller.change_pin(pin, new_pin) click.echo('New PIN set.') except AuthenticationBlocked as e: logger.debug('PIN is blocked.', exc_info=e) ctx.fail('PIN is blocked.') except WrongPin as e: logger.debug( 'Failed to change PIN, %d tries left', e.tries_left, exc_info=e) ctx.fail('PIN change failed - %d tries left.' % e.tries_left) @piv.command('change-puk') @click.pass_context @click.option('-p', '--puk', help='Current PUK code.') @click.option('-n', '--new-puk', help='A new PUK code.') def change_puk(ctx, puk, new_puk): """ Change the PUK code. If the PIN is lost or blocked it can be reset using a PUK. The PUK must be between 6 and 8 characters long, and supports any type of alphanumeric characters. """ controller = ctx.obj['controller'] if not puk: puk = _prompt_pin(ctx, prompt='Enter your current PUK') if not new_puk: new_puk = click.prompt( 'Enter your new PUK', default='', hide_input=True, show_default=False, confirmation_prompt=True, err=True) if not _valid_pin_length(puk): ctx.fail('Current PUK must be between 6 and 8 characters long.') if not _valid_pin_length(new_puk): ctx.fail('New PUK must be between 6 and 8 characters long.') try: controller.change_puk(puk, new_puk) click.echo('New PUK set.') except AuthenticationBlocked as e: logger.debug('PUK is blocked.', exc_info=e) ctx.fail('PUK is blocked.') except WrongPuk as e: logger.debug( 'Failed to change PUK, %d tries left', e.tries_left, exc_info=e) ctx.fail('PUK change failed - %d tries left.' % e.tries_left) @piv.command('change-management-key') @click.pass_context @click_pin_option @click.option( '-t', '--touch', is_flag=True, help='Require touch on YubiKey when prompted for management key.') @click.option( '-n', '--new-management-key', help='A new management key.', callback=click_parse_management_key) @click.option( '-m', '--management-key', help='Current management key.', callback=click_parse_management_key) @click.option( '-p', '--protect', is_flag=True, help='Store new management key on your YubiKey, protected by PIN.' ' A random key will be used if no key is provided.') @click.option( '-g', '--generate', is_flag=True, help='Generate a random management key. ' 'Implied by --protect unless --new-management-key is also given. ' 'Conflicts with --new-management-key.') @click_force_option def change_management_key( ctx, management_key, pin, new_management_key, touch, protect, generate, force): """ Change the management key. Management functionality is guarded by a 24 byte management key. This key is required for administrative tasks, such as generating key pairs. A random key may be generated and stored on the YubiKey, protected by PIN. """ controller = ctx.obj['controller'] pin_verified = _ensure_authenticated( ctx, controller, pin, management_key, require_pin_and_key=protect, mgm_key_prompt='Enter your current management key ' '[blank to use default key]', no_prompt=force) if new_management_key and generate: ctx.fail('Invalid options: --new-management-key conflicts with ' '--generate') # Touch not supported on NEO. if touch and controller.version < (4, 0, 0): ctx.fail('Require touch not supported on this YubiKey.') # If an old stored key needs to be cleared, the PIN is needed. if not pin_verified and controller.has_stored_key: if pin: _verify_pin(ctx, controller, pin, no_prompt=force) elif not force: click.confirm( 'The current management key is stored on the YubiKey' ' and will not be cleared if no PIN is provided. Continue?', abort=True, err=True) if not new_management_key and not protect: if generate: new_management_key = generate_random_management_key() if not protect: click.echo( 'Generated management key: {}'.format( b2a_hex(new_management_key).decode('utf-8'))) elif force: ctx.fail('New management key not given. Please remove the --force ' 'flag, or set the --generate flag or the ' '--new-management-key option.') else: new_management_key = click.prompt( 'Enter your new management key', hide_input=True, confirmation_prompt=True, err=True) if new_management_key and type(new_management_key) is not bytes: try: new_management_key = a2b_hex(new_management_key) except Exception: ctx.fail('New management key has the wrong format.') try: controller.set_mgm_key( new_management_key, touch=touch, store_on_device=protect) except APDUError as e: logger.error('Failed to change management key', exc_info=e) ctx.fail('Changing the management key failed.') @piv.command('unblock-pin') @click.pass_context @click.option('-p', '--puk', required=False) @click.option('-n', '--new-pin', required=False, metavar='NEW-PIN') def unblock_pin(ctx, puk, new_pin): """ Unblock the PIN. Reset the PIN using the PUK code. """ controller = ctx.obj['controller'] if not puk: puk = click.prompt( 'Enter PUK', default='', show_default=False, hide_input=True, err=True) if not new_pin: new_pin = click.prompt( 'Enter a new PIN', default='', show_default=False, hide_input=True, err=True) controller.unblock_pin(puk, new_pin) @piv.command('read-object') @click_pin_option @click.pass_context @click.argument( 'object-id', callback=lambda ctx, param, value: int(value, 16), metavar='OBJECT-ID') def read_object(ctx, pin, object_id): """ Read arbitrary PIV object. Read PIV object by providing the object id. \b OBJECT-ID Id of PIV object in HEX. """ controller = ctx.obj['controller'] def do_read_object(retry=True): try: click.echo(controller.get_data(object_id), nl=False) except APDUError as e: if e.sw == SW.NOT_FOUND: ctx.fail('No data found.') elif e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED: _verify_pin(ctx, controller, pin) do_read_object(retry=False) else: raise do_read_object() @piv.command('write-object') @click_pin_option @click_management_key_option @click.pass_context @click.argument( 'object-id', callback=lambda ctx, param, value: int(value, 16), metavar='OBJECT-ID') @click.argument('data', type=click.File('rb'), metavar='DATA') def write_object(ctx, pin, management_key, object_id, data): """ Write an arbitrary PIV object. Write a PIV object by providing the object id. Yubico writable PIV objects are available in the range 5f0000 - 5fffff. \b OBJECT-ID Id of PIV object in HEX. DATA File containing the data to be written. Use '-' to use stdin. """ controller = ctx.obj['controller'] _ensure_authenticated(ctx, controller, pin, management_key) def do_write_object(retry=True): try: controller.put_data(object_id, data.read()) except APDUError as e: logger.debug('Failed writing object', exc_info=e) if e.sw == SW.INCORRECT_PARAMETERS: ctx.fail('Something went wrong, is the object id valid?') raise do_write_object() def _prompt_management_key( ctx, prompt='Enter a management key [blank to use default key]'): management_key = click.prompt( prompt, default='', hide_input=True, show_default=False, err=True) if management_key == '': return DEFAULT_MANAGEMENT_KEY try: return a2b_hex(management_key) except Exception: ctx.fail('Management key has the wrong format.') def _prompt_pin(ctx, prompt='Enter PIN'): return click.prompt( prompt, default='', hide_input=True, show_default=False, err=True) def _valid_pin_length(pin): return 6 <= len(pin) <= 8 def _ensure_authenticated( ctx, controller, pin=None, management_key=None, require_pin_and_key=False, mgm_key_prompt=None, no_prompt=False): pin_verified = False if controller.has_protected_key: if not management_key: pin_verified = _verify_pin( ctx, controller, pin, no_prompt=no_prompt) else: _authenticate(ctx, controller, management_key, mgm_key_prompt, no_prompt=no_prompt) else: if require_pin_and_key: pin_verified = _verify_pin( ctx, controller, pin, no_prompt=no_prompt) _authenticate(ctx, controller, management_key, mgm_key_prompt, no_prompt=no_prompt) return pin_verified def _verify_pin(ctx, controller, pin, no_prompt=False): if not pin: if no_prompt: ctx.fail('PIN required.') else: pin = _prompt_pin(ctx) try: controller.verify(pin, touch_callback=prompt_for_touch) return True except WrongPin as e: ctx.fail('PIN verification failed, {} tries left.'.format(e.tries_left)) except AuthenticationBlocked: ctx.fail('PIN is blocked.') except Exception: ctx.fail('PIN verification failed.') def _authenticate(ctx, controller, management_key, mgm_key_prompt, no_prompt=False): if not management_key: if no_prompt: ctx.fail('Management key required.') else: if mgm_key_prompt is None: management_key = _prompt_management_key(ctx) else: management_key = _prompt_management_key(ctx, mgm_key_prompt) try: controller.authenticate(management_key, touch_callback=prompt_for_touch) except AuthenticationFailed: ctx.fail('Incorrect management key.') except Exception as e: logger.error('Authentication with management key failed.', exc_info=e) ctx.fail('Authentication with management key failed.') def _check_key_size(ctx, controller, private_key): if (private_key.key_size == 1024 and ALGO.RSA1024 not in controller.supported_algorithms): ctx.fail('1024 is not a supported key size on this YubiKey.') def _check_pin_policy(ctx, dev, controller, pin_policy): if pin_policy is not None and not controller.supports_pin_policies: ctx.fail('PIN policy is not supported by this YubiKey.') if dev.is_fips and pin_policy == PIN_POLICY.NEVER: ctx.fail('PIN policy NEVER is not supported by this YubiKey.') def _check_touch_policy(ctx, controller, touch_policy): if touch_policy is not None: if len(controller.supported_touch_policies) == 0: ctx.fail('Touch policy is not supported by this YubiKey.') elif touch_policy not in controller.supported_touch_policies: ctx.fail('Touch policy {} not supported by this YubiKey.'.format( touch_policy.name)) piv.transports = TRANSPORT.CCID yubikey-manager-3.1.1/ykman/cli/util.py0000644000175100001630000001217013614233340020622 0ustar runnerdocker00000000000000# Copyright (c) 2015 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import import functools import click import sys from ..util import parse_b32_key from collections import OrderedDict, MutableMapping from cryptography.hazmat.primitives import serialization class UpperCaseChoice(click.Choice): """ Support lowercase option values for uppercase options. Does not support token normalization. Choice options MUST be all uppercase. """ def __init__(self, choices): for v in choices: assert v.upper() == v, 'Choice not all uppercase: ' + v click.Choice.__init__(self, choices) def convert(self, value, param, ctx): if value.upper() in self.choices: return value.upper() self.fail( 'invalid choice: %s. (choose from %s)' % ( value, ', '.join(self.choices)), param, ctx) class EnumChoice(UpperCaseChoice): """ Use an enum's member names as the definition for a choice option. Enum member names MUST be all uppercase. Options are not case sensitive. Underscores in enum names are translated to dashes in the option choice. """ def __init__(self, choices_enum): super(EnumChoice, self).__init__( [v.name.replace('_', '-') for v in choices_enum]) self.choices_enum = choices_enum def convert(self, value, param, ctx): name = super(EnumChoice, self).convert( value, param, ctx).replace('-', '_') return self.choices_enum[name] def click_callback(invoke_on_missing=False): def wrap(f): @functools.wraps(f) def inner(ctx, param, val): if not invoke_on_missing and not param.required and val is None: return None try: return f(ctx, param, val) except Exception as e: ctx.fail('Invalid value for "{}": {}'.format( param.name, str(e))) return inner return wrap @click_callback() def click_parse_format(ctx, param, val): if val == 'PEM': return serialization.Encoding.PEM elif val == 'DER': return serialization.Encoding.DER else: raise ValueError(val) click_force_option = click.option( '-f', '--force', is_flag=True, help='Confirm the action without prompting.') click_format_option = click.option( '-F', '--format', type=UpperCaseChoice(['PEM', 'DER']), default='PEM', show_default=True, help='Encoding format.', callback=click_parse_format) class YkmanContextObject(MutableMapping): def __init__(self): self._objects = OrderedDict() self._resolved = False def add_resolver(self, key, f): if self._resolved: f = f() self._objects[key] = f def resolve(self): if not self._resolved: self._resolved = True for k, f in self._objects.copy().items(): self._objects[k] = f() def __getitem__(self, key): self.resolve() return self._objects[key] def __setitem__(self, key, value): if not self._resolved: raise ValueError('BUG: Attempted to set item when unresolved.') self._objects[key] = value def __delitem__(self, key): del self._objects[key] def __len__(self): return len(self._objects) def __iter__(self): return iter(self._objects) def click_postpone_execution(f): @functools.wraps(f) def inner(*args, **kwargs): click.get_current_context().obj.add_resolver( str(f), lambda: f(*args, **kwargs) ) return inner @click_callback() def click_parse_b32_key(ctx, param, val): return parse_b32_key(val) def prompt_for_touch(): try: click.echo('Touch your YubiKey...', err=True) except Exception: sys.stderr.write('Touch your YubiKey...\n') yubikey-manager-3.1.1/ykman/descriptor.py0000644000175100001630000001776513614233340021273 0ustar runnerdocker00000000000000# Copyright (c) 2015 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import from .util import PID, TRANSPORT, Mode, YUBIKEY from .device import YubiKey from .driver_ccid import open_devices as open_ccid from .driver_fido import open_devices as open_fido from .driver_otp import open_devices as open_otp from .native.pyusb import get_usb_backend import logging import usb.core import time logger = logging.getLogger(__name__) class FailedOpeningDeviceException(Exception): pass class Descriptor(object): def __init__( self, key_type, mode, version, fingerprint, serial=None, backend=None): self._logger = logger.getChild('Descriptor') self._version = version self._key_type = key_type self._mode = mode self._fingerprint = fingerprint self._backend = backend @property def fingerprint(self): return self._fingerprint @property def version(self): return self._version @property def mode(self): return self._mode @property def key_type(self): return self._key_type @property def name(self): if self.key_type == YUBIKEY.SKY and self.version < (5, 0, 0): return 'FIDO U2F Security Key' elif self.key_type == YUBIKEY.YK4 and self.version >= (5, 0, 0): return 'YubiKey 5' else: return self.key_type.value def open_device(self, transports=sum(TRANSPORT), serial=None, attempts=10): self._logger.debug('transports: 0x%x, self.mode.transports: 0x%x', transports, self.mode.transports) transports &= self.mode.transports logger.debug('Opening driver for serial: %s, type: %s, mode: %s', serial, self.key_type, self.mode) for attempt in range(1, attempts + 1): logger.debug('Attempt %d of %d', attempt, attempts) sleep_time = attempt * 0.1 for drv in _list_drivers(transports): logger.debug('Found driver: %s, key_type: %s, mode: %s', drv, drv.key_type, drv.mode) dev = YubiKey(self, drv) if serial is not None and dev.serial != serial: logger.debug('Serial does not match. Want: %s, got: %s', serial, dev.serial) del dev continue if (drv.key_type, drv.mode) != (self.key_type, self.mode): logger.debug('Descriptor mismatch. Want: %s, got: %s', (self.key_type, self.mode), (drv.key_type, drv.mode)) del dev continue return dev # Wait a little before trying again. logger.debug('Sleeping for %f s', sleep_time) time.sleep(sleep_time) logger.debug('No matching device found') raise FailedOpeningDeviceException() @classmethod def from_usb(cls, usb_dev, backend): v_int = usb_dev.bcdDevice version = ((v_int >> 8) % 16, (v_int >> 4) % 16, v_int % 16) try: pid = PID(usb_dev.idProduct) except ValueError: logger.debug('Ignoring unknown PID: {:x}'.format(usb_dev.idProduct)) return None fp = (pid, version, usb_dev.bus, usb_dev.address, usb_dev.iSerialNumber) return cls( pid.get_type(), Mode.from_pid(pid), version, fp, backend=backend) @classmethod def from_driver(cls, driver): fp = (driver.key_type, driver.mode) return cls(driver.key_type, driver.mode, None, fp) def _gen_descriptors(): found = [] # Composite devices are listed multiple times on Windows... backend = get_usb_backend() for dev in usb.core.find(True, idVendor=0x1050, backend=backend): addr = (dev.bus, dev.address) if addr not in found: found.append(addr) desc = Descriptor.from_usb(dev, backend) if desc: yield desc def get_descriptors(): return list(_gen_descriptors()) def _list_drivers(transports): if TRANSPORT.CCID & transports: for dev in open_ccid(): if dev: yield dev if TRANSPORT.OTP & transports: for dev in open_otp(): if dev: yield dev if TRANSPORT.FIDO & transports: for dev in open_fido(): if dev: yield dev def list_devices(transports=sum(TRANSPORT)): for d in _list_drivers(transports): yield YubiKey(Descriptor.from_driver(d), d) def _open_driver(transports, serial, key_type, mode, attempts): logger.debug('Opening driver for transports: %s, serial: %s, key_type: %s, ' 'mode: %s', transports, serial, key_type, mode) for attempt in range(1, attempts + 1): logger.debug('Attempt %d of %d', attempt, attempts) sleep_time = attempt * 0.1 for dev in list_devices(transports): logger.debug('Found driver: %s serial: %s, key_type: %s, mode: %s', dev.driver, dev.serial, dev.driver.key_type, dev.driver.mode) if serial is not None and dev.serial != serial: logger.debug('Serial does not match. Want: %s, got: %s', serial, dev.serial) del dev continue if key_type is not None and dev.driver.key_type != key_type: logger.debug('Key type does not match. Want: %s, got: %s', key_type, dev.driver.key_type) del dev continue if mode is not None and dev.driver.mode != mode: logger.debug('Mode does not match. Want: %s, got: %s', mode, dev.driver.mode) del dev continue return dev.driver # Wait a little before trying again. logger.debug('Sleeping for %f s', sleep_time) time.sleep(sleep_time) logger.debug('No driver found for serial: %s, key_type: %s, mode: %s', serial, key_type, mode) raise FailedOpeningDeviceException() def open_device(transports=sum(TRANSPORT), serial=None, key_type=None, mode=None, attempts=10): driver = _open_driver(transports, serial, key_type, mode, attempts) matches = [d for d in get_descriptors() if (d.key_type, d.mode) == (driver.key_type, driver.mode)] if len(matches) == 1: # Only one matching descriptor, must be it descriptor = matches[0] else: descriptor = Descriptor.from_driver(driver) return YubiKey(descriptor, driver) yubikey-manager-3.1.1/ykman/device.py0000644000175100001630000003643213614233340020344 0ustar runnerdocker00000000000000# Copyright (c) 2015 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import from .util import (APPLICATION, TRANSPORT, YUBIKEY, FORM_FACTOR, Tlv, bytes2int, int2bytes) from .driver import AbstractDriver, NotSupportedError from enum import IntEnum, unique import logging import struct import six logger = logging.getLogger(__name__) @unique class TAG(IntEnum): USB_SUPPORTED = 0x01 SERIAL = 0x02 USB_ENABLED = 0x03 FORMFACTOR = 0x04 VERSION = 0x05 AUTO_EJECT_TIMEOUT = 0x06 CHALRESP_TIMEOUT = 0x07 DEVICE_FLAGS = 0x08 APP_VERSIONS = 0x09 CONFIG_LOCK = 0x0a USE_LOCK_KEY = 0x0b REBOOT = 0x0c NFC_SUPPORTED = 0x0d NFC_ENABLED = 0x0e @unique class FLAGS(IntEnum): MODE_FLAG_EJECT = 0x80 MODE_REMOTE_WAKEUP = 0x40 def _struct_pair(fmt): return (lambda v: struct.unpack(fmt, v)[0], lambda v: struct.pack(fmt, v)) _parse_config = { TAG.USB_SUPPORTED: (bytes2int, int2bytes), TAG.SERIAL: (bytes2int, int2bytes), TAG.USB_ENABLED: (bytes2int, int2bytes), TAG.FORMFACTOR: (lambda v: FORM_FACTOR.from_code(bytes2int(v)), int2bytes), TAG.VERSION: (lambda v: struct.unpack('>BBB', v), lambda v: struct.pack('>BBB', *v)), TAG.AUTO_EJECT_TIMEOUT: _struct_pair('>H'), TAG.CHALRESP_TIMEOUT: _struct_pair('>B'), TAG.DEVICE_FLAGS: _struct_pair('>B'), TAG.APP_VERSIONS: (lambda v: v, lambda v: v), TAG.CONFIG_LOCK: (_struct_pair('>?')[0], lambda v: v), TAG.USE_LOCK_KEY: (None, lambda v: v), TAG.NFC_SUPPORTED: (bytes2int, int2bytes), TAG.NFC_ENABLED: (bytes2int, int2bytes) } def _set_value(data, tag, value): data[tag] = _parse_config[tag][1](value) if tag in _parse_config else value def device_config(usb_enabled=None, nfc_enabled=None, flags=None, auto_eject_timeout=None, chalresp_timeout=None, config_lock=None): values = {} if config_lock is not None: if len(config_lock) != 16: raise ValueError('Config lock key must be 16 bytes') _set_value(values, TAG.CONFIG_LOCK, config_lock) if usb_enabled is not None: # Always add the unused CCID transport usb_enabled |= TRANSPORT.CCID _set_value(values, TAG.USB_ENABLED, usb_enabled) if nfc_enabled is not None: _set_value(values, TAG.NFC_ENABLED, nfc_enabled) if flags is not None: _set_value(values, TAG.DEVICE_FLAGS, flags) if auto_eject_timeout is not None: _set_value(values, TAG.AUTO_EJECT_TIMEOUT, auto_eject_timeout) if chalresp_timeout is not None: _set_value(values, TAG.CHALRESP_TIMEOUT, chalresp_timeout) return values class DeviceConfig(object): def __init__(self, data=None): if not data: logger.debug('Config data empty/missing') self._tags = {} return c_len, data = six.indexbytes(data, 0), data[1:] data = data[:c_len] self._tags = Tlv.parse_dict(data) def _get(self, tag, default=None): if tag not in self._tags: return default val = self._tags[tag] return _parse_config[tag][0](val) if tag in _parse_config else val def _set(self, tag, value): _set_value(self._tags, tag, value) @property def serial(self): return self._get(TAG.SERIAL) @property def version(self): return self._get(TAG.VERSION) @property def form_factor(self): return self._get(TAG.FORMFACTOR, FORM_FACTOR.UNKNOWN) @property def usb_supported(self): return self._get(TAG.USB_SUPPORTED, 0) @property def usb_enabled(self): return self._get(TAG.USB_ENABLED, 0) @property def nfc_supported(self): return self._get(TAG.NFC_SUPPORTED, 0) @property def nfc_enabled(self): return self._get(TAG.NFC_ENABLED, 0) @property def app_versions(self): return self._get(TAG.APP_VERSIONS) @property def configuration_locked(self): return self._get(TAG.CONFIG_LOCK) @property def device_flags(self): return self._get(TAG.DEVICE_FLAGS, 0) _NULL_DRIVER = AbstractDriver(0, 0) _NEO_BASE_CAPABILITIES = TRANSPORT.CCID | APPLICATION.OTP | APPLICATION.OATH \ | APPLICATION.OPGP | APPLICATION.PIV class YubiKey(object): """ YubiKey device handle """ device_name = 'YubiKey' _can_mode_switch = True _can_write_config = False def __init__(self, descriptor, driver): self._key_type = driver.key_type self.device_name = self._key_type.value self._descriptor = descriptor self._driver = driver try: logger.debug('Read config from device...') config = DeviceConfig(driver.read_config()) logger.debug('Success!') if not config.version: # This will succeed, 4.2 <= fw < 5 config._set(TAG.VERSION, driver.read_version()) if config.version >= (5, 0, 0): # New capabilities self._can_write_config = True elif config.version == (4, 2, 4): # Doesn't report correctly config._set(TAG.USB_SUPPORTED, 0x3f) if config.usb_supported ==\ (APPLICATION.OTP | APPLICATION.U2F | TRANSPORT.CCID): self.device_name = 'YubiKey Edge' config._set(TAG.USB_SUPPORTED, config.usb_supported ^ TRANSPORT.CCID) except NotSupportedError as e: logger.debug('Failed to read config from device', exc_info=e) config = DeviceConfig() version = descriptor.version or driver.read_version() if version is not None: config._set(TAG.VERSION, version) serial = driver.read_serial() if serial is not None: config._set(TAG.SERIAL, serial) if self._key_type == YUBIKEY.SKY: logger.debug('Identified SKY 1') config._set(TAG.USB_SUPPORTED, APPLICATION.U2F) elif self._key_type == YUBIKEY.NEO: logger.debug('Identified NEO') if driver.transport == TRANSPORT.CCID: logger.debug('CCID available, probe capabilities...') usb_supported = driver.probe_capabilities() else: # Assume base capabilities logger.debug('CCID not available, guess capabilities') usb_supported = _NEO_BASE_CAPABILITIES # NEO over 3.3.0 have U2F (which might be blocked by OS) if TRANSPORT.has(self.mode.transports, TRANSPORT.FIDO) \ or (version and version >= (3, 3, 0)): usb_supported |= APPLICATION.U2F config._set(TAG.USB_SUPPORTED, usb_supported) config._set(TAG.NFC_SUPPORTED, usb_supported) config._set(TAG.NFC_ENABLED, usb_supported) elif self._key_type == YUBIKEY.YKP: logger.debug('YK Plus identified') config._set(TAG.USB_SUPPORTED, APPLICATION.OTP | APPLICATION.U2F) self._can_mode_switch = False elif self._key_type == YUBIKEY.YKS: logger.debug('YK Standard identified') config._set(TAG.USB_SUPPORTED, APPLICATION.OTP) self._can_mode_switch = False # Fix usb_enabled if not config.usb_enabled: usb_enabled = config.usb_supported if not TRANSPORT.has(self.mode.transports, TRANSPORT.OTP): usb_enabled &= ~APPLICATION.OTP if not TRANSPORT.has(self.mode.transports, TRANSPORT.FIDO): usb_enabled &= ~(APPLICATION.U2F | APPLICATION.FIDO2) if not TRANSPORT.has(self.mode.transports, TRANSPORT.CCID): usb_enabled &= ~( TRANSPORT.CCID | APPLICATION.OATH | APPLICATION.OPGP | APPLICATION.PIV) config._set(TAG.USB_ENABLED, usb_enabled) # Workaround for invalid configurations. # Assume all form factors except USB_A_KEYCHAIN and # USB_C_KEYCHAIN >= 5.2.4 does not support NFC. if not ((config.form_factor is FORM_FACTOR.USB_A_KEYCHAIN) or (config.form_factor is FORM_FACTOR.USB_C_KEYCHAIN and config.version >= (5, 2, 4))): config._set(TAG.NFC_SUPPORTED, 0) config._set(TAG.NFC_ENABLED, 0) self._config = config if self._key_type == YUBIKEY.SKY: self._can_mode_switch = False # New capabilities if not APPLICATION.has(config.usb_supported, APPLICATION.FIDO2): logger.debug('SKY has no FIDO2, SKY 1') self.device_name = 'FIDO U2F Security Key' # SKY 1 if config.nfc_supported: self.device_name = 'Security Key NFC' elif self._key_type == YUBIKEY.YK4: if (5, 0, 0) <= self.version < (5, 1, 0) or \ self.version in [(5, 2, 0), (5, 2, 1), (5, 2, 2)]: self.device_name = 'YubiKey Preview' elif self.version >= (5, 1, 0): logger.debug('Identified YubiKey 5') self.device_name = 'YubiKey 5' if (config.form_factor == FORM_FACTOR.USB_A_KEYCHAIN and not config.nfc_supported): self.device_name += 'A' elif config.form_factor == FORM_FACTOR.USB_A_KEYCHAIN: self.device_name += ' NFC' elif config.form_factor == FORM_FACTOR.USB_A_NANO: self.device_name += ' Nano' elif config.form_factor == FORM_FACTOR.USB_C_KEYCHAIN: self.device_name += 'C' if config.nfc_supported: self.device_name += ' NFC' elif config.form_factor == FORM_FACTOR.USB_C_NANO: self.device_name += 'C Nano' elif config.form_factor == FORM_FACTOR.USB_C_LIGHTNING: self.device_name += 'Ci' elif self.is_fips: self.device_name = 'YubiKey FIPS' @property def driver(self): return self._driver @property def config(self): return self._config @property def can_mode_switch(self): return self._can_mode_switch @property def can_write_config(self): return self._can_write_config @property def version(self): return self._config.version @property def serial(self): return self._config.serial @property def form_factor(self): return self._config.form_factor @property def key_type(self): return self._key_type @property def transport(self): return self._driver.transport @property def mode(self): return self._driver.mode @property def is_fips(self): return YubiKey.is_fips_version(self.version) @staticmethod def is_fips_version(version): return (4, 4, 0) <= version < (4, 5, 0) @mode.setter def mode(self, mode): if not self.has_mode(mode): raise ValueError('Mode not supported: %s' % mode) self.set_mode(mode) def write_config(self, values, reboot=False, lock_key=None): if not self._can_write_config: raise NotSupportedError() payload = b''.join(Tlv(k, v) for (k, v) in values.items()) if lock_key: payload += Tlv(TAG.USE_LOCK_KEY, lock_key) elif self.config.configuration_locked: raise ValueError('Configuration locked!') if reboot: payload += Tlv(TAG.REBOOT) payload = struct.pack('>B', len(payload)) + payload self._driver.write_config(payload) if reboot: self.close() else: self.config = DeviceConfig(self._driver.read_config()) def has_mode(self, mode): return self.mode == mode or \ (self.can_mode_switch and TRANSPORT.has(self._config.usb_supported, mode.transports)) def set_mode(self, mode, cr_timeout=None, autoeject_time=None): flags = 0 # If autoeject_time is set, then set the touch eject flag. if autoeject_time is not None: flags |= 0x80 else: autoeject_time = 0 # NEO < 3.3.1 (?) should always set 82 instead of 2. if self.version <= (3, 3, 1) and mode.code == 2: flags = 0x80 if not self._can_write_config: self._driver.set_mode(flags | mode.code, cr_timeout or 0, autoeject_time) else: self.write_config(device_config( usb_enabled=self.config.usb_supported & ( ((APPLICATION.U2F | APPLICATION.FIDO2) * mode.has_transport(TRANSPORT.FIDO)) | ((APPLICATION.OATH | APPLICATION.OPGP | APPLICATION.PIV) * mode.has_transport(TRANSPORT.CCID)) | ((APPLICATION.OTP) * mode.has_transport(TRANSPORT.OTP)) ), flags=flags, chalresp_timeout=cr_timeout, auto_eject_timeout=autoeject_time ), reboot=True) def use_transport(self, transport): if self.transport == transport: return self if not TRANSPORT.has(self.mode.transports, transport): raise ValueError('%s transport not enabled!' % transport) del self._driver self._driver = _NULL_DRIVER return self._descriptor.open_device(transport, self.serial) def close(self): self._driver.close() def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.close() def __str__(self): return '{0} {1[0]}.{1[1]}.{1[2]} {2} [{3.name}] ' \ 'serial: {4}' \ .format( self.device_name, self.version, self.mode, self.transport, self.serial ) yubikey-manager-3.1.1/ykman/driver.py0000644000175100001630000000530113614233340020367 0ustar runnerdocker00000000000000# Copyright (c) 2015 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. import logging logger = logging.getLogger(__name__) class ModeSwitchError(Exception): def __init__(self): super(ModeSwitchError, self).__init__('Failed to switch mode.') class NotSupportedError(Exception): pass class AbstractDriver(object): """Abstract driver class for communicating with a YubiKey""" transport = None def __init__(self, key_type, mode): self._key_type = key_type self._mode = mode @property def key_type(self): return self._key_type @property def mode(self): return self._mode def read_serial(self): """ Attempt to read the serial number from the YubiKey, if available. This will only be called if read_config() fails to provide the serial. """ return None def set_mode(self, mode_code): raise NotImplementedError() def read_version(self): """ Attempt to read the firmware version from the YubiKey, if possible. If we cannot determine the firmware version with certainty this way, return None. """ return None @property def is_in_fips_mode(self): raise NotImplementedError() def read_config(self): raise NotImplementedError() def write_config(self, data): raise NotImplementedError() def close(self): pass yubikey-manager-3.1.1/ykman/driver_ccid.py0000644000175100001630000002462413614233340021362 0ustar runnerdocker00000000000000# Copyright (c) 2015 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import import logging import struct import subprocess import time import six from enum import IntEnum, unique from binascii import b2a_hex from smartcard import System from smartcard.Exceptions import CardConnectionException from smartcard.pcsc.PCSCExceptions import ListReadersException from smartcard.pcsc.PCSCContext import PCSCContext from .driver import AbstractDriver, ModeSwitchError, NotSupportedError from .util import AID, APPLICATION, TRANSPORT, YUBIKEY, Mode GP_INS_SELECT = 0xa4 YK_READER_NAME = 'yubico yubikey' @unique class SW(IntEnum): MORE_DATA = 0x61 NO_INPUT_DATA = 0x6285 VERIFY_FAIL_NO_RETRY = 0x63c0 WRONG_LENGTH = 0x6700 SECURITY_CONDITION_NOT_SATISFIED = 0x6982 AUTH_METHOD_BLOCKED = 0x6983 DATA_INVALID = 0x6984 CONDITIONS_NOT_SATISFIED = 0x6985 COMMAND_NOT_ALLOWED = 0x6986 INCORRECT_PARAMETERS = 0x6a80 NOT_FOUND = 0x6a82 NO_SPACE = 0x6a84 INVALID_INSTRUCTION = 0x6d00 COMMAND_ABORTED = 0x6f00 OK = 0x9000 @staticmethod def is_verify_fail(sw): return 0x63c0 <= sw <= 0x63cf @classmethod def tries_left(cls, sw): if sw == SW.AUTH_METHOD_BLOCKED: return 0 if not cls.is_verify_fail(sw): raise ValueError( 'Cannot read remaining tries from status word: %x' % sw) return sw & 0xf @unique class MGR_INS(IntEnum): READ_CONFIG = 0x1d WRITE_CONFIG = 0x1c SET_MODE = 0x16 @unique class OTP_INS(IntEnum): YK2_REQ = 0x01 @unique class SLOT(IntEnum): DEVICE_SERIAL = 0x10 DEVICE_CONFIG = 0x11 KNOWN_APPLETS = { AID.OTP: APPLICATION.OTP, AID.U2F: APPLICATION.U2F, AID.U2F_YUBICO: APPLICATION.U2F, AID.PIV: APPLICATION.PIV, AID.OPGP: APPLICATION.OPGP, AID.OATH: APPLICATION.OATH } logger = logging.getLogger(__name__) class CCIDError(Exception): """Thrown when smart card communication fails.""" class APDUError(CCIDError): """Thrown when an APDU response has the wrong SW code""" def __init__(self, data, sw): self.data = data self.sw = sw def __str__(self): return 'APDU error: SW=0x{:04x}'.format(self.sw) def _pid_from_name(name): transports = 0 for t in TRANSPORT: if t.name in name: transports += t if 'U2F' in name: transports += TRANSPORT.FIDO key_type = YUBIKEY.NEO if 'NEO' in name else YUBIKEY.YK4 return key_type.get_pid(transports) class CCIDDriver(AbstractDriver): """ Pyscard based CCID driver """ transport = TRANSPORT.CCID def __init__(self, connection, name): self._conn = connection if name.lower().startswith(YK_READER_NAME): pid = _pid_from_name(name) key_type = pid.get_type() mode = Mode.from_pid(pid) else: key_type, mode = self._probe_type_and_mode() super(CCIDDriver, self).__init__(key_type, mode) def _probe_type_and_mode(self): try: s = self.select(AID.OTP) version = tuple(c for c in six.iterbytes(s[:3])) if version < (4, 0, 0): return YUBIKEY.NEO, Mode(TRANSPORT.CCID) except APDUError: pass try: self.select(AID.MGR) return YUBIKEY.YK4, Mode(TRANSPORT.CCID) except APDUError: pass raise ValueError('Couldn\'t select OTP nor MGR applet!') def read_serial(self): try: self.select(AID.OTP) serial = self.send_apdu(0, OTP_INS.YK2_REQ, SLOT.DEVICE_SERIAL, 0) if serial: return struct.unpack('>I', serial)[0] except APDUError: pass return None def read_version(self): if self.key_type == YUBIKEY.YK4: try: # Attempt to read OTP applet version which should match s = self.select(AID.OTP) return tuple(c for c in six.iterbytes(s[:3])) except APDUError: pass return None def read_config(self): if self.key_type == YUBIKEY.NEO: raise NotSupportedError() self.select(AID.MGR) return self.send_apdu(0, MGR_INS.READ_CONFIG, 0, 0) def write_config(self, data): if self.key_type == YUBIKEY.NEO: raise NotSupportedError() self.select(AID.MGR) self.send_apdu(0, MGR_INS.WRITE_CONFIG, 0, 0, data) def probe_capabilities(self): capa = TRANSPORT.CCID for aid, code in KNOWN_APPLETS.items(): try: self.select(aid) capa |= code logger.debug( 'Found applet: aid: %s , capability: %s', aid, code) except APDUError: logger.debug( 'Missing applet: aid: %s , capability: %s', aid, code) except CCIDError as e: logger.debug( 'Failed reading applet: aid: %s , capability: %s , %s', aid, code, e) return capa def send_apdu(self, cl, ins, p1, p2, data=b'', check=SW.OK): header = [cl, ins, p1, p2, len(data)] body = list(six.iterbytes(data)) try: logger.debug('SEND: %s', b2a_hex(bytearray(header + body))) resp, sw1, sw2 = self._conn.transmit(header + body) logger.debug('RECV: %s', b2a_hex(bytearray(resp + [sw1, sw2]))) except CardConnectionException as e: raise CCIDError(e) sw = sw1 << 8 | sw2 resp = bytes(bytearray(resp)) if check is None: return resp, sw elif check == sw: return resp else: raise APDUError(resp, sw) def select(self, aid): return self.send_apdu(0, GP_INS_SELECT, 0x04, 0, aid) def set_mode(self, mode_code, cr_timeout=0, autoeject_time=0): mode_data = struct.pack('BBH', mode_code, cr_timeout, autoeject_time) try: if self.key_type == YUBIKEY.NEO: self._set_mode_otp(mode_data) else: self._set_mode_mgr(mode_data) except CCIDError: raise ModeSwitchError() def _set_mode_otp(self, mode_data): resp = self.select(AID.OTP) pgm_seq_old = six.indexbytes(resp, 3) resp = self.send_apdu(0, OTP_INS.YK2_REQ, SLOT.DEVICE_CONFIG, 0, mode_data) pgm_seq_new = six.indexbytes(resp, 3) if not _pgm_seq_ok(pgm_seq_old, pgm_seq_new): raise ModeSwitchError() def _set_mode_mgr(self, mode_data): self.select(AID.MGR) self.send_apdu(0, MGR_INS.SET_MODE, SLOT.DEVICE_CONFIG, 0, mode_data) def close(self): logger.debug('Close %s', self) self._conn.disconnect() def __del__(self): logger.debug('Destroy %s', self) try: self.close() except Exception as e: logger.debug('Exception in destructor', exc_info=e) def _pgm_seq_ok(pgm_seq_old, pgm_seq_new): return pgm_seq_new == pgm_seq_old == 0 or pgm_seq_new > pgm_seq_old def kill_scdaemon(): killed = False try: # Works for Windows. from win32com.client import GetObject from win32api import OpenProcess, CloseHandle, TerminateProcess wmi = GetObject('winmgmts:') ps = wmi.InstancesOf('Win32_Process') for p in ps: if p.Properties_('Name').Value == 'scdaemon.exe': pid = p.Properties_('ProcessID').Value handle = OpenProcess(1, False, pid) TerminateProcess(handle, -1) CloseHandle(handle) killed = True except ImportError: # Works for Linux and OS X. pids = subprocess.check_output( "ps ax | grep scdaemon | grep -v grep | awk '{ print $1 }'", shell=True).strip() if pids: for pid in pids.split(): subprocess.call(['kill', '-9', pid]) killed = True if killed: time.sleep(0.1) return killed def list_readers(): try: return System.readers() except ListReadersException: # If the PCSC system has restarted the context might be stale, try # forcing a new context (This happens on Windows if the last reader is # removed): PCSCContext.instance = None return System.readers() def open_devices(name_filter=YK_READER_NAME): readers = list_readers() while readers: try_again = [] for reader in readers: if name_filter.lower() in reader.name.lower(): try: conn = reader.createConnection() conn.connect() yield CCIDDriver(conn, reader.name) except CardConnectionException: try_again.append(reader) except Exception as e: # Try with next reader. logger.debug( 'Failed to connect to reader %s', reader, exc_info=e) if try_again and kill_scdaemon(): readers = try_again else: return yubikey-manager-3.1.1/ykman/driver_fido.py0000644000175100001630000000673613614233340021405 0ustar runnerdocker00000000000000# Copyright (c) 2015 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import from .driver import AbstractDriver, NotSupportedError from .util import TRANSPORT, PID, YUBIKEY, Mode from fido2.hid import CtapHidDevice, CTAPHID from enum import IntEnum, unique import logging import struct logger = logging.getLogger(__name__) @unique class CMD(IntEnum): YUBIKEY_DEVICE_CONFIG = CTAPHID.VENDOR_FIRST READ_CONFIG = CTAPHID.VENDOR_FIRST + 2 WRITE_CONFIG = CTAPHID.VENDOR_FIRST + 3 @unique class FIPS_U2F_CMD(IntEnum): ECHO = CTAPHID.VENDOR_FIRST WRITE_CONFIG = CTAPHID.VENDOR_FIRST + 1 APP_VERSION = CTAPHID.VENDOR_FIRST + 2 VERIFY_PIN = CTAPHID.VENDOR_FIRST + 3 SET_PIN = CTAPHID.VENDOR_FIRST + 4 RESET = CTAPHID.VENDOR_FIRST + 5 VERIFY_FIPS_MODE = CTAPHID.VENDOR_FIRST + 6 class FidoDriver(AbstractDriver): transport = TRANSPORT.FIDO def __init__(self, dev): pid = PID(dev.descriptor['product_id']) super(FidoDriver, self).__init__(pid.get_type(), Mode.from_pid(pid)) self._dev = dev def read_config(self): if self.key_type == YUBIKEY.NEO: raise NotSupportedError() if self.key_type == YUBIKEY.SKY: if self._dev.device_version < (4, 0, 0): # Old SKY 1 raise NotSupportedError() return self._dev.call(CMD.READ_CONFIG) def write_config(self, data): self._dev.call(CMD.WRITE_CONFIG, data) def read_version(self): version = self._dev.device_version if version[0] < 4: # Before yK 4 this wasn't the fw version return None return version def set_mode(self, mode_code, cr_timeout=0, autoeject_time=0): data = struct.pack('BBH', mode_code, cr_timeout, autoeject_time) self._dev.call(CMD.YUBIKEY_DEVICE_CONFIG, data) def descriptor_filter(desc): return desc['vendor_id'] == 0x1050 \ and desc['usage_page'] == 0xf1d0 \ and desc['usage'] == 1 def open_devices(): for dev in CtapHidDevice.list_devices(descriptor_filter): try: yield FidoDriver(dev) except Exception as e: logger.debug('Failed opening FIDO device', exc_info=e) yubikey-manager-3.1.1/ykman/driver_otp.py0000644000175100001630000001703213614233340021255 0ustar runnerdocker00000000000000# Copyright (c) 2015 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import import logging from .native.ykpers import Ykpers from ctypes import byref, c_int, c_uint, c_size_t, create_string_buffer from .driver import AbstractDriver, ModeSwitchError, NotSupportedError from .util import PID, TRANSPORT, Mode, MissingLibrary logger = logging.getLogger(__name__) CONFIG1_VALID = 0x01 CONFIG2_VALID = 0x02 CMD_VERIFY_FIPS_MODE = 0x14 MISSING_LIBYKPERS_MSG = 'libykpers not found, OTP functionality not available' try: ykpers = Ykpers('ykpers-1', '1') if not ykpers.yk_init(): raise Exception('yk_init failed.') libversion = tuple(int(x) for x in ykpers.ykpers_check_version(None) .decode('ascii').split('.')) except Exception as e: logger.error('libykpers not found', exc_info=e) ykpers = MissingLibrary(MISSING_LIBYKPERS_MSG) libversion = None class YkpersError(Exception): """Thrown if a ykpers call fails.""" def __init__(self, errno): self.errno = errno self.message = ykpers.yk_strerror(errno) def __str__(self): return 'ykpers error {}, {}'.format(self.errno, self.message) def check(status): if not status: raise YkpersError(ykpers.yk_get_errno()) class OTPDriver(AbstractDriver): """ libykpers based OTP driver """ transport = TRANSPORT.OTP def __init__(self, dev): self._dev = dev pid = self._read_pid() super(OTPDriver, self).__init__(pid.get_type(), Mode.from_pid(pid)) self._access_code = None self._slot1_valid = False self._slot2_valid = False self._read_status() @property def ykpers_dev(self): return self._dev @property def version(self): return self._version @property def slot_status(self): return (self._slot1_valid, self._slot2_valid) def _read_pid(self): vid, pid = c_int(), c_int() check(ykpers.yk_get_key_vid_pid(self._dev, byref(vid), byref(pid))) return PID(pid.value) def _read_status(self): status = ykpers.ykds_alloc() try: if ykpers.yk_get_status(self._dev, status): self._version = ( ykpers.ykds_version_major(status), ykpers.ykds_version_minor(status), ykpers.ykds_version_build(status) ) touch_level = ykpers.ykds_touch_level(status) self._slot1_valid = touch_level & CONFIG1_VALID != 0 self._slot2_valid = touch_level & CONFIG2_VALID != 0 finally: ykpers.ykds_free(status) def read_version(self): if self._version[0] == 3: # This is the OTP applet version. return None return self._version def read_serial(self): serial = c_uint() if ykpers.yk_get_serial(self._dev, 0, 0, byref(serial)): return serial.value else: logger.debug('Failed to read serial from device.') return None # Serial not visible def read_config(self): if self._version < (4, 1, 0): raise NotSupportedError() buf_size = c_size_t(1024) resp = create_string_buffer(buf_size.value) try: check(ykpers.yk_get_capabilities( self._dev, 0, 0, resp, byref(buf_size))) return resp.raw[:buf_size.value] except YkpersError: logger.debug( 'Failed reading config.' 'OTP interface might be locked, try waiting 3 seconds...') import time time.sleep(3) check(ykpers.yk_get_capabilities( self._dev, 0, 0, resp, byref(buf_size))) return resp.raw[:buf_size.value] def write_config(self, data): if self._version < (5, 0, 0): raise NotSupportedError() if libversion < (1, 19, 0): raise NotSupportedError('This action requires libykpers >= 1.19') check(ykpers.yk_write_device_info(self._dev, data, len(data))) def set_mode(self, mode_code, cr_timeout=0, autoeject_time=0): config = ykpers.ykp_alloc_device_config() ykpers.ykp_set_device_mode(config, mode_code) ykpers.ykp_set_device_chalresp_timeout(config, cr_timeout) ykpers.ykp_set_device_autoeject_time(config, autoeject_time) try: check(ykpers.yk_write_device_config(self._dev, config)) except YkpersError: raise ModeSwitchError() finally: ykpers.ykp_free_device_config(config) def write_to_and_read_from_key(self, cmd, expected_output_length, input_bytes=None, read_flags=0, result_bufsize=16): input_bufcount = 0 if input_bytes is None else len(input_bytes) result_buf = create_string_buffer(result_bufsize) bytes_read = c_uint() check(ykpers.yk_write_to_key(self._dev, cmd, input_bytes, input_bufcount)) check(ykpers.yk_read_response_from_key( self._dev, cmd, read_flags, result_buf, result_bufsize, expected_output_length, byref(bytes_read))) result = bytearray(result_buf) return (result[0:expected_output_length], result[0:bytes_read.value], result) @property def is_in_fips_mode(self): (result, _, _) = self.write_to_and_read_from_key( CMD_VERIFY_FIPS_MODE, expected_output_length=1) return result == b'\x01' def close(self): if self._dev is not None: logger.debug('Close %s', self) ykpers.yk_close_key(self._dev) self._dev = None def __del__(self): logger.debug('Destroy %s', self) self.close() def open_devices(): if not libversion: logger.error(MISSING_LIBYKPERS_MSG) return if libversion < (1, 18): yield OTPDriver(ykpers.yk_open_first_key()) else: for i in range(255): dev = ykpers.yk_open_key(i) if not dev: logger.debug('Failed to open key at position %s', i) break logger.debug('Success in opening key at position %s', i) yield OTPDriver(dev) yubikey-manager-3.1.1/ykman/fido.py0000644000175100001630000001272213614233340020022 0ustar runnerdocker00000000000000# Copyright (c) 2018 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import import six import time import logging from fido2.ctap1 import CTAP1, ApduError from fido2.ctap2 import CTAP2, PinProtocolV1, CredentialManagement from threading import Timer from .driver_ccid import SW from .driver_fido import FIPS_U2F_CMD logger = logging.getLogger(__name__) class ResidentCredential(object): def __init__(self, raw_credential, raw_rp): self._raw_credential = raw_credential self._raw_rp = raw_rp @property def credential_id(self): return self._raw_credential[CredentialManagement.RESULT.CREDENTIAL_ID] @property def rp_id(self): return self._raw_rp[CredentialManagement.RESULT.RP]['id'] @property def user_name(self): return self._raw_credential[CredentialManagement.RESULT.USER]['name'] class Fido2Controller(object): def __init__(self, driver): self.ctap = CTAP2(driver._dev) self.pin = PinProtocolV1(self.ctap) self._info = self.ctap.get_info() self._pin = self._info.options['clientPin'] @property def has_pin(self): return self._pin def get_resident_credentials(self, pin): _credman = CredentialManagement( self.ctap, self.pin.VERSION, self.pin.get_pin_token(pin)) for rp in _credman.enumerate_rps(): for cred in _credman.enumerate_creds( rp[CredentialManagement.RESULT.RP_ID_HASH]): yield ResidentCredential(cred, rp) def delete_resident_credential(self, credential_id, pin): _credman = CredentialManagement( self.ctap, self.pin.VERSION, self.pin.get_pin_token(pin)) for cred in self.get_resident_credentials(pin): if credential_id == cred.credential_id: _credman.delete_cred(credential_id) def get_pin_retries(self): return self.pin.get_pin_retries() def set_pin(self, pin): self.pin.set_pin(pin) self._pin = True def change_pin(self, old_pin, new_pin): self.pin.change_pin(old_pin, new_pin) def reset(self, touch_callback=None): if (touch_callback): touch_timer = Timer(0.500, touch_callback) touch_timer.start() try: self.ctap.reset() self._pin = False finally: if (touch_callback): touch_timer.cancel() @property def is_fips(self): return False class FipsU2fController(object): def __init__(self, driver): self.driver = driver self.ctap = CTAP1(driver._dev) @property def has_pin(self): # We don't know, but the change and set commands are the same here. return True def set_pin(self, pin): raise NotImplementedError('Use the change_pin method instead.') def change_pin(self, old_pin, new_pin): new_length = len(new_pin) old_pin = old_pin.encode('utf-8') new_pin = new_pin.encode('utf-8') data = six.int2byte(new_length) + old_pin + new_pin self.ctap.send_apdu(ins=FIPS_U2F_CMD.SET_PIN, data=data) return True def verify_pin(self, pin): self.ctap.send_apdu( ins=FIPS_U2F_CMD.VERIFY_PIN, data=pin.encode('utf-8')) def reset(self, touch_callback=None): if (touch_callback): touch_timer = Timer(0.500, touch_callback) touch_timer.start() try: while True: try: self.ctap.send_apdu(ins=FIPS_U2F_CMD.RESET) self._pin = False return True except ApduError as e: if e.code == SW.CONDITIONS_NOT_SATISFIED: time.sleep(0.5) else: raise e finally: if (touch_callback): touch_timer.cancel() @property def is_fips(self): return True @property def is_in_fips_mode(self): try: self.ctap.send_apdu(ins=FIPS_U2F_CMD.VERIFY_FIPS_MODE) return True except ApduError: return False yubikey-manager-3.1.1/ykman/logging_setup.py0000644000175100001630000000446713614233340021756 0ustar runnerdocker00000000000000# Copyright (c) 2015 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import import logging import ykman LOG_LEVELS = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL] LOG_LEVEL_NAMES = [logging.getLevelName(lvl) for lvl in LOG_LEVELS] def setup(log_level_name, log_file=None): log_level_value = next( (lvl for lvl in LOG_LEVELS if logging.getLevelName(lvl) == log_level_name), None ) if log_level_value is None: raise ValueError('Unknown log level: ' + log_level_name) logging.disable(logging.NOTSET) logging.basicConfig( datefmt='%Y-%m-%dT%H:%M:%S%z', filename=log_file, format='%(asctime)s %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s', # noqa: E501 level=log_level_value ) logger = logging.getLogger(__name__) logger.info('Initialized logging for %s version: %s', ykman.__name__, ykman.__version__) logging.disable(logging.CRITICAL * 2) yubikey-manager-3.1.1/ykman/native/0000755000175100001630000000000013614233377020023 5ustar runnerdocker00000000000000yubikey-manager-3.1.1/ykman/native/__init__.py0000644000175100001630000000253313614233340022125 0ustar runnerdocker00000000000000# Copyright (c) 2015 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. yubikey-manager-3.1.1/ykman/native/libloader.py0000644000175100001630000002737513614233340022336 0ustar runnerdocker00000000000000# ---------------------------------------------------------------------------- # Copyright (c) 2008 David James # Copyright (c) 2006-2008 Alex Holkner # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in # the documentation and/or other materials provided with the # distribution. # * Neither the name of pyglet nor the names of its # contributors may be used to endorse or promote products # derived from this software without specific prior written # permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ---------------------------------------------------------------------------- from __future__ import absolute_import import os.path import re import sys import glob import platform import ctypes import ctypes.util def _environ_path(name): if name in os.environ: return os.environ[name].split(':') else: return [] class LibraryLoader(object): def __init__(self): self.other_dirs = [] def load_library(self, libname, version=None, extra_paths=[]): """Given the name of a library, load it.""" paths = self.getpaths(libname, extra_paths) for path in paths: if os.path.exists(path): return self.load(path) raise ImportError('%s not found.' % libname) def load(self, path): """Given a path to a library, load it.""" try: # Darwin requires dlopen to be called with mode RTLD_GLOBAL instead # of the default RTLD_LOCAL. Without this, you end up with # libraries not being loadable, resulting in "Symbol not found" # errors if sys.platform == 'darwin': return ctypes.CDLL(path, ctypes.RTLD_GLOBAL) else: return ctypes.cdll.LoadLibrary(path) except OSError as e: raise ImportError(e) def getpaths(self, libname, extra_paths): """Return a list of paths where the library might be found.""" if os.path.isabs(libname): yield libname else: for path in self.getplatformpaths(libname, extra_paths): yield path path = ctypes.util.find_library(libname) if path: yield path def getplatformpaths(self, libname, extra_paths): return [] # Darwin (Mac OS X) class DarwinLibraryLoader(LibraryLoader): name_formats = ['lib%s.dylib', 'lib%s.so', 'lib%s.bundle', '%s.dylib', '%s.so', '%s.bundle', '%s'] def getplatformpaths(self, libname, extra_paths): if os.path.pathsep in libname: names = [libname] else: names = [format % libname for format in self.name_formats] for dir in extra_paths + self.getdirs(libname): for name in names: yield os.path.join(dir, name) def getdirs(self, libname): '''Implements the dylib search as specified in Apple documentation: http://developer.apple.com/documentation/DeveloperTools/Conceptual/ DynamicLibraries/Articles/DynamicLibraryUsageGuidelines.html Before commencing the standard search, the method first checks the bundle's ``Frameworks`` directory if the application is running within a bundle (OS X .app). ''' dyld_fallback_library_path = _environ_path( 'DYLD_FALLBACK_LIBRARY_PATH') if not dyld_fallback_library_path: dyld_fallback_library_path = [os.path.expanduser('~/lib'), '/usr/local/lib', '/usr/lib'] dirs = [] if '/' in libname: dirs.extend(_environ_path('DYLD_LIBRARY_PATH')) else: dirs.extend(_environ_path('LD_LIBRARY_PATH')) dirs.extend(_environ_path('DYLD_LIBRARY_PATH')) dirs.extend(self.other_dirs) dirs.append('.') dirs.append(os.path.dirname(__file__)) if hasattr(sys, 'frozen') and sys.frozen == 'macosx_app': dirs.append(os.path.join( os.environ['RESOURCEPATH'], '..', 'Frameworks')) if hasattr(sys, 'frozen'): dirs.append(sys._MEIPASS) dirs.append( os.path.join(os.path.dirname(sys.executable), '../Frameworks')) dirs.extend(dyld_fallback_library_path) return dirs # Posix class PosixLibraryLoader(LibraryLoader): _ld_so_cache = None def load_library(self, libname, version=None, extra_paths=[]): for dir in extra_paths: # Favor extra_paths for path in glob.glob('%s/lib%s*.s[ol]*' % (dir, libname)): return self.load(path) try: found = ctypes.util.find_library(libname) if found is not None: return self.load(found) except ImportError: pass return super(PosixLibraryLoader, self).load_library( libname, version, extra_paths) def _create_ld_so_cache(self): # Recreate search path followed by ld.so. This is going to be # slow to build, and incorrect (ld.so uses ld.so.cache, which may # not be up-to-date). Used only as fallback for distros without # /sbin/ldconfig. # # We assume the DT_RPATH and DT_RUNPATH binary sections are omitted. directories = [] for name in ('LD_LIBRARY_PATH', 'SHLIB_PATH', # HPUX 'LIBPATH', # OS/2, AIX 'LIBRARY_PATH', # BE/OS ): if name in os.environ: directories.extend(os.environ[name].split(os.pathsep)) directories.extend(self.other_dirs) directories.append('.') directories.append(os.path.dirname(__file__)) try: directories.extend([dir.strip() for dir in open('/etc/ld.so.conf')]) except IOError: pass unix_lib_dirs_list = ['/lib', '/usr/lib', '/lib64', '/usr/lib64'] if sys.platform.startswith('linux'): # Try and support multiarch work in Ubuntu # https://wiki.ubuntu.com/MultiarchSpec bitage = platform.architecture()[0] if bitage.startswith('32'): # Assume Intel/AMD x86 compat unix_lib_dirs_list += [ '/lib/i386-linux-gnu', '/usr/lib/i386-linux-gnu'] elif bitage.startswith('64'): # Assume Intel/AMD x86 compat unix_lib_dirs_list += [ '/lib/x86_64-linux-gnu', '/usr/lib/x86_64-linux-gnu'] else: # guess... unix_lib_dirs_list += glob.glob('/lib/*linux-gnu') directories.extend(unix_lib_dirs_list) cache = {} lib_re = re.compile(r'lib(.*)\.s[ol]') for dir in directories: try: for path in glob.glob('%s/*.s[ol]*' % dir): file = os.path.basename(path) # Index by filename if file not in cache: cache[file] = path # Index by library name match = lib_re.match(file) if match: library = match.group(1) if library not in cache: cache[library] = path except OSError: pass self._ld_so_cache = cache def getplatformpaths(self, libname, extra_paths): if self._ld_so_cache is None: self._create_ld_so_cache() for dir in extra_paths: for path in glob.glob('%s/lib%s*.s[ol]*' % (dir, libname)): yield path result = self._ld_so_cache.get(libname) if result: yield result path = ctypes.util.find_library(libname) if path: yield os.path.join('/lib', path) # Windows class _WindowsLibrary(object): def __init__(self, path): # If the DLL loads additional DLLs we need to be in the correct dir cwd = os.getcwd() dir = os.path.dirname(path) try: if dir: os.chdir(dir) self.cdll = ctypes.cdll.LoadLibrary(path) self.windll = ctypes.windll.LoadLibrary(path) finally: if dir: os.chdir(cwd) def __getattr__(self, name): try: return getattr(self.cdll, name) except AttributeError: try: return getattr(self.windll, name) except AttributeError: raise class WindowsLibraryLoader(LibraryLoader): name_formats = ['lib%s*.dll'] def load_library(self, libname, version=None, extra_paths=[]): tmp = os.environ['PATH'] try: os.environ['PATH'] = '' result = LibraryLoader.load_library(self, libname, version, extra_paths) except ImportError: result = None if os.path.sep not in libname: formats = self.name_formats[:] if version: formats.append('lib%%s-%s.dll' % version) for name in formats: try: result = getattr(ctypes.cdll, name % libname) if result: break except WindowsError: result = None if result is None: try: result = getattr(ctypes.cdll, libname) except WindowsError: result = None if result is None: raise ImportError('%s not found.' % libname) finally: os.environ['PATH'] = tmp return result def load(self, path): return _WindowsLibrary(path) def getplatformpaths(self, libname, extra_paths): if os.path.sep not in libname: for name in self.name_formats: for dir in extra_paths: pattern = os.path.abspath(os.path.join(dir, name % libname)) for path in glob.glob(pattern): yield path path = ctypes.util.find_library(name % libname) if path: yield path # Platform switching # If your value of sys.platform does not appear in this dict, please contact # the Ctypesgen maintainers. loaderclass = { 'darwin': DarwinLibraryLoader, 'cygwin': WindowsLibraryLoader, 'win32': WindowsLibraryLoader } loader = loaderclass.get(sys.platform, PosixLibraryLoader)() def add_library_search_dirs(other_dirs): loader.other_dirs = other_dirs load_library = loader.load_library del loaderclass yubikey-manager-3.1.1/ykman/native/pyusb.py0000644000175100001630000000622513614233340021532 0ustar runnerdocker00000000000000# Copyright (c) 2015 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import import ctypes import ctypes.util import os import sys import usb.backend.libusb1 as libusb1 def _find_library_local(libname): # For .app bundles if sys.platform == 'darwin': libpath = os.path.join( os.path.dirname( sys.executable), '../Frameworks', libname + '.dylib') if os.path.isfile(libpath): return libpath else: # Look in ykman/native/ libpath = os.path.join(os.path.dirname(__file__), libname) if os.path.isfile(libpath): return libpath elif sys.platform == 'win32' and os.path.isfile(libpath + '.dll'): return libpath + '.dll' def _load_usb_backend(): # First try to find backend locally, if not found try the systems. try: tmp = os.environ['PATH'] os.environ['PATH'] = '' backend = libusb1.get_backend(find_library=_find_library_local) if backend is not None: return backend finally: os.environ['PATH'] = tmp backend = libusb1.get_backend() if backend is not None: return backend def get_usb_backend(): return _load_usb_backend() class LibUsb1Version(ctypes.Structure): _fields_ = [ ('major', ctypes.c_uint16), ('minor', ctypes.c_uint16), ('micro', ctypes.c_uint16), ('nano', ctypes.c_uint16), ('rc', ctypes.c_char_p), ('describe', ctypes.c_char_p) ] def get_usb_backend_version(): backend = get_usb_backend() if backend is None: return None elif isinstance(backend, libusb1._LibUSB): lib = backend.lib lib.libusb_get_version.restype = ctypes.POINTER(LibUsb1Version) version = lib.libusb_get_version().contents return 'libusb {0.major}.{0.minor}.{0.micro}'.format(version) yubikey-manager-3.1.1/ykman/native/util.py0000644000175100001630000000517113614233340021344 0ustar runnerdocker00000000000000# Copyright (c) 2013 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from .libloader import load_library import os import sys def use_library(libname, version=None, extra_paths=[]): lib = load_library(libname, version, extra_paths) def define(func_name, argtypes, restype=None): try: f = getattr(lib, func_name) f.argtypes = argtypes f.restype = restype except AttributeError: print('Undefined symbol: %s' % func_name) def error(*args, **kwargs): raise Exception('Undefined symbol: %s' % func_name) f = error return f return define class CLibrary(object): """ Base class for extending to create python wrappers for c libraries. Example: class Foo(CLibrary): foo_func = [c_bool, c_char_p], int foo = Foo('libfoo') assert foo.foo_func(True, 'Hello!') == 7 """ def __init__(self, libname, version=None): module_path = sys.modules[self.__class__.__module__].__file__ extra_paths = [os.path.dirname(module_path)] self._lib = use_library(libname, version, extra_paths) def __getattribute__(self, name): val = object.__getattribute__(self, name) if isinstance(val, tuple) and len(val) == 2: return self._lib(name, *val) return val yubikey-manager-3.1.1/ykman/native/ykpers.py0000644000175100001630000001246413614233340021707 0ustar runnerdocker00000000000000# Copyright (c) 2013 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import print_function, absolute_import from ctypes import (Structure, POINTER, c_int, c_uint8, c_uint, c_ubyte, c_char_p, c_ushort, c_size_t, c_ulong) from .util import CLibrary YK_KEY = type('YK_KEY', (Structure,), {}) YK_STATUS = type('YK_STATUS', (Structure,), {}) YK_TICKET = type('YK_TICKET', (Structure,), {}) YK_CONFIG = type('YK_CONFIG', (Structure,), {}) YK_NAV = type('YK_NAV', (Structure,), {}) YK_FRAME = type('YK_FRAME', (Structure,), {}) YK_NDEF = type('YK_NDEF', (Structure,), {}) YK_DEVICE_CONFIG = type('YK_DEVICE_CONFIG', (Structure,), {}) YKP_CONFIG = type('YKP_CONFIG', (Structure,), {}) class Ykpers(CLibrary): _yk_errno_location = [], POINTER(c_int) yk_strerror = [c_int], c_char_p ykpers_check_version = [c_char_p], c_char_p yk_init = [], bool yk_release = [], bool yk_open_key = [c_int], POINTER(YK_KEY) yk_open_first_key = [], POINTER(YK_KEY) yk_close_key = [POINTER(YK_KEY)], bool yk_get_status = [POINTER(YK_KEY), POINTER(YK_STATUS)], bool yk_get_serial = [POINTER(YK_KEY), c_uint8, c_uint, POINTER(c_uint)], bool yk_write_command = [POINTER(YK_KEY), POINTER(YK_CONFIG), c_uint8, c_char_p ], bool yk_write_device_config = [POINTER(YK_KEY), POINTER(YK_DEVICE_CONFIG)], bool yk_write_to_key = [POINTER(YK_KEY), c_uint8, c_char_p, c_int], bool yk_read_response_from_key = [POINTER(YK_KEY), c_uint8, c_uint, c_char_p, c_uint, c_uint, POINTER(c_uint)], bool yk_get_key_vid_pid = [POINTER(YK_KEY), POINTER(c_int), POINTER(c_int)], bool yk_get_capabilities = [POINTER(YK_KEY), c_uint8, c_uint, c_char_p], bool yk_challenge_response = [ POINTER(YK_KEY), c_uint8, c_int, c_uint, c_char_p, c_uint, c_char_p], bool ykds_alloc = [], POINTER(YK_STATUS) ykds_free = [POINTER(YK_STATUS)], None ykds_version_major = [POINTER(YK_STATUS)], c_int ykds_version_minor = [POINTER(YK_STATUS)], c_int ykds_version_build = [POINTER(YK_STATUS)], c_int ykds_touch_level = [POINTER(YK_STATUS)], c_int ykp_alloc = [], POINTER(YKP_CONFIG) ykp_free_config = [POINTER(YKP_CONFIG)], bool ykp_configure_version = [POINTER(YKP_CONFIG), POINTER(YK_STATUS)], None ykp_configure_command = [POINTER(YKP_CONFIG), c_uint8], bool ykp_core_config = [POINTER(YKP_CONFIG)], POINTER(YK_CONFIG) ykp_alloc_device_config = [], POINTER(YK_DEVICE_CONFIG) ykp_free_device_config = [POINTER(YK_DEVICE_CONFIG)], bool ykp_set_device_mode = [POINTER(YK_DEVICE_CONFIG), c_ubyte], bool ykp_set_device_chalresp_timeout = [POINTER(YK_DEVICE_CONFIG), c_ubyte], bool ykp_set_device_autoeject_time = [POINTER(YK_DEVICE_CONFIG), c_ushort], bool ykp_set_fixed = [POINTER(YKP_CONFIG), c_char_p, c_size_t], bool ykp_set_uid = [POINTER(YKP_CONFIG), c_char_p, c_size_t], bool ykp_set_access_code = [POINTER(YKP_CONFIG), c_char_p, c_size_t], bool ykp_AES_key_from_raw = [POINTER(YKP_CONFIG), c_char_p], bool ykp_HMAC_key_from_raw = [POINTER(YKP_CONFIG), c_char_p], bool ykp_set_oath_imf = [POINTER(YKP_CONFIG), c_ulong], bool ykp_alloc_ndef = [], POINTER(YK_NDEF) ykp_free_ndef = [POINTER(YK_NDEF)], bool yk_write_ndef2 = [POINTER(YK_KEY), POINTER(YK_NDEF), c_uint], bool ykp_construct_ndef_uri = [POINTER(YK_NDEF), c_char_p], bool yk_write_device_info = [POINTER(YK_KEY), c_char_p, c_uint], bool def yk_get_errno(self): return self._yk_errno_location().contents.value def _ykp_set(self, cfg, name, value=True): cmd = self._lib(name, [POINTER(YKP_CONFIG), c_uint8], bool) return cmd(cfg, value) def ykp_set_tktflag(self, cfg, name, value=True): return self._ykp_set(cfg, 'ykp_set_tktflag_' + name, value) def ykp_set_cfgflag(self, cfg, name, value=True): return self._ykp_set(cfg, 'ykp_set_cfgflag_' + name, value) def ykp_set_extflag(self, cfg, name, value=True): return self._ykp_set(cfg, 'ykp_set_extflag_' + name, value) yubikey-manager-3.1.1/ykman/oath.py0000644000175100001630000003105713614233340020036 0ustar runnerdocker00000000000000# Copyright (c) 2015 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import import os import re import struct import six import time from base64 import b64encode from functools import total_ordering from enum import IntEnum, unique from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives import hmac, hashes from cryptography.hazmat.backends import default_backend from six.moves.urllib.parse import unquote, urlparse, parse_qs from .driver_ccid import APDUError, SW from .util import ( AID, Tlv, time_challenge, parse_b32_key, format_code, parse_truncated, hmac_shorten_key ) HMAC_MINIMUM_KEY_SIZE = 14 @unique class TAG(IntEnum): NAME = 0x71 NAME_LIST = 0x72 KEY = 0x73 CHALLENGE = 0x74 RESPONSE = 0x75 TRUNCATED_RESPONSE = 0x76 NO_RESPONSE = 0x77 PROPERTY = 0x78 VERSION = 0x79 IMF = 0x7a ALGORITHM = 0x7b TOUCH = 0x7c @unique class ALGO(IntEnum): SHA1 = 0x01 SHA256 = 0x02 SHA512 = 0x03 @unique class OATH_TYPE(IntEnum): HOTP = 0x10 TOTP = 0x20 @unique class PROPERTIES(IntEnum): REQUIRE_TOUCH = 0x02 @unique class INS(IntEnum): PUT = 0x01 DELETE = 0x02 SET_CODE = 0x03 RESET = 0x04 LIST = 0xa1 CALCULATE = 0xa2 VALIDATE = 0xa3 CALCULATE_ALL = 0xa4 SEND_REMAINING = 0xa5 @unique class MASK(IntEnum): ALGO = 0x0f TYPE = 0xf0 class CredentialData(object): def __init__(self, secret, issuer, name, oath_type=OATH_TYPE.TOTP, algorithm=ALGO.SHA1, digits=6, period=30, counter=0, touch=False): self.secret = secret self.issuer = issuer self.name = name self.oath_type = oath_type self.algorithm = algorithm self.digits = digits self.period = period self.counter = counter self.touch = touch @classmethod def from_uri(cls, uri): parsed = urlparse(uri.strip()) if parsed.scheme != 'otpauth': raise ValueError('Invalid URI scheme') params = dict((k, v[0]) for k, v in parse_qs(parsed.query).items()) issuer = None name = unquote(parsed.path)[1:] # Unquote and strip leading / if ':' in name: issuer, name = name.split(':', 1) return cls( secret=parse_b32_key(params['secret']), issuer=params.get('issuer', issuer), name=name, oath_type=OATH_TYPE[parsed.hostname.upper()], algorithm=ALGO[params.get('algorithm', 'SHA1').upper()], digits=int(params.get('digits', 6)), period=int(params.get('period', 30)), counter=int(params.get('counter', 0)) ) def make_key(self): key = self.name if self.issuer: key = '%s:%s' % (self.issuer, key) if self.oath_type == OATH_TYPE.TOTP and self.period != 30: key = '%d/%s' % (self.period, key) return key.encode('utf-8') @total_ordering class Credential(object): def __init__(self, key, oath_type=OATH_TYPE.TOTP, touch=False): self.key = key self.oath_type = oath_type self.touch = touch self.issuer, self.name, period = Credential.parse_key(key) self.period = period if oath_type == OATH_TYPE.TOTP else None def __lt__(self, other): a = ((self.issuer or self.name).lower(), self.name.lower()) b = ((other.issuer or other.name).lower(), other.name.lower()) return a < b @property def is_steam(self): return self.issuer == 'Steam' @property def is_hidden(self): return self.issuer == '_hidden' @property def printable_key(self): return self.key.decode('utf-8') @staticmethod def parse_key(data): if re.match(br'^\d+/', data): period, data = data.split(b'/', 1) period = int(period) else: period = 30 if b':' in data: issuer, data = data.split(b':', 1) issuer = issuer.decode('utf-8') else: issuer = None return issuer, data.decode('utf-8'), period def _derive_key(salt, passphrase): kdf = PBKDF2HMAC(hashes.SHA1(), 16, salt, 1000, default_backend()) return kdf.derive(passphrase.encode('utf-8')) def _get_device_id(device_salt): h = hashes.Hash(hashes.SHA256(), default_backend()) h.update(device_salt) d = h.finalize()[:16] return b64encode(d).replace(b'=', b'').decode() class Code(object): def __init__(self, value, valid_from, valid_to): self.value = value self.valid_from = valid_from self.valid_to = valid_to def __str__(self): return self.value class OathController(object): def __init__(self, driver): resp = driver.select(AID.OATH) tags = Tlv.parse_dict(resp) self._version = tuple(six.iterbytes(tags[TAG.VERSION])) self._salt = tags[TAG.NAME] self._id = _get_device_id(self._salt) self._challenge = tags.get(TAG.CHALLENGE) self._driver = driver @property def version(self): return self._version @property def id(self): return self._id @property def locked(self): return self._challenge is not None @property def is_in_fips_mode(self): return self.locked @property def _426device(self): return (4, 2, 0) <= self.version <= (4, 2, 6) def derive_key(self, password): return _derive_key(self._salt, password) def send_apdu(self, ins, p1, p2, data=b''): resp, sw = self._driver.send_apdu(0, ins, p1, p2, data, check=None) while (sw >> 8) == SW.MORE_DATA: more, sw = self._driver.send_apdu( 0, INS.SEND_REMAINING, 0, 0, b'', check=None) resp += more if sw != SW.OK: raise APDUError(resp, sw) return resp def reset(self): self.send_apdu(INS.RESET, 0xde, 0xad) resp = self._driver.select(AID.OATH) tags = Tlv.parse_dict(resp) self._salt = tags[TAG.NAME] self._id = _get_device_id(self._salt) def put(self, credential_data): d = credential_data key = d.make_key() secret_header = bytearray([d.oath_type | d.algorithm, d.digits]) secret = hmac_shorten_key(d.secret, d.algorithm.name) secret = secret.ljust(HMAC_MINIMUM_KEY_SIZE, b'\x00') data = Tlv(TAG.NAME, key) + Tlv(TAG.KEY, secret_header + secret) properties = 0 if d.touch: properties |= PROPERTIES.REQUIRE_TOUCH if properties: data += bytearray([TAG.PROPERTY, properties]) if d.counter > 0: data += Tlv(TAG.IMF, struct.pack('>I', d.counter)) self.send_apdu(INS.PUT, 0, 0, bytes(data)) return Credential(key, d.oath_type, d.touch) def list(self): def _gen_creds(): resp = self.send_apdu(INS.LIST, 0, 0) while resp: length = six.indexbytes(resp, 1) - 1 oath_type = OATH_TYPE(MASK.TYPE & six.indexbytes(resp, 2)) key = resp[3:3 + length] yield Credential(key, oath_type) resp = resp[3 + length:] return list(_gen_creds()) def calculate(self, cred, timestamp=None): # The 4.2.0-4.2.6 firmwares have a known issue with credentials that # require touch: If this action is performed within 2 seconds of a # command resulting in a long response (over 54 bytes), # the command will hang. A workaround is to send an invalid command # (resulting in a short reply) prior to the "calculate" command. if self._426device and cred.touch: self._driver.send_apdu(0, 0, 0, 0, '', check=SW.INVALID_INSTRUCTION) if timestamp is None: timestamp = int(time.time()) if cred.oath_type == OATH_TYPE.TOTP: valid_from = timestamp - (timestamp % cred.period) valid_to = valid_from + cred.period challenge = time_challenge(timestamp, period=cred.period) else: valid_from = timestamp valid_to = float('Inf') challenge = b'' data = Tlv(TAG.NAME, cred.key) + Tlv(TAG.CHALLENGE, challenge) resp = self.send_apdu(INS.CALCULATE, 0, 0, data) resp = Tlv(resp).value # Manual dynamic truncation is required # for Steam entries, so let's do it for all. digits = six.indexbytes(resp, 0) resp = resp[1:] offset = six.indexbytes(resp, -1) & 0xF code_data = resp[offset:offset + 4] code_data = parse_truncated(code_data) code_value = format_code(code_data, digits, steam=cred.is_steam) return Code(code_value, valid_from, valid_to) def delete(self, cred): data = Tlv(TAG.NAME, cred.key) self.send_apdu(INS.DELETE, 0, 0, data) def calculate_all(self, timestamp=None): if timestamp is None: timestamp = int(time.time()) def _gen_all(): valid_from = timestamp - (timestamp % 30) valid_to = valid_from + 30 data = Tlv(TAG.CHALLENGE, time_challenge(timestamp)) resp = self.send_apdu(INS.CALCULATE_ALL, 0, 0x01, data) tlvs = Tlv.parse_list(resp) while tlvs: key = tlvs.pop(0).value resp = tlvs.pop(0) oath_type = OATH_TYPE.HOTP if resp.tag == TAG.NO_RESPONSE else \ OATH_TYPE.TOTP touch = resp.tag == TAG.TOUCH cred = Credential(key, oath_type, touch) if resp.tag == TAG.TRUNCATED_RESPONSE: if cred.period != 30 or cred.is_steam: code = self.calculate(cred, timestamp) else: digits = six.indexbytes(resp.value, 0) code_value = parse_truncated(resp.value[1:]) code_value = format_code(code_value, digits) code = Code(code_value, valid_from, valid_to) else: code = None yield cred, code return list(_gen_all()) def set_password(self, password): key = self.derive_key(password) keydata = bytearray([OATH_TYPE.TOTP | ALGO.SHA1]) + key challenge = os.urandom(8) h = hmac.HMAC(key, hashes.SHA1(), default_backend()) h.update(challenge) response = h.finalize() data = Tlv(TAG.KEY, keydata) + Tlv(TAG.CHALLENGE, challenge) + Tlv( TAG.RESPONSE, response) self.send_apdu(INS.SET_CODE, 0, 0, data) return key def clear_password(self): self.send_apdu(INS.SET_CODE, 0, 0, Tlv(TAG.KEY, b'')) def validate(self, key): h = hmac.HMAC(key, hashes.SHA1(), default_backend()) h.update(self._challenge) response = h.finalize() challenge = os.urandom(8) h = hmac.HMAC(key, hashes.SHA1(), default_backend()) h.update(challenge) verification = h.finalize() data = Tlv(TAG.RESPONSE, response) + Tlv(TAG.CHALLENGE, challenge) resp = self.send_apdu(INS.VALIDATE, 0, 0, data) if Tlv(resp).value != verification: raise ValueError( 'Response from validation does not match verification!') self._challenge = None yubikey-manager-3.1.1/ykman/opgp.py0000644000175100001630000004131113614233340020042 0ustar runnerdocker00000000000000# Copyright (c) 2015 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import import six import time import struct import logging from .util import AID, Tlv, ensure_not_cve201715361_vulnerable_firmware_version from .driver_ccid import (APDUError, SW, GP_INS_SELECT) from enum import Enum, IntEnum, unique from binascii import b2a_hex from collections import namedtuple from cryptography import x509 from cryptography.utils import int_to_bytes, int_from_bytes from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.serialization import ( Encoding, PrivateFormat, NoEncryption ) from cryptography.hazmat.primitives.asymmetric import rsa, ec logger = logging.getLogger(__name__) _KeySlot = namedtuple('KeySlot', [ 'value', 'index', 'key_id', 'fingerprint', 'gen_time', 'uif', # touch policy 'crt' # Control Reference Template ]) @unique class KEY_SLOT(_KeySlot, Enum): # noqa: N801 SIG = _KeySlot('SIGNATURE', 1, 0xc1, 0xc7, 0xce, 0xd6, Tlv(0xb6)) ENC = _KeySlot('ENCRYPTION', 2, 0xc2, 0xc8, 0xcf, 0xd7, Tlv(0xb8)) AUT = _KeySlot('AUTHENTICATION', 3, 0xc3, 0xc9, 0xd0, 0xd8, Tlv(0xa4)) ATT = _KeySlot('ATTESTATION', 4, 0xda, 0xdb, 0xdd, 0xd9, Tlv(0xb6, Tlv(0x84, b'\x81'))) @unique class TOUCH_MODE(IntEnum): # noqa: N801 OFF = 0x00 ON = 0x01 FIXED = 0x02 CACHED = 0x03 CACHED_FIXED = 0x04 def __str__(self): if self == TOUCH_MODE.OFF: return 'Off' elif self == TOUCH_MODE.ON: return 'On' elif self == TOUCH_MODE.FIXED: return 'On (fixed)' elif self == TOUCH_MODE.CACHED: return 'Cached' elif self == TOUCH_MODE.CACHED_FIXED: return 'Cached (fixed)' @unique class INS(IntEnum): # noqa: N801 GET_DATA = 0xca GET_VERSION = 0xf1 SET_PIN_RETRIES = 0xf2 VERIFY = 0x20 TERMINATE = 0xe6 ACTIVATE = 0x44 GENERATE_ASYM = 0x47 PUT_DATA = 0xda PUT_DATA_ODD = 0xdb GET_ATTESTATION = 0xfb SEND_REMAINING = 0xc0 SELECT_DATA = 0xa5 PinRetries = namedtuple('PinRetries', ['pin', 'reset', 'admin']) PW1 = 0x81 PW3 = 0x83 INVALID_PIN = b'\0'*8 TOUCH_METHOD_BUTTON = 0x20 @unique class DO(IntEnum): AID = 0x4f PW_STATUS = 0xc4 CARDHOLDER_CERTIFICATE = 0x7f21 ATT_CERTIFICATE = 0xfc @unique class OID(bytes, Enum): SECP256R1 = b'\x2a\x86\x48\xce\x3d\x03\x01\x07' SECP256K1 = b'\x2b\x81\x04\x00\x0a' SECP384R1 = b'\x2b\x81\x04\x00\x22' SECP521R1 = b'\x2b\x81\x04\x00\x23' BRAINPOOLP256R1 = b'\x2b\x24\x03\x03\x02\x08\x01\x01\x07' BRAINPOOLP384R1 = b'\x2b\x24\x03\x03\x02\x08\x01\x01\x0b' BRAINPOOLP512R1 = b'\x2b\x24\x03\x03\x02\x08\x01\x01\x0d' X25519 = b'\x2b\x06\x01\x04\x01\x97\x55\x01\x05\x01' ED25519 = b'\x2b\x06\x01\x04\x01\xda\x47\x0f\x01' @classmethod def for_name(cls, name): try: return getattr(cls, name.upper()) except AttributeError: raise ValueError('Unsupported curve: ' + name) def _get_curve_name(key): if isinstance(key, ec.EllipticCurvePrivateKey): return key.curve.name cls_name = key.__class__.__name__ if 'Ed25519' in cls_name: return 'ed25519' if 'X25519' in cls_name: return 'x25519' raise ValueError('Unsupported private key') def _format_rsa_attributes(key_size): return struct.pack('>BHHB', 0x01, key_size, 32, 0) def _format_ec_attributes(key_slot, curve_name): if curve_name in ('ed25519', 'x25519'): algorithm = b'\x16' elif key_slot == KEY_SLOT.ENC: algorithm = b'\x12' else: algorithm = b'\x13' return algorithm + OID.for_name(curve_name) def _get_key_attributes(key, key_slot): if isinstance(key, rsa.RSAPrivateKey): if key.private_numbers().public_numbers.e != 65537: raise ValueError('RSA keys with e != 65537 are not supported!') return _format_rsa_attributes(key.key_size) curve_name = _get_curve_name(key) return _format_ec_attributes(key_slot, curve_name) def _get_key_template(key, key_slot, crt=False): def _pack_tlvs(tlvs): header = b'' body = b'' for tlv in tlvs: header += tlv[:-tlv.length] body += tlv.value return Tlv(0x7f48, header) + Tlv(0x5f48, body) if isinstance(key, rsa.RSAPrivateKey): private_numbers = key.private_numbers() ln = (key.key_size // 8) // 2 e = Tlv(0x91, b'\x01\x00\x01') # e=65537 p = Tlv(0x92, int_to_bytes(private_numbers.p, ln)) q = Tlv(0x93, int_to_bytes(private_numbers.q, ln)) values = (e, p, q) if crt: dp = Tlv(0x94, int_to_bytes(private_numbers.dmp1, ln)) dq = Tlv(0x95, int_to_bytes(private_numbers.dmq1, ln)) qinv = Tlv(0x96, int_to_bytes(private_numbers.iqmp, ln)) n = Tlv(0x97, int_to_bytes(private_numbers.public_numbers.n, 2*ln)) values += (dp, dq, qinv, n) elif isinstance(key, ec.EllipticCurvePrivateKey): private_numbers = key.private_numbers() ln = key.key_size // 8 privkey = Tlv(0x92, int_to_bytes(private_numbers.private_value, ln)) values = (privkey,) elif _get_curve_name(key) in ('ed25519', 'x25519'): privkey = Tlv( 0x92, key.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption()) ) values = (privkey,) return Tlv(0x4d, key_slot.crt + _pack_tlvs(values)) class OpgpController(object): def __init__(self, driver): self._driver = driver # Use send_apdu instead of driver.select() # to get OpenPGP specific error handling. self.send_apdu(0, GP_INS_SELECT, 0x04, 0, AID.OPGP) self._version = self._read_version() @property def version(self): return self._version def send_apdu(self, cl, ins, p1, p2, data=b'', check=SW.OK): try: return self._driver.send_apdu(cl, ins, p1, p2, data, check) except APDUError as e: # If OpenPGP is in a terminated state send activate. if e.sw in (SW.NO_INPUT_DATA, SW.CONDITIONS_NOT_SATISFIED): self._driver.send_apdu(0, INS.ACTIVATE, 0, 0) return self._driver.send_apdu(cl, ins, p1, p2, data, check) raise def send_cmd(self, cl, ins, p1=0, p2=0, data=b'', check=SW.OK): while len(data) > 0xff: self._driver.send_apdu(0x10, ins, p1, p2, data[:0xff]) data = data[0xff:] resp, sw = self._driver.send_apdu(0, ins, p1, p2, data, check=None) while (sw >> 8) == SW.MORE_DATA: more, sw = self._driver.send_apdu( 0, INS.SEND_REMAINING, 0, 0, b'', check=None) resp += more if check is None: return resp, sw elif sw != check: raise APDUError(resp, sw) return resp def _get_data(self, do): return self.send_cmd(0, INS.GET_DATA, do >> 8, do & 0xff) def _put_data(self, do, data): self.send_cmd(0, INS.PUT_DATA, do >> 8, do & 0xff, data) def _select_certificate(self, key_slot): self.send_cmd( 0, INS.SELECT_DATA, 3 - key_slot.index, 0x04, Tlv(0, Tlv(0x60, Tlv(0x5c, b'\x7f\x21')))[1:] ) def _read_version(self): bcd_hex = b2a_hex(self.send_apdu(0, INS.GET_VERSION, 0, 0)) return tuple(int(bcd_hex[i:i+2]) for i in range(0, 6, 2)) def get_openpgp_version(self): data = self._get_data(DO.AID) return (six.indexbytes(data, 6), six.indexbytes(data, 7)) def get_remaining_pin_tries(self): data = self._get_data(DO.PW_STATUS) return PinRetries(*six.iterbytes(data[4:7])) def _block_pins(self): retries = self.get_remaining_pin_tries() for _ in range(retries.pin): self.send_apdu(0, INS.VERIFY, 0, PW1, INVALID_PIN, check=None) for _ in range(retries.admin): self.send_apdu(0, INS.VERIFY, 0, PW3, INVALID_PIN, check=None) def reset(self): if self.version < (1, 0, 6): raise ValueError('Resetting OpenPGP data requires version 1.0.6 or ' 'later.') self._block_pins() self.send_apdu(0, INS.TERMINATE, 0, 0) self.send_apdu(0, INS.ACTIVATE, 0, 0) def _verify(self, pw, pin): try: pin = pin.encode('utf-8') self.send_apdu(0, INS.VERIFY, 0, pw, pin) except APDUError: pw_remaining = self.get_remaining_pin_tries()[pw-PW1] raise ValueError('Invalid PIN, {} tries remaining.'.format( pw_remaining)) def verify_pin(self, pin): self._verify(PW1, pin) def verify_admin(self, admin_pin): self._verify(PW3, admin_pin) @property def supported_touch_policies(self): if self.version < (4, 2, 0): return [] if self.version < (5, 2, 1): return [TOUCH_MODE.ON, TOUCH_MODE.OFF, TOUCH_MODE.FIXED] if self.version >= (5, 2, 1): return [ TOUCH_MODE.ON, TOUCH_MODE.OFF, TOUCH_MODE.FIXED, TOUCH_MODE.CACHED, TOUCH_MODE.CACHED_FIXED] @property def supports_attestation(self): return self.version >= (5, 2, 1) def get_touch(self, key_slot): if not self.supported_touch_policies: raise ValueError('Touch policy is available on YubiKey 4 or later.') if key_slot == KEY_SLOT.ATT and not self.supports_attestation: raise ValueError('Attestation key not available on this device.') data = self._get_data(key_slot.uif) return TOUCH_MODE(six.indexbytes(data, 0)) def set_touch(self, key_slot, mode): """Requires Admin PIN verification.""" if not self.supported_touch_policies: raise ValueError('Touch policy is available on YubiKey 4 or later.') if mode not in self.supported_touch_policies: raise ValueError('Touch policy not available on this device.') self._put_data(key_slot.uif, struct.pack('>BB', mode, TOUCH_METHOD_BUTTON)) def set_pin_retries(self, pw1_tries, pw2_tries, pw3_tries): """Requires Admin PIN verification.""" if self.version < (1, 0, 7): # For YubiKey NEO raise ValueError('Setting PIN retry counters requires version ' '1.0.7 or later.') if (4, 0, 0) <= self.version < (4, 3, 1): # For YubiKey 4 raise ValueError('Setting PIN retry counters requires version ' '4.3.1 or later.') self.send_apdu(0, INS.SET_PIN_RETRIES, 0, 0, struct.pack('>BBB', pw1_tries, pw2_tries, pw3_tries)) def read_certificate(self, key_slot): if key_slot == KEY_SLOT.ATT: data = self._get_data(DO.ATT_CERTIFICATE) else: self._select_certificate(key_slot) data = self._get_data(DO.CARDHOLDER_CERTIFICATE) if not data: raise ValueError('No certificate found!') return x509.load_der_x509_certificate(data, default_backend()) def import_certificate(self, key_slot, certificate): """Requires Admin PIN verification.""" cert_data = certificate.public_bytes(Encoding.DER) if key_slot == KEY_SLOT.ATT: self._put_data(DO.ATT_CERTIFICATE, cert_data) else: self._select_certificate(key_slot) self._put_data(DO.CARDHOLDER_CERTIFICATE, cert_data) def import_key(self, key_slot, key, fingerprint=None, timestamp=None): """Requires Admin PIN verification.""" if self.version >= (4, 0, 0): attributes = _get_key_attributes(key, key_slot) self._put_data(key_slot.key_id, attributes) template = _get_key_template(key, key_slot, self.version < (4, 0, 0)) self.send_cmd(0, INS.PUT_DATA_ODD, 0x3f, 0xff, template) if fingerprint is not None: self._put_data(key_slot.fingerprint, fingerprint) if timestamp is not None: self._put_data(key_slot.gen_time, struct.pack('>I', timestamp)) def generate_rsa_key(self, key_slot, key_size, timestamp=None): """Requires Admin PIN verification.""" ensure_not_cve201715361_vulnerable_firmware_version(self.version) if timestamp is None: timestamp = int(time.time()) neo = self.version < (4, 0, 0) if not neo: attributes = _format_rsa_attributes(key_size) self._put_data(key_slot.key_id, attributes) elif key_size != 2048: raise ValueError('Unsupported key size!') resp = self.send_cmd(0, INS.GENERATE_ASYM, 0x80, 0x00, key_slot.crt) data = Tlv.parse_dict(Tlv.unpack(0x7f49, resp)) numbers = rsa.RSAPublicNumbers( int_from_bytes(data[0x82], 'big'), int_from_bytes(data[0x81], 'big') ) self._put_data(key_slot.gen_time, struct.pack('>I', timestamp)) # TODO: Calculate and write fingerprint return numbers.public_key(default_backend()) def generate_ec_key(self, key_slot, curve_name, timestamp=None): """Requires Admin PIN verification.""" if timestamp is None: timestamp = int(time.time()) attributes = _format_ec_attributes(key_slot, curve_name) self._put_data(key_slot.key_id, attributes) resp = self.send_cmd(0, INS.GENERATE_ASYM, 0x80, 0x00, key_slot.crt) data = Tlv.parse_dict(Tlv.unpack(0x7f49, resp)) pubkey_enc = data[0x86] self._put_data(key_slot.gen_time, struct.pack('>I', timestamp)) # TODO: Calculate and write fingerprint if curve_name == 'x25519': # Added in 2.0 from cryptography.hazmat.primitives.asymmetric import x25519 return x25519.X25519PublicKey.from_public_bytes(pubkey_enc) if curve_name == 'ed25519': # Added in 2.6 from cryptography.hazmat.primitives.asymmetric import ed25519 return ed25519.Ed25519PublicKey.from_public_bytes(pubkey_enc) curve = getattr(ec, curve_name.upper()) try: # Added in cryptography 2.5 return ec.EllipticCurvePublicKey.from_encoded_point( curve(), pubkey_enc ) except AttributeError: return ec.EllipticCurvePublicNumbers.from_encoded_point( curve(), pubkey_enc ).public_key(default_backend()) def delete_key(self, key_slot): """Requires Admin PIN verification.""" if self.version < (4, 0, 0): # Import over the key self.import_key( key_slot, rsa.generate_private_key(65537, 2048, default_backend()), b'\0' * 20, 0 ) else: # Delete key by changing the key attributes twice. self._put_data(key_slot.key_id, _format_rsa_attributes(4096)) self._put_data(key_slot.key_id, _format_rsa_attributes(2048)) def delete_certificate(self, key_slot): """Requires Admin PIN verification.""" if key_slot == KEY_SLOT.ATT: self._put_data(DO.ATT_CERTIFICATE, b'') else: self._select_certificate(key_slot) self._put_data(DO.CARDHOLDER_CERTIFICATE, b'') def attest(self, key_slot): """Requires User PIN verification.""" self.send_apdu(0x80, INS.GET_ATTESTATION, key_slot.index, 0) return self.read_certificate(key_slot) yubikey-manager-3.1.1/ykman/otp.py0000644000175100001630000004312713614233340017706 0ustar runnerdocker00000000000000# Copyright (c) 2018 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import import json import logging import time from ctypes import sizeof, byref, c_uint, create_string_buffer from enum import Enum from six.moves import http_client from .driver_otp import ykpers, check, YkpersError from .util import (time_challenge, parse_totp_hash, format_code, hmac_shorten_key, modhex_encode) from .scancodes import encode, KEYBOARD_LAYOUT from . import __version__ from enum import IntEnum, unique from binascii import a2b_hex, b2a_hex logger = logging.getLogger(__name__) UPLOAD_HOST = 'upload.yubico.com' UPLOAD_PATH = '/prepare' @unique class SLOT(IntEnum): CONFIG = 0x01 CONFIG2 = 0x03 UPDATE1 = 0x04 UPDATE2 = 0x05 SWAP = 0x06 SLOTS = [-1, 0x30, 0x38] _ACCESS_CODE_LENGTH = 6 _RESET_ACCESS_CODE = b'\x00' * _ACCESS_CODE_LENGTH def slot_to_cmd(slot, update=False): if slot == 1: return SLOT.UPDATE1 if update else SLOT.CONFIG elif slot == 2: return SLOT.UPDATE2 if update else SLOT.CONFIG2 else: raise ValueError('slot must be 1 or 2') class PrepareUploadError(Enum): # Defined here CONNECTION_FAILED = 'Failed to open HTTPS connection.' NOT_FOUND = 'Upload request not recognized by server.' SERVICE_UNAVAILABLE = 'Service temporarily unavailable, please try again later.' # noqa: E501 # Defined in upload project PRIVATE_ID_INVALID_LENGTH = 'Private ID must be 12 characters long.' PRIVATE_ID_NOT_HEX = 'Private ID must consist only of hex characters (0-9A-F).' # noqa: E501 PRIVATE_ID_UNDEFINED = 'Private ID is required.' PUBLIC_ID_INVALID_LENGTH = 'Public ID must be 12 characters long.' PUBLIC_ID_NOT_MODHEX = 'Public ID must consist only of modhex characters (cbdefghijklnrtuv).' # noqa: E501 PUBLIC_ID_NOT_VV = 'Public ID must begin with "vv".' PUBLIC_ID_OCCUPIED = 'Public ID is already in use.' PUBLIC_ID_UNDEFINED = 'Public ID is required.' SECRET_KEY_INVALID_LENGTH = 'Secret key must be 32 character long.' SECRET_KEY_NOT_HEX = 'Secret key must consist only of hex characters (0-9A-F).' # noqa: E501 SECRET_KEY_UNDEFINED = 'Secret key is required.' SERIAL_NOT_INT = 'Serial number must be an integer.' SERIAL_TOO_LONG = 'Serial number is too long.' def message(self): return self.value class PrepareUploadFailed(Exception): def __init__(self, status, content, error_ids): super(PrepareUploadFailed, self).__init__( 'Upload to YubiCloud failed with status {}: {}' .format(status, content)) self.status = status self.content = content self.errors = [ e if isinstance(e, PrepareUploadError) else PrepareUploadError[e] for e in error_ids] def messages(self): return [e.message() for e in self.errors] class SlotConfig(object): def __init__( self, serial_api_visible=True, allow_update=True, append_cr=True, pacing=None, numeric_keypad=False, ): self.serial_api_visible = serial_api_visible self.allow_update = allow_update self.append_cr = append_cr self.pacing = pacing self.numeric_keypad = numeric_keypad class _SlotConfigContext(object): def __init__(self, dev, cmd, conf): st = ykpers.ykds_alloc() self.cfg = ykpers.ykp_alloc() try: check(ykpers.yk_get_status(dev, st)) ykpers.ykp_configure_version(self.cfg, st) ykpers.ykp_configure_command(self.cfg, cmd) except YkpersError: ykpers.ykp_free_config(self.cfg) raise finally: ykpers.ykds_free(st) if conf is None: conf = SlotConfig() self._apply(conf) def _apply(self, config): if config.serial_api_visible: check(ykpers.ykp_set_extflag(self.cfg, 'SERIAL_API_VISIBLE')) if config.allow_update: check(ykpers.ykp_set_extflag(self.cfg, 'ALLOW_UPDATE')) if config.append_cr: check(ykpers.ykp_set_tktflag(self.cfg, 'APPEND_CR')) # Output speed throttling if config.pacing == 20: check(ykpers.ykp_set_cfgflag(self.cfg, 'PACING_10MS')) elif config.pacing == 40: check(ykpers.ykp_set_cfgflag(self.cfg, 'PACING_20MS')) elif config.pacing == 60: check(ykpers.ykp_set_cfgflag(self.cfg, 'PACING_10MS')) check(ykpers.ykp_set_cfgflag(self.cfg, 'PACING_20MS')) if config.numeric_keypad: check(ykpers.ykp_set_extflag(self.cfg, 'USE_NUMERIC_KEYPAD')) def __enter__(self): return self.cfg def __exit__(self, type, value, traceback): ykpers.ykp_free_config(self.cfg) class OtpController(object): def __init__(self, driver): self._driver = driver self._dev = driver.ykpers_dev self._access_code = None @property def access_code(self): return self._access_code @access_code.setter def access_code(self, value): self._access_code = value def _create_cfg(self, cmd, conf=None): context = _SlotConfigContext(self._dev, cmd, conf) if self.access_code is not None: check(ykpers.ykp_set_access_code( context.cfg, self.access_code, _ACCESS_CODE_LENGTH)) return context @property def slot_status(self): return self._driver.slot_status def program_otp(self, slot, key, fixed, uid, config=None): if len(key) != 16: raise ValueError('key must be 16 bytes') if len(uid) != 6: raise ValueError('private ID must be 6 bytes') if len(fixed) > 16: raise ValueError('public ID must be <= 16 bytes') cmd = slot_to_cmd(slot) with self._create_cfg(cmd, config) as cfg: check(ykpers.ykp_set_fixed(cfg, fixed, len(fixed))) check(ykpers.ykp_set_uid(cfg, uid, 6)) ykpers.ykp_AES_key_from_raw(cfg, key) check(ykpers.yk_write_command( self._dev, ykpers.ykp_core_config(cfg), cmd, self.access_code )) def prepare_upload_key(self, key, public_id, private_id, serial=None, user_agent='python-yubikey-manager/' + __version__): modhex_public_id = modhex_encode(public_id) data = { 'aes_key': b2a_hex(key).decode('utf-8'), 'serial': serial or 0, 'public_id': modhex_public_id, 'private_id': b2a_hex(private_id).decode('utf-8'), } httpconn = http_client.HTTPSConnection(UPLOAD_HOST, timeout=1) try: httpconn.request('POST', UPLOAD_PATH, body=json.dumps(data, indent=False, sort_keys=True) .encode('utf-8'), headers={ 'Content-Type': 'application/json', 'User-Agent': user_agent, }) except Exception as e: logger.error('Failed to connect to %s', UPLOAD_HOST, exc_info=e) raise PrepareUploadFailed( None, None, [PrepareUploadError.CONNECTION_FAILED]) resp = httpconn.getresponse() if resp.status == 200: url = json.loads(resp.read().decode('utf-8'))['finish_url'] return url else: resp_body = resp.read() logger.debug('Upload failed with status %d: %s', resp.status, resp_body) if resp.status == 404: raise PrepareUploadFailed( resp.status, resp_body, [PrepareUploadError.NOT_FOUND]) elif resp.status == 503: raise PrepareUploadFailed( resp.status, resp_body, [PrepareUploadError.SERVICE_UNAVAILABLE]) else: try: errors = json.loads(resp_body.decode('utf-8')).get('errors') except Exception: errors = [] raise PrepareUploadFailed(resp.status, resp_body, errors) def program_static( self, slot, password, keyboard_layout=KEYBOARD_LAYOUT.MODHEX, config=None ): pw_len = len(password) if self._driver.version < (2, 0, 0): raise ValueError('static password requires YubiKey 2.0.0 or later') elif self._driver.version < (2, 2, 0) and pw_len > 16: raise ValueError('password too long, this device supports a ' 'maximum of %d characters' % 16) elif pw_len > 38: raise ValueError('password too long, this device supports a ' 'maximum of %d characters' % 38) cmd = slot_to_cmd(slot) with self._create_cfg(cmd, config) as cfg: check(ykpers.ykp_set_cfgflag(cfg, 'SHORT_TICKET')) pw_bytes = encode(password, keyboard_layout=keyboard_layout) if pw_len <= 16: # All in fixed check(ykpers.ykp_set_fixed(cfg, pw_bytes, pw_len)) elif pw_len <= 16 + 6: # All in fixed and uid check(ykpers.ykp_set_fixed(cfg, pw_bytes[:-6], pw_len - 6)) check(ykpers.ykp_set_uid(cfg, pw_bytes[-6:], 6)) else: # All in fixed + uid + key check(ykpers.ykp_set_fixed(cfg, pw_bytes[:-22], pw_len - 22)) check(ykpers.ykp_set_uid(cfg, pw_bytes[-22:-16], 6)) ykpers.ykp_AES_key_from_raw(cfg, pw_bytes[-16:]) check(ykpers.yk_write_command( self._dev, ykpers.ykp_core_config(cfg), cmd, self.access_code)) def program_chalresp(self, slot, key, touch=False, config=None): if self._driver.version < (2, 2, 0): raise ValueError('challenge-response requires YubiKey 2.2.0 or ' 'later') key = hmac_shorten_key(key, 'SHA1') if len(key) > 20: raise ValueError('key lengths >20 bytes not supported') cmd = slot_to_cmd(slot) key = key.ljust(20, b'\0') # Pad key to 20 bytes with self._create_cfg(cmd, config) as cfg: check(ykpers.ykp_set_tktflag(cfg, 'CHAL_RESP')) check(ykpers.ykp_set_cfgflag(cfg, 'CHAL_HMAC')) check(ykpers.ykp_set_cfgflag(cfg, 'HMAC_LT64')) if touch: check(ykpers.ykp_set_cfgflag(cfg, 'CHAL_BTN_TRIG')) ykpers.ykp_HMAC_key_from_raw(cfg, key) check(ykpers.yk_write_command( self._dev, ykpers.ykp_core_config(cfg), cmd, self.access_code)) def calculate( self, slot, challenge=None, totp=False, digits=6, wait_for_touch=True): if totp: if challenge is None: challenge = time_challenge(time.time()) else: challenge = time_challenge(challenge) else: challenge = a2b_hex(challenge) # Pad challenge when < 64 bytes challenge = challenge.ljust( 64, b'\1' if challenge.endswith(b'\0') else b'\0') resp = create_string_buffer(64) # Some versions of the NEO firmware returns error 11 too often. # Give the YubiKey 10 tries to do the calculation. for idx in range(10): try: logger.debug( 'Sending a challenge to the device. Slot %s. ' 'Attempt %s. Wait for touch is %s.', slot, idx + 1, wait_for_touch) check(ykpers.yk_challenge_response( self._dev, SLOTS[slot], wait_for_touch, len(challenge), challenge, sizeof(resp), resp)) except YkpersError as e: if idx < 10 and e.errno == 11 and wait_for_touch is True: # Error 11 when wait_for_touch is true is an unexpected # state, let's try again. continue elif wait_for_touch is False: logger.debug('Got %s as expected.', e) # NEOs and very old YK4s might still be blinking, # lets try to read the serial to cancel it. ykpers.yk_get_serial(self._dev, 0, 0, byref(c_uint())) raise else: logger.debug('YkpersError: %s', e) raise # We got a result, break the loop. break if totp: return format_code(parse_totp_hash(resp.raw[:20]), digits) else: return b2a_hex(resp.raw[:20]) def program_hotp(self, slot, key, imf=0, hotp8=False, config=None): if self._driver.version < (2, 1, 0): raise ValueError('HOTP requires YubiKey 2.1.0 or later') key = hmac_shorten_key(key, 'SHA1') if len(key) > 20: raise ValueError('key lengths >20 bytes not supported') key = key.ljust(20, b'\0') # Pad key to 20 bytes if imf % 16 != 0: raise ValueError('imf must be a multiple of 16') cmd = slot_to_cmd(slot) with self._create_cfg(cmd, config) as cfg: check(ykpers.ykp_set_tktflag(cfg, 'OATH_HOTP')) check(ykpers.ykp_set_oath_imf(cfg, imf)) if hotp8: check(ykpers.ykp_set_cfgflag(cfg, 'OATH_HOTP8')) ykpers.ykp_HMAC_key_from_raw(cfg, key) check(ykpers.yk_write_command( self._dev, ykpers.ykp_core_config(cfg), cmd, self.access_code)) def zap_slot(self, slot): check(ykpers.yk_write_command(self._dev, None, slot_to_cmd(slot), self.access_code)) def swap_slots(self): if self._driver.version < (2, 3, 0): raise ValueError('swapping slots requires YubiKey 2.3.0 or later') with self._create_cfg(SLOT.SWAP) as cfg: ycfg = ykpers.ykp_core_config(cfg) check(ykpers.yk_write_command(self._dev, ycfg, SLOT.SWAP, None)) def configure_ndef_slot(self, slot, prefix='https://my.yubico.com/yk/#'): ndef = ykpers.ykp_alloc_ndef() try: check(ykpers.ykp_construct_ndef_uri(ndef, prefix.encode())) check(ykpers.yk_write_ndef2(self._dev, ndef, slot)) finally: ykpers.ykp_free_ndef(ndef) @property def _has_update_access_code_bug(self): return (4, 3, 1) < self._driver.version < (4, 3, 6) def set_access_code(self, slot, new_code=None, update=True, allow_zero=False): if update and self._driver.version < (2, 3, 0): raise ValueError('Update requires YubiKey 2.3.0 or later') if not update and new_code is not None: raise ValueError('Cannot set new access code unless updating slot') if new_code == _RESET_ACCESS_CODE: raise ValueError('Cannot set access code to special value zero.') if new_code is not None and self._has_update_access_code_bug: raise ValueError( 'This YubiKey firmware does not support updating the access ' 'code after programming the slot. Please set the access ' 'code when initially programming the slot instead.') cmd = slot_to_cmd(slot, update) with self._create_cfg(cmd) as cfg: check(ykpers.ykp_set_access_code( cfg, new_code or _RESET_ACCESS_CODE, _ACCESS_CODE_LENGTH)) ycfg = ykpers.ykp_core_config(cfg) check(ykpers.yk_write_command(self._dev, ycfg, cmd, self.access_code)) self.access_code = new_code def delete_access_code(self, slot): if self._has_update_access_code_bug: raise ValueError( 'This YubiKey firmware does not support deleting the access ' 'code after programming the slot. Please delete and re-program ' 'the slot instead.') self.set_access_code(slot, None) def update_settings(self, slot, config=None): cmd = slot_to_cmd(slot, update=True) with self._create_cfg(cmd, config) as cfg: check(ykpers.yk_write_command( self._dev, ykpers.ykp_core_config(cfg), cmd, self.access_code)) @property def is_in_fips_mode(self): return self._driver.is_in_fips_mode yubikey-manager-3.1.1/ykman/piv.py0000644000175100001630000011035313614233340017676 0ustar runnerdocker00000000000000# Copyright (c) 2017 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import from enum import IntEnum, unique from .device import YubiKey from .driver_ccid import APDUError, SW from .util import ( AID, Tlv, is_cve201715361_vulnerable_firmware_version, ensure_not_cve201715361_vulnerable_firmware_version) from cryptography import x509 from cryptography.exceptions import InvalidSignature from cryptography.utils import int_to_bytes, int_from_bytes from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.constant_time import bytes_eq from cryptography.hazmat.primitives.asymmetric import rsa, ec, padding from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.backends import default_backend from cryptography.x509.oid import NameOID from collections import OrderedDict from threading import Timer import logging import struct import six import os logger = logging.getLogger(__name__) @unique class INS(IntEnum): VERIFY = 0x20 CHANGE_REFERENCE = 0x24 RESET_RETRY = 0x2c GENERATE_ASYMMETRIC = 0x47 AUTHENTICATE = 0x87 SEND_REMAINING = 0xc0 GET_DATA = 0xcb PUT_DATA = 0xdb SET_MGMKEY = 0xff IMPORT_KEY = 0xfe GET_VERSION = 0xfd RESET = 0xfb SET_PIN_RETRIES = 0xfa ATTEST = 0xf9 @unique class ALGO(IntEnum): TDES = 0x03 RSA1024 = 0x06 RSA2048 = 0x07 ECCP256 = 0x11 ECCP384 = 0x14 @classmethod def from_public_key(cls, key): if isinstance(key, rsa.RSAPublicKey): return getattr(cls, 'RSA%d' % key.key_size) elif isinstance(key, ec.EllipticCurvePublicKey): curve_name = key.curve.name if curve_name == 'secp256r1': return cls.ECCP256 elif curve_name == 'secp384r1': return cls.ECCP384 raise UnsupportedAlgorithm( 'Unsupported key type: %s' % type(key), key=key) @classmethod def is_rsa(cls, algorithm_int): # Implemented as "not not RSA" to reduce risk of false negatives if # more algorithms are added return not ( algorithm_int == cls.TDES or algorithm_int == cls.ECCP256 or algorithm_int == cls.ECCP384 ) @unique class SLOT(IntEnum): AUTHENTICATION = 0x9a CARD_MANAGEMENT = 0x9b SIGNATURE = 0x9c KEY_MANAGEMENT = 0x9d CARD_AUTH = 0x9e RETIRED1 = 0x82 RETIRED2 = 0x83 RETIRED3 = 0x84 RETIRED4 = 0x85 RETIRED5 = 0x86 RETIRED6 = 0x87 RETIRED7 = 0x88 RETIRED8 = 0x89 RETIRED9 = 0x8a RETIRED10 = 0x8b RETIRED11 = 0x8c RETIRED12 = 0x8d RETIRED13 = 0x8e RETIRED14 = 0x8f RETIRED15 = 0x90 RETIRED16 = 0x91 RETIRED17 = 0x92 RETIRED18 = 0x93 RETIRED19 = 0x94 RETIRED20 = 0x95 ATTESTATION = 0xf9 @unique class OBJ(IntEnum): CAPABILITY = 0x5fc107 CHUID = 0x5fc102 AUTHENTICATION = 0x5fc105 # cert for 9a key FINGERPRINTS = 0x5fc103 SECURITY = 0x5fc106 FACIAL = 0x5fc108 SIGNATURE = 0x5fc10a # cert for 9c key KEY_MANAGEMENT = 0x5fc10b # cert for 9d key CARD_AUTH = 0x5fc101 # cert for 9e key DISCOVERY = 0x7e KEY_HISTORY = 0x5fc10c IRIS = 0x5fc121 RETIRED1 = 0x5fc10d RETIRED2 = 0x5fc10e RETIRED3 = 0x5fc10f RETIRED4 = 0x5fc110 RETIRED5 = 0x5fc111 RETIRED6 = 0x5fc112 RETIRED7 = 0x5fc113 RETIRED8 = 0x5fc114 RETIRED9 = 0x5fc115 RETIRED10 = 0x5fc116 RETIRED11 = 0x5fc117 RETIRED12 = 0x5fc118 RETIRED13 = 0x5fc119 RETIRED14 = 0x5fc11a RETIRED15 = 0x5fc11b RETIRED16 = 0x5fc11c RETIRED17 = 0x5fc11d RETIRED18 = 0x5fc11e RETIRED19 = 0x5fc11f RETIRED20 = 0x5fc120 PIVMAN_DATA = 0x5fff00 PIVMAN_PROTECTED_DATA = 0x5fc109 # Use slot for printed information. ATTESTATION = 0x5fff01 @classmethod def from_slot(cls, slot): return getattr(cls, SLOT(slot).name) @unique class TAG(IntEnum): DYN_AUTH = 0x7c OBJ_ID = 0x5c OBJ_DATA = 0x53 CERTIFICATE = 0x70 CERT_INFO = 0x71 ALGO = 0x80 PIN_POLICY = 0xaa TOUCH_POLICY = 0xab LRC = 0xfe @unique class PIN_POLICY(IntEnum): DEFAULT = 0x0 NEVER = 0x1 ONCE = 0x2 ALWAYS = 0x3 @unique class TOUCH_POLICY(IntEnum): DEFAULT = 0x0 NEVER = 0x1 ALWAYS = 0x2 CACHED = 0x3 class AuthenticationFailed(Exception): def __init__(self, message, sw, applet_version): super(AuthenticationFailed, self).__init__(message) self.tries_left = ( tries_left(sw, applet_version) if is_verify_fail(sw, applet_version) else None) class AuthenticationBlocked(AuthenticationFailed): def __init__(self, message, sw): # Dummy applet_version since sw will always be "authentication blocked" super(AuthenticationBlocked, self).__init__(message, sw, ()) class BadFormat(Exception): def __init__(self, message, bad_value): super(BadFormat, self).__init__(message) self.bad_value = bad_value class InvalidCertificate(Exception): def __init__(self, slot): super(InvalidCertificate, self).__init__( 'Failed to parse certificate in slot {:x}'.format(slot)) self.slot = slot class KeypairMismatch(Exception): def __init__(self, slot, cert): super(KeypairMismatch, self).__init__( 'The certificate does not match the private key in slot %s.' % slot) self.slot = slot self.cert = cert class UnsupportedAlgorithm(Exception): def __init__(self, message, algorithm_id=None, key=None, ): super(UnsupportedAlgorithm, self).__init__(message) if algorithm_id is None and key is None: raise ValueError( 'At least one of algorithm_id and key must be given.') self.algorithm_id = algorithm_id self.key = key class WrongPin(AuthenticationFailed): def __init__(self, sw, applet_version): super(WrongPin, self).__init__( 'Incorrect PIN', sw, applet_version) class WrongPuk(AuthenticationFailed): def __init__(self, sw, applet_version): super(WrongPuk, self).__init__( 'Incorrect PUK', sw, applet_version) PIN = 0x80 PUK = 0x81 # 010203040506070801020304050607080102030405060708 DEFAULT_MANAGEMENT_KEY = b'\x01\x02\x03\x04\x05\x06\x07\x08' \ + b'\x01\x02\x03\x04\x05\x06\x07\x08' \ + b'\x01\x02\x03\x04\x05\x06\x07\x08' def _pack_pin(pin): if isinstance(pin, six.text_type): pin = pin.encode('utf8') if len(pin) > 8: raise BadFormat( 'PIN/PUK too large (max 8 bytes, was %d)' % len(pin), pin) return pin.ljust(8, b'\xff') def _get_key_data(key): if isinstance(key, rsa.RSAPrivateKey): if key.public_key().public_numbers().e != 65537: raise UnsupportedAlgorithm( 'Unsupported RSA exponent: %d' % key.public_key().public_numbers().e, key=key) if key.key_size == 1024: algo = ALGO.RSA1024 ln = 64 elif key.key_size == 2048: algo = ALGO.RSA2048 ln = 128 else: raise UnsupportedAlgorithm( 'Unsupported RSA key size: %d' % key.key_size, key=key) priv = key.private_numbers() data = Tlv(0x01, int_to_bytes(priv.p, ln)) + \ Tlv(0x02, int_to_bytes(priv.q, ln)) + \ Tlv(0x03, int_to_bytes(priv.dmp1, ln)) + \ Tlv(0x04, int_to_bytes(priv.dmq1, ln)) + \ Tlv(0x05, int_to_bytes(priv.iqmp, ln)) elif isinstance(key, ec.EllipticCurvePrivateKey): if isinstance(key.curve, ec.SECP256R1): algo = ALGO.ECCP256 ln = 32 elif isinstance(key.curve, ec.SECP384R1): algo = ALGO.ECCP384 ln = 48 else: raise UnsupportedAlgorithm( 'Unsupported elliptic curve: %s', key.curve, key=key) priv = key.private_numbers() data = Tlv(0x06, int_to_bytes(priv.private_value, ln)) else: raise UnsupportedAlgorithm('Unsupported key type!', key=key) return algo, data def _dummy_key(algorithm): if algorithm == ALGO.RSA1024: return rsa.generate_private_key(65537, 1024, default_backend()) if algorithm == ALGO.RSA2048: return rsa.generate_private_key(65537, 2048, default_backend()) if algorithm == ALGO.ECCP256: return ec.generate_private_key(ec.SECP256R1(), default_backend()) if algorithm == ALGO.ECCP384: return ec.generate_private_key(ec.SECP384R1(), default_backend()) raise UnsupportedAlgorithm( 'Unsupported algorithm: %s' % algorithm, algorithm_id=algorithm) def _pkcs1_15_pad(algorithm, message): h = hashes.Hash(hashes.SHA256(), default_backend()) h.update(message) t = b'\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05' + \ b'\x00\x04\x20' + h.finalize() em_len = 128 if algorithm == ALGO.RSA1024 else 256 f_len = em_len - len(t) - 3 return b'\0\1' + b'\xff' * f_len + b'\0' + t _sign_len_conditions = { ALGO.RSA1024: lambda ln: ln == 128, ALGO.RSA2048: lambda ln: ln == 256, ALGO.ECCP256: lambda ln: ln <= 32, ALGO.ECCP384: lambda ln: ln <= 48 } _decrypt_len_conditions = { ALGO.RSA1024: lambda ln: ln == 128, ALGO.RSA2048: lambda ln: ln == 256, ALGO.ECCP256: lambda ln: ln == 65, ALGO.ECCP384: lambda ln: ln == 97 } def _derive_key(pin, salt): kdf = PBKDF2HMAC(hashes.SHA1(), 24, salt, 10000, default_backend()) return kdf.derive(pin.encode('utf-8')) def generate_random_management_key(): return os.urandom(24) def is_verify_fail(sw, applet_version): if applet_version < (1, 0, 4): return 0x6300 <= sw <= 0x63ff else: return SW.is_verify_fail(sw) def tries_left(sw, applet_version): if applet_version < (1, 0, 4): if sw == SW.AUTH_METHOD_BLOCKED: return 0 if not is_verify_fail(sw, applet_version): raise ValueError( 'Cannot read remaining tries from status word: %x' % sw) return sw & 0xff else: return SW.tries_left(sw) class PivmanData(object): def __init__(self, raw_data=Tlv(0x80)): data = Tlv.parse_dict(Tlv(raw_data).value) self._flags = struct.unpack( '>B', data[0x81])[0] if 0x81 in data else None self.salt = data.get(0x82) self.pin_timestamp = struct.unpack('>I', data[0x83]) \ if 0x83 in data else None def _get_flag(self, mask): return bool((self._flags or 0) & mask) def _set_flag(self, mask, value): if value: self._flags = (self._flags or 0) | mask elif self._flags is not None: self._flags &= ~mask @property def puk_blocked(self): return self._get_flag(0x01) @puk_blocked.setter def puk_blocked(self, value): self._set_flag(0x01, value) @property def mgm_key_protected(self): return self._get_flag(0x02) @mgm_key_protected.setter def mgm_key_protected(self, value): self._set_flag(0x02, value) def get_bytes(self): data = b'' if self._flags is not None: data += Tlv(0x81, struct.pack('>B', self._flags)) if self.salt is not None: data += Tlv(0x82, self.salt) if self.pin_timestamp is not None: data += Tlv(0x83, struct.pack('>I', self.pin_timestamp)) return Tlv(0x80, data) class PivmanProtectedData(object): def __init__(self, raw_data=Tlv(0x88)): data = Tlv.parse_dict(Tlv(raw_data).value) self.key = data.get(0x89) def get_bytes(self): data = b'' if self.key is not None: data += Tlv(0x89, self.key) return Tlv(0x88, data) class PivController(object): def __init__(self, driver): driver.select(AID.PIV) self._authenticated = False self._driver = driver self._version = self._read_version() self._update_pivman_data() def _update_pivman_data(self): try: self._pivman_data = PivmanData(self.get_data(OBJ.PIVMAN_DATA)) except APDUError: self._pivman_data = PivmanData() @property def version(self): return self._version @property def has_protected_key(self): return self.has_derived_key or self.has_stored_key @property def has_derived_key(self): return self._pivman_data.salt is not None @property def has_stored_key(self): return self._pivman_data.mgm_key_protected @property def puk_blocked(self): return self._pivman_data.puk_blocked def send_cmd(self, ins, p1=0, p2=0, data=b'', check=SW.OK): while len(data) > 0xff: self._driver.send_apdu(0x10, ins, p1, p2, data[:0xff]) data = data[0xff:] resp, sw = self._driver.send_apdu(0, ins, p1, p2, data, check=None) while (sw >> 8) == SW.MORE_DATA: more, sw = self._driver.send_apdu( 0, INS.SEND_REMAINING, 0, 0, b'', check=None) resp += more if check is None: return resp, sw elif sw != check: raise APDUError(resp, sw) return resp def _read_version(self): return tuple(six.iterbytes(self.send_cmd(INS.GET_VERSION))) def _init_pivman_protected(self): try: self._pivman_protected_data = PivmanProtectedData( self.get_data(OBJ.PIVMAN_PROTECTED_DATA)) except APDUError as e: if e.sw == SW.NOT_FOUND: # No data there, initialise a new object. self._pivman_protected_data = PivmanProtectedData() else: raise def verify(self, pin, touch_callback=None): try: self.send_cmd(INS.VERIFY, 0, PIN, _pack_pin(pin)) except APDUError as e: if e.sw == SW.AUTH_METHOD_BLOCKED: raise AuthenticationBlocked('PIN is blocked.', e.sw) elif is_verify_fail(e.sw, self.version): raise WrongPin(e.sw, self.version) raise if self.has_derived_key and not self._authenticated: self.authenticate( _derive_key(pin, self._pivman_data.salt), touch_callback) self.verify(pin, touch_callback) if self.has_stored_key and not self._authenticated: self._init_pivman_protected() self.authenticate(self._pivman_protected_data.key, touch_callback) self.verify(pin, touch_callback) def change_pin(self, old_pin, new_pin): try: self.send_cmd(INS.CHANGE_REFERENCE, 0, PIN, _pack_pin(old_pin) + _pack_pin(new_pin)) except APDUError as e: if e.sw == SW.AUTH_METHOD_BLOCKED: raise AuthenticationBlocked('PIN is blocked.', e.sw) elif is_verify_fail(e.sw, self.version): raise WrongPin(e.sw, self.version) raise if self.has_derived_key: if not self._authenticated: self.authenticate(_derive_key(old_pin, self._pivman_data.salt)) self.use_derived_key(new_pin) def change_puk(self, old_puk, new_puk): try: self.send_cmd(INS.CHANGE_REFERENCE, 0, PUK, _pack_pin(old_puk) + _pack_pin(new_puk)) except APDUError as e: if e.sw == SW.AUTH_METHOD_BLOCKED: raise AuthenticationBlocked('PUK is blocked.', e.sw) elif is_verify_fail(e.sw, self.version): raise WrongPuk(e.sw, self.version) raise def unblock_pin(self, puk, new_pin): try: self.send_cmd( INS.RESET_RETRY, 0, PIN, _pack_pin(puk) + _pack_pin(new_pin)) except APDUError as e: if e.sw == SW.AUTH_METHOD_BLOCKED: raise AuthenticationBlocked('PUK is blocked.', e.sw) elif is_verify_fail(e.sw, self.version): raise WrongPuk(e.sw, self.version) raise def set_pin_retries(self, pin_retries, puk_retries): self.send_cmd(INS.SET_PIN_RETRIES, pin_retries, puk_retries) def use_derived_key(self, pin, touch=False): self.verify(pin) if not self.puk_blocked: self._block_puk() self._pivman_data.puk_blocked = True new_salt = os.urandom(16) new_key = _derive_key(pin, new_salt) self.send_cmd(INS.SET_MGMKEY, 0xff, 0xfe if touch else 0xff, six.int2byte(ALGO.TDES) + Tlv(SLOT.CARD_MANAGEMENT, new_key)) self._pivman_data.salt = new_salt self.put_data(OBJ.PIVMAN_DATA, self._pivman_data.get_bytes()) def set_pin_timestamp(self, timestamp): self._pivman_data.pin_timestamp = timestamp self.put_data(OBJ.PIVMAN_DATA, self._pivman_data.get_bytes()) def authenticate(self, key, touch_callback=None): ct1 = self.send_cmd(INS.AUTHENTICATE, ALGO.TDES, SLOT.CARD_MANAGEMENT, Tlv(TAG.DYN_AUTH, Tlv(0x80)))[4:12] backend = default_backend() try: cipher_key = algorithms.TripleDES(key) except ValueError: raise BadFormat('Management key must be exactly 24 bytes long, ' 'was: {}'.format(len(key)), None) cipher = Cipher(cipher_key, modes.ECB(), backend) decryptor = cipher.decryptor() pt1 = decryptor.update(ct1) + decryptor.finalize() ct2 = os.urandom(8) if touch_callback is not None: touch_timer = Timer(0.500, touch_callback) touch_timer.start() try: pt2 = self.send_cmd( INS.AUTHENTICATE, ALGO.TDES, SLOT.CARD_MANAGEMENT, Tlv(TAG.DYN_AUTH, Tlv(0x80, pt1) + Tlv(0x81, ct2)) )[4:12] except APDUError as e: if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED: raise AuthenticationFailed( 'Incorrect management key', e.sw, self.version) logger.error('Failed to authenticate management key.', exc_info=e) raise except Exception as e: logger.error('Failed to authenticate management key.', exc_info=e) raise finally: if touch_callback is not None: touch_timer.cancel() encryptor = cipher.encryptor() pt2_cmp = encryptor.update(ct2) + encryptor.finalize() if not bytes_eq(pt2, pt2_cmp): raise ValueError('Device challenge did not match!') self._authenticated = True def set_mgm_key(self, new_key, touch=False, store_on_device=False): # If the key should be protected by PIN and no key is given, # we generate a random key. if not new_key: if store_on_device: new_key = generate_random_management_key() else: raise ValueError('new_key was not given and ' 'store_on_device was not True') if len(new_key) != 24: raise BadFormat( 'Management key must be exactly 24 bytes long, was: {}'.format( len(new_key)), new_key) if store_on_device or (not store_on_device and self.has_stored_key): # Ensure we have access to protected data before overwriting key try: self._init_pivman_protected() except Exception as e: logger.debug('Failed to initialize protected pivman data', exc_info=e) if store_on_device: raise # Set the new management key self.send_cmd( INS.SET_MGMKEY, 0xff, 0xfe if touch else 0xff, six.int2byte(ALGO.TDES) + Tlv(SLOT.CARD_MANAGEMENT, new_key)) if self.has_derived_key: # Clear salt for old derived keys. self._pivman_data.salt = None # Set flag for stored or not stored key. self._pivman_data.mgm_key_protected = store_on_device # Update readable pivman data self.put_data(OBJ.PIVMAN_DATA, self._pivman_data.get_bytes()) if store_on_device: # Store key in protected pivman data self._pivman_protected_data.key = new_key self.put_data( OBJ.PIVMAN_PROTECTED_DATA, self._pivman_protected_data.get_bytes()) elif not store_on_device and self.has_stored_key: # If new key should not be stored and there is an old stored key, # try to clear it. try: self._pivman_protected_data.key = None self.put_data( OBJ.PIVMAN_PROTECTED_DATA, self._pivman_protected_data.get_bytes()) except APDUError as e: logger.debug("No PIN provided, can't clear key..", exc_info=e) # Update CHUID and CCC if not set try: self.get_data(OBJ.CAPABILITY) except APDUError as e: if e.sw == SW.NOT_FOUND: self.update_ccc() else: logger.debug('Failed to read CCC...', exc_info=e) try: self.get_data(OBJ.CHUID) except APDUError as e: if e.sw == SW.NOT_FOUND: self.update_chuid() else: logger.debug('Failed to read CHUID...', exc_info=e) def get_pin_tries(self): """ Returns the number of PIN retries left, 0 PIN authentication blocked. Note that 15 is the highest value that will be returned even if remaining tries is higher. """ # Verify without PIN gives number of tries left. _, sw = self.send_cmd(INS.VERIFY, 0, PIN, check=None) return tries_left(sw, self.version) def _get_puk_tries(self): # A failed unblock pin will return number of PUK tries left, # but also uses one try. _, sw = self.send_cmd(INS.RESET_RETRY, 0, PIN, _pack_pin('')*2, check=None) return tries_left(sw, self.version) def _block_pin(self): while self.get_pin_tries() > 0: self.send_cmd(INS.VERIFY, 0, PIN, _pack_pin(''), check=None) def _block_puk(self): while self._get_puk_tries() > 0: self.send_cmd(INS.RESET_RETRY, 0, PIN, _pack_pin('')*2, check=None) def reset(self): self._block_pin() self._block_puk() self.send_cmd(INS.RESET) self._update_pivman_data() def get_data(self, object_id): id_bytes = struct.pack(b'>I', object_id).lstrip(b'\0') tlv = Tlv(self.send_cmd(INS.GET_DATA, 0x3f, 0xff, Tlv(TAG.OBJ_ID, id_bytes))) if tlv.tag not in [TAG.OBJ_DATA, OBJ.DISCOVERY]: raise ValueError('Wrong tag in response data!') return tlv.value def put_data(self, object_id, data): id_bytes = struct.pack(b'>I', object_id).lstrip(b'\0') self.send_cmd(INS.PUT_DATA, 0x3f, 0xff, Tlv(TAG.OBJ_ID, id_bytes) + Tlv(TAG.OBJ_DATA, data)) def generate_key(self, slot, algorithm, pin_policy=PIN_POLICY.DEFAULT, touch_policy=TOUCH_POLICY.DEFAULT): if ALGO.is_rsa(algorithm): ensure_not_cve201715361_vulnerable_firmware_version(self.version) if algorithm not in self.supported_algorithms: raise UnsupportedAlgorithm( 'Algorithm not supported on this YubiKey: {}' .format(algorithm), algorithm_id=algorithm) data = Tlv(TAG.ALGO, six.int2byte(algorithm)) if pin_policy: data += Tlv(TAG.PIN_POLICY, six.int2byte(pin_policy)) if touch_policy: data += Tlv(TAG.TOUCH_POLICY, six.int2byte(touch_policy)) data = Tlv(0xac, data) resp = self.send_cmd(INS.GENERATE_ASYMMETRIC, 0, slot, data) key_data = Tlv.parse_dict(Tlv.unpack(0x7f49, resp)) if algorithm in [ALGO.RSA1024, ALGO.RSA2048]: return rsa.RSAPublicNumbers( int_from_bytes(key_data[0x82], 'big'), int_from_bytes(key_data[0x81], 'big') ).public_key(default_backend()) elif algorithm in [ALGO.ECCP256, ALGO.ECCP384]: curve = ec.SECP256R1 if algorithm == ALGO.ECCP256 else ec.SECP384R1 try: # Added in cryptography 2.5 return ec.EllipticCurvePublicKey.from_encoded_point( curve(), key_data[0x86] ) except AttributeError: return ec.EllipticCurvePublicNumbers.from_encoded_point( curve(), key_data[0x86] ).public_key(default_backend()) raise UnsupportedAlgorithm( 'Invalid algorithm: {}'.format(algorithm), algorithm_id=algorithm) def generate_self_signed_certificate( self, slot, public_key, common_name, valid_from, valid_to, touch_callback=None): algorithm = ALGO.from_public_key(public_key) builder = x509.CertificateBuilder() builder = builder.public_key(public_key) builder = builder.subject_name( x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, common_name), ])) # Same as subject on self-signed certificates. builder = builder.issuer_name( x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, common_name), ])) # x509.random_serial_number added in cryptography 1.6 serial = int_from_bytes(os.urandom(20), 'big') >> 1 builder = builder.serial_number(serial) builder = builder.not_valid_before(valid_from) builder = builder.not_valid_after(valid_to) try: cert = self.sign_cert_builder( slot, algorithm, builder, touch_callback) except APDUError as e: logger.error('Failed to generate certificate for slot %s', slot, exc_info=e) raise self.import_certificate(slot, cert, verify=False) def generate_certificate_signing_request(self, slot, public_key, subject, touch_callback=None): builder = x509.CertificateSigningRequestBuilder() builder = builder.subject_name( x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, subject), ])) try: return self.sign_csr_builder( slot, public_key, builder, touch_callback=touch_callback) except APDUError as e: logger.error( 'Failed to generate Certificate Signing Request for slot %s', slot, exc_info=e) raise def import_key(self, slot, key, pin_policy=PIN_POLICY.DEFAULT, touch_policy=TOUCH_POLICY.DEFAULT): algorithm, data = _get_key_data(key) if pin_policy: data += Tlv(TAG.PIN_POLICY, six.int2byte(pin_policy)) if touch_policy: data += Tlv(TAG.TOUCH_POLICY, six.int2byte(touch_policy)) self.send_cmd(INS.IMPORT_KEY, algorithm, slot, data) return algorithm def import_certificate( self, slot, certificate, verify=False, touch_callback=None): cert_data = certificate.public_bytes(Encoding.DER) if verify: # Verify that the public key used in the certificate # is from the same keypair as the private key. try: public_key = certificate.public_key() test_data = b'test' if touch_callback is not None: touch_timer = Timer(0.500, touch_callback) touch_timer.start() test_sig = self.sign( slot, ALGO.from_public_key(public_key), test_data) if touch_callback is not None: touch_timer.cancel() if isinstance(public_key, rsa.RSAPublicKey): public_key.verify( test_sig, test_data, padding.PKCS1v15(), certificate.signature_hash_algorithm) elif isinstance(public_key, ec.EllipticCurvePublicKey): public_key.verify( test_sig, test_data, ec.ECDSA(hashes.SHA256())) else: raise ValueError('Unknown key type: ' + type(public_key)) except APDUError as e: if e.sw == SW.INCORRECT_PARAMETERS: raise KeypairMismatch(slot, certificate) raise except InvalidSignature: raise KeypairMismatch(slot, certificate) self.put_data(OBJ.from_slot(slot), Tlv(TAG.CERTIFICATE, cert_data) + Tlv(TAG.CERT_INFO, b'\0') + Tlv(TAG.LRC)) self.update_chuid() def read_certificate(self, slot): data = Tlv.parse_dict(self.get_data(OBJ.from_slot(slot))) if TAG.CERT_INFO in data: # Not available in attestation slot if data[TAG.CERT_INFO] != b'\0': raise ValueError('Compressed certificates are not supported!') try: return x509.load_der_x509_certificate(data[TAG.CERTIFICATE], default_backend()) except Exception: raise InvalidCertificate(slot) def delete_certificate(self, slot): self.put_data(OBJ.from_slot(slot), b'') def attest(self, slot): return x509.load_der_x509_certificate(self.send_cmd(INS.ATTEST, slot), default_backend()) def _raw_sign_decrypt(self, slot, algorithm, payload, condition): if not condition(len(payload.value)): raise BadFormat( 'Input has invalid length for algorithm %s' % algorithm, len(payload.value)) data = Tlv(TAG.DYN_AUTH, Tlv(0x82) + payload) resp = self.send_cmd(INS.AUTHENTICATE, algorithm, slot, data) return Tlv.unpack(0x82, Tlv.unpack(0x7c, resp)) def sign_raw(self, slot, algorithm, message): return self._raw_sign_decrypt(slot, algorithm, Tlv(0x81, message), _sign_len_conditions[algorithm]) def sign(self, slot, algorithm, message): if algorithm in (ALGO.RSA1024, ALGO.RSA2048): message = _pkcs1_15_pad(algorithm, message) elif algorithm in (ALGO.ECCP256, ALGO.ECCP384): h = hashes.Hash(hashes.SHA256(), default_backend()) h.update(message) message = h.finalize() return self.sign_raw(slot, algorithm, message) def decrypt_raw(self, slot, algorithm, message): return self._raw_sign_decrypt(slot, algorithm, Tlv(0x85, message), _decrypt_len_conditions[algorithm]) def list_certificates(self): certs = OrderedDict() for slot in set(SLOT) - {SLOT.CARD_MANAGEMENT, SLOT.ATTESTATION}: try: certs[slot] = self.read_certificate(slot) except APDUError: pass except InvalidCertificate: certs[slot] = None return certs def update_chuid(self): # Non-Federal Issuer FASC-N # [9999-9999-999999-0-1-0000000000300001] FASC_N = b'\xd4\xe7\x39\xda\x73\x9c\xed\x39\xce\x73\x9d\x83\x68' + \ b'\x58\x21\x08\x42\x10\x84\x21\xc8\x42\x10\xc3\xeb' # Expires on: 2030-01-01 EXPIRY = b'\x32\x30\x33\x30\x30\x31\x30\x31' self.put_data( OBJ.CHUID, Tlv(0x30, FASC_N) + Tlv(0x34, os.urandom(16)) + Tlv(0x35, EXPIRY) + Tlv(0x3e) + Tlv(TAG.LRC) ) def update_ccc(self): self.put_data( OBJ.CAPABILITY, Tlv(0xf0, b'\xa0\x00\x00\x01\x16\xff\x02' + os.urandom(14)) + Tlv(0xf1, b'\x21') + Tlv(0xf2, b'\x21') + Tlv(0xf3) + Tlv(0xf4, b'\x00') + Tlv(0xf5, b'\x10') + Tlv(0xf6) + Tlv(0xf7) + Tlv(0xfa) + Tlv(0xfb) + Tlv(0xfc) + Tlv(0xfd) + Tlv(TAG.LRC) ) def sign_cert_builder(self, slot, algorithm, builder, touch_callback=None): dummy_key = _dummy_key(algorithm) cert = builder.sign(dummy_key, hashes.SHA256(), default_backend()) if touch_callback is not None: touch_timer = Timer(0.500, touch_callback) touch_timer.start() sig = self.sign(slot, algorithm, cert.tbs_certificate_bytes) if touch_callback is not None: touch_timer.cancel() seq = Tlv.parse_list(Tlv.unpack(0x30, cert.public_bytes(Encoding.DER))) # Replace signature, add unused bits = 0 seq[2] = Tlv(seq[2].tag, b'\0' + sig) # Re-assemble sequence der = Tlv(0x30, b''.join(seq)) return x509.load_der_x509_certificate(der, default_backend()) def sign_csr_builder(self, slot, public_key, builder, touch_callback=None): algorithm = ALGO.from_public_key(public_key) dummy_key = _dummy_key(algorithm) csr = builder.sign(dummy_key, hashes.SHA256(), default_backend()) seq = Tlv.parse_list(Tlv.unpack(0x30, csr.public_bytes(Encoding.DER))) # Replace public key pub_format = PublicFormat.PKCS1 if algorithm.name.startswith('RSA') \ else PublicFormat.SubjectPublicKeyInfo dummy_bytes = dummy_key.public_key().public_bytes( Encoding.DER, pub_format) pub_bytes = public_key.public_bytes(Encoding.DER, pub_format) seq[0] = seq[0].replace(dummy_bytes, pub_bytes) if touch_callback is not None: touch_timer = Timer(0.500, touch_callback) touch_timer.start() sig = self.sign(slot, algorithm, seq[0]) if touch_callback is not None: touch_timer.cancel() # Replace signature, add unused bits = 0 seq[2] = Tlv(seq[2].tag, b'\0' + sig) # Re-assemble sequence der = Tlv(0x30, b''.join(seq)) return x509.load_der_x509_csr(der, default_backend()) @property def supports_pin_policies(self): return self.version >= (4, 0, 0) @property def supported_touch_policies(self): if self.version < (4, 0, 0): return [] # Touch policy not supported on NEO. elif self.version < (4, 3, 0): return [TOUCH_POLICY.DEFAULT, TOUCH_POLICY.NEVER, TOUCH_POLICY.ALWAYS] # Cached policy was added in 4.3 else: return [policy for policy in TOUCH_POLICY] @property def supported_algorithms(self): return [ alg for alg in ALGO if not alg == ALGO.TDES if not (ALGO.is_rsa(alg) and is_cve201715361_vulnerable_firmware_version(self.version)) if not (alg == ALGO.ECCP384 and self.version < (4, 0, 0)) if not (alg == ALGO.RSA1024 and YubiKey.is_fips_version(self.version)) ] yubikey-manager-3.1.1/ykman/scancodes/0000755000175100001630000000000013614233377020477 5ustar runnerdocker00000000000000yubikey-manager-3.1.1/ykman/scancodes/__init__.py0000644000175100001630000000345613614233340022606 0ustar runnerdocker00000000000000# Copyright (c) 2018 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import from enum import Enum from . import us, de, modhex, norman class KEYBOARD_LAYOUT(Enum): MODHEX = modhex.scancodes US = us.scancodes DE = de.scancodes NORMAN = norman.scancodes def encode(data, keyboard_layout=KEYBOARD_LAYOUT.MODHEX): try: return bytes(bytearray(keyboard_layout.value[c] for c in data)) except KeyError as e: raise ValueError('Unsupported character: %s' % e.args[0]) yubikey-manager-3.1.1/ykman/scancodes/de.py0000644000175100001630000000647013614233340021436 0ustar runnerdocker00000000000000# vim: set fileencoding=utf-8 : # Copyright (c) 2018 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import unicode_literals """Scancode map for DE German keyboard layout""" SHIFT = 0x80 scancodes = { 'a': 0x04, 'b': 0x05, 'c': 0x06, 'd': 0x07, 'e': 0x08, 'f': 0x09, 'g': 0x0a, 'h': 0x0b, 'i': 0x0c, 'j': 0x0d, 'k': 0x0e, 'l': 0x0f, 'm': 0x10, 'n': 0x11, 'o': 0x12, 'p': 0x13, 'q': 0x14, 'r': 0x15, 's': 0x16, 't': 0x17, 'u': 0x18, 'v': 0x19, 'w': 0x1a, 'x': 0x1b, 'y': 0x1d, 'z': 0x1c, 'A': 0x04 | SHIFT, 'B': 0x05 | SHIFT, 'C': 0x06 | SHIFT, 'D': 0x07 | SHIFT, 'E': 0x08 | SHIFT, 'F': 0x09 | SHIFT, 'G': 0x0a | SHIFT, 'H': 0x0b | SHIFT, 'I': 0x0c | SHIFT, 'J': 0x0d | SHIFT, 'K': 0x0e | SHIFT, 'L': 0x0f | SHIFT, 'M': 0x10 | SHIFT, 'N': 0x11 | SHIFT, 'O': 0x12 | SHIFT, 'P': 0x13 | SHIFT, 'Q': 0x14 | SHIFT, 'R': 0x15 | SHIFT, 'S': 0x16 | SHIFT, 'T': 0x17 | SHIFT, 'U': 0x18 | SHIFT, 'V': 0x19 | SHIFT, 'W': 0x1a | SHIFT, 'X': 0x1b | SHIFT, 'Y': 0x1d | SHIFT, 'Z': 0x1c | SHIFT, '0': 0x27, '1': 0x1e, '2': 0x1f, '3': 0x20, '4': 0x21, '5': 0x22, '6': 0x23, '7': 0x24, '8': 0x25, '9': 0x26, '\t': 0x2b, '\n': 0x28, '!': 0x1e | SHIFT, '"': 0x1f | SHIFT, '#': 0x32, '$': 0x21 | SHIFT, '%': 0x22 | SHIFT, '&': 0x23 | SHIFT, "'": 0x32 | SHIFT, '(': 0x25 | SHIFT, ')': 0x26 | SHIFT, '*': 0x30 | SHIFT, '+': 0x30, ',': 0x36, '-': 0x38, '.': 0x37, '/': 0x24 | SHIFT, ':': 0x37 | SHIFT, ';': 0x36 | SHIFT, '<': 0x64, '=': 0x27 | SHIFT, '>': 0x64 | SHIFT, '?': 0x2d | SHIFT, '^': 0x35, '_': 0x38 | SHIFT, ' ': 0x2c, '`': 0x2d | SHIFT, '§': 0x20 | SHIFT, '´': 0x2e, 'Ä': 0x34 | SHIFT, 'Ö': 0x33 | SHIFT, 'Ü': 0x2f | SHIFT, 'ß': 0x2d, 'ä': 0x34, 'ö': 0x33, 'ü': 0x2f } yubikey-manager-3.1.1/ykman/scancodes/modhex.py0000644000175100001630000000417413614233340022331 0ustar runnerdocker00000000000000# vim: set fileencoding=utf-8 : # Copyright (c) 2018 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. """Scancode map for keyboard layout based on Modhex. Note that this layouts allows both upper and lowercase characters.""" SHIFT = 0x80 scancodes = { 'b': 0x05, 'c': 0x06, 'd': 0x07, 'e': 0x08, 'f': 0x09, 'g': 0x0a, 'h': 0x0b, 'i': 0x0c, 'j': 0x0d, 'k': 0x0e, 'l': 0x0f, 'n': 0x11, 'r': 0x15, 't': 0x17, 'u': 0x18, 'v': 0x19, 'B': 0x05 | SHIFT, 'C': 0x06 | SHIFT, 'D': 0x07 | SHIFT, 'E': 0x08 | SHIFT, 'F': 0x09 | SHIFT, 'G': 0x0a | SHIFT, 'H': 0x0b | SHIFT, 'I': 0x0c | SHIFT, 'J': 0x0d | SHIFT, 'K': 0x0e | SHIFT, 'L': 0x0f | SHIFT, 'N': 0x11 | SHIFT, 'R': 0x15 | SHIFT, 'T': 0x17 | SHIFT, 'U': 0x18 | SHIFT, 'V': 0x19 | SHIFT, } yubikey-manager-3.1.1/ykman/scancodes/norman.py0000644000175100001630000000635713614233340022344 0ustar runnerdocker00000000000000# vim: set fileencoding=utf-8 : # Copyright (c) 2018 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. """Scancode map for US English Norman keyboard layout""" SHIFT = 0x80 scancodes = { 'a': 0x04, 'b': 0x05, 'c': 0x06, 'd': 0x08, 'e': 0x07, 'f': 0x15, 'g': 0x0a, 'h': 0x33, 'i': 0x0e, 'j': 0x1c, 'k': 0x17, 'l': 0x12, 'm': 0x10, 'n': 0x0d, 'o': 0x0f, 'p': 0x11, 'q': 0x14, 'r': 0x0c, 's': 0x16, 't': 0x09, 'u': 0x18, 'v': 0x19, 'w': 0x1a, 'x': 0x1b, 'y': 0x0b, 'z': 0x1d, 'A': 0x04 | SHIFT, 'B': 0x05 | SHIFT, 'C': 0x06 | SHIFT, 'D': 0x08 | SHIFT, 'E': 0x07 | SHIFT, 'F': 0x15 | SHIFT, 'G': 0x0a | SHIFT, 'H': 0x33 | SHIFT, 'I': 0x0e | SHIFT, 'J': 0x1c | SHIFT, 'K': 0x17 | SHIFT, 'L': 0x12 | SHIFT, 'M': 0x10 | SHIFT, 'N': 0x0d | SHIFT, 'O': 0x0f | SHIFT, 'P': 0x11 | SHIFT, 'Q': 0x14 | SHIFT, 'R': 0x0c | SHIFT, 'S': 0x16 | SHIFT, 'T': 0x09 | SHIFT, 'U': 0x18 | SHIFT, 'V': 0x19 | SHIFT, 'W': 0x1a | SHIFT, 'X': 0x1b | SHIFT, 'Y': 0x0b | SHIFT, 'Z': 0x1d | SHIFT, '0': 0x27, '1': 0x1e, '2': 0x1f, '3': 0x20, '4': 0x21, '5': 0x22, '6': 0x23, '7': 0x24, '8': 0x25, '9': 0x26, '\t': 0x2b, '\n': 0x28, '!': 0x1e | SHIFT, '"': 0x34 | SHIFT, '#': 0x20 | SHIFT, '$': 0x21 | SHIFT, '%': 0x22 | SHIFT, '&': 0x24 | SHIFT, "'": 0x34, '`': 0x35, '(': 0x26 | SHIFT, ')': 0x27 | SHIFT, '*': 0x25 | SHIFT, '+': 0x2e | SHIFT, ',': 0x36, '-': 0x2d, '.': 0x37, '/': 0x38, ':': 0x33 | SHIFT, ';': 0x13, '<': 0x36 | SHIFT, '=': 0x2e, '>': 0x37 | SHIFT, '?': 0x38 | SHIFT, '@': 0x1f | SHIFT, '[': 0x2f, '\\': 0x32, ']': 0x30, '^': 0xa3, '_': 0xad, '{': 0x2f | SHIFT, '}': 0x30 | SHIFT, '|': 0x32 | SHIFT, '~': 0x35 | SHIFT, ' ': 0x2c } yubikey-manager-3.1.1/ykman/scancodes/us.py0000644000175100001630000000635013614233340021472 0ustar runnerdocker00000000000000# vim: set fileencoding=utf-8 : # Copyright (c) 2018 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. """Scancode map for US English keyboard layout""" SHIFT = 0x80 scancodes = { 'a': 0x04, 'b': 0x05, 'c': 0x06, 'd': 0x07, 'e': 0x08, 'f': 0x09, 'g': 0x0a, 'h': 0x0b, 'i': 0x0c, 'j': 0x0d, 'k': 0x0e, 'l': 0x0f, 'm': 0x10, 'n': 0x11, 'o': 0x12, 'p': 0x13, 'q': 0x14, 'r': 0x15, 's': 0x16, 't': 0x17, 'u': 0x18, 'v': 0x19, 'w': 0x1a, 'x': 0x1b, 'y': 0x1c, 'z': 0x1d, 'A': 0x04 | SHIFT, 'B': 0x05 | SHIFT, 'C': 0x06 | SHIFT, 'D': 0x07 | SHIFT, 'E': 0x08 | SHIFT, 'F': 0x09 | SHIFT, 'G': 0x0a | SHIFT, 'H': 0x0b | SHIFT, 'I': 0x0c | SHIFT, 'J': 0x0d | SHIFT, 'K': 0x0e | SHIFT, 'L': 0x0f | SHIFT, 'M': 0x10 | SHIFT, 'N': 0x11 | SHIFT, 'O': 0x12 | SHIFT, 'P': 0x13 | SHIFT, 'Q': 0x14 | SHIFT, 'R': 0x15 | SHIFT, 'S': 0x16 | SHIFT, 'T': 0x17 | SHIFT, 'U': 0x18 | SHIFT, 'V': 0x19 | SHIFT, 'W': 0x1a | SHIFT, 'X': 0x1b | SHIFT, 'Y': 0x1c | SHIFT, 'Z': 0x1d | SHIFT, '0': 0x27, '1': 0x1e, '2': 0x1f, '3': 0x20, '4': 0x21, '5': 0x22, '6': 0x23, '7': 0x24, '8': 0x25, '9': 0x26, '\t': 0x2b, '\n': 0x28, '!': 0x1e | SHIFT, '"': 0x34 | SHIFT, '#': 0x20 | SHIFT, '$': 0x21 | SHIFT, '%': 0x22 | SHIFT, '&': 0x24 | SHIFT, "'": 0x34, '`': 0x35, '(': 0x26 | SHIFT, ')': 0x27 | SHIFT, '*': 0x25 | SHIFT, '+': 0x2e | SHIFT, ',': 0x36, '-': 0x2d, '.': 0x37, '/': 0x38, ':': 0x33 | SHIFT, ';': 0x33, '<': 0x36 | SHIFT, '=': 0x2e, '>': 0x37 | SHIFT, '?': 0x38 | SHIFT, '@': 0x1f | SHIFT, '[': 0x2f, '\\': 0x32, ']': 0x30, '^': 0xa3, '_': 0xad, '{': 0x2f | SHIFT, '}': 0x30 | SHIFT, '|': 0x32 | SHIFT, '~': 0x35 | SHIFT, ' ': 0x2c } yubikey-manager-3.1.1/ykman/settings.py0000644000175100001630000000441413614233340020740 0ustar runnerdocker00000000000000# Copyright (c) 2017 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import import os import json DIR_NAME = '.ykman' def _get_conf_dir(): if os.path.isdir(DIR_NAME): return os.path.abspath(DIR_NAME) return os.path.join(os.path.expanduser('~'), DIR_NAME) class Settings(dict): def __init__(self, name): self.fname = os.path.join(_get_conf_dir(), name + '.json') if os.path.isfile(self.fname): with open(self.fname, 'r') as f: self.update(json.load(f)) def __eq__(self, other): return other is not None and self.fname == other.fname def __ne__(self, other): return other is not None or self.fname != other.fname def write(self): conf_dir = os.path.dirname(self.fname) if not os.path.isdir(conf_dir): os.makedirs(conf_dir) data = json.dumps(self, indent=2) with open(self.fname, 'w') as f: f.write(data) __hash__ = None yubikey-manager-3.1.1/ykman/util.py0000644000175100001630000003715313614233340020063 0ustar runnerdocker00000000000000# Copyright (c) 2015 Yubico AB # All rights reserved. # # Redistribution and use in source and binary forms, with or # without modification, are permitted provided that the following # conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import import six import struct import re import logging import random from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.backends import default_backend from cryptography import x509 from enum import Enum, IntEnum, unique from base64 import b32decode from binascii import b2a_hex, a2b_hex from OpenSSL import crypto from .scancodes import KEYBOARD_LAYOUT logger = logging.getLogger(__name__) PEM_IDENTIFIER = b'-----BEGIN' class BitflagEnum(IntEnum): @classmethod def split(cls, flags): return (c for c in cls if c & flags) @staticmethod def has(flags, check): return flags & check == check @unique class AID(bytes, Enum): OTP = b'\xa0\x00\x00\x05\x27\x20\x01' MGR = b'\xa0\x00\x00\x05\x27\x47\x11\x17' OPGP = b'\xd2\x76\x00\x01\x24\x01' OATH = b'\xa0\x00\x00\x05\x27\x21\x01' PIV = b'\xa0\x00\x00\x03\x08' U2F = b'\xa0\x00\x00\x06\x47\x2f\x00\x01' # Official U2F_YUBICO = b'\xa0\x00\x00\x05\x27\x10\x02' # Yubico - No longer used @unique class TRANSPORT(BitflagEnum): OTP = 0x01 FIDO = 0x02 CCID = 0x04 @staticmethod def usb_transports(): return TRANSPORT.OTP | TRANSPORT.CCID | TRANSPORT.FIDO @unique class APPLICATION(BitflagEnum): OTP = 0x01 U2F = 0x02 OPGP = 0x08 PIV = 0x10 OATH = 0x20 FIDO2 = 0x200 @staticmethod def dependent_on_ccid(): return APPLICATION.OPGP | APPLICATION.OATH | APPLICATION.PIV def __str__(self): if self == APPLICATION.U2F: return 'FIDO U2F' elif self == APPLICATION.FIDO2: return 'FIDO2' elif self == APPLICATION.OPGP: return 'OpenPGP' else: return self.name @unique class FORM_FACTOR(IntEnum): UNKNOWN = 0x00 USB_A_KEYCHAIN = 0x01 USB_A_NANO = 0x02 USB_C_KEYCHAIN = 0x03 USB_C_NANO = 0x04 USB_C_LIGHTNING = 0x05 def __str__(self): if self == FORM_FACTOR.USB_A_KEYCHAIN: return 'Keychain (USB-A)' elif self == FORM_FACTOR.USB_A_NANO: return 'Nano (USB-A)' elif self == FORM_FACTOR.USB_C_KEYCHAIN: return 'Keychain (USB-C)' elif self == FORM_FACTOR.USB_C_NANO: return 'Nano (USB-C)' elif self == FORM_FACTOR.USB_C_LIGHTNING: return 'Keychain (USB-C, Lightning)' elif self == FORM_FACTOR.UNKNOWN: return 'Unknown.' @classmethod def from_code(cls, code): if code and not isinstance(code, int): raise ValueError('Invalid form factor code: {}'.format(code)) return cls(code) if code in cls.__members__.values() else cls.UNKNOWN class Cve201715361VulnerableError(Exception): """Thrown if on-chip RSA key generation is attempted on a YubiKey vulnerable to CVE-2017-15361.""" def __init__(self, f_version): self.f_version = f_version def __str__(self): return ( 'On-chip RSA key generation on this YubiKey has been blocked.\n' 'Please see https://yubi.co/ysa201701 for details.' ) @unique class YUBIKEY(Enum): YKS = 'YubiKey Standard' NEO = 'YubiKey NEO' SKY = 'Security Key by Yubico' YKP = 'YubiKey Plus' YK4 = 'YubiKey 4' def get_pid(self, transports): suffix = '_'.join(t.name for t in TRANSPORT.split(transports)) return PID[self.name + '_' + suffix] @unique class PID(IntEnum): YKS_OTP = 0x0010 NEO_OTP = 0x0110 NEO_OTP_CCID = 0x0111 NEO_CCID = 0x0112 NEO_FIDO = 0x0113 NEO_OTP_FIDO = 0x0114 NEO_FIDO_CCID = 0x0115 NEO_OTP_FIDO_CCID = 0x0116 SKY_FIDO = 0x0120 YK4_OTP = 0x0401 YK4_FIDO = 0x0402 YK4_OTP_FIDO = 0x0403 YK4_CCID = 0x0404 YK4_OTP_CCID = 0x0405 YK4_FIDO_CCID = 0x0406 YK4_OTP_FIDO_CCID = 0x0407 YKP_OTP_FIDO = 0x0410 def get_type(self): return YUBIKEY[self.name.split('_', 1)[0]] def get_transports(self): return sum(TRANSPORT[x] for x in self.name.split('_')[1:]) class Mode(object): _modes = [ TRANSPORT.OTP, # 0x00 TRANSPORT.CCID, # 0x01 TRANSPORT.OTP | TRANSPORT.CCID, # 0x02 TRANSPORT.FIDO, # 0x03 TRANSPORT.OTP | TRANSPORT.FIDO, # 0x04 TRANSPORT.FIDO | TRANSPORT.CCID, # 0x05 TRANSPORT.OTP | TRANSPORT.FIDO | TRANSPORT.CCID # 0x06 ] def __init__(self, transports): try: self.code = self._modes.index(transports) self._transports = transports except ValueError: raise ValueError('Invalid mode!') @property def transports(self): return self._transports def has_transport(self, transport): return TRANSPORT.has(self._transports, transport) def __eq__(self, other): return other is not None and self.code == other.code def __ne__(self, other): return other is None or self.code != other.code def __str__(self): return '+'.join((t.name for t in TRANSPORT.split(self._transports))) @classmethod def from_code(cls, code): code = code & 0b00000111 return cls(cls._modes[code]) @classmethod def from_pid(cls, pid): return cls(PID(pid).get_transports()) __hash__ = None def _tlv_parse_tag(data, offs=0): t = six.indexbytes(data, offs) if t & 0x1f != 0x1f: return t, 1 else: t = t << 8 | six.indexbytes(data, offs+1) return t, 2 def _tlv_parse_length(data, offs=0): ln = six.indexbytes(data, offs) offs += 1 if ln > 0x80: n_bytes = ln - 0x80 ln = bytes2int(data[offs:offs + n_bytes]) else: n_bytes = 0 return ln, n_bytes + 1 class Tlv(bytes): @property def tag(self): return _tlv_parse_tag(self)[0] @property def length(self): _, offs = _tlv_parse_tag(self) return _tlv_parse_length(self, offs)[0] @property def value(self): ln = self.length if ln == 0: return b'' return bytes(self[-ln:]) def __repr__(self): return u'{}(tag={:02x}, value={})'.format( self.__class__.__name__, self.tag, b2a_hex(self.value).decode('ascii') ) def __new__(cls, *args): if len(args) == 1: data = args[0] if isinstance(data, int): # Called with tag only, blank value tag = data value = b'' else: # Called with binary TLV data tag, tag_ln = _tlv_parse_tag(data) ln, ln_ln = _tlv_parse_length(data, tag_ln) offs = tag_ln + ln_ln value = data[offs:offs+ln] elif len(args) == 2: # Called with tag and value. (tag, value) = args else: raise TypeError('{}() takes at most 2 arguments ({} given)'.format( cls, len(args))) data = bytearray([]) if tag <= 0xff: data.append(tag) else: tag_1 = tag >> 8 if tag_1 > 0xff or tag_1 & 0x1f != 0x1f: raise ValueError('Unsupported tag value') tag_2 = tag & 0xff data.extend([tag_1, tag_2]) length = len(value) if length < 0x80: data.append(length) elif length < 0xff: data.extend([0x81, length]) else: data.extend([0x82, length >> 8, length & 0xff]) data += value return super(Tlv, cls).__new__(cls, bytes(data)) @classmethod def parse_from(cls, data): tlv = cls(data) return tlv, data[len(tlv):] @classmethod def parse_list(cls, data): res = [] while data: tlv, data = cls.parse_from(data) res.append(tlv) return res @classmethod def parse_dict(cls, data): return dict((tlv.tag, tlv.value) for tlv in cls.parse_list(data)) @classmethod def unpack(cls, tag, data): tlv = cls(data) if tlv.tag != tag: raise ValueError('Wrong tag, got {:02x} expected {:02x}'.format( tlv.tag, tag )) return tlv.value parse_tlvs = Tlv.parse_list # Deprecated, use Tlv.parse_list directly class MissingLibrary(object): def __init__(self, message): self._message = message def __getattr__(self, name): raise AttributeError(self._message) def int2bytes(value): buf = [] while value > 0xff: buf.append(value & 0xff) value >>= 8 buf.append(value) return bytes(bytearray(reversed(buf))) def bytes2int(data): return int(b2a_hex(data), 16) _HEX = b'0123456789abcdef' _MODHEX = b'cbdefghijklnrtuv' _MODHEX_TO_HEX = dict((_MODHEX[i], _HEX[i:i+1]) for i in range(16)) _HEX_TO_MODHEX = dict((_HEX[i], _MODHEX[i:i+1]) for i in range(16)) DEFAULT_PW_CHAR_BLACKLIST = ['\t', '\n', ' '] def ensure_not_cve201715361_vulnerable_firmware_version(f_version): if is_cve201715361_vulnerable_firmware_version(f_version): raise Cve201715361VulnerableError(f_version) def is_cve201715361_vulnerable_firmware_version(f_version): return (4, 2, 0) <= f_version < (4, 3, 5) def modhex_decode(value): if isinstance(value, six.text_type): value = value.encode('ascii') return a2b_hex(b''.join(_MODHEX_TO_HEX[c] for c in value)) def modhex_encode(value): return b''.join(_HEX_TO_MODHEX[c] for c in b2a_hex(value)).decode('ascii') def generate_static_pw( length, keyboard_layout=KEYBOARD_LAYOUT.MODHEX, blacklist=DEFAULT_PW_CHAR_BLACKLIST): chars = [k for k in keyboard_layout.value.keys() if k not in blacklist] sr = random.SystemRandom() return ''.join([sr.choice(chars) for _ in range(length)]) def format_code(code, digits=6, steam=False): STEAM_CHAR_TABLE = '23456789BCDFGHJKMNPQRTVWXY' if steam: chars = [] for i in range(5): chars.append(STEAM_CHAR_TABLE[code % len(STEAM_CHAR_TABLE)]) code //= len(STEAM_CHAR_TABLE) return ''.join(chars) else: return ('%%0%dd' % digits) % (code % 10 ** digits) def parse_totp_hash(resp): offs = six.indexbytes(resp, -1) & 0xf return parse_truncated(resp[offs:offs+4]) def parse_truncated(resp): return struct.unpack('>I', resp)[0] & 0x7fffffff def hmac_shorten_key(key, algo): if algo.upper() == 'SHA1': h = hashes.SHA1() elif algo.upper() == 'SHA256': h = hashes.SHA256() elif algo.upper() == 'SHA512': h = hashes.SHA512() else: raise ValueError('Unsupported algorithm!') if len(key) > h.block_size: h = hashes.Hash(h, default_backend()) h.update(key) key = h.finalize() return key def time_challenge(timestamp, period=30): return struct.pack('>q', int(timestamp // period)) def parse_key(val): val = val.upper() if re.match(r'^([0-9A-F]{2})+$', val): # hex return a2b_hex(val) else: # Key should be b32 encoded return parse_b32_key(val) def parse_b32_key(key): key = key.upper().replace(' ', '') key += '=' * (-len(key) % 8) # Support unpadded return b32decode(key) def parse_private_key(data, password): """ Identifies, decrypts and returns a cryptography private key object. """ # PEM if is_pem(data): if b'ENCRYPTED' in data: if password is None: raise TypeError('No password provided for encrypted key.') try: return serialization.load_pem_private_key( data, password, backend=default_backend()) except ValueError: # Cryptography raises ValueError if decryption fails. raise except Exception: pass # PKCS12 if is_pkcs12(data): try: p12 = crypto.load_pkcs12(data, password) data = crypto.dump_privatekey( crypto.FILETYPE_PEM, p12.get_privatekey()) return serialization.load_pem_private_key( data, password=None, backend=default_backend()) except crypto.Error as e: raise ValueError(e) # DER try: return serialization.load_der_private_key( data, password, backend=default_backend()) except Exception: pass # All parsing failed raise ValueError('Could not parse private key.') def parse_certificates(data, password): """ Identifies, decrypts and returns list of cryptography x509 certificates. """ # PEM if is_pem(data): certs = [] for cert in data.split(PEM_IDENTIFIER): try: certs.append( x509.load_pem_x509_certificate( PEM_IDENTIFIER + cert, default_backend())) except Exception: pass # Could be valid PEM but not certificates. if len(certs) > 0: return certs # PKCS12 if is_pkcs12(data): try: p12 = crypto.load_pkcs12(data, password) data = crypto.dump_certificate( crypto.FILETYPE_PEM, p12.get_certificate()) return [x509.load_pem_x509_certificate(data, default_backend())] except crypto.Error as e: raise ValueError(e) # DER try: return [x509.load_der_x509_certificate(data, default_backend())] except Exception: pass raise ValueError('Could not parse certificate.') def get_leaf_certificates(certs): """ Extracts the leaf certificates from a list of certificates. Leaf certificates are ones whose subject does not appear as issuer among the others. """ issuers = [cert.issuer.get_attributes_for_oid(x509.NameOID.COMMON_NAME) for cert in certs] leafs = [cert for cert in certs if (cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME) not in issuers)] return leafs def is_pem(data): return PEM_IDENTIFIER in data if data else False def is_pkcs12(data): """ Tries to identify a PKCS12 container. The PFX PDU version is assumed to be v3. See: https://tools.ietf.org/html/rfc7292. """ if isinstance(data, bytes): tlv = Tlv(data) if tlv.tag == 0x30: header = Tlv(tlv.value) return header.tag == 0x02 and header.value == b'\x03' return False else: return False yubikey-manager-3.1.1/yubikey_manager.egg-info/0000755000175100001630000000000013614233377022263 5ustar runnerdocker00000000000000yubikey-manager-3.1.1/yubikey_manager.egg-info/PKG-INFO0000644000175100001630000000133713614233377023364 0ustar runnerdocker00000000000000Metadata-Version: 1.2 Name: yubikey-manager Version: 3.1.1 Summary: Tool for managing your YubiKey configuration. Home-page: https://github.com/Yubico/yubikey-manager Author: Dain Nilsson Author-email: dain@yubico.com Maintainer: Yubico Open Source Maintainers Maintainer-email: ossmaint@yubico.com License: BSD 2 clause Description: UNKNOWN Platform: UNKNOWN Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: X11 Applications :: Qt Classifier: Intended Audience :: End Users/Desktop Classifier: Topic :: Security :: Cryptography Classifier: Topic :: Utilities yubikey-manager-3.1.1/yubikey_manager.egg-info/SOURCES.txt0000644000175100001630000000435313614233377024154 0ustar runnerdocker00000000000000COPYING MANIFEST.in NEWS README.adoc setup.cfg setup.py doc/development.adoc doc/integration-tests.adoc man/ykman.1 test/__init__.py test/test_device.py test/test_external_libs.py test/test_oath.py test/test_piv.py test/test_scancodes.py test/test_util.py test/util.py test/files/rsa_1024_key.pem test/files/rsa_2048_cert.der test/files/rsa_2048_cert.pem test/files/rsa_2048_cert_metadata.pem test/files/rsa_2048_key.pem test/files/rsa_2048_key_cert.pfx test/files/rsa_2048_key_cert_encrypted.pfx test/files/rsa_2048_key_encrypted.pem test/on_yubikey/__init__.py test/on_yubikey/test_cli_config.py test/on_yubikey/test_cli_misc.py test/on_yubikey/test_cli_oath.py test/on_yubikey/test_cli_openpgp.py test/on_yubikey/test_cli_otp.py test/on_yubikey/test_fips_u2f_commands.py test/on_yubikey/test_interfaces.py test/on_yubikey/test_opgp.py test/on_yubikey/test_piv.py test/on_yubikey/cli_piv/__init__.py test/on_yubikey/cli_piv/test_fips.py test/on_yubikey/cli_piv/test_generate_cert_and_csr.py test/on_yubikey/cli_piv/test_key_management.py test/on_yubikey/cli_piv/test_management_key.py test/on_yubikey/cli_piv/test_misc.py test/on_yubikey/cli_piv/test_pin_puk.py test/on_yubikey/cli_piv/test_read_write_object.py test/on_yubikey/cli_piv/util.py test/on_yubikey/framework/__init__.py test/on_yubikey/framework/yubikey_conditions.py ykman/VERSION ykman/__init__.py ykman/descriptor.py ykman/device.py ykman/driver.py ykman/driver_ccid.py ykman/driver_fido.py ykman/driver_otp.py ykman/fido.py ykman/logging_setup.py ykman/oath.py ykman/opgp.py ykman/otp.py ykman/piv.py ykman/settings.py ykman/util.py ykman/cli/__init__.py ykman/cli/__main__.py ykman/cli/config.py ykman/cli/fido.py ykman/cli/info.py ykman/cli/mode.py ykman/cli/oath.py ykman/cli/opgp.py ykman/cli/otp.py ykman/cli/piv.py ykman/cli/util.py ykman/native/__init__.py ykman/native/libloader.py ykman/native/pyusb.py ykman/native/util.py ykman/native/ykpers.py ykman/scancodes/__init__.py ykman/scancodes/de.py ykman/scancodes/modhex.py ykman/scancodes/norman.py ykman/scancodes/us.py yubikey_manager.egg-info/PKG-INFO yubikey_manager.egg-info/SOURCES.txt yubikey_manager.egg-info/dependency_links.txt yubikey_manager.egg-info/entry_points.txt yubikey_manager.egg-info/requires.txt yubikey_manager.egg-info/top_level.txtyubikey-manager-3.1.1/yubikey_manager.egg-info/dependency_links.txt0000644000175100001630000000000113614233377026331 0ustar runnerdocker00000000000000 yubikey-manager-3.1.1/yubikey_manager.egg-info/entry_points.txt0000644000175100001630000000006313614233377025560 0ustar runnerdocker00000000000000[console_scripts] ykman = ykman.cli.__main__:main yubikey-manager-3.1.1/yubikey_manager.egg-info/requires.txt0000644000175100001630000000007213614233377024662 0ustar runnerdocker00000000000000six pyscard pyusb click cryptography pyopenssl fido2>=0.7 yubikey-manager-3.1.1/yubikey_manager.egg-info/top_level.txt0000644000175100001630000000000613614233377025011 0ustar runnerdocker00000000000000ykman