././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1618212188.055259 yubikey-manager-4.0.7/COPYING0000644000000000000000000000245200000000000014000 0ustar0000000000000000Copyright (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. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631098170.4235685 yubikey-manager-4.0.7/NEWS0000644000000000000000000003155700000000000013454 0ustar0000000000000000* Version 4.0.7 (released 2021-09-08) ** Bugfix release: Fix broken naming for "YubiKey 4", and a small OATH issue with touch Steam credentials. * Version 4.0.6 (released 2021-09-08) ** Improve handling of YubiKey device reboots. ** More consistently mask PIN/password input in prompts. ** Support switching mode over CCID for YubiKey Edge. ** Run pkill from PATH instead of fixed location. * Version 4.0.5 (released 2021-07-16) ** Bugfix: Fix PIV feature detection for some YubiKey NEO versions. ** Bugfix: Fix argument short form for --period when adding TOTP credentials. ** Bugfix: More strict validation for some arguments, resulting in better error messages. ** Bugfix: Correctly handle TOTP credentials using period != 30 AND touch_required. ** Bugfix: Fix prompting for access code in the otp settings command (now uses "-A -"). * Version 4.0.3 (released 2021-05-17) ** Add support for fido reset over NFC. ** Bugfix: The --touch argument to piv change-management-key was ignored. ** Bugfix: Don't prompt for password when importing PIV key/cert if file is invalid. ** Bugfix: Fix setting touch-eject/auto-eject for YubiKey 4 and NEO. ** Bugfix: Detect PKCS#12 format when outer sequence uses indefinite length. ** Dependency: Add support for Click 8. * Version 4.0.2 (released 2021-04-12) ** Update device names. ** Add read_info output to the --diagnose command, and show exception types. ** Bugfix: Fix read_info for YubiKey Plus. * Version 4.0.1 (released 2021-03-29) ** Add support for YK5-based FIPS YubiKeys. ** Bugfix: Fix OTP device enumeration on Win32. * Version 4.0.0 (released 2021-03-02) ** Drop support for Python < 3.6. ** Drop reliance on libusb and libykpersonalize. ** Support the "fido" and "otp" subcommands over NFC (using the --reader flag) ** New "ykman --diagnose" command to aid in troubleshooting. ** New "ykman apdu" command for sending raw APDUs over the smart card interface. ** Restructuring of subcommands, with aliases for old versions (to be removed in a future release). ** Major changes to the underlying "library" code: *** New "yubikit" package added for custom development and advanced scripting. *** Type hints added for a large part of the "public" API. ** OpenPGP: Add support for KDF enabled YubiKeys. ** Static password: Add support for FR, IT, UK and BEPO keyboard layouts. * Version 3.1.2 (released 2021-01-21) ** Bugfix release: Fix dependency on python-fido2 version. * 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. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631098162.2405598 yubikey-manager-4.0.7/README.adoc0000644000000000000000000001120000000000000014521 0ustar0000000000000000== 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 3.6 (or later) 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 Show version information about the app -d, --device SERIAL Specify which YubiKey to interact with by serial number. -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. --diagnose Show diagnostics information useful for troubleshooting. -h, --help Show this message and exit. Commands: info Show general information. list List connected YubiKeys. config Enable/Disable applications. fido Manage the FIDO applications. oath Manage the OATH Application. openpgp Manage the OpenPGP Application. otp Manage the OTP Application. piv Manage the PIV Application. .... The `--help` argument can also be used to get detailed information about specific subcommands: ykman oath --help === Installation YubiKey Manager can be installed independently of platform by using pip (or equivalent): pip install --user yubikey-manager On Linux platforms you will need `pcscd` installed and running to be able to communicate with a YubiKey over the SmartCard interface. Additionally, you may need to set permissions for your user to access YubiKeys via the HID interfaces. More information available link:doc/Device_Permissions.adoc[here]. Some of the libraries used by yubikey-manager have C-extensions, and may require additional dependencies to build, such as http://www.swig.org/[swig] and potentially https://pcsclite.alioth.debian.org/pcsclite.html[PCSC lite]. === Pre-build packages Pre-built packages specific to your platform may be available from Yubico or third parties. Please refer to your platforms native package manager for detailed instructions on how to install, if available. ==== Windows The command line tool ykman.exe is provided as part of the installer for the https://developers.yubico.com/yubikey-manager-qt/[YubiKey Manager] on Windows. ==== MacOS Packages for MacOS are available from Homebrew and MacPorts. ==== Linux Packages are available for several Linux distributions by third party package maintainers. Yubico also provides packages for Ubuntu in the yubico/stable PPA (for amd64 ONLY, other architectures such as arm should use the general `pip` instructions above instead): $ sudo apt-add-repository ppa:yubico/stable $ sudo apt update $ sudo apt install yubikey-manager ==== FreeBSD Althought not being officially supported on this platform, YubiKey Manager can be installed on FreeBSD. It's available via its ports tree or as pre-built package. Should you opt to install and use YubiKey Manager on this platform, please be aware that it's **NOT** maintained by Yubico. For more information about how to install packages or ports on FreeBSD, please refer to its official documentation: https://docs.freebsd.org/en/books/handbook/ports[FreeBSD Handbook]. ==== Source To install from source, see the link:doc/Development.adoc[development] instructions. === Shell completion Experimental shell completion for the command line tool is available, provided by the underlying CLI library (`click`) but it is not enabled by default. To enable it, run this command once (for Bash): $ source <(_YKMAN_COMPLETE=bash_source ykman | sudo tee /etc/bash_completion.d/ykman) More information on shell completion is available here: https://click.palletsprojects.com/en/8.0.x/shell-completion NOTE: If your version of the Click dependency is older than 8.0 you need to use `source_bash` for the variable instead. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631098180.2819815 yubikey-manager-4.0.7/man/ykman.10000644000000000000000000000273300000000000014723 0ustar0000000000000000.TH YKMAN "1" "September 2021" "ykman 4.0.7" "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 .TP \fB\-d\fR, \fB\-\-device\fR SERIAL Specify which YubiKey to interact with by serial number. .TP \fB\-r\fR, \fB\-\-reader\fR NAME Use an external smart card reader. Conflicts with --device and list. .TP \fB\-l\fR, \fB\-\-log\fR-level [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 --log-level is also set. .TP \fB\-\-diagnose\fR Show diagnostics information useful for troubleshooting. .TP \fB\-v\fR, \fB\-\-version\fR Show version information about the app .TP \fB\-\-full\-help\fR Show --help, including hidden commands, and exit. .TP \fB\-h\fR, \fB\-\-help\fR Show this message and exit. .SS "Commands:" .TP info Show general information. .TP list List connected YubiKeys. .TP config Enable/Disable applications. .TP fido Manage the FIDO applications. .TP oath Manage the OATH application. .TP openpgp Manage the OpenPGP application. .TP otp Manage the YubiOTP application. .TP piv Manage the 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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631098170.4241965 yubikey-manager-4.0.7/pyproject.toml0000644000000000000000000000233700000000000015663 0ustar0000000000000000[tool.poetry] name = "yubikey-manager" version = "4.0.7" description = "Tool for managing your YubiKey configuration." authors = ["Dain Nilsson "] license = "BSD" homepage = "https://github.com/Yubico/yubikey-manager" repository = "https://github.com/Yubico/yubikey-manager" keywords = ["yubikey", "yubiotp", "piv", "fido"] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: End Users/Desktop", "Topic :: Security :: Cryptography", "Topic :: Utilities" ] include = [ "COPYING", "NEWS", "README.adoc", "man/", "tests/", ] packages = [ { include = "yubikit" }, { include = "ykman" }, ] [tool.poetry.dependencies] python = "^3.6" dataclasses = {version = "^0.8", python = "<3.7"} cryptography = "^2.1 || ^3.0" pyOpenSSL = {version = ">=0.15.1", optional = true} pyscard = "^1.9 || ^2.0" fido2 = ">=0.9, <1.0" click = "^6.0 || ^7.0 || ^8.0" pywin32 = {version = ">=223", platform = "win32"} [tool.poetry.dev-dependencies] pytest = "^6.0" pyOpenSSL = "^17.0" makefun = "^1.9.5" [tool.poetry.scripts] ykman = "ykman.cli.__main__:main" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] testpaths = ["tests"] ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1618212188.071963 yubikey-manager-4.0.7/tests/__init__.py0000644000000000000000000000000000000000000016203 0ustar0000000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.0725389 yubikey-manager-4.0.7/tests/conftest.py0000644000000000000000000000030300000000000016277 0ustar0000000000000000def pytest_addoption(parser): parser.addoption("--device", action="store", type=int) parser.addoption("--reader", action="store") parser.addoption("--no-serial", action="store_true") ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1618212188.073691 yubikey-manager-4.0.7/tests/device/__init__.py0000644000000000000000000000000000000000000017442 0ustar0000000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1618212188.074843 yubikey-manager-4.0.7/tests/device/cli/__init__.py0000644000000000000000000000000000000000000020211 0ustar0000000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1618212188.075419 yubikey-manager-4.0.7/tests/device/cli/conftest.py0000644000000000000000000000140400000000000020310 0ustar0000000000000000from yubikit.core import TRANSPORT from ykman.cli.__main__ import cli from ykman.cli.aliases import apply_aliases from click.testing import CliRunner from functools import partial import pytest @pytest.fixture(scope="module") def ykman_cli(device, info): if device.transport == TRANSPORT.NFC: return partial(_ykman_cli, "--reader", device.reader.name) elif info.serial is not None: return partial(_ykman_cli, "--device", info.serial) else: return _ykman_cli def _ykman_cli(*argv, **kwargs): argv = apply_aliases(["ykman"] + [str(a) for a in argv]) runner = CliRunner(mix_stderr=False) result = runner.invoke(cli, argv[1:], obj={}, **kwargs) if result.exit_code != 0: raise result.exception return result ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.0777278 yubikey-manager-4.0.7/tests/device/cli/piv/__init__.py0000644000000000000000000000000000000000000021007 0ustar0000000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.0788794 yubikey-manager-4.0.7/tests/device/cli/piv/conftest.py0000644000000000000000000000032700000000000021111 0ustar0000000000000000from yubikit.management import CAPABILITY from ... import condition import pytest @pytest.fixture(autouse=True) @condition.capability(CAPABILITY.PIV) def ensure_piv(ykman_cli): ykman_cli("piv", "reset", "-f") ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.0800452 yubikey-manager-4.0.7/tests/device/cli/piv/test_fips.py0000644000000000000000000000133100000000000021260 0ustar0000000000000000from yubikit.management import CAPABILITY from yubikit.core import NotSupportedError from ....util import open_file from ... import condition import pytest @pytest.fixture(autouse=True) @condition.fips(True) @condition.capability(CAPABILITY.PIV) def ensure_piv(ykman_cli): ykman_cli("piv", "reset", "-f") class TestFIPS: def test_rsa1024_generate_blocked(self, ykman_cli): with pytest.raises(NotSupportedError): ykman_cli("piv", "keys", "generate", "9a", "-a", "RSA1024", "-") def test_rsa1024_import_blocked(self, ykman_cli): with pytest.raises(NotSupportedError): with open_file("rsa_1024_key.pem") as f: ykman_cli("piv", "keys", "import", "9a", f.name) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1618212188.081184 yubikey-manager-4.0.7/tests/device/cli/piv/test_generate_cert_and_csr.py0000644000000000000000000002403500000000000024625 0ustar0000000000000000from binascii import b2a_hex from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec, rsa, padding from .util import DEFAULT_PIN, DEFAULT_MANAGEMENT_KEY, NON_DEFAULT_MANAGEMENT_KEY from ... import condition import pytest def _verify_cert(cert, pubkey): cert_signature = cert.signature cert_bytes = cert.tbs_certificate_bytes if isinstance(pubkey, rsa.RSAPublicKey): pubkey.verify( cert_signature, cert_bytes, padding.PKCS1v15(), cert.signature_hash_algorithm, ) elif isinstance(pubkey, ec.EllipticCurvePublicKey): pubkey.verify( cert_signature, cert_bytes, ec.ECDSA(cert.signature_hash_algorithm) ) else: raise ValueError("Unsupported public key value") def not_roca(version): return not ((4, 2, 0) <= version < (4, 3, 5)) class TestNonDefaultMgmKey: @pytest.fixture(autouse=True) def set_mgmt_key(self, ykman_cli): ykman_cli( "piv", "access", "change-management-key", "-P", DEFAULT_PIN, "-m", DEFAULT_MANAGEMENT_KEY, "-n", NON_DEFAULT_MANAGEMENT_KEY, ) def _test_generate_self_signed(self, ykman_cli, slot, algo): pubkey_output = ykman_cli( "piv", "keys", "generate", slot, "-a", algo, "-m", NON_DEFAULT_MANAGEMENT_KEY, "-", ).output ykman_cli( "piv", "certificates", "generate", slot, "-m", NON_DEFAULT_MANAGEMENT_KEY, "-s", "subject-" + algo, "-P", DEFAULT_PIN, "-", input=pubkey_output, ) output = ykman_cli("piv", "certificates", "export", slot, "-").output cert = x509.load_pem_x509_certificate(output.encode(), default_backend()) _verify_cert(cert, cert.public_key()) fingerprint = b2a_hex(cert.fingerprint(hashes.SHA256())).decode("ascii") output = ykman_cli("piv", "info").output assert "Fingerprint:\t" + fingerprint in output @condition.fips(False) @condition.check(not_roca) def test_generate_self_signed_slot_9a_rsa1024(self, ykman_cli): self._test_generate_self_signed(ykman_cli, "9a", "RSA1024") def test_generate_self_signed_slot_9a_eccp256(self, ykman_cli): self._test_generate_self_signed(ykman_cli, "9a", "ECCP256") @condition.fips(False) @condition.check(not_roca) def test_generate_self_signed_slot_9c_rsa1024(self, ykman_cli): self._test_generate_self_signed(ykman_cli, "9c", "RSA1024") def test_generate_self_signed_slot_9c_eccp256(self, ykman_cli): self._test_generate_self_signed(ykman_cli, "9c", "ECCP256") @condition.fips(False) @condition.check(not_roca) def test_generate_self_signed_slot_9d_rsa1024(self, ykman_cli): self._test_generate_self_signed(ykman_cli, "9d", "RSA1024") def test_generate_self_signed_slot_9d_eccp256(self, ykman_cli): self._test_generate_self_signed(ykman_cli, "9d", "ECCP256") @condition.fips(False) @condition.check(not_roca) def test_generate_self_signed_slot_9e_rsa1024(self, ykman_cli): self._test_generate_self_signed(ykman_cli, "9e", "RSA1024") def test_generate_self_signed_slot_9e_eccp256(self, ykman_cli): self._test_generate_self_signed(ykman_cli, "9e", "ECCP256") def _test_generate_csr(self, ykman_cli, slot, algo): subject_input = "subject-" + algo pubkey_output = ykman_cli( "piv", "keys", "generate", slot, "-a", algo, "-m", NON_DEFAULT_MANAGEMENT_KEY, "-", ).output csr_output = ykman_cli( "piv", "certificates", "request", slot, "-P", DEFAULT_PIN, "-", "-", "-s", subject_input, input=pubkey_output, ).output csr = x509.load_pem_x509_csr(csr_output.encode("utf-8"), default_backend()) subject_output = csr.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[ 0 ].value assert subject_input == subject_output @condition.fips(False) @condition.check(not_roca) def test_generate_csr_slot_9a_rsa1024(self, ykman_cli): self._test_generate_csr(ykman_cli, "9a", "RSA1024") def test_generate_csr_slot_9a_eccp256(self, ykman_cli): self._test_generate_csr(ykman_cli, "9a", "ECCP256") @condition.fips(False) @condition.check(not_roca) def test_generate_csr_slot_9c_rsa1024(self, ykman_cli): self._test_generate_csr(ykman_cli, "9c", "RSA1024") def test_generate_csr_slot_9c_eccp256(self, ykman_cli): self._test_generate_csr(ykman_cli, "9c", "ECCP256") @condition.fips(False) @condition.check(not_roca) def test_generate_csr_slot_9d_rsa1024(self, ykman_cli): self._test_generate_csr(ykman_cli, "9d", "RSA1024") def test_generate_csr_slot_9d_eccp256(self, ykman_cli): self._test_generate_csr(ykman_cli, "9d", "ECCP256") @condition.fips(False) @condition.check(not_roca) def test_generate_csr_slot_9e_rsa1024(self, ykman_cli): self._test_generate_csr(ykman_cli, "9e", "RSA1024") def test_generate_csr_slot_9e_eccp256(self, ykman_cli): self._test_generate_csr(ykman_cli, "9e", "ECCP256") class TestProtectedMgmKey: @pytest.fixture(autouse=True) def protect_mgmt_key(self, ykman_cli): ykman_cli( "piv", "access", "change-management-key", "-p", "-P", DEFAULT_PIN, "-m", DEFAULT_MANAGEMENT_KEY, ) def _test_generate_self_signed(self, ykman_cli, slot, algo): pubkey_output = ykman_cli( "piv", "keys", "generate", slot, "-a", algo, "-P", DEFAULT_PIN, "-" ).output ykman_cli( "piv", "certificates", "generate", slot, "-P", DEFAULT_PIN, "-s", "subject-" + algo, "-", input=pubkey_output, ) output = ykman_cli("piv", "certificates", "export", slot, "-").output cert = x509.load_pem_x509_certificate(output.encode(), default_backend()) _verify_cert(cert, cert.public_key()) fingerprint = b2a_hex(cert.fingerprint(hashes.SHA256())).decode("ascii") output = ykman_cli("piv", "info").output assert "Fingerprint:\t" + fingerprint in output @condition.fips(False) @condition.check(not_roca) def test_generate_self_signed_slot_9a_rsa1024(self, ykman_cli): self._test_generate_self_signed(ykman_cli, "9a", "RSA1024") def test_generate_self_signed_slot_9a_eccp256(self, ykman_cli): self._test_generate_self_signed(ykman_cli, "9a", "ECCP256") @condition.fips(False) @condition.check(not_roca) def test_generate_self_signed_slot_9c_rsa1024(self, ykman_cli): self._test_generate_self_signed(ykman_cli, "9c", "RSA1024") def test_generate_self_signed_slot_9c_eccp256(self, ykman_cli): self._test_generate_self_signed(ykman_cli, "9c", "ECCP256") @condition.fips(False) @condition.check(not_roca) def test_generate_self_signed_slot_9d_rsa1024(self, ykman_cli): self._test_generate_self_signed(ykman_cli, "9d", "RSA1024") def test_generate_self_signed_slot_9d_eccp256(self, ykman_cli): self._test_generate_self_signed(ykman_cli, "9d", "ECCP256") @condition.fips(False) @condition.check(not_roca) def test_generate_self_signed_slot_9e_rsa1024(self, ykman_cli): self._test_generate_self_signed(ykman_cli, "9e", "RSA1024") def test_generate_self_signed_slot_9e_eccp256(self, ykman_cli): self._test_generate_self_signed(ykman_cli, "9e", "ECCP256") def _test_generate_csr(self, ykman_cli, slot, algo): subject_input = "subject-" + algo pubkey_output = ykman_cli( "piv", "keys", "generate", slot, "-a", algo, "-P", DEFAULT_PIN, "-" ).output csr_output = ykman_cli( "piv", "certificates", "request", slot, "-P", DEFAULT_PIN, "-", "-", "-s", subject_input, input=pubkey_output, ).output csr = x509.load_pem_x509_csr(csr_output.encode("utf-8"), default_backend()) subject_output = csr.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[ 0 ].value assert subject_input == subject_output @condition.fips(False) @condition.check(not_roca) def test_generate_csr_slot_9a_rsa1024(self, ykman_cli): self._test_generate_csr(ykman_cli, "9a", "RSA1024") def test_generate_csr_slot_9a_eccp256(self, ykman_cli): self._test_generate_csr(ykman_cli, "9a", "ECCP256") @condition.fips(False) @condition.check(not_roca) def test_generate_csr_slot_9c_rsa1024(self, ykman_cli): self._test_generate_csr(ykman_cli, "9c", "RSA1024") def test_generate_csr_slot_9c_eccp256(self, ykman_cli): self._test_generate_csr(ykman_cli, "9c", "ECCP256") @condition.fips(False) @condition.check(not_roca) def test_generate_csr_slot_9d_rsa1024(self, ykman_cli): self._test_generate_csr(ykman_cli, "9d", "RSA1024") def test_generate_csr_slot_9d_eccp256(self, ykman_cli): self._test_generate_csr(ykman_cli, "9d", "ECCP256") @condition.fips(False) @condition.check(not_roca) def test_generate_csr_slot_9e_rsa1024(self, ykman_cli): self._test_generate_csr(ykman_cli, "9e", "RSA1024") def test_generate_csr_slot_9e_eccp256(self, ykman_cli): self._test_generate_csr(ykman_cli, "9e", "ECCP256") ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.0823362 yubikey-manager-4.0.7/tests/device/cli/piv/test_key_management.py0000644000000000000000000003664700000000000023325 0ustar0000000000000000from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ec from yubikit.core import NotSupportedError from .util import DEFAULT_PIN, DEFAULT_MANAGEMENT_KEY from ... import condition import tempfile import os import pytest def generate_pem_eccp256_keypair(): pk = ec.generate_private_key(ec.SECP256R1(), default_backend()) return ( pk.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption(), ), pk.public_key().public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo, ), ) def roca(version): """Not ROCA affected""" return (4, 2, 0) <= version < (4, 3, 5) def not_roca(version): """ROCA affected""" return not roca(version) @pytest.fixture() def tmp_file(): tmp = tempfile.NamedTemporaryFile(delete=False) tmp.close() yield tmp.name os.remove(tmp.name) class TestKeyExport: @condition.min_version(5, 3) def test_from_metadata(self, ykman_cli): pair = generate_pem_eccp256_keypair() ykman_cli( "piv", "keys", "import", "9a", "-m", DEFAULT_MANAGEMENT_KEY, "-", input=pair[0], ) exported = ykman_cli("piv", "keys", "export", "9a", "-").stdout_bytes assert exported == pair[1] @condition.min_version(4, 3) def test_from_metadata_or_attestation(self, ykman_cli): der = ykman_cli( "piv", "keys", "generate", "9a", "-a", "ECCP256", "-F", "der", "-m", DEFAULT_MANAGEMENT_KEY, "-", ).stdout_bytes exported = ykman_cli( "piv", "keys", "export", "9a", "-F", "der", "-" ).stdout_bytes assert der == exported def test_from_metadata_or_cert(self, ykman_cli): private_key_pem, public_key_pem = generate_pem_eccp256_keypair() ykman_cli( "piv", "keys", "import", "9a", "-m", DEFAULT_MANAGEMENT_KEY, "-", input=private_key_pem, ) ykman_cli( "piv", "certificates", "generate", "9a", "-", "-m", DEFAULT_MANAGEMENT_KEY, "-P", DEFAULT_PIN, "-s", "test", input=public_key_pem, ) exported = ykman_cli("piv", "keys", "export", "9a", "-").stdout_bytes assert public_key_pem == exported @condition.max_version(5, 2, 9) def test_from_cert_verify(self, ykman_cli): private_key_pem, public_key_pem = generate_pem_eccp256_keypair() ykman_cli( "piv", "keys", "import", "9a", "-m", DEFAULT_MANAGEMENT_KEY, "-", input=private_key_pem, ) ykman_cli( "piv", "certificates", "generate", "9a", "-", "-m", DEFAULT_MANAGEMENT_KEY, "-P", DEFAULT_PIN, "-s", "test", input=public_key_pem, ) ykman_cli("piv", "keys", "export", "9a", "--verify", "-P", DEFAULT_PIN, "-") @condition.max_version(5, 2, 9) def test_from_cert_verify_fails(self, ykman_cli): private_key_pem = generate_pem_eccp256_keypair()[0] public_key_pem = generate_pem_eccp256_keypair()[1] ykman_cli( "piv", "keys", "import", "9a", "-m", DEFAULT_MANAGEMENT_KEY, "-", input=private_key_pem, ) ykman_cli( "piv", "certificates", "generate", "9a", "-", "-m", DEFAULT_MANAGEMENT_KEY, "-P", DEFAULT_PIN, "-s", "test", input=public_key_pem, ) with pytest.raises(SystemExit): ykman_cli("piv", "keys", "export", "9a", "--verify", "-P", DEFAULT_PIN, "-") class TestKeyManagement: @condition.check(not_roca) def test_generate_key_default(self, ykman_cli): output = ykman_cli( "piv", "keys", "generate", "9a", "-m", DEFAULT_MANAGEMENT_KEY, "-" ).output assert "BEGIN PUBLIC KEY" in output @condition.check(roca) def test_generate_key_default_cve201715361(self, ykman_cli): with pytest.raises(NotSupportedError): ykman_cli( "piv", "keys", "generate", "9a", "-m", DEFAULT_MANAGEMENT_KEY, "-" ) @condition.check(not_roca) @condition.fips(False) def test_generate_key_rsa1024(self, ykman_cli): output = ykman_cli( "piv", "keys", "generate", "9a", "-a", "RSA1024", "-m", DEFAULT_MANAGEMENT_KEY, "-", ).output assert "BEGIN PUBLIC KEY" in output @condition.check(not_roca) def test_generate_key_rsa2048(self, ykman_cli): output = ykman_cli( "piv", "keys", "generate", "9a", "-a", "RSA2048", "-m", DEFAULT_MANAGEMENT_KEY, "-", ).output assert "BEGIN PUBLIC KEY" in output @condition.fips(False) @condition.check(roca) def test_generate_key_rsa1024_cve201715361(self, ykman_cli): with pytest.raises(NotSupportedError): ykman_cli( "piv", "keys", "generate", "9a", "-a", "RSA1024", "-m", DEFAULT_MANAGEMENT_KEY, "-", ) @condition.check(roca) def test_generate_key_rsa2048_cve201715361(self, ykman_cli): with pytest.raises(NotSupportedError): ykman_cli( "piv", "keys", "generate", "9a", "-a", "RSA2048", "-m", DEFAULT_MANAGEMENT_KEY, "-", ) def test_generate_key_eccp256(self, ykman_cli): output = ykman_cli( "piv", "keys", "generate", "9a", "-a", "ECCP256", "-m", DEFAULT_MANAGEMENT_KEY, "-", ).output assert "BEGIN PUBLIC KEY" in output def test_import_key_eccp256(self, ykman_cli): ykman_cli( "piv", "keys", "import", "9a", "-m", DEFAULT_MANAGEMENT_KEY, "-", input=generate_pem_eccp256_keypair()[0], ) @condition.min_version(4) def test_generate_key_eccp384(self, ykman_cli): output = ykman_cli( "piv", "keys", "generate", "9a", "-a", "ECCP384", "-m", DEFAULT_MANAGEMENT_KEY, "-", ).output assert "BEGIN PUBLIC KEY" in output @condition.min_version(4) def test_generate_key_pin_policy_always(self, ykman_cli): output = ykman_cli( "piv", "keys", "generate", "9a", "--pin-policy", "ALWAYS", "-m", DEFAULT_MANAGEMENT_KEY, "-a", "ECCP256", "-", ).output assert "BEGIN PUBLIC KEY" in output @condition.min_version(4) def test_import_key_pin_policy_always(self, ykman_cli): for pin_policy in ["ALWAYS", "always"]: ykman_cli( "piv", "keys", "import", "9a", "--pin-policy", pin_policy, "-m", DEFAULT_MANAGEMENT_KEY, "-", input=generate_pem_eccp256_keypair()[0], ) @condition.min_version(4) def test_generate_key_touch_policy_always(self, ykman_cli): output = ykman_cli( "piv", "keys", "generate", "9a", "--touch-policy", "ALWAYS", "-m", DEFAULT_MANAGEMENT_KEY, "-a", "ECCP256", "-", ).output assert "BEGIN PUBLIC KEY" in output @condition.min_version(4) def test_import_key_touch_policy_always(self, ykman_cli): for touch_policy in ["ALWAYS", "always"]: ykman_cli( "piv", "keys", "import", "9a", "--touch-policy", touch_policy, "-m", DEFAULT_MANAGEMENT_KEY, "-", input=generate_pem_eccp256_keypair()[0], ) @condition.min_version(4, 3) def test_attest_key(self, ykman_cli): ykman_cli( "piv", "keys", "generate", "9a", "-a", "ECCP256", "-m", DEFAULT_MANAGEMENT_KEY, "-", ) output = ykman_cli("piv", "keys", "attest", "9a", "-").output assert "BEGIN CERTIFICATE" in output def _test_generate_csr(self, ykman_cli, tmp_file, algo): ykman_cli( "piv", "keys", "generate", "9a", "-a", algo, "-m", DEFAULT_MANAGEMENT_KEY, tmp_file, ) output = ykman_cli( "piv", "certificates", "request", "9a", tmp_file, "-s", "test-subject", "-P", DEFAULT_PIN, "-", ).output csr = x509.load_pem_x509_csr(output.encode(), default_backend()) assert csr.is_signature_valid @condition.fips(False) @condition.check(not_roca) def test_generate_csr_rsa1024(self, ykman_cli, tmp_file): self._test_generate_csr(ykman_cli, tmp_file, "RSA1024") def test_generate_csr_eccp256(self, ykman_cli, tmp_file): self._test_generate_csr(ykman_cli, tmp_file, "ECCP256") def test_import_verify_correct_cert_succeeds_with_pin(self, ykman_cli, tmp_file): # Set up a key in the slot and create a certificate for it public_key_pem = ykman_cli( "piv", "keys", "generate", "9a", "-a", "ECCP256", "-m", DEFAULT_MANAGEMENT_KEY, "-", ).output ykman_cli( "piv", "certificates", "generate", "9a", "-", "-m", DEFAULT_MANAGEMENT_KEY, "-P", DEFAULT_PIN, "-s", "test", input=public_key_pem, ) ykman_cli("piv", "certificates", "export", "9a", tmp_file) with pytest.raises(SystemExit): ykman_cli( "piv", "certificates", "import", "--verify", "9a", tmp_file, "-m", DEFAULT_MANAGEMENT_KEY, ) ykman_cli( "piv", "certificates", "import", "--verify", "9a", tmp_file, "-m", DEFAULT_MANAGEMENT_KEY, "-P", DEFAULT_PIN, ) ykman_cli( "piv", "certificates", "import", "--verify", "9a", tmp_file, "-m", DEFAULT_MANAGEMENT_KEY, input=DEFAULT_PIN, ) def test_import_verify_wrong_cert_fails(self, ykman_cli): # Set up a key in the slot and create a certificate for it public_key_pem = ykman_cli( "piv", "keys", "generate", "9a", "-a", "ECCP256", "-m", DEFAULT_MANAGEMENT_KEY, "-", ).output ykman_cli( "piv", "certificates", "generate", "9a", "-", "-m", DEFAULT_MANAGEMENT_KEY, "-P", DEFAULT_PIN, "-s", "test", input=public_key_pem, ) cert_pem = ykman_cli("piv", "certificates", "export", "9a", "-").output # Overwrite the key with a new one ykman_cli( "piv", "keys", "generate", "9a", "-a", "ECCP256", "-m", DEFAULT_MANAGEMENT_KEY, "-", input=public_key_pem, ) with pytest.raises(SystemExit): ykman_cli( "piv", "certificates", "import", "--verify", "9a", "-", "-m", DEFAULT_MANAGEMENT_KEY, "-P", DEFAULT_PIN, input=cert_pem, ) def test_import_no_verify_wrong_cert_succeeds(self, ykman_cli): # Set up a key in the slot and create a certificate for it public_key_pem = ykman_cli( "piv", "keys", "generate", "9a", "-a", "ECCP256", "-m", DEFAULT_MANAGEMENT_KEY, "-", ).output ykman_cli( "piv", "certificates", "generate", "9a", "-", "-m", DEFAULT_MANAGEMENT_KEY, "-P", DEFAULT_PIN, "-s", "test", input=public_key_pem, ) cert_pem = ykman_cli("piv", "certificates", "export", "9a", "-").output # Overwrite the key with a new one ykman_cli( "piv", "keys", "generate", "9a", "-a", "ECCP256", "-m", DEFAULT_MANAGEMENT_KEY, "-", input=public_key_pem, ) with pytest.raises(SystemExit): ykman_cli( "piv", "certificates", "import", "--verify", "9a", "-", "-m", DEFAULT_MANAGEMENT_KEY, "-P", DEFAULT_PIN, input=cert_pem, ) ykman_cli( "piv", "certificates", "import", "9a", "-", "-m", DEFAULT_MANAGEMENT_KEY, "-P", DEFAULT_PIN, input=cert_pem, ) @condition.min_version(4, 3) def test_export_attestation_certificate(self, ykman_cli): output = ykman_cli("piv", "certificates", "export", "f9", "-").output assert "BEGIN CERTIFICATE" in output ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.0834875 yubikey-manager-4.0.7/tests/device/cli/piv/test_management_key.py0000644000000000000000000001257600000000000023320 0ustar0000000000000000from .util import ( old_new_new, DEFAULT_PIN, DEFAULT_MANAGEMENT_KEY, NON_DEFAULT_MANAGEMENT_KEY, ) import re import pytest class TestManagementKey: def test_change_management_key_force_fails_without_generate(self, ykman_cli): with pytest.raises(SystemExit): ykman_cli( "piv", "access", "change-management-key", "-P", DEFAULT_PIN, "-m", DEFAULT_MANAGEMENT_KEY, "-f", ) def test_change_management_key_protect_random(self, ykman_cli): ykman_cli( "piv", "access", "change-management-key", "-p", "-P", DEFAULT_PIN, "-m", DEFAULT_MANAGEMENT_KEY, ) output = ykman_cli("piv", "info").output assert "Management key is stored on the YubiKey, protected by PIN" in output with pytest.raises(SystemExit): # Should fail - wrong current key ykman_cli( "piv", "access", "change-management-key", "-p", "-P", DEFAULT_PIN, "-m", DEFAULT_MANAGEMENT_KEY, ) # Should succeed - PIN as key ykman_cli("piv", "access", "change-management-key", "-p", "-P", DEFAULT_PIN) def test_change_management_key_protect_prompt(self, ykman_cli): ykman_cli( "piv", "access", "change-management-key", "-p", "-P", DEFAULT_PIN, input=DEFAULT_MANAGEMENT_KEY, ) output = ykman_cli("piv", "info").output assert "Management key is stored on the YubiKey, protected by PIN" in output with pytest.raises(SystemExit): # Should fail - wrong current key ykman_cli( "piv", "access", "change-management-key", "-p", "-P", DEFAULT_PIN, "-m", DEFAULT_MANAGEMENT_KEY, ) # Should succeed - PIN as key ykman_cli("piv", "access", "change-management-key", "-p", "-P", DEFAULT_PIN) def test_change_management_key_no_protect_generate(self, ykman_cli): output = ykman_cli( "piv", "access", "change-management-key", "-m", DEFAULT_MANAGEMENT_KEY, "-g" ).output assert re.match( r"^Generated management key: [a-f0-9]{48}$", output, re.MULTILINE ) output = ykman_cli("piv", "info").output assert "Management key is stored on the YubiKey" not in output def test_change_management_key_no_protect_arg(self, ykman_cli): output = ykman_cli( "piv", "access", "change-management-key", "-m", DEFAULT_MANAGEMENT_KEY, "-n", NON_DEFAULT_MANAGEMENT_KEY, ).output assert "" == output output = ykman_cli("piv", "info").output assert "Management key is stored on the YubiKey" not in output with pytest.raises(SystemExit): ykman_cli( "piv", "access", "change-management-key", "-m", DEFAULT_MANAGEMENT_KEY, "-n", NON_DEFAULT_MANAGEMENT_KEY, ) output = ykman_cli( "piv", "access", "change-management-key", "-m", NON_DEFAULT_MANAGEMENT_KEY, "-n", DEFAULT_MANAGEMENT_KEY, ).output assert "" == output def test_change_management_key_no_protect_arg_bad_length(self, ykman_cli): with pytest.raises(SystemExit): ykman_cli( "piv", "access", "change-management-key", "-m", DEFAULT_MANAGEMENT_KEY, "-n", "10020304050607080102030405060708", ) def test_change_management_key_no_protect_prompt(self, ykman_cli): output = ykman_cli( "piv", "access", "change-management-key", input=old_new_new(DEFAULT_MANAGEMENT_KEY, NON_DEFAULT_MANAGEMENT_KEY), ).output assert "Generated" not in output output = ykman_cli("piv", "info").output assert "Management key is stored on the YubiKey" not in output with pytest.raises(SystemExit): ykman_cli( "piv", "access", "change-management-key", input=old_new_new(DEFAULT_MANAGEMENT_KEY, NON_DEFAULT_MANAGEMENT_KEY), ) ykman_cli( "piv", "access", "change-management-key", input=old_new_new(NON_DEFAULT_MANAGEMENT_KEY, DEFAULT_MANAGEMENT_KEY), ) assert "Generated" not in output def test_change_management_key_new_key_conflicts_with_generate(self, ykman_cli): with pytest.raises(SystemExit): ykman_cli( "piv", "access", "change-management-key", "-m", DEFAULT_MANAGEMENT_KEY, "-n", NON_DEFAULT_MANAGEMENT_KEY, "-g", ) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1618212188.084654 yubikey-manager-4.0.7/tests/device/cli/piv/test_misc.py0000644000000000000000000000242700000000000021261 0ustar0000000000000000from yubikit.piv import OBJECT_ID import pytest DEFAULT_MANAGEMENT_KEY = "010203040506070801020304050607080102030405060708" class TestMisc: def setUp(self, ykman_cli): ykman_cli("piv", "reset", "-f") def test_info(self, ykman_cli): output = ykman_cli("piv", "info").output assert "PIV version:" in output def test_reset(self, ykman_cli): output = ykman_cli("piv", "reset", "-f").output assert "Success!" in output def test_export_invalid_certificate_fails(self, ykman_cli): ykman_cli( "piv", "objects", "import", hex(OBJECT_ID.AUTHENTICATION), "-", "-m", DEFAULT_MANAGEMENT_KEY, input="This is not a cert", ) with pytest.raises(SystemExit): ykman_cli( "piv", "certificates", "export", hex(OBJECT_ID.AUTHENTICATION), "-" ) def test_info_with_invalid_certificate_does_not_crash(self, ykman_cli): ykman_cli( "piv", "objects", "import", hex(OBJECT_ID.AUTHENTICATION), "-", "-m", DEFAULT_MANAGEMENT_KEY, input="This is not a cert", ) ykman_cli("piv", "info") ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.0863688 yubikey-manager-4.0.7/tests/device/cli/piv/test_pin_puk.py0000644000000000000000000000412700000000000021772 0ustar0000000000000000from .util import ( old_new_new, DEFAULT_PIN, NON_DEFAULT_PIN, DEFAULT_PUK, NON_DEFAULT_PUK, ) import contextlib import io import pytest class TestPin: def test_change_pin(self, ykman_cli): ykman_cli( "piv", "access", "change-pin", "-P", DEFAULT_PIN, "-n", NON_DEFAULT_PIN ) ykman_cli( "piv", "access", "change-pin", "-P", NON_DEFAULT_PIN, "-n", DEFAULT_PIN ) def test_change_pin_alias(self, ykman_cli): with io.StringIO() as buf: with contextlib.redirect_stderr(buf): ykman_cli("piv", "change-pin", "-P", DEFAULT_PIN, "-n", NON_DEFAULT_PIN) err = buf.getvalue() assert "piv access change-pin" in err ykman_cli( "piv", "access", "change-pin", "-P", NON_DEFAULT_PIN, "-n", DEFAULT_PIN ) def test_change_pin_prompt(self, ykman_cli): ykman_cli( "piv", "access", "change-pin", input=old_new_new(DEFAULT_PIN, NON_DEFAULT_PIN), ) ykman_cli( "piv", "access", "change-pin", input=old_new_new(NON_DEFAULT_PIN, DEFAULT_PIN), ) class TestPuk: def test_change_puk(self, ykman_cli): o1 = ykman_cli( "piv", "access", "change-puk", "-p", DEFAULT_PUK, "-n", NON_DEFAULT_PUK ).output assert "New PUK set." in o1 o2 = ykman_cli( "piv", "access", "change-puk", "-p", NON_DEFAULT_PUK, "-n", DEFAULT_PUK ).output assert "New PUK set." in o2 with pytest.raises(SystemExit): ykman_cli( "piv", "access", "change-puk", "-p", NON_DEFAULT_PUK, "-n", DEFAULT_PUK ) def test_change_puk_prompt(self, ykman_cli): ykman_cli( "piv", "access", "change-puk", input=old_new_new(DEFAULT_PUK, NON_DEFAULT_PUK), ) ykman_cli( "piv", "access", "change-puk", input=old_new_new(NON_DEFAULT_PUK, DEFAULT_PUK), ) ././@PaxHeader0000000000000000000000000000003200000000000011450 xustar000000000000000026 mtime=1618212188.08752 yubikey-manager-4.0.7/tests/device/cli/piv/test_read_write_object.py0000644000000000000000000000737200000000000024005 0ustar0000000000000000import os from cryptography.hazmat.primitives import serialization from ....util import generate_self_signed_certificate from yubikit.core import Tlv from yubikit.piv import OBJECT_ID, SLOT import contextlib import io import pytest DEFAULT_MANAGEMENT_KEY = "010203040506070801020304050607080102030405060708" class TestReadWriteObject: def test_write_read_preserves_ansi_escapes(self, ykman_cli): red = b"\x00\x1b[31m" blue = b"\x00\x1b[34m" reset = b"\x00\x1b[0m" data = ( b"Hello, " + red + b"red" + reset + b" and " + blue + b"blue" + reset + b" world!" ) ykman_cli( "piv", "objects", "import", "-m", DEFAULT_MANAGEMENT_KEY, "0x5f0001", "-", input=data, ) output_data = ykman_cli( "piv", "objects", "export", "0x5f0001", "-" ).stdout_bytes assert data == output_data def test_read_write_read_is_noop(self, ykman_cli): data = os.urandom(32) ykman_cli( "piv", "objects", "import", hex(OBJECT_ID.AUTHENTICATION), "-", "-m", DEFAULT_MANAGEMENT_KEY, input=data, ) output1 = ykman_cli( "piv", "objects", "export", hex(OBJECT_ID.AUTHENTICATION), "-" ).stdout_bytes assert output1 == data ykman_cli( "piv", "objects", "import", hex(OBJECT_ID.AUTHENTICATION), "-", "-m", DEFAULT_MANAGEMENT_KEY, input=output1, ) output2 = ykman_cli( "piv", "objects", "export", hex(OBJECT_ID.AUTHENTICATION), "-" ).stdout_bytes assert output2 == data def test_read_write_aliases(self, ykman_cli): data = os.urandom(32) with io.StringIO() as buf: with contextlib.redirect_stderr(buf): ykman_cli( "piv", "write-object", hex(OBJECT_ID.AUTHENTICATION), "-", "-m", DEFAULT_MANAGEMENT_KEY, input=data, ) output1 = ykman_cli( "piv", "read-object", hex(OBJECT_ID.AUTHENTICATION), "-" ).stdout_bytes err = buf.getvalue() assert output1 == data assert "piv objects import" in err assert "piv objects export" in err def test_read_write_certificate_as_object(self, ykman_cli): with pytest.raises(SystemExit): ykman_cli("piv", "objects", "export", hex(OBJECT_ID.AUTHENTICATION), "-") cert = generate_self_signed_certificate() cert_bytes_der = cert.public_bytes(encoding=serialization.Encoding.DER) input_tlv = Tlv(0x70, cert_bytes_der) + Tlv(0x71, b"\0") + Tlv(0xFE, b"") ykman_cli( "piv", "objects", "import", hex(OBJECT_ID.AUTHENTICATION), "-", "-m", DEFAULT_MANAGEMENT_KEY, input=input_tlv, ) output1 = ykman_cli( "piv", "objects", "export", hex(OBJECT_ID.AUTHENTICATION), "-" ).stdout_bytes output_cert_bytes = Tlv.parse_dict(output1)[0x70] assert output_cert_bytes == cert_bytes_der output2 = ykman_cli( "piv", "certificates", "export", hex(SLOT.AUTHENTICATION), "-", "--format", "DER", ).stdout_bytes assert output2 == cert_bytes_der ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.0886722 yubikey-manager-4.0.7/tests/device/cli/piv/util.py0000644000000000000000000000050500000000000020237 0ustar0000000000000000DEFAULT_PIN = "123456" NON_DEFAULT_PIN = "654321" DEFAULT_PUK = "12345678" NON_DEFAULT_PUK = "87654321" DEFAULT_MANAGEMENT_KEY = "010203040506070801020304050607080102030405060708" NON_DEFAULT_MANAGEMENT_KEY = "010103040506070801020304050607080102030405060708" def old_new_new(old, new): return f"{old}\n{new}\n{new}\n" ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1618212188.089824 yubikey-manager-4.0.7/tests/device/cli/test_config.py0000644000000000000000000002152300000000000020773 0ustar0000000000000000from yubikit.core import TRANSPORT from yubikit.management import CAPABILITY from ykman.base import YUBIKEY from .. import condition import contextlib import io import pytest VALID_LOCK_CODE = "a" * 32 INVALID_LOCK_CODE_NON_HEX = "z" * 32 def _fido_only(capabilities): return capabilities & ~(CAPABILITY.U2F | CAPABILITY.FIDO2) == 0 def not_sky(device, info): if device.transport == TRANSPORT.NFC: return not ( info.serial is None and _fido_only(info.supported_capabilities[TRANSPORT.USB]) ) else: return device.pid.get_type() != YUBIKEY.SKY class TestConfigUSB: @pytest.fixture(autouse=True) @condition.check(not_sky) @condition.min_version(5) def enable_all(self, ykman_cli, await_reboot): ykman_cli("config", "usb", "--enable-all", "-f") await_reboot() yield None ykman_cli("config", "usb", "--enable-all", "-f") await_reboot() @condition.capability(CAPABILITY.OTP, TRANSPORT.USB) def test_disable_otp(self, ykman_cli, await_reboot): ykman_cli("config", "usb", "--disable", "OTP", "-f") await_reboot() output = ykman_cli("config", "usb", "--list").output assert "OTP" not in output @condition.capability(CAPABILITY.U2F, TRANSPORT.USB) def test_disable_u2f(self, ykman_cli, await_reboot): ykman_cli("config", "usb", "--disable", "U2F", "-f") await_reboot() output = ykman_cli("config", "usb", "--list").output assert "FIDO U2F" not in output @condition.capability(CAPABILITY.OPENPGP, TRANSPORT.USB) def test_disable_openpgp(self, ykman_cli, await_reboot): ykman_cli("config", "usb", "--disable", "OPENPGP", "-f") await_reboot() output = ykman_cli("config", "usb", "--list").output assert "OpenPGP" not in output @condition.capability(CAPABILITY.OPENPGP, TRANSPORT.USB) def test_disable_openpgp_alternative_syntax(self, ykman_cli, await_reboot): ykman_cli("config", "usb", "--disable", "openpgp", "-f") await_reboot() output = ykman_cli("config", "usb", "--list").output assert "OpenPGP" not in output @condition.capability(CAPABILITY.PIV, TRANSPORT.USB) def test_disable_piv(self, ykman_cli, await_reboot): ykman_cli("config", "usb", "--disable", "PIV", "-f") await_reboot() output = ykman_cli("config", "usb", "--list").output assert "PIV" not in output @condition.capability(CAPABILITY.OATH, TRANSPORT.USB) def test_disable_oath(self, ykman_cli, await_reboot): ykman_cli("config", "usb", "--disable", "OATH", "-f") await_reboot() output = ykman_cli("config", "usb", "--list").output assert "OATH" not in output @condition.capability(CAPABILITY.FIDO2, TRANSPORT.USB) def test_disable_fido2(self, ykman_cli, await_reboot): ykman_cli("config", "usb", "--disable", "FIDO2", "-f") await_reboot() output = ykman_cli("config", "usb", "--list").output assert "FIDO2" not in output @condition.capability(CAPABILITY.FIDO2, TRANSPORT.USB) def test_disable_and_enable(self, ykman_cli): with pytest.raises(SystemExit): ykman_cli("config", "usb", "--disable", "FIDO2", "--enable", "FIDO2", "-f") with pytest.raises(SystemExit): ykman_cli("config", "usb", "--enable-all", "--disable", "FIDO2", "-f") def test_disable_all(self, ykman_cli): with pytest.raises(SystemExit): ykman_cli( "config", "usb", "-d", "FIDO2", "-d", "U2F", "-d", "OATH", "-d", "OPENPGP", "-d", "PIV", "-d", "OTP", ) def test_mode_command(self, ykman_cli, await_reboot): ykman_cli("config", "mode", "ccid", "-f") await_reboot() output = ykman_cli("config", "usb", "--list").output assert "FIDO U2F" not in output assert "FIDO2" not in output assert "OTP" not in output ykman_cli("config", "mode", "otp", "-f") await_reboot() output = ykman_cli("config", "usb", "--list").output assert "FIDO U2F" not in output assert "FIDO2" not in output assert "OpenPGP" not in output assert "PIV" not in output assert "OATH" not in output ykman_cli("config", "mode", "fido", "-f") await_reboot() output = ykman_cli("config", "usb", "--list").output assert "OTP" not in output assert "OATH" not in output assert "PIV" not in output assert "OpenPGP" not in output def test_mode_alias(self, ykman_cli, await_reboot): with io.StringIO() as buf: with contextlib.redirect_stderr(buf): ykman_cli("mode", "ccid", "-f") await_reboot() output = ykman_cli("config", "usb", "--list").output assert "FIDO U2F" not in output assert "FIDO2" not in output assert "OTP" not in output err = buf.getvalue() assert "config mode ccid" in err class TestConfigNFC: @pytest.fixture(autouse=True) @condition.check(not_sky) @condition.min_version(5) @condition.has_transport(TRANSPORT.NFC) def enable_all_nfc(self, ykman_cli, await_reboot): ykman_cli("config", "nfc", "--enable-all", "-f") await_reboot() yield None ykman_cli("config", "nfc", "--enable-all", "-f") await_reboot() @condition.capability(CAPABILITY.OTP, TRANSPORT.NFC) def test_disable_otp(self, ykman_cli): ykman_cli("config", "nfc", "--disable", "OTP", "-f") output = ykman_cli("config", "nfc", "--list").output assert "OTP" not in output @condition.capability(CAPABILITY.U2F, TRANSPORT.NFC) def test_disable_u2f(self, ykman_cli): ykman_cli("config", "nfc", "--disable", "U2F", "-f") output = ykman_cli("config", "nfc", "--list").output assert "FIDO U2F" not in output @condition.capability(CAPABILITY.OPENPGP, TRANSPORT.NFC) def test_disable_openpgp(self, ykman_cli): ykman_cli("config", "nfc", "--disable", "OPENPGP", "-f") output = ykman_cli("config", "nfc", "--list").output assert "OpenPGP" not in output @condition.capability(CAPABILITY.PIV, TRANSPORT.NFC) def test_disable_piv(self, ykman_cli): ykman_cli("config", "nfc", "--disable", "PIV", "-f") output = ykman_cli("config", "nfc", "--list").output assert "PIV" not in output @condition.capability(CAPABILITY.OATH, TRANSPORT.NFC) def test_disable_oath(self, ykman_cli): ykman_cli("config", "nfc", "--disable", "OATH", "-f") output = ykman_cli("config", "nfc", "--list").output assert "OATH" not in output @condition.capability(CAPABILITY.FIDO2, TRANSPORT.NFC) def test_disable_fido2(self, ykman_cli): ykman_cli("config", "nfc", "--disable", "FIDO2", "-f") output = ykman_cli("config", "nfc", "--list").output assert "FIDO2" not in output @condition.transport(TRANSPORT.USB) def test_disable_all(self, ykman_cli): ykman_cli("config", "nfc", "--disable-all", "-f") output = ykman_cli("config", "nfc", "--list").output assert not output @condition.capability(CAPABILITY.FIDO2, TRANSPORT.NFC) def test_disable_and_enable(self, ykman_cli): with pytest.raises(SystemExit): ykman_cli("config", "nfc", "--disable", "FIDO2", "--enable", "FIDO2", "-f") with pytest.raises(SystemExit): ykman_cli("config", "nfc", "--disable-all", "--enable", "FIDO2", "-f") with pytest.raises(SystemExit): ykman_cli("config", "nfc", "--enable-all", "--disable", "FIDO2", "-f") with pytest.raises(SystemExit): ykman_cli("config", "nfc", "--enable-all", "--disable-all", "FIDO2", "-f") class TestConfigLockCode: @pytest.fixture(autouse=True) @condition.min_version(5) def preconditions(self): pass def test_set_lock_code(self, ykman_cli): ykman_cli("config", "set-lock-code", "--new-lock-code", VALID_LOCK_CODE) output = ykman_cli("info").output assert "Configured capabilities are protected by a lock code" in output ykman_cli("config", "set-lock-code", "-l", VALID_LOCK_CODE, "--clear") output = ykman_cli("info").output assert "Configured capabilities are protected by a lock code" not in output def test_set_invalid_lock_code(self, ykman_cli): with pytest.raises(SystemExit): ykman_cli("config", "set-lock-code", "--new-lock-code", "aaaa") with pytest.raises(SystemExit): ykman_cli( "config", "set-lock-code", "--new-lock-code", INVALID_LOCK_CODE_NON_HEX ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.0909717 yubikey-manager-4.0.7/tests/device/cli/test_misc.py0000644000000000000000000000145000000000000020456 0ustar0000000000000000from .. import condition import pytest class TestYkmanInfo: def test_ykman_info(self, ykman_cli, info): output = ykman_cli("info").output assert "Device type:" in output if info.serial is not None: assert "Serial number:" in output assert "Firmware version:" in output @condition.fips(False) def test_ykman_info_does_not_report_fips_for_non_fips_device(self, ykman_cli): with pytest.raises(SystemExit): ykman_cli("info", "--check-fips") @condition.fips(True) def test_ykman_info_reports_fips_status(self, ykman_cli): info = ykman_cli("info", "--check-fips").output assert "FIPS Approved Mode:" in info assert " FIDO U2F:" in info assert " OATH:" in info assert " OTP:" in info ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.0921242 yubikey-manager-4.0.7/tests/device/cli/test_oath.py0000644000000000000000000002056000000000000020461 0ustar0000000000000000# -*- coding: utf-8 -*- from ykman.oath import STEAM_CHAR_TABLE from yubikit.management import CAPABILITY from .. import condition from base64 import b32encode import contextlib import io import pytest 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" @pytest.fixture(autouse=True) @condition.capability(CAPABILITY.OATH) def preconditions(ykman_cli): ykman_cli("oath", "reset", "-f") class TestOATH: def test_oath_info(self, ykman_cli): output = ykman_cli("oath", "info").output assert "version:" in output @condition.fips(False) def test_info_does_not_indicate_fips_mode_for_non_fips_key(self, ykman_cli): info = ykman_cli("oath", "info").output assert "FIPS:" not in info def test_oath_add_credential(self, ykman_cli): ykman_cli("oath", "accounts", "add", "test-name", "abba") creds = ykman_cli("oath", "accounts", "list").output assert "test-name" in creds def test_oath_add_credential_prompt(self, ykman_cli): ykman_cli("oath", "accounts", "add", "test-name-2", input="abba") creds = ykman_cli("oath", "accounts", "list").output assert "test-name-2" in creds def test_oath_add_credential_with_space(self, ykman_cli): ykman_cli("oath", "accounts", "add", "test-name-space", "ab ba") creds = ykman_cli("oath", "accounts", "list").output assert "test-name-space" in creds def test_oath_add_credential_alias(self, ykman_cli): with io.StringIO() as buf: with contextlib.redirect_stderr(buf): ykman_cli("oath", "add", "test-name", "abba") ykman_cli("oath", "code", "test-name") creds = ykman_cli("oath", "list").output ykman_cli("oath", "delete", "test-name", "-f") err = buf.getvalue() assert "test-name" in creds assert "oath accounts add" in err assert "oath accounts code" in err assert "oath accounts list" in err assert "oath accounts delete" in err def test_oath_hidden_cred(self, ykman_cli): ykman_cli("oath", "accounts", "add", "_hidden:name", "abba") creds = ykman_cli("oath", "accounts", "code").output assert "_hidden:name" not in creds creds = ykman_cli("oath", "accounts", "code", "-H").output assert "_hidden:name" in creds def test_oath_add_uri_hotp(self, ykman_cli): ykman_cli("oath", "accounts", "uri", URI_HOTP_EXAMPLE) creds = ykman_cli("oath", "accounts", "list").output assert "Example:demo" in creds def test_oath_add_uri_totp(self, ykman_cli): ykman_cli("oath", "accounts", "uri", URI_TOTP_EXAMPLE) creds = ykman_cli("oath", "accounts", "list").output assert "john.doe" in creds def test_oath_add_uri_totp_extra_parameter(self, ykman_cli): ykman_cli("oath", "accounts", "uri", URI_TOTP_EXAMPLE_EXTRA_PARAMETER) creds = ykman_cli("oath", "accounts", "list").output assert "john.doe.extra" in creds def test_oath_add_uri_totp_prompt(self, ykman_cli): ykman_cli("oath", "accounts", "uri", input=URI_TOTP_EXAMPLE_B) creds = ykman_cli("oath", "accounts", "list").output assert "john.doe" in creds def test_oath_code(self, ykman_cli): ykman_cli("oath", "accounts", "add", "test-name2", "abba") creds = ykman_cli("oath", "accounts", "code").output assert "test-name2" in creds def test_oath_code_query(self, ykman_cli): ykman_cli("oath", "accounts", "add", "query-me", "abba") creds = ykman_cli("oath", "accounts", "code", "query-me").output assert "query-me" in creds def test_oath_reset(self, ykman_cli): output = ykman_cli("oath", "reset", "-f").output assert "Success! All OATH accounts have been deleted from the YubiKey" in output def test_oath_hotp_vectors_6(self, ykman_cli): ykman_cli( "oath", "accounts", "add", "-o", "HOTP", "testvector", b32encode(b"12345678901234567890").decode(), ) for code in ["755224", "287082", "359152", "969429", "338314"]: words = ykman_cli("oath", "accounts", "code", "testvector").output.split() assert code in words def test_oath_hotp_vectors_8(self, ykman_cli): ykman_cli( "oath", "accounts", "add", "-o", "HOTP", "-d", "8", "testvector8", b32encode(b"12345678901234567890").decode(), ) for code in ["84755224", "94287082", "37359152", "26969429", "40338314"]: words = ykman_cli("oath", "accounts", "code", "testvector8").output.split() assert code in words def test_oath_hotp_code(self, ykman_cli): ykman_cli("oath", "accounts", "add", "-o", "HOTP", "hotp-cred", "abba") words = ykman_cli("oath", "accounts", "code", "hotp-cred").output.split() assert "659165" in words def test_oath_totp_steam_code(self, ykman_cli): ykman_cli("oath", "accounts", "add", "-o", "TOTP", "Steam:steam-cred", "abba") cred = ykman_cli("oath", "accounts", "code", "-s", "steam-cred").output.strip() assert 5, len(cred) == f"cred wrong length: {cred!r}" assert all( c in STEAM_CHAR_TABLE for c in cred ), f"{cred!r} contains non-steam characters" def test_oath_delete(self, ykman_cli): ykman_cli("oath", "accounts", "add", "delete-me", "abba") ykman_cli("oath", "accounts", "delete", "delete-me", "-f") assert "delete-me", ykman_cli("oath", "accounts" not in "list") def test_oath_unicode(self, ykman_cli): ykman_cli("oath", "accounts", "add", "😃", "abba") ykman_cli("oath", "accounts", "code") ykman_cli("oath", "accounts", "list") ykman_cli("oath", "accounts", "delete", "😃", "-f") @condition.fips(False) @condition.min_version(4, 3, 1) def test_oath_sha512(self, ykman_cli): ykman_cli("oath", "accounts", "add", "abba", "abba", "--algorithm", "SHA512") ykman_cli("oath", "accounts", "delete", "abba", "-f") # NEO credential capacity may vary based on configuration @condition.min_version(4) def test_add_32_creds(self, ykman_cli): for i in range(32): ykman_cli("oath", "accounts", "add", "test" + str(i), "abba") output = ykman_cli("oath", "accounts", "list").output lines = output.strip().split("\n") assert len(lines) == i + 1 with pytest.raises(SystemExit): ykman_cli("oath", "accounts", "add", "testx", "abba") @condition.min_version(5, 3, 1) def test_rename(self, ykman_cli): ykman_cli("oath", "accounts", "uri", URI_TOTP_EXAMPLE) ykman_cli( "oath", "accounts", "rename", "john.doe", "Example:user@example.com", "-f" ) creds = ykman_cli("oath", "accounts", "list").output assert "john.doe" not in creds assert "Example:user@example.com" in creds class TestOathFips: @pytest.fixture(autouse=True) @condition.fips(True) def check_fips(self): pass def test_no_fips_mode_without_password(self, ykman_cli): output = ykman_cli("oath", "info").output assert "FIPS Approved Mode: No" in output def test_fips_mode_with_password(self, ykman_cli): ykman_cli("oath", "access", "change", "-n", PASSWORD) output = ykman_cli("oath", "info").output assert "FIPS Approved Mode: Yes" in output def test_sha512_not_supported(self, ykman_cli): with pytest.raises(SystemExit): ykman_cli( "oath", "accounts", "add", "abba", "abba", "--algorithm", "SHA512" ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.0927002 yubikey-manager-4.0.7/tests/device/cli/test_openpgp.py0000644000000000000000000000110500000000000021170 0ustar0000000000000000from yubikit.management import CAPABILITY from .. import condition import pytest @pytest.fixture(autouse=True) @condition.capability(CAPABILITY.OPENPGP) def preconditions(ykman_cli): ykman_cli("openpgp", "reset", "-f") class TestOpenPGP: def test_openpgp_info(self, ykman_cli): output = ykman_cli("openpgp", "info").output assert "OpenPGP version:" in output def test_openpgp_reset(self, ykman_cli): output = ykman_cli("openpgp", "reset", "-f").output assert "Success! All data has been cleared and default PINs are set." in output ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631098162.2450252 yubikey-manager-4.0.7/tests/device/cli/test_otp.py0000644000000000000000000005555000000000000020337 0ustar0000000000000000# 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 yubikit.management import CAPABILITY from .. import condition from time import sleep import re import pytest @pytest.fixture(autouse=True) @condition.capability(CAPABILITY.OTP) def ensure_otp(): pass class TestSlotStatus: def test_ykman_otp_info(self, ykman_cli): info = ykman_cli("otp", "info").output assert "Slot 1:" in info assert "Slot 2:" in info def test_ykman_swap_slots(self, ykman_cli): info = ykman_cli("otp", "info").output if "programmed" not in info: ykman_cli("otp", "static", "2", "incredible") output = ykman_cli("otp", "swap", "-f").output assert "Swapping slots..." in output output = ykman_cli("otp", "swap", "-f").output assert "Swapping slots..." in output @condition.fips(False) def test_ykman_otp_info_does_not_indicate_fips_mode_for_non_fips_key( self, ykman_cli ): # noqa: E501 info = ykman_cli("otp", "info").output assert "FIPS Approved Mode:" not in info class TestReclaimTimeout: def test_update_after_reclaim(self, ykman_cli): info = ykman_cli("otp", "info").output if "programmed" not in info: ykman_cli("otp", "static", "2", "incredible") ykman_cli("otp", "swap", "-f") ykman_cli("otp", "swap", "-f") sleep(4) # Ensure reclaim ykman_cli("otp", "swap", "-f") ykman_cli("otp", "swap", "-f") class TestSlotStaticPassword: @pytest.fixture(autouse=True) def delete_slot(self, ykman_cli): try: ykman_cli("otp", "delete", "2", "-f") except SystemExit: pass yield None try: ykman_cli("otp", "delete", "2", "-f") except SystemExit: pass def test_too_long(self, ykman_cli): with pytest.raises(SystemExit): ykman_cli("otp", "static", "2", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") def test_unsupported_chars(self, ykman_cli): with pytest.raises(ValueError): ykman_cli("otp", "static", "2", "ö") with pytest.raises(ValueError): ykman_cli("otp", "static", "2", "@") def test_provide_valid_pw(self, ykman_cli): ykman_cli("otp", "static", "2", "higngdukgerjktbbikrhirngtlkkttkb") assert "Slot 2: programmed" in ykman_cli("otp", "info").output def test_provide_valid_pw_prompt(self, ykman_cli): ykman_cli("otp", "static", "2", input="higngdukgerjktbbikrhirngtlkkttkb\ny\n") assert "Slot 2: programmed" in ykman_cli("otp", "info").output def test_generate_pw_too_long(self, ykman_cli): with pytest.raises(SystemExit): ykman_cli("otp", "static", "2", "--generate", "--length", "39") def test_generate_pw_blank_length(self, ykman_cli): with pytest.raises(SystemExit): ykman_cli("otp", "static", "2", "--generate", "--length") def test_generate_zero_length(self, ykman_cli): with pytest.raises(SystemExit): ykman_cli("otp", "static", "2", "--generate", "--length", "0") def test_generate_pw(self, ykman_cli): ykman_cli("otp", "static", "2", "--generate", "--length", "38") assert "Slot 2: programmed" in ykman_cli("otp", "info").output def test_generate_pw_default_length(self, ykman_cli): ykman_cli("otp", "static", "2", "--generate") assert "Slot 2: programmed" in ykman_cli("otp", "info").output def test_us_scancodes(self, ykman_cli): 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): 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): ykman_cli("otp", "static", "2", "bbb") with pytest.raises(SystemExit): ykman_cli("otp", "static", "2", "ccc") ykman_cli("otp", "static", "2", "ddd", "-f") assert "Slot 2: programmed" in ykman_cli("otp", "info").output class TestSlotProgramming: @pytest.fixture(autouse=True) def delete_slot(self, ykman_cli): try: ykman_cli("otp", "delete", "2", "-f") except SystemExit: pass yield None try: ykman_cli("otp", "delete", "2", "-f") except SystemExit: pass def test_ykman_program_otp_slot_2(self, ykman_cli): ykman_cli( "otp", "yubiotp", "2", "--public-id", "vvccccfiluij", "--private-id", "267e0a88949b", "--key", "b8e31ab90bb8830e3c1fe1b483a8e0d4", "-f", ) self._check_slot_2_programmed(ykman_cli) def test_ykman_program_otp_slot_2_prompt(self, ykman_cli): ykman_cli( "otp", "yubiotp", "2", input="vvccccfiluij\n" "267e0a88949b\n" "b8e31ab90bb8830e3c1fe1b483a8e0d4\n" "n\n" "y\n", ) self._check_slot_2_programmed(ykman_cli) def test_ykman_program_otp_slot_2_options(self, ykman_cli): output = ykman_cli( "otp", "yubiotp", "2", "--public-id", "vvccccfiluij", "--private-id", "267e0a88949b", "--key", "b8e31ab90bb8830e3c1fe1b483a8e0d4", "-f", ).output assert "" == output self._check_slot_2_programmed(ykman_cli) def test_ykman_program_otp_slot_2_generated_all(self, ykman_cli): output = ykman_cli( "otp", "yubiotp", "2", "-f", "--serial-public-id", "--generate-private-id", "--generate-key", ).output assert "Using YubiKey serial as public ID" in output assert "Using a randomly generated private ID" in output assert "Using a randomly generated secret key" in output self._check_slot_2_programmed(ykman_cli) def test_ykman_program_otp_slot_2_serial_public_id(self, ykman_cli): output = ykman_cli( "otp", "yubiotp", "2", "--serial-public-id", "--private-id", "267e0a88949b", "--key", "b8e31ab90bb8830e3c1fe1b483a8e0d4", "-f", ).output assert "Using YubiKey serial as public ID" in output assert "generated private ID" not in output assert "generated secret key" not in output self._check_slot_2_programmed(ykman_cli) def test_invalid_public_id(self, ykman_cli): with pytest.raises(SystemExit): ykman_cli("otp", "yubiotp", "-P", "imnotmodhex!") def test_ykman_program_otp_slot_2_generated_private_id(self, ykman_cli): output = ykman_cli( "otp", "yubiotp", "2", "--public-id", "vvccccfiluij", "--generate-private-id", "--key", "b8e31ab90bb8830e3c1fe1b483a8e0d4", "-f", ).output assert "serial as public ID" not in output assert "Using a randomly generated private ID" in output assert "generated secret key" not in output self._check_slot_2_programmed(ykman_cli) def test_ykman_program_otp_slot_2_generated_secret_key(self, ykman_cli): output = ykman_cli( "otp", "yubiotp", "2", "--public-id", "vvccccfiluij", "--private-id", "267e0a88949b", "--generate-key", "-f", ).output assert "serial as public ID" not in output assert "generated private ID" not in output assert "Using a randomly generated secret key" in output self._check_slot_2_programmed(ykman_cli) def test_ykman_program_otp_slot_2_serial_id_conflicts_public_id(self, ykman_cli): with pytest.raises(SystemExit): ykman_cli( "otp", "yubiotp", "2", "-f", "--serial-public-id", "--public-id", "vvccccfiluij", "--generate-private-id", "--generate-key", ) self._check_slot_2_not_programmed(ykman_cli) def test_ykman_program_otp_slot_2_generate_id_conflicts_private_id( self, ykman_cli ): # noqa: E501 with pytest.raises(SystemExit): ykman_cli( "otp", "yubiotp", "2", "-f", "--serial-public-id", "--generate-private-id", "--private-id", "267e0a88949b", "--generate-key", ) self._check_slot_2_not_programmed(ykman_cli) def test_ykman_program_otp_slot_2_generate_key_conflicts_key(self, ykman_cli): with pytest.raises(SystemExit): ykman_cli( "otp", "yubiotp", "2", "-f", "--serial-public-id", "--generate-private-id", "--generate-key", "--key", "b8e31ab90bb8830e3c1fe1b483a8e0d4", ) self._check_slot_2_not_programmed(ykman_cli) def test_ykman_program_chalresp_slot_2(self, ykman_cli): ykman_cli("otp", "chalresp", "2", "abba", "-f") self._check_slot_2_programmed(ykman_cli) ykman_cli("otp", "chalresp", "2", "--totp", "abba", "-f") self._check_slot_2_programmed(ykman_cli) ykman_cli("otp", "chalresp", "2", "--touch", "abba", "-f") self._check_slot_2_programmed(ykman_cli) def test_ykman_program_chalresp_slot_2_force_fails_without_key(self, ykman_cli): with pytest.raises(SystemExit): ykman_cli("otp", "chalresp", "2", "-f") self._check_slot_2_not_programmed(ykman_cli) def test_ykman_program_chalresp_slot_2_generated(self, ykman_cli): output = ykman_cli("otp", "chalresp", "2", "-f", "-g").output assert re.match("Using a randomly generated key: [0-9a-f]{40}$", output) self._check_slot_2_programmed(ykman_cli) def test_ykman_program_chalresp_slot_2_generated_fails_if_also_given( self, ykman_cli ): # noqa: E501 with pytest.raises(SystemExit): ykman_cli("otp", "chalresp", "2", "-f", "-g", "abababab") def test_ykman_program_chalresp_slot_2_prompt(self, ykman_cli): ykman_cli("otp", "chalresp", "2", input="abba\ny\n") self._check_slot_2_programmed(ykman_cli) def test_ykman_program_hotp_slot_2(self, ykman_cli): ykman_cli("otp", "hotp", "2", "27KIZZE3SD7GE2FVJJBAXEI3I6RRTPGM", "-f") self._check_slot_2_programmed(ykman_cli) def test_ykman_program_hotp_slot_2_prompt(self, ykman_cli): ykman_cli("otp", "hotp", "2", input="abba\ny\n") self._check_slot_2_programmed(ykman_cli) def test_update_settings_enter_slot_2(self, ykman_cli): ykman_cli("otp", "static", "2", "-f", "-g", "-l", "20") output = ykman_cli("otp", "settings", "2", "-f", "--no-enter").output assert "Updating settings for slot" in output def test_delete_slot_2(self, ykman_cli): ykman_cli("otp", "static", "2", "-f", "-g", "-l", "20") output = ykman_cli("otp", "delete", "2", "-f").output assert "Deleting the configuration" in output status = ykman_cli("otp", "info").output assert "Slot 2: empty" in status def test_access_code_slot_2(self, ykman_cli): ykman_cli( "otp", "--access-code", "111111111111", "static", "2", "--generate", "--length", "10", ) self._check_slot_2_programmed(ykman_cli) self._check_slot_2_has_access_code(ykman_cli) ykman_cli("otp", "--access-code", "111111111111", "delete", "2", "-f") status = ykman_cli("otp", "info").output assert "Slot 2: empty" in status @condition.min_version(4, 3, 2) @condition.max_version(4, 3, 5) def test_update_access_code_fails_on_yk_432_to_435(self, ykman_cli): ykman_cli("otp", "static", "2", "--generate", "--length", "10") self._check_slot_2_programmed(ykman_cli) with pytest.raises(SystemExit): ykman_cli("otp", "settings", "--new-access-code", "111111111111", "2", "-f") ykman_cli( "otp", "--access-code", "111111111111", "static", "2", "-f", "--generate", "--length", "10", ) with pytest.raises(SystemExit): ykman_cli("otp", "delete", "2", "-f") with pytest.raises(SystemExit): ykman_cli( "otp", "--access-code", "111111111111", "settings", "--new-access-code", "222222222222", "2", "-f", ) ykman_cli("otp", "--access-code", "111111111111", "delete", "2", "-f") @condition.min_version(4, 3, 2) @condition.max_version(4, 3, 5) def test_delete_access_code_fails_on_yk_432_to_435(self, ykman_cli): ykman_cli( "otp", "--access-code", "111111111111", "static", "2", "--generate", "--length", "10", ) self._check_slot_2_programmed(ykman_cli) with pytest.raises(SystemExit): ykman_cli( "otp", "--access-code", "111111111111", "settings", "--delete-access-code", "2", "-f", ) with pytest.raises(SystemExit): ykman_cli("otp", "delete", "2", "-f") ykman_cli("otp", "--access-code", "111111111111", "delete", "2", "-f") @condition.check(lambda version: not (4, 3, 2) <= version <= (4, 3, 5)) def test_update_access_code_slot_2(self, ykman_cli): ykman_cli("otp", "static", "2", "--generate", "--length", "10") self._check_slot_2_programmed(ykman_cli) self._check_slot_2_does_not_have_access_code(ykman_cli) ykman_cli("otp", "settings", "--new-access-code", "111111111111", "2", "-f") self._check_slot_2_has_access_code(ykman_cli) ykman_cli( "otp", "--access-code", "111111111111", "settings", "--delete-access-code", "2", "-f", ) self._check_slot_2_does_not_have_access_code(ykman_cli) ykman_cli("otp", "delete", "2", "-f") @condition.check(lambda version: not (4, 3, 2) <= version <= (4, 3, 5)) def test_update_access_code_prompt_slot_2(self, ykman_cli): ykman_cli("otp", "static", "2", "--generate", "--length", "10") self._check_slot_2_programmed(ykman_cli) self._check_slot_2_does_not_have_access_code(ykman_cli) ykman_cli( "otp", "settings", "--new-access-code", "-", "2", "-f", input="111111111111" ) self._check_slot_2_has_access_code(ykman_cli) ykman_cli( "otp", "--access-code", "-", "settings", "--delete-access-code", "2", "-f", input="111111111111", ) self._check_slot_2_does_not_have_access_code(ykman_cli) ykman_cli("otp", "delete", "2", "-f") @condition.check(lambda version: not (4, 3, 2) <= version <= (4, 3, 5)) def test_new_access_code_conflicts_with_delete_access_code(self, ykman_cli): ykman_cli("otp", "static", "2", "--generate", "--length", "10") self._check_slot_2_programmed(ykman_cli) self._check_slot_2_does_not_have_access_code(ykman_cli) with pytest.raises(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) ykman_cli("otp", "settings", "--new-access-code", "111111111111", "2", "-f") with pytest.raises(SystemExit): ykman_cli( "otp", "settings", "--delete-access-code", "--new-access-code", "111111111111", "2", "-f", ) self._check_slot_2_has_access_code(ykman_cli) ykman_cli("otp", "--access-code", "111111111111", "delete", "2", "-f") def _check_slot_2_programmed(self, ykman_cli): status = ykman_cli("otp", "info").output assert "Slot 2: programmed" in status def _check_slot_2_not_programmed(self, ykman_cli): status = ykman_cli("otp", "info").output assert "Slot 2: empty" in status def _check_slot_2_has_access_code(self, ykman_cli): with pytest.raises(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): ykman_cli("otp", "settings", "--pacing", "0", "2", "-f") class TestSlotCalculate: @pytest.fixture(autouse=True) def delete_slot(self, ykman_cli): try: ykman_cli("otp", "delete", "2", "-f") except SystemExit: pass yield None try: ykman_cli("otp", "delete", "2", "-f") except SystemExit: pass def test_calculate_hex(self, ykman_cli): ykman_cli("otp", "chalresp", "2", "abba", "-f") output = ykman_cli("otp", "calculate", "2", "abba").output assert "f8de2586056d89d8b961a072d1245a495d2155e1" in output def test_calculate_totp(self, ykman_cli): ykman_cli("otp", "chalresp", "2", "abba", "-f") output = ykman_cli("otp", "calculate", "2", "999", "-T").output assert "533486" == output.strip() output = ykman_cli("otp", "calculate", "2", "999", "-T", "-d", "8").output assert "04533486" == output.strip() output = ykman_cli("otp", "calculate", "2", "-T").output assert 6 == len(output.strip()) output = ykman_cli("otp", "calculate", "2", "-T", "-d", "8").output assert 8 == len(output.strip()) class TestFipsMode: @pytest.fixture(autouse=True) @condition.fips(True) def delete_slots(self, ykman_cli): try: ykman_cli("otp", "delete", "1", "-f") except SystemExit: pass try: ykman_cli("otp", "delete", "2", "-f") except SystemExit: pass yield None def test_not_fips_mode_if_no_slot_programmed(self, ykman_cli): info = ykman_cli("otp", "info").output assert "FIPS Approved Mode: No" in info def test_not_fips_mode_if_slot_1_not_programmed(self, ykman_cli): ykman_cli("otp", "static", "2", "--generate", "--length", "10") info = ykman_cli("otp", "info").output assert "FIPS Approved Mode: No" in info def test_not_fips_mode_if_slot_2_not_programmed(self, ykman_cli): ykman_cli("otp", "static", "1", "--generate", "--length", "10") info = ykman_cli("otp", "info").output assert "FIPS Approved Mode: No" in info def test_not_fips_mode_if_no_slot_has_access_code(self, ykman_cli): ykman_cli("otp", "static", "1", "--generate", "--length", "10") ykman_cli("otp", "static", "2", "--generate", "--length", "10") info = ykman_cli("otp", "info").output assert "FIPS Approved Mode: No" in info def test_not_fips_mode_if_only_slot_1_has_access_code(self, ykman_cli): 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").output assert "FIPS Approved Mode: No" in info ykman_cli("otp", "--access-code", "111111111111", "delete", "1", "-f") def test_not_fips_mode_if_only_slot_2_has_access_code(self, ykman_cli): 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").output assert "FIPS Approved Mode: No" in info ykman_cli("otp", "--access-code", "111111111111", "delete", "2", "-f") def test_fips_mode_if_both_slots_have_access_code(self, ykman_cli): 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").output assert "FIPS Approved Mode: Yes" in info ykman_cli("otp", "--access-code", "111111111111", "delete", "1", "-f") ykman_cli("otp", "--access-code", "111111111111", "delete", "2", "-f") ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.0950031 yubikey-manager-4.0.7/tests/device/condition.py0000644000000000000000000000454000000000000017706 0ustar0000000000000000from ykman.device import is_fips_version from inspect import signature, Parameter, isgeneratorfunction from makefun import wraps import pytest def check(check, message="Condition not satisfied"): check_sig = signature(check) message = check.__doc__ or message def deco(func): func_sig = signature(func) added_params = [] for p in check_sig.parameters: if p not in func_sig.parameters: added_params.append(Parameter(p, kind=Parameter.POSITIONAL_OR_KEYWORD)) new_sig = func_sig.replace( parameters=list(func_sig.parameters.values()) + added_params ) def make_func_args(args, kwargs): check_args = {k: v for k, v in kwargs.items() if k in check_sig.parameters} if not check(**check_args): pytest.skip(message) return {k: v for k, v in kwargs.items() if k in func_sig.parameters} if isgeneratorfunction(func): def wrapper(*args, **kwargs): yield from func(**make_func_args(args, kwargs)) else: def wrapper(*args, **kwargs): return func(**make_func_args(args, kwargs)) return wraps(func, new_sig=new_sig)(wrapper) return deco def transport(required_transport): return check( lambda transport: transport == required_transport, f"Requires {required_transport.name}", ) def has_transport(transport): return check( lambda info: info.supported_capabilities.get(transport), f"Requires {transport.name}", ) def capability(capability, transport=None): return check( lambda info, device: capability in info.config.enabled_capabilities.get(transport or device.transport, []), f"Requires {capability}", ) def min_version(major, minor=0, micro=0): if isinstance(major, tuple): vers = major else: vers = (major, minor, micro) return check(lambda version: version >= vers, f"Version < {vers}") def max_version(major, minor=0, micro=0): if isinstance(major, tuple): vers = major else: vers = (major, minor, micro) return check(lambda version: version <= vers, f"Version > {vers}") def fips(status=True): return check( lambda version: status == is_fips_version(version), f"Requires FIPS = {status}", ) ././@PaxHeader0000000000000000000000000000003200000000000011450 xustar000000000000000026 mtime=1618212188.09558 yubikey-manager-4.0.7/tests/device/conftest.py0000644000000000000000000000562000000000000017545 0ustar0000000000000000from ykman.device import connect_to_device, list_all_devices, read_info from ykman.pcsc import list_devices from yubikit.core import TRANSPORT from yubikit.core.otp import OtpConnection from yubikit.core.fido import FidoConnection from yubikit.core.smartcard import SmartCardConnection from yubikit.management import USB_INTERFACE from functools import partial from . import condition import pytest import time import os @pytest.fixture(scope="session") def _device(pytestconfig): serial = pytestconfig.getoption("device") no_serial = pytestconfig.getoption("no_serial") if not serial: if no_serial: serial = None else: pytest.skip("No serial specified for device tests") reader = pytestconfig.getoption("reader") if reader: readers = list_devices(reader) if len(readers) != 1: pytest.exit("No/Multiple readers matched") dev = readers[0] with dev.open_connection(SmartCardConnection) as conn: info = read_info(None, conn) else: devices = list_all_devices() if len(devices) != 1: pytest.exit("Device tests require a single YubiKey") dev, info = devices[0] if info.serial != serial: pytest.exit("Device serial does not match: %d != %r" % (serial, info.serial)) return dev, info @pytest.fixture(scope="session") def device(_device): return _device[0] @pytest.fixture(scope="session") def info(_device): return _device[1] @pytest.fixture(scope="session") def version(info): return info.version @pytest.fixture(scope="session") def transport(device): return device.transport @pytest.fixture(scope="session") def pid(device): return device.pid @pytest.fixture(scope="session") def await_reboot(transport): delay = float(os.environ.get("REBOOT_TIME", "2.0")) return partial(time.sleep, delay) if transport == TRANSPORT.USB else lambda: None connection_scope = os.environ.get("CONNECTION_SCOPE", "function") @pytest.fixture(scope=connection_scope) @condition.transport(TRANSPORT.USB) def otp_connection(device, info): if USB_INTERFACE.OTP in device.pid.get_interfaces(): with connect_to_device(info.serial, [OtpConnection])[0] as c: yield c @pytest.fixture(scope=connection_scope) @condition.transport(TRANSPORT.USB) def fido_connection(device, info): if USB_INTERFACE.FIDO in device.pid.get_interfaces(): with connect_to_device(info.serial, [FidoConnection])[0] as c: yield c @pytest.fixture(scope=connection_scope) def ccid_connection(device, info): if device.transport == TRANSPORT.NFC: with device.open_connection(SmartCardConnection) as c: yield c elif USB_INTERFACE.CCID in device.pid.get_interfaces(): with connect_to_device(info.serial, [SmartCardConnection])[0] as c: yield c else: pytest.skip("CCID connection not available") ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.0967324 yubikey-manager-4.0.7/tests/device/test_ccid.py0000644000000000000000000000042400000000000017656 0ustar0000000000000000from yubikit.core.smartcard import SmartCardProtocol, ApplicationNotAvailableError import pytest def test_select_wrong_app(ccid_connection): p = SmartCardProtocol(ccid_connection) with pytest.raises(ApplicationNotAvailableError): p.select(b"not_a_real_aid") ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.0978832 yubikey-manager-4.0.7/tests/device/test_fips_u2f_commands.py0000644000000000000000000000374200000000000022360 0ustar0000000000000000from fido2.ctap1 import ApduError from yubikit.core import TRANSPORT from yubikit.management import CAPABILITY from yubikit.core.smartcard import SW from ykman.fido import fips_change_pin, fips_verify_pin, fips_reset, is_in_fips_mode from . import condition import pytest @pytest.fixture(autouse=True) @condition.fips(True) @condition.capability(CAPABILITY.U2F) @condition.transport(TRANSPORT.USB) def preconditions(): pass class TestFipsU2fCommands: def test_pin_commands(self, fido_connection): # Assumes PIN is 012345 or not set at beginning of test # Make sure PIN is 012345 try: fips_verify_pin(fido_connection, "012345") fips_change_pin(fido_connection, "012345", "012345") except ApduError as e: if e.code == SW.VERIFY_FAIL_NO_RETRY: pytest.skip("PIN set to something other than 012345") elif e.code == SW.AUTH_METHOD_BLOCKED: pytest.skip("PIN blocked") elif e.code == SW.COMMAND_NOT_ALLOWED: fips_change_pin(fido_connection, None, "012345") # Verify with correct PIN fips_verify_pin(fido_connection, "012345") # Change the PIN, verify, then change back fips_change_pin(fido_connection, "012345", "012012") fips_verify_pin(fido_connection, "012012") fips_change_pin(fido_connection, "012012", "012345") # Verify with incorrect PIN with pytest.raises(ApduError) as ctx: fips_verify_pin(fido_connection, "543210") assert SW.VERIFY_FAIL_NO_RETRY == ctx.value.code # Verify with correct PIN fips_verify_pin(fido_connection, "012345") def test_reset_command(self, fido_connection): try: fips_reset(fido_connection) except ApduError as e: assert e.code in [SW.COMMAND_NOT_ALLOWED, SW.CONDITIONS_NOT_SATISFIED] def test_verify_fips_mode_command(self, fido_connection): is_in_fips_mode(fido_connection) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1618212188.098459 yubikey-manager-4.0.7/tests/device/test_interfaces.py0000644000000000000000000000213500000000000021100 0ustar0000000000000000from ykman.device import connect_to_device from yubikit.core import TRANSPORT from yubikit.core.otp import OtpConnection from yubikit.core.fido import FidoConnection from yubikit.core.smartcard import SmartCardConnection from yubikit.management import USB_INTERFACE from . import condition def try_connection(conn_type): with connect_to_device(None, [conn_type])[0]: return True @condition.transport(TRANSPORT.USB) def test_switch_interfaces(pid): interfaces = pid.get_interfaces() if USB_INTERFACE.FIDO in interfaces: assert try_connection(FidoConnection) if USB_INTERFACE.OTP in interfaces: assert try_connection(OtpConnection) if USB_INTERFACE.FIDO in interfaces: assert try_connection(FidoConnection) if USB_INTERFACE.CCID in interfaces: assert try_connection(SmartCardConnection) if USB_INTERFACE.OTP in interfaces: assert try_connection(OtpConnection) if USB_INTERFACE.CCID in interfaces: assert try_connection(SmartCardConnection) if USB_INTERFACE.FIDO in interfaces: assert try_connection(FidoConnection) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1618212188.099611 yubikey-manager-4.0.7/tests/device/test_oath.py0000644000000000000000000001753400000000000017721 0ustar0000000000000000import pytest from yubikit.core import AID from yubikit.core.smartcard import ApduError, SW from yubikit.management import CAPABILITY from yubikit.oath import ( OathSession, CredentialData, HASH_ALGORITHM, OATH_TYPE, ) from ykman.device import is_fips_version from . import condition KEY = bytes.fromhex("01020304050607080102030405060708") @pytest.fixture @condition.capability(CAPABILITY.OATH) def session(ccid_connection): oath = OathSession(ccid_connection) oath.reset() yield oath CRED_DATA = CredentialData("name", OATH_TYPE.TOTP, HASH_ALGORITHM.SHA1, b"secret") class TestFunctions: @condition.min_version(5, 3) def test_rename(self, session): cred = session.put_credential(CRED_DATA) new_id = session.rename_credential(cred.id, "newname", "newissuer") with pytest.raises(ApduError): session.calculate(cred.id, b"challenge") session.calculate(new_id, b"challenge") @condition.min_version(5, 3) def test_rename_to_existing(self, session): cred = session.put_credential(CRED_DATA) new_id = session.rename_credential(cred.id, "newname", "newissuer") with pytest.raises(ApduError): session.rename_credential(new_id, "newname", "newissuer") class TestLockPreventsAccess: @pytest.fixture(autouse=True) def set_lock(self, session): assert not session.locked session.put_credential(CRED_DATA) session.set_key(KEY) # Force re-select to lock session.protocol.connection.connection.disconnect() session.protocol.connection.connection.connect() session.protocol.select(AID.OATH) def test_list(self, session): with pytest.raises(ApduError) as ctx: session.list_credentials() assert ctx.value.sw == SW.SECURITY_CONDITION_NOT_SATISFIED def test_calculate(self, session): with pytest.raises(ApduError) as ctx: session.calculate(CRED_DATA.get_id(), b"challenge") assert ctx.value.sw == SW.SECURITY_CONDITION_NOT_SATISFIED def test_calculate_all(self, session): with pytest.raises(ApduError) as ctx: session.calculate_all() assert ctx.value.sw == SW.SECURITY_CONDITION_NOT_SATISFIED def test_delete(self, session): with pytest.raises(ApduError) as ctx: session.delete_credential(CRED_DATA.get_id()) assert ctx.value.sw == SW.SECURITY_CONDITION_NOT_SATISFIED @condition.min_version(5, 3) def test_rename(self, session): with pytest.raises(ApduError) as ctx: session.rename_credential(CRED_DATA.get_id(), "renamed") assert ctx.value.sw == SW.SECURITY_CONDITION_NOT_SATISFIED HMAC_VECTORS = { b"\x0B" * 20: { b"Hi There": { HASH_ALGORITHM.SHA256: bytes.fromhex( "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7" ), HASH_ALGORITHM.SHA512: bytes.fromhex( "87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cde" "daa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854" ), } }, b"Jefe": { b"what do ya want for nothing?": { HASH_ALGORITHM.SHA256: bytes.fromhex( "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843" ), HASH_ALGORITHM.SHA512: bytes.fromhex( "164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea250554" "9758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737" ), } }, b"\xAA" * 20: { b"\xDD" * 50: { HASH_ALGORITHM.SHA256: bytes.fromhex( "773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe" ), HASH_ALGORITHM.SHA512: bytes.fromhex( "fa73b0089d56a284efb0f0756c890be9b1b5dbdd8ee81a3655f83e33b2279d39" "bf3e848279a722c806b485a47e67c807b946a337bee8942674278859e13292fb" ), } }, bytes.fromhex("0102030405060708090a0b0c0d0e0f10111213141516171819"): { b"\xCD" * 50: { HASH_ALGORITHM.SHA256: bytes.fromhex( "82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b" ), HASH_ALGORITHM.SHA512: bytes.fromhex( "b0ba465637458c6990e5a8c5f61d4af7e576d97ff94b872de76f8050361ee3db" "a91ca5c11aa25eb4d679275cc5788063a5f19741120c4f2de2adebeb10a298dd" ), } }, } HMAC_PARAMS = [ (key, timestamp, algo, HMAC_VECTORS[key][timestamp][algo]) for key in HMAC_VECTORS for timestamp in HMAC_VECTORS[key] for algo in HMAC_VECTORS[key][timestamp] ] def _ids_hmac(params): key, challenge, hash_algorithm, expected = params key_s = key.hex() if len(key) < 6 else key[:6].hex() + "..." challenge_s = challenge.hex() if len(challenge) < 6 else challenge[:6].hex() + "..." return f"{hash_algorithm.name}-{key_s}-{challenge_s}" class TestHmacVectors: @pytest.mark.parametrize("params", HMAC_PARAMS, ids=_ids_hmac) def test_vector(self, session, params): key, challenge, hash_algorithm, expected = params if hash_algorithm == HASH_ALGORITHM.SHA512: if session.version < (4, 3, 1) or is_fips_version(session.version): pytest.skip("SHA512 requires (non-FIPS) YubiKey 4.3.1 or later") cred = session.put_credential( CredentialData("test", OATH_TYPE.TOTP, hash_algorithm, key) ) value = session.calculate(cred.id, challenge) assert value == expected TOTP_VECTOR_KEYS = { HASH_ALGORITHM.SHA1: b"12345678901234567890", HASH_ALGORITHM.SHA256: b"12345678901234567890123456789012", HASH_ALGORITHM.SHA512: b"12345678901234567890123456789012" b"34567890123456789012345678901234", } TOTP_VECTORS = { 59: { HASH_ALGORITHM.SHA1: "94287082", HASH_ALGORITHM.SHA256: "46119246", HASH_ALGORITHM.SHA512: "90693936", }, 1111111109: { HASH_ALGORITHM.SHA1: "07081804", HASH_ALGORITHM.SHA256: "68084774", HASH_ALGORITHM.SHA512: "25091201", }, } TOTP_PARAMS = [ (timestamp, algo, TOTP_VECTORS[timestamp][algo], TOTP_VECTOR_KEYS[algo]) for timestamp in TOTP_VECTORS for algo in TOTP_VECTORS[timestamp] ] class TestTotpVectors: @pytest.mark.parametrize("digits", [6, 8]) @pytest.mark.parametrize( "params", TOTP_PARAMS, ids=lambda x: "{1.name}-{0}".format(*x) ) def test_vector(self, session, params, digits): timestamp, hash_algorithm, value, key = params if hash_algorithm == HASH_ALGORITHM.SHA512: if session.version < (4, 3, 1) or is_fips_version(session.version): pytest.skip("SHA512 requires (non-FIPS) YubiKey 4.3.1 or later") cred = session.put_credential( CredentialData("test", OATH_TYPE.TOTP, hash_algorithm, key, digits) ) code = session.calculate_code(cred, timestamp) assert len(code.value) == digits assert value.endswith(code.value) HOTP_VECTORS = { b"12345678901234567890": [ "84755224", "94287082", "37359152", "26969429", "40338314", "68254676", "18287922", "82162583", "73399871", "45520489", ] } class TestHotpVectors: @pytest.mark.parametrize("digits", [6, 8]) @pytest.mark.parametrize( "params", HOTP_VECTORS.items(), ids=lambda x: "{0}".format(*x) ) def test_vector(self, session, params, digits): key, values = params cred = session.put_credential( CredentialData("test", OATH_TYPE.HOTP, HASH_ALGORITHM.SHA1, key, digits) ) for expected in values: code = session.calculate_code(cred) assert len(code.value) == digits assert expected.endswith(code.value) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.1007636 yubikey-manager-4.0.7/tests/device/test_openpgp.py0000644000000000000000000000646700000000000020441 0ustar0000000000000000from __future__ import unicode_literals 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.openpgp import OpenPgpController, KEY_SLOT from yubikit.management import CAPABILITY from yubikit.core.smartcard import ApduError from . import condition import pytest E = 65537 DEFAULT_PIN = "123456" NON_DEFAULT_PIN = "654321" DEFAULT_ADMIN_PIN = "12345678" NON_DEFAULT_ADMIN_PIN = "87654321" @pytest.fixture @condition.capability(CAPABILITY.OPENPGP) def controller(ccid_connection): pgp = OpenPgpController(ccid_connection) pgp.reset() return pgp def not_roca(version): """ROCA affected""" return not ((4, 2, 0) <= version < (4, 3, 5)) def test_generate_requires_admin(controller): with pytest.raises(ApduError): controller.generate_rsa_key(KEY_SLOT.SIG, 2048) @condition.check(not_roca) def test_generate_rsa2048(controller): controller.verify_admin(DEFAULT_ADMIN_PIN) pub = controller.generate_rsa_key(KEY_SLOT.SIG, 2048) assert pub.key_size == 2048 controller.delete_key(KEY_SLOT.SIG) @condition.check(not_roca) @condition.min_version(4) def test_generate_rsa4096(controller): controller.verify_admin(DEFAULT_ADMIN_PIN) pub = controller.generate_rsa_key(KEY_SLOT.SIG, 4096) assert pub.key_size == 4096 @condition.min_version(5, 2) def test_generate_secp256r1(controller): controller.verify_admin(DEFAULT_ADMIN_PIN) pub = controller.generate_ec_key(KEY_SLOT.SIG, "secp256r1") assert pub.key_size == 256 assert pub.curve.name == "secp256r1" @condition.min_version(5, 2) def test_generate_ed25519(controller): controller.verify_admin(DEFAULT_ADMIN_PIN) pub = controller.generate_ec_key(KEY_SLOT.SIG, "ed25519") assert len(pub.public_bytes(Encoding.Raw, PublicFormat.Raw)) == 32 @condition.min_version(5, 2) def test_generate_x25519(controller): controller.verify_admin(DEFAULT_ADMIN_PIN) pub = controller.generate_ec_key(KEY_SLOT.ENC, "x25519") assert len(pub.public_bytes(Encoding.Raw, PublicFormat.Raw)) == 32 def test_import_rsa2048(controller): priv = rsa.generate_private_key(E, 2048, default_backend()) controller.verify_admin(DEFAULT_ADMIN_PIN) controller.import_key(KEY_SLOT.SIG, priv) @condition.min_version(4) def test_import_rsa4096(controller): priv = rsa.generate_private_key(E, 4096, default_backend()) controller.verify_admin(DEFAULT_ADMIN_PIN) controller.import_key(KEY_SLOT.SIG, priv) @condition.min_version(5, 2) def test_import_secp256r1(controller): priv = ec.generate_private_key(ec.SECP256R1(), default_backend()) controller.verify_admin(DEFAULT_ADMIN_PIN) controller.import_key(KEY_SLOT.SIG, priv) @condition.min_version(5, 2) def test_import_ed25519(controller): from cryptography.hazmat.primitives.asymmetric import ed25519 priv = ed25519.Ed25519PrivateKey.generate() controller.verify_admin(DEFAULT_ADMIN_PIN) controller.import_key(KEY_SLOT.SIG, priv) @condition.min_version(5, 2) def test_import_x25519(controller): from cryptography.hazmat.primitives.asymmetric import x25519 priv = x25519.X25519PrivateKey.generate() controller.verify_admin(DEFAULT_ADMIN_PIN) controller.import_key(KEY_SLOT.ENC, priv) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1618212188.101915 yubikey-manager-4.0.7/tests/device/test_otp.py0000644000000000000000000001162500000000000017563 0ustar0000000000000000from yubikit.core import TRANSPORT from yubikit.core.otp import OtpConnection from yubikit.core.smartcard import SmartCardConnection from yubikit.yubiotp import ( YubiOtpSession, SLOT, HmacSha1SlotConfiguration, StaticPasswordSlotConfiguration, ) from yubikit.management import CAPABILITY, ManagementSession from ykman.device import connect_to_device from . import condition import pytest @pytest.fixture(params=[OtpConnection, SmartCardConnection]) def conn_type(request, version, transport): conn_type = request.param if transport == TRANSPORT.NFC: if conn_type != SmartCardConnection: pytest.skip("Using NFC") else: if conn_type == SmartCardConnection and (4, 0) <= version < (5, 3): pytest.skip("3.x/5.3+ only") return conn_type @pytest.fixture() @condition.capability(CAPABILITY.OTP) def session(conn_type, info, device): if device.transport == TRANSPORT.NFC: with device.open_connection(conn_type) as c: yield YubiOtpSession(c) else: with connect_to_device(info.serial, [conn_type])[0] as c: yield YubiOtpSession(c) def test_status(info, session): assert session.get_serial() == info.serial def not_usb_ccid(conn_type, transport): return transport != TRANSPORT.USB or conn_type != SmartCardConnection @pytest.fixture() def read_config(session, conn_type, info, transport, await_reboot): need_reboot = conn_type == SmartCardConnection and (4, 0) <= info.version < (5, 5) if need_reboot and info.version[0] == 4: pytest.skip("Can't read config") def call(): otp = session if need_reboot: protocol = session.backend.protocol if transport == TRANSPORT.NFC: protocol.connection.connection.disconnect() conn = protocol.connection conn.connection.connect() else: ManagementSession(protocol.connection).write_device_config(reboot=True) await_reboot() conn = connect_to_device(info.serial, [SmartCardConnection])[0] otp = YubiOtpSession(conn) session.backend = otp.backend return otp.get_config_state() return call class TestProgrammingState: @pytest.fixture(autouse=True) @condition.min_version(2, 1) def clear_slots(self, session, read_config): state = read_config() for slot in (SLOT.ONE, SLOT.TWO): if state.is_configured(slot): session.delete_slot(slot) def test_slot_configured(self, session, read_config): state = read_config() assert not state.is_configured(SLOT.ONE) assert not state.is_configured(SLOT.TWO) session.put_configuration(SLOT.ONE, HmacSha1SlotConfiguration(b"a" * 16)) state = read_config() assert state.is_configured(SLOT.ONE) assert not state.is_configured(SLOT.TWO) session.put_configuration(SLOT.TWO, HmacSha1SlotConfiguration(b"a" * 16)) state = read_config() assert state.is_configured(SLOT.ONE) assert state.is_configured(SLOT.TWO) session.delete_slot(SLOT.ONE) state = read_config() assert not state.is_configured(SLOT.ONE) assert state.is_configured(SLOT.TWO) session.swap_slots() state = read_config() assert state.is_configured(SLOT.ONE) assert not state.is_configured(SLOT.TWO) session.delete_slot(SLOT.ONE) state = read_config() assert not state.is_configured(SLOT.ONE) assert not state.is_configured(SLOT.TWO) @condition.min_version(3) @pytest.mark.parametrize("slot", [SLOT.ONE, SLOT.TWO]) def test_slot_touch_triggered(self, session, read_config, slot): session.put_configuration(slot, HmacSha1SlotConfiguration(b"a" * 16)) state = read_config() assert state.is_configured(slot) assert not state.is_touch_triggered(slot) session.put_configuration(slot, StaticPasswordSlotConfiguration(b"a")) state = read_config() assert state.is_configured(slot) assert state.is_touch_triggered(slot) session.delete_slot(slot) state = read_config() assert not state.is_configured(slot) assert not state.is_touch_triggered(slot) class TestChallengeResponse: @pytest.fixture(autouse=True) @condition.check(not_usb_ccid) def clear_slot2(self, session, read_config): state = read_config() if state.is_configured(SLOT.TWO): session.delete_slot(SLOT.TWO) def test_calculate_hmac_sha1(self, session): session.put_configuration( SLOT.TWO, HmacSha1SlotConfiguration( bytes.fromhex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b") ), ) output = session.calculate_hmac_sha1(SLOT.TWO, b"Hi There") assert output == bytes.fromhex("b617318655057264e28bc0b6fb378c8ef146be00") ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1621233183.3135097 yubikey-manager-4.0.7/tests/device/test_piv.py0000644000000000000000000005273300000000000017564 0ustar0000000000000000import datetime import random import pytest from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import rsa, ec, padding from yubikit.core import AID, NotSupportedError from yubikit.core.smartcard import ApduError from yubikit.management import CAPABILITY from yubikit.piv import ( PivSession, ALGORITHM, KEY_TYPE, PIN_POLICY, TOUCH_POLICY, SLOT, MANAGEMENT_KEY_TYPE, InvalidPinError, ) from ykman.piv import ( check_key, get_pivman_data, get_pivman_protected_data, generate_self_signed_certificate, generate_csr, pivman_set_mgm_key, ) from ykman.util import parse_certificates, parse_private_key from ykman.device import is_fips_version from ..util import open_file from . import condition DEFAULT_PIN = "123456" NON_DEFAULT_PIN = "654321" DEFAULT_PUK = "12345678" NON_DEFAULT_PUK = "87654321" DEFAULT_MANAGEMENT_KEY = bytes.fromhex( "010203040506070801020304050607080102030405060708" ) NON_DEFAULT_MANAGEMENT_KEY = bytes.fromhex( "010103040506070801020304050607080102030405060708" ) 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) @pytest.fixture @condition.capability(CAPABILITY.PIV) def session(ccid_connection): piv = PivSession(ccid_connection) piv.reset() yield piv reset_state(piv) def not_roca(version): return not ((4, 2, 0) <= version < (4, 3, 5)) def reset_state(session): session.protocol.connection.connection.disconnect() session.protocol.connection.connection.connect() session.protocol.select(AID.PIV) def assert_mgm_key_is(session, key): session.authenticate(MANAGEMENT_KEY_TYPE.TDES, key) def assert_mgm_key_is_not(session, key): with pytest.raises(ApduError): session.authenticate(MANAGEMENT_KEY_TYPE.TDES, key) def generate_key( session, slot=SLOT.AUTHENTICATION, alg=KEY_TYPE.ECCP256, pin_policy=PIN_POLICY.DEFAULT, ): session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) key = session.generate_key(slot, alg, pin_policy=pin_policy) reset_state(session) return key def import_key( session, slot=SLOT.AUTHENTICATION, key_type=KEY_TYPE.ECCP256, pin_policy=PIN_POLICY.DEFAULT, ): if key_type.algorithm == ALGORITHM.RSA: private_key = rsa.generate_private_key( 65537, key_type.bit_len, default_backend() ) elif key_type == KEY_TYPE.ECCP256: private_key = ec.generate_private_key(ec.SECP256R1(), default_backend()) elif key_type == KEY_TYPE.ECCP384: private_key = ec.generate_private_key(ec.SECP384R1(), default_backend()) session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) session.put_key(slot, private_key, pin_policy) reset_state(session) return private_key.public_key() def verify_cert_signature(cert, public_key=None): if not public_key: public_key = cert.public_key args = [cert.signature, cert.tbs_certificate_bytes, cert.signature_hash_algorithm] if KEY_TYPE.from_public_key(public_key).algorithm == ALGORITHM.RSA: args.insert(2, padding.PKCS1v15()) else: args[2] = ec.ECDSA(args[2]) public_key.verify(*args) class TestCertificateSignatures: @pytest.mark.parametrize("key_type", list(KEY_TYPE)) @pytest.mark.parametrize( "hash_algorithm", (hashes.SHA1, hashes.SHA256, hashes.SHA384, hashes.SHA512) ) def test_generate_self_signed_certificate(self, session, key_type, hash_algorithm): if key_type == KEY_TYPE.ECCP384 and session.version < (4, 0, 0): pytest.skip("ECCP384 requires YubiKey 4 or later") if key_type == KEY_TYPE.RSA1024 and is_fips_version(session.version): pytest.skip("RSA1024 not available on YubiKey FIPS") slot = SLOT.SIGNATURE public_key = import_key(session, slot, key_type) session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) session.verify_pin(DEFAULT_PIN) cert = generate_self_signed_certificate( session, slot, public_key, "CN=alice", NOW, NOW, hash_algorithm ) assert cert.public_key().public_numbers() == public_key.public_numbers() verify_cert_signature(cert, public_key) class TestKeyManagement: def test_delete_certificate_requires_authentication(self, session): generate_key(session, SLOT.AUTHENTICATION) with pytest.raises(ApduError): session.delete_certificate(SLOT.AUTHENTICATION) session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) session.delete_certificate(SLOT.AUTHENTICATION) def test_generate_csr_works(self, session): public_key = generate_key(session, SLOT.AUTHENTICATION) session.verify_pin(DEFAULT_PIN) csr = generate_csr(session, SLOT.AUTHENTICATION, public_key, "CN=alice") assert csr.public_key().public_numbers() == public_key.public_numbers() assert ( csr.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0].value == "alice" ) def test_generate_self_signed_certificate_requires_pin(self, session): session.verify_pin(DEFAULT_PIN) public_key = generate_key(session, SLOT.AUTHENTICATION) with pytest.raises(ApduError): generate_self_signed_certificate( session, SLOT.AUTHENTICATION, public_key, "CN=alice", NOW, NOW ) session.verify_pin(DEFAULT_PIN) generate_self_signed_certificate( session, SLOT.AUTHENTICATION, public_key, "CN=alice", NOW, NOW ) @pytest.mark.parametrize("slot", (SLOT.SIGNATURE, SLOT.AUTHENTICATION)) def test_generate_self_signed_certificate(self, session, slot): public_key = generate_key(session, slot) session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) session.verify_pin(DEFAULT_PIN) cert = generate_self_signed_certificate( session, slot, public_key, "CN=alice", NOW, NOW ) assert cert.public_key().public_numbers() == public_key.public_numbers() assert ( cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0].value == "alice" ) def test_generate_key_requires_authentication(self, session): with pytest.raises(ApduError): session.generate_key( SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, touch_policy=TOUCH_POLICY.DEFAULT ) session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) session.generate_key(SLOT.AUTHENTICATION, KEY_TYPE.ECCP256) def test_put_certificate_requires_authentication(self, session): cert = get_test_cert() with pytest.raises(ApduError): session.put_certificate(SLOT.AUTHENTICATION, cert) session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) session.put_certificate(SLOT.AUTHENTICATION, cert) def _test_put_key_pairing(self, session, alg1, alg2): # Set up a key in the slot and create a certificate for it public_key = generate_key(session, SLOT.AUTHENTICATION, alg=alg1) session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) session.verify_pin(DEFAULT_PIN) cert = generate_self_signed_certificate( session, SLOT.AUTHENTICATION, public_key, "CN=test", NOW, NOW ) session.put_certificate(SLOT.AUTHENTICATION, cert) assert check_key(session, SLOT.AUTHENTICATION, cert.public_key()) cert2 = session.get_certificate(SLOT.AUTHENTICATION) assert cert == cert2 session.delete_certificate(SLOT.AUTHENTICATION) # Overwrite the key with one of the same type generate_key(session, SLOT.AUTHENTICATION, alg=alg1) session.verify_pin(DEFAULT_PIN) assert not check_key(session, SLOT.AUTHENTICATION, cert.public_key()) # Overwrite the key with one of a different type generate_key(session, SLOT.AUTHENTICATION, alg=alg2) session.verify_pin(DEFAULT_PIN) assert not check_key(session, SLOT.AUTHENTICATION, cert.public_key()) @condition.check(not_roca) @condition.fips(False) def test_put_certificate_verifies_key_pairing_rsa1024(self, session): self._test_put_key_pairing(session, KEY_TYPE.RSA1024, KEY_TYPE.ECCP256) @condition.check(not_roca) def test_put_certificate_verifies_key_pairing_rsa2048(self, session): self._test_put_key_pairing(session, KEY_TYPE.RSA2048, KEY_TYPE.ECCP256) @condition.check(not_roca) def test_put_certificate_verifies_key_pairing_eccp256_a(self, session): self._test_put_key_pairing(session, KEY_TYPE.ECCP256, KEY_TYPE.RSA2048) @condition.min_version(4) def test_put_certificate_verifies_key_pairing_eccp256_b(self, session): self._test_put_key_pairing(session, KEY_TYPE.ECCP256, KEY_TYPE.ECCP384) @condition.min_version(4) def test_put_certificate_verifies_key_pairing_eccp384(self, session): self._test_put_key_pairing(session, KEY_TYPE.ECCP384, KEY_TYPE.ECCP256) def test_put_key_requires_authentication(self, session): private_key = get_test_key() with pytest.raises(ApduError): session.put_key(SLOT.AUTHENTICATION, private_key) session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) session.put_key(SLOT.AUTHENTICATION, private_key) def test_get_certificate_does_not_require_authentication(self, session): cert = get_test_cert() session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) session.put_certificate(SLOT.AUTHENTICATION, cert) reset_state(session) assert session.get_certificate(SLOT.AUTHENTICATION) class TestManagementKeyReadOnly: """ Tests after which the management key is always the default management key. Placing compatible tests here reduces the amount of slow reset calls needed. """ def test_authenticate_twice_does_not_throw(self, session): session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) def test_reset_resets_has_stored_key_flag(self, session): pivman = get_pivman_data(session) assert not pivman.has_stored_key session.verify_pin(DEFAULT_PIN) session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) pivman_set_mgm_key( session, NON_DEFAULT_MANAGEMENT_KEY, MANAGEMENT_KEY_TYPE.TDES, store_on_device=True, ) pivman = get_pivman_data(session) assert pivman.has_stored_key reset_state(session) session.reset() pivman = get_pivman_data(session) assert not pivman.has_stored_key # Should this really fail? def disabled_test_reset_while_verified_throws_nice_ValueError(self, session): session.verify_pin(DEFAULT_PIN) with pytest.raises(ValueError) as cm: session.reset() assert "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, session): with pytest.raises(ApduError): session.set_management_key( MANAGEMENT_KEY_TYPE.TDES, NON_DEFAULT_MANAGEMENT_KEY ) assert_mgm_key_is(session, DEFAULT_MANAGEMENT_KEY) @condition.min_version(3, 5) def test_set_stored_mgm_key_does_not_destroy_key_if_pin_not_verified(self, session): session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) with pytest.raises(ApduError): pivman_set_mgm_key( session, NON_DEFAULT_MANAGEMENT_KEY, MANAGEMENT_KEY_TYPE.TDES, store_on_device=True, ) assert_mgm_key_is(session, DEFAULT_MANAGEMENT_KEY) class TestManagementKeyReadWrite: """ Tests after which the management key may not be the default management key. """ def test_set_mgm_key_changes_mgm_key(self, session): session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) session.set_management_key(MANAGEMENT_KEY_TYPE.TDES, NON_DEFAULT_MANAGEMENT_KEY) assert_mgm_key_is_not(session, DEFAULT_MANAGEMENT_KEY) assert_mgm_key_is(session, NON_DEFAULT_MANAGEMENT_KEY) def test_set_stored_mgm_key_succeeds_if_pin_is_verified(self, session): session.verify_pin(DEFAULT_PIN) session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) pivman_set_mgm_key( session, NON_DEFAULT_MANAGEMENT_KEY, MANAGEMENT_KEY_TYPE.TDES, store_on_device=True, ) assert_mgm_key_is_not(session, DEFAULT_MANAGEMENT_KEY) assert_mgm_key_is(session, NON_DEFAULT_MANAGEMENT_KEY) pivman_prot = get_pivman_protected_data(session) assert pivman_prot.key == NON_DEFAULT_MANAGEMENT_KEY pivman_prot = get_pivman_protected_data(session) assert_mgm_key_is(session, pivman_prot.key) def sign(session, slot, key_type, message): return session.sign(slot, key_type, message, hashes.SHA256(), padding.PKCS1v15()) class TestOperations: @condition.min_version(4) def test_sign_with_pin_policy_always_requires_pin_every_time(self, session): generate_key(session, pin_policy=PIN_POLICY.ALWAYS) with pytest.raises(ApduError): sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") session.verify_pin(DEFAULT_PIN) sig = sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") assert sig with pytest.raises(ApduError): sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") session.verify_pin(DEFAULT_PIN) sig = sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") assert sig @condition.fips(False) @condition.min_version(4) def test_sign_with_pin_policy_never_does_not_require_pin(self, session): generate_key(session, pin_policy=PIN_POLICY.NEVER) sig = sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") assert sig @condition.fips(True) def test_pin_policy_never_blocked_on_fips(self, session): with pytest.raises(NotSupportedError): generate_key(session, pin_policy=PIN_POLICY.NEVER) @condition.min_version(4) def test_sign_with_pin_policy_once_requires_pin_once_per_session(self, session): generate_key(session, pin_policy=PIN_POLICY.ONCE) with pytest.raises(ApduError): sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") session.verify_pin(DEFAULT_PIN) sig = sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") assert sig sig = sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") assert sig reset_state(session) with pytest.raises(ApduError): sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") session.verify_pin(DEFAULT_PIN) sig = sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") assert sig sig = sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") assert sig def test_signature_can_be_verified_by_public_key(self, session): public_key = generate_key(session) signed_data = bytes(random.randint(0, 255) for i in range(32)) session.verify_pin(DEFAULT_PIN) sig = sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, signed_data) assert sig public_key.verify(sig, signed_data, ec.ECDSA(hashes.SHA256())) def block_pin(session): while session.get_pin_attempts() > 0: try: session.verify_pin(NON_DEFAULT_PIN) except Exception: pass class TestUnblockPin: def test_unblock_pin_requires_no_previous_authentication(self, session): session.unblock_pin(DEFAULT_PUK, NON_DEFAULT_PIN) def test_unblock_pin_with_wrong_puk_throws_InvalidPinError(self, session): with pytest.raises(InvalidPinError): session.unblock_pin(NON_DEFAULT_PUK, NON_DEFAULT_PIN) def test_unblock_pin_resets_pin_and_retries(self, session): session.reset() reset_state(session) block_pin(session) with pytest.raises(InvalidPinError): session.verify_pin(DEFAULT_PIN) session.unblock_pin(DEFAULT_PUK, NON_DEFAULT_PIN) assert session.get_pin_attempts() == 3 session.verify_pin(NON_DEFAULT_PIN) def test_set_pin_retries_requires_pin_and_mgm_key(self, session): # Fails with no authentication with pytest.raises(ApduError): session.set_pin_attempts(4, 4) # Fails with only PIN session.verify_pin(DEFAULT_PIN) with pytest.raises(ApduError): session.set_pin_attempts(4, 4) reset_state(session) session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) # Fails with only management key (requirement added in 0.1.3) if session.version >= (0, 1, 3): with pytest.raises(ApduError): session.set_pin_attempts(4, 4) # Succeeds with both PIN and management key session.verify_pin(DEFAULT_PIN) session.set_pin_attempts(4, 4) def test_set_pin_retries_sets_pin_and_puk_tries(self, session): pin_tries = 9 puk_tries = 7 session.verify_pin(DEFAULT_PIN) session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) session.set_pin_attempts(pin_tries, puk_tries) reset_state(session) assert session.get_pin_attempts() == pin_tries with pytest.raises(InvalidPinError) as ctx: session.change_puk(NON_DEFAULT_PUK, DEFAULT_PUK) assert ctx.value.attempts_remaining == puk_tries - 1 class TestMetadata: @pytest.fixture(autouse=True) @condition.min_version(5, 3) def preconditions(self): pass def test_pin_metadata(self, session): data = session.get_pin_metadata() assert data.default_value is True assert data.total_attempts == 3 assert data.attempts_remaining == 3 def test_management_key_metadata(self, session): data = session.get_management_key_metadata() assert data.key_type == MANAGEMENT_KEY_TYPE.TDES assert data.default_value is True assert data.touch_policy is TOUCH_POLICY.NEVER session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) session.set_management_key( MANAGEMENT_KEY_TYPE.AES192, NON_DEFAULT_MANAGEMENT_KEY ) data = session.get_management_key_metadata() assert data.key_type == MANAGEMENT_KEY_TYPE.AES192 assert data.default_value is False assert data.touch_policy is TOUCH_POLICY.NEVER session.set_management_key(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) data = session.get_management_key_metadata() assert data.default_value is True session.set_management_key(MANAGEMENT_KEY_TYPE.AES192, DEFAULT_MANAGEMENT_KEY) data = session.get_management_key_metadata() assert data.default_value is False @pytest.mark.parametrize("key_type", list(KEY_TYPE)) def test_slot_metadata_generate(self, session, key_type): slot = SLOT.SIGNATURE key = generate_key(session, slot, key_type) data = session.get_slot_metadata(slot) assert data.key_type == key_type assert data.pin_policy == PIN_POLICY.ALWAYS assert data.touch_policy == TOUCH_POLICY.NEVER assert data.generated is True assert data.public_key.public_bytes( encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) == key.public_bytes( encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) @pytest.mark.parametrize( "key", [ rsa.generate_private_key(65537, 1024, default_backend()), rsa.generate_private_key(65537, 2048, default_backend()), ec.generate_private_key(ec.SECP256R1(), default_backend()), ec.generate_private_key(ec.SECP384R1(), default_backend()), ], ) @pytest.mark.parametrize( "slot, pin_policy", [ (SLOT.AUTHENTICATION, PIN_POLICY.ONCE), (SLOT.SIGNATURE, PIN_POLICY.ALWAYS), (SLOT.KEY_MANAGEMENT, PIN_POLICY.ONCE), (SLOT.CARD_AUTH, PIN_POLICY.NEVER), ], ) def test_slot_metadata_put(self, session, key, slot, pin_policy): session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) session.put_key(slot, key) data = session.get_slot_metadata(slot) assert data.key_type == KEY_TYPE.from_public_key(key.public_key()) assert data.pin_policy == pin_policy assert data.touch_policy == TOUCH_POLICY.NEVER assert data.generated is False assert data.public_key.public_bytes( encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) == key.public_key().public_bytes( encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1618212188.104219 yubikey-manager-4.0.7/tests/files/rsa_1024_key.pem0000644000000000000000000000156700000000000020025 0ustar0000000000000000-----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----- ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1618212188.104795 yubikey-manager-4.0.7/tests/files/rsa_2048_cert.der0000644000000000000000000000133700000000000020165 0ustar00000000000000000‚Ú0‚C ü0  *†H†÷ 0›1 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 ô“LÔpÑB )’sPwö‰¬,Öñ«­lÀÙÕ¦«ÊÍZÒV&QåKНÌ%4°*)AUñk“Û³ÌÜìëÇU×B%ÞI52’œÆiã<ûôšøû‹Åà~ýO%º?å–Wš$yI'׉Kj. ‡QÙ#=…VøX1î™xhÍnD~ÉÚŒZ{¿$@)HÑœïÜ®*]ø÷jÇé¼Å°Yö•üËØœíÃü“xZu´VƒúüA„ödy45¬z…sxrç$‰%žÚe¼¯‡“ŒÛu¶à0ÇøY0  *†H†÷ @Ëþ[ÆtÅs‘ßÿ¶ž…sþà o:D/ÌSs2?yd9èxŒbIj²æ‘…·O8Ú¹i.ÉI–„Âëã#ôë¬hKWZQ×ëÀ1l† öU¨øÐB”¥àh§Ÿ¶óœÐá";«…=¡'›P2b¸ìzúÖ}+)æ­²iM(´ø ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.1059477 yubikey-manager-4.0.7/tests/files/rsa_2048_cert.pem0000644000000000000000000000203200000000000020165 0ustar0000000000000000-----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----- ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.1071002 yubikey-manager-4.0.7/tests/files/rsa_2048_cert_metadata.pem0000644000000000000000000000210000000000000022021 0ustar0000000000000000Subject: 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----- ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.1082523 yubikey-manager-4.0.7/tests/files/rsa_2048_key.pem0000644000000000000000000000321700000000000020026 0ustar0000000000000000-----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----- ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1618212188.109407 yubikey-manager-4.0.7/tests/files/rsa_2048_key_cert.pfx0000644000000000000000000000464500000000000021065 0ustar00000000000000000‚ ¡0‚ g *†H†÷  ‚ X‚ T0‚ P0‚ *†H†÷  ‚ø0‚ô0‚í *†H†÷ 0 *†H†÷  0݆|¬o–€‚À©@‹Sšz­ÛY<ÐXtÕZKˆø7õgÀE€ ëî¦:ƒõš¥òÌA@SÈÿ‚‘;¢¿|w’ºCŽ£–4T\Ål*]Z«ÅVƒðÕ@ûÓ:f/Cf—t£•Úˆ ¦ðÕ¢®ÞáÒ²ôV/@A] ffŠ¡¹ƒWgËñqðæ oìm–!e¦§gåç8Dš•ÝØXqO¼s£<ÇpxsLqéì…GÊ|Ŷÿ…®½g„vÏ•¢¬ò)uê7¬“°f5Id±É*\‚ófkš˜ŠY#è·VùI jÎþÚy¬¶ç,KhkФû™Üƒ†à»íÌ:?H „œÆrlLŒryh–Z^žèv¯õ GúS¶ÂîGZ²´ËãxyV¤ýlßþfÝEŠ`=ßPRÛ®÷^nS“@±”ø²Fþ»ÊjyÈNÞT7‘ïu£Ç†˜ÖjÕXä¹l…e`ºº¿p’}àxYw’[ùk´†’1aÝ ßÀŠÆXÅÂIûÏ$Ÿ}yšî¿¨b1Õr+õ”ê¿§É|D¯ù ~ý˜ 8v©4»e‡ÅÎo2#SGÑ1•´T µå:Ÿ*¨Ÿ¡OÈ™ËC;ß6Œ·i#Ôö~rEÌÙ[ü)mãJ•f8#:Vk RZCËI¡m'F4˜" õøwç÷P,ÔS‰‘~hU0§} Yâö.×4||m<ÊÃ-ÓIþñ™kï÷)ÈóêÈO±~ò•°–h:¹ýE¢¶;uPdS^¬ø]Y ¿bÞPsZ(ùÌ^˜G(U Ê­ÿcC·N²òo¾Ô¦¯ñ.¦hŒÓ#Uªïw>ÔIÉâ­GlÿÞ²…ŒQÇìòY°ÀÇ|5ËÎ¥€|³ýØ¥¿†¦ªP»]Ý,âÛ[P‘OWI,I­XŠæÇ¤Ñ éœ×§øt"÷To7T^ G%〘2ˆB¸Õý´Åd6D{ý°-ÞYók€îyM Š+î€;0Ñ:2bA'¬¥L8‡ßãÙxƒë»q"CÂå\Ùߘ™÷¨Sø}£„P;ª©ÿô4.5û¾_´ë»Ï2`–;²µ«±‚ÚìW%”™1ÒªíH ‚­Âäqc輌õ\„@“˜àL!ÉdAºô*íäiDz˜·ÕgІg¼vjz¹$§­eNBŒ|N÷çœ÷sQ­[8#'W…ýÃê;Š—>¦ð#*7Ê0‚A *†H†÷  ‚2‚.0‚*0‚& *†H†÷   ‚î0‚ê0 *†H†÷  0 …è ‚Ȫ+2Á´Å4•ȯè‹:sî4Bjr+…ÏÃýÒaËéM…}äB¤2·U£’üÇDªR\AÈ\ý¯¼öl¾ß, ®FAá‹F^¥KÄK—ý?ƒÞ–²išD-O­*S‹‰ÔÇÉ¿I ?ŸÞl¨8Ï]’ŒEÛB0gSC@EÓ)eO(½ÉÕA˜Áe=ÍR»ü[¯ª]‰3Ãw“µ‹å—‘^9Γ׵ ษ‡É½ë ñP mƒ4HšÿË£ÜÐÂOjÁ‡MSË«ö€÷ébô0…œ¸BÜùIú×u”Û¿Ÿ|¶‚Y2¬ šS‹î06Æ}35]ÿ= •¡;´åD_/F’ãŽá*ˆ¦@po¯âËq;âÁÊ 5^6év(µô¬gë’šy¯üž^šfW©i ¨ q»„kÈÈŠ$B7…Ι4»oÐÉlqäÝW¨©ãÍ<Šâ/åX®[¾Þºü½i\dÛ1Åwç°ŒL¾—Þ¯iÞë Új]ÚL@p™ê‘ )‘Lî…mƒÆô¼X‹Ô`v殨NJ±¬<#¤˜½6‡×s9–c R]ê; è Ѽ㨈ÂB«ôæöBji] Sš\ùaI\‘(~ó¾ûþ¾~%—_†¥ãå¨Û%½cJ£éÜ’¨#’ãLÎZk½¯0†±.sV܇y;߈6L¼¤¸<çÃÁ.Å<Ì$.’í3äa´4}¨œ«Â£¬±¿ ÓÒ£ÜaüÒ¨H\I³#ÉÎÙåb‡Á¬ÿܶ1ZŠéÑ_˜¯Fs r*à¦`ÃM÷’a\´Ðl«xîpéš—bvùC|šx Õ6Ìo0Õ}gœ‡ÕJ!Ü=‡ÄATx£á¯^Šq^„µÖdÕ]3/Ý]ÅJÐV/Û2O³A‘_8ÙV/NåÊ?”„Â}ø:ÁÜDðëc”ñEžõCßnlw˜ŽÞ÷ž ¾_™c»†È ]åDs;Z„~-ræJÍœ2G˜u¦Ì–wÊføc-õÛ=¥?6?~Æ3Jº%¿XSI5'´åÄ=;SkjÆíeR΀¼Hï÷²‰»— õ¬mÛÜ=2Že«fö͹MòtÄ’ÄSÞ—ýô¦ÏÑqkeåÿT0ø+Dàª}ù¾¬I†eqvج‰cD^#°¾^°æ>2ô"Lsô‡tÕ?|£‡®Ž•–¢ê7 1%0# *†H†÷  1g (»eÁȦÊáã3„­þÆÎd010!0 + g¡Ì!o"êgµ½ï§ÔLmC­÷ooCµÅ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.1099796 yubikey-manager-4.0.7/tests/files/rsa_2048_key_cert_encrypted.pfx0000644000000000000000000000464500000000000023142 0ustar00000000000000000‚ ¡0‚ g *†H†÷  ‚ X‚ T0‚ P0‚ *†H†÷  ‚ø0‚ô0‚í *†H†÷ 0 *†H†÷  0æ.儫ꀂÀ[ NÿT‰¤‚6æw1uó Ê0J®sÜyS7*—É«kVS/ÐdPu1}•/çK—à .• œ’?â>wPÛ•ÖbÝßÐÙÿ/ÒØ\Ò¦]¡‹:¶ÝÑîp2˜~ŠÜQ¬vF©zL´Ë‘ ÉÀtæ%®\¤þûÜ;%A†;Ú)¦Už;Hó ,Û{¼¢Â+…µ4•ÑwMN ÞŸÜn,C¸Öb÷dôzgûØ·kñùä¿×“m¯1ñt6B]9ÛqÂÉ;”­Ì«¶.è8%$B’B«‘íÔN&¢0Æ íŠ½«ž,EÝ0Ñ…F/"¤pˆeFÝ;(ʦ‚|u–Ë‘À-á{‘LeÇì"Ð)=*{ É"T¹RþÏ×…`? ãl‘ª´33÷$1:äñݼ 2è }³nкõY\å.àJï!ã"êFßW¹JS ×*à)ßá½’ùK?^L¬`œ›ÀÜ¢G–’;ì<½ ô(a&–Ó:ä]ÇpŒ¼óyßxV º&÷Š ï,1Keâêeùøp]·!‡´³ø_s1N`²ë³ÉPû¯Î¸u'ãòš çgãŒÓkÞ8²ëûrWº:¸wòˆÉ[[ˆ@H é?hçÞñTè¥@f5%'Õ´ðÏ[Å@2Ú2 ã¦gC;sò>ª¯ÏäÉSk”RÆ6¸anúÝ<˜EýÉ #j5ÒpŸÀòˆ2ÕH %SÁÚº% OBɹõ–y©“„ÍÛ`k›²»Hðlö»w m…‹z[/ ü)†ùýiZýÞk~ÛºÒgZÑB]§Üédúsh¶€´å†G(²ý±\«*ˆ5XL¢ªs‡1ç0Yiôþ¿àïWº”žz*@‹çÙÓ”1‡r«CÒ§7|-ÐE¶¤JÞ$Q/ï~!ôBËûôšñXÃãôƒ@"üùS²rÒ}¼°”Óï’ÕÛ¸<Ÿ¡b­irO 0@M]á q£›Á\»/ܬ3ìèÒ¯°Æ Rð]wçt˜Û–ÅAT¦ZîQ ¦H[_ u†`Ž9-‘¯äž o*CΚ¢ÆJ*¥5J‘>ÞEA¿ÎyjÙ’+{óî¼){ð:í§Ëc¦ÚtÜBËSŸx¢Ôsx£\_¦©Z&Çêî× [ÇÒ‹ ý¨è?_Òùf oXÞ@ÀǹÆë", 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("ö") ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1621233183.3135097 yubikey-manager-4.0.7/tests/test_util.py0000644000000000000000000001444700000000000016504 0ustar0000000000000000# vim: set fileencoding=utf-8 : from yubikit.core import Tlv, bytes2int from yubikit.core.otp import modhex_encode, modhex_decode from yubikit.management import FORM_FACTOR from ykman.util import is_pkcs12, is_pem, parse_private_key, parse_certificates from ykman.util import _parse_pkcs12_pyopenssl, _parse_pkcs12_cryptography from ykman.otp import format_oath_code, generate_static_pw, time_challenge from .util import open_file from cryptography.hazmat.primitives.serialization import pkcs12 from OpenSSL import crypto import unittest 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_oath_code(self): self.assertEqual("000000", format_oath_code(b"\0" * 20)) self.assertEqual("00000000", format_oath_code(b"\0" * 20, 8)) self.assertEqual("345678", format_oath_code(b"\x00\xbc\x61\x4e" + b"\0" * 16)) self.assertEqual( "34567890", format_oath_code(b"\x49\x96\x02\xd2" + b"\0" * 16, 8) ) def test_generate_static_pw(self): for i in range(0, 38): self.assertRegex( generate_static_pw(i), "^[cbdefghijklnrtuvCBDEFGHIJKLNRTUV]{%d}$" % i ) 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 = Tlv.parse_list(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_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), # type: ignore ) def test_tlv(self): self.assertEqual(Tlv(b"\xfe\6foobar"), Tlv(0xFE, b"foobar")) tlv1 = Tlv(b"\0\5hello") tlv2 = Tlv(0xFE, b"") tlv3 = Tlv(0x12, b"hi" * 200) self.assertEqual(b"\0\5hello", tlv1) self.assertEqual(b"\xfe\0", tlv2) self.assertEqual(b"\x12\x82\x01\x90" + b"hi" * 200, tlv3) self.assertEqual( b"\0\5hello\xfe\0\x12\x82\x01\x90" + b"hi" * 200, tlv1 + tlv2 + tlv3 ) def test_is_pkcs12(self): with self.assertRaises(TypeError): 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: data = rsa_2048_key_cert_pfx.read() self.assertTrue(is_pkcs12(data)) parse_private_key(data, None) parse_certificates(data, None) 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_parse_pkcs12(self): with open_file("rsa_2048_key_cert.pfx") as rsa_2048_key_cert_pfx: data = rsa_2048_key_cert_pfx.read() key1, certs1 = _parse_pkcs12_cryptography(pkcs12, data, None) key2, certs2 = _parse_pkcs12_pyopenssl(crypto, data, None) self.assertEqual(key1.private_numbers(), key2.private_numbers()) self.assertEqual(1, len(certs1)) self.assertEqual(certs1, certs2) 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): with self.assertRaises(ValueError): FORM_FACTOR.from_code("im a string") # type: ignore 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(0x99)) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.1186187 yubikey-manager-4.0.7/tests/util.py0000644000000000000000000000242600000000000015437 0ustar0000000000000000import datetime import logging import os 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.x509.oid import NameOID 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 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 = ec.generate_private_key(ec.SECP256R1(), default_backend()) public_key = private_key.public_key() subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, common_name)]) return ( x509.CertificateBuilder() .public_key(public_key) .subject_name(subject) .issuer_name(subject) # Same as subject on self-signed certificate. .serial_number(x509.random_serial_number()) .not_valid_before(valid_from) .not_valid_after(valid_to) ).sign(private_key, hashes.SHA256(), default_backend()) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1631098170.426263 yubikey-manager-4.0.7/ykman/__init__.py0000644000000000000000000000304400000000000016173 0ustar0000000000000000# 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 .base import YUBIKEY, PID, YkmanDevice # noqa from .device import ( # noqa scan_devices, list_all_devices, connect_to_device, get_name, read_info, ) __version__ = "4.0.7" ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631098162.2470205 yubikey-manager-4.0.7/ykman/base.py0000644000000000000000000000634500000000000015355 0ustar0000000000000000# Copyright (c) 2015-2020 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 yubikit.core import TRANSPORT, YubiKeyDevice from yubikit.management import USB_INTERFACE from enum import Enum, IntEnum, unique from typing import Optional, Hashable @unique class YUBIKEY(Enum): """YubiKey hardware platforms.""" YKS = "YubiKey Standard" NEO = "YubiKey NEO" SKY = "Security Key by Yubico" YKP = "YubiKey Plus" YK4 = "YubiKey" # This includes YubiKey 5 def get_pid(self, interfaces: USB_INTERFACE) -> "PID": suffix = "_".join( t.name for t in USB_INTERFACE if t in USB_INTERFACE(interfaces) ) return PID[self.name + "_" + suffix] @unique class PID(IntEnum): """USB Product ID values for YubiKey devices.""" 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) -> YUBIKEY: return YUBIKEY[self.name.split("_", 1)[0]] def get_interfaces(self) -> USB_INTERFACE: return USB_INTERFACE(sum(USB_INTERFACE[x] for x in self.name.split("_")[1:])) class YkmanDevice(YubiKeyDevice): """YubiKey device reference, with optional PID""" def __init__(self, transport: TRANSPORT, fingerprint: Hashable, pid: Optional[PID]): super(YkmanDevice, self).__init__(transport, fingerprint) self._pid = pid @property def pid(self) -> Optional[PID]: """Return the PID of the YubiKey, if available.""" return self._pid def __repr__(self): return "%s(pid=%04x, fingerprint=%r)" % ( type(self).__name__, self.pid or 0, self.fingerprint, ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.1243794 yubikey-manager-4.0.7/ykman/cli/__init__.py0000644000000000000000000000253300000000000016744 0ustar0000000000000000# 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. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631098162.2486222 yubikey-manager-4.0.7/ykman/cli/__main__.py0000644000000000000000000003060400000000000016725 0ustar0000000000000000# 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 yubikit.core import ApplicationNotAvailableError from yubikit.core.otp import OtpConnection from yubikit.core.fido import FidoConnection from yubikit.core.smartcard import SmartCardConnection from yubikit.management import USB_INTERFACE import ykman.logging_setup from .. import __version__ from ..pcsc import list_devices as list_ccid, list_readers from ..device import ( read_info, get_name, list_all_devices, scan_devices, connect_to_device, ConnectionNotAvailableException, ) from ..util import get_windows_version from ..diagnostics import get_diagnostics from .util import YkmanContextObject, ykman_group, cli_fail from .info import info from .otp import otp from .openpgp import openpgp from .oath import oath from .piv import piv from .fido import fido from .config import config from .aliases import apply_aliases from .apdu import apdu import click import ctypes import time import sys import logging logger = logging.getLogger(__name__) CLICK_CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"], max_content_width=999) USB_INTERFACE_MAPPING = { SmartCardConnection: USB_INTERFACE.CCID, OtpConnection: USB_INTERFACE.OTP, FidoConnection: USB_INTERFACE.FIDO, } WIN_CTAP_RESTRICTED = ( sys.platform == "win32" and not bool(ctypes.windll.shell32.IsUserAnAdmin()) and get_windows_version() >= (10, 0, 18362) ) def _scan_changes(state, attempts=10): for _ in range(attempts): time.sleep(0.25) devices, new_state = scan_devices() if new_state != state: return devices, new_state raise TimeoutError("Timed out waiting for state change") def retrying_connect(serial, connections, attempts=10, state=None): while True: try: return connect_to_device(serial, connections) except ConnectionNotAvailableException as e: logger.error("Failed opening connection", exc_info=e) raise # No need to retry except Exception as e: logger.error("Failed opening connection", exc_info=e) try: _, state = _scan_changes(state) logger.debug("State changed, re-try connect...") except TimeoutError: raise e def print_version(ctx, param, value): if not value or ctx.resilient_parsing: return click.echo(f"YubiKey Manager (ykman) version: {__version__}") ctx.exit() def print_diagnostics(ctx, param, value): if not value or ctx.resilient_parsing: return click.echo(get_diagnostics()) ctx.exit() def _disabled_interface(connections, cmd_name): interfaces = [USB_INTERFACE_MAPPING[c] for c in connections] req = ", ".join((t.name for t in interfaces)) cli_fail( f"Command '{cmd_name}' requires one of the following USB interfaces " f"to be enabled: '{req}'.\n\n" "Use 'ykman config usb' to set the enabled USB interfaces." ) def _run_cmd_for_serial(cmd, connections, serial): try: return retrying_connect(serial, connections) except ValueError: try: # Serial not found, see if it's among other interfaces in USB enabled: conn = connect_to_device(serial)[0] conn.close() _disabled_interface(connections, cmd) except ValueError: cli_fail( f"Failed connecting to a YubiKey with serial: {serial}.\n" "Make sure the application has the required permissions." ) def _run_cmd_for_single(ctx, cmd, connections, reader_name=None): # Use a specific CCID reader if reader_name: if SmartCardConnection in connections or cmd in (fido.name, otp.name): readers = list_ccid(reader_name) if len(readers) == 1: dev = readers[0] try: conn = dev.open_connection(SmartCardConnection) info = read_info(dev.pid, conn) if cmd == fido.name: conn.close() conn = dev.open_connection(FidoConnection) return conn, dev, info except Exception as e: logger.error("Failure connecting to card", exc_info=e) cli_fail(f"Failed to connect: {e}") elif len(readers) > 1: cli_fail("Multiple YubiKeys on external readers detected.") else: cli_fail("No YubiKey found on external reader.") else: ctx.fail("Not a CCID command.") # Find all connected devices devices, state = scan_devices() n_devs = sum(devices.values()) if n_devs == 0: # The device might not yet be ready, wait a bit try: devices, state = _scan_changes(state) n_devs = sum(devices.values()) except TimeoutError: cli_fail("No YubiKey detected!") if n_devs > 1: cli_fail( "Multiple YubiKeys detected. Use --device SERIAL to specify " "which one to use." ) # Only one connected device, check if any needed interfaces are available pid = next(iter(devices.keys())) for c in connections: if USB_INTERFACE_MAPPING[c] & pid.get_interfaces(): if WIN_CTAP_RESTRICTED and connections == FidoConnection: # FIDO-only command on Windows without Admin won't work. cli_fail("FIDO access on Windows requires running as Administrator.") return retrying_connect(None, connections, state=state) _disabled_interface(connections, cmd) @ykman_group(context_settings=CLICK_CONTEXT_SETTINGS) @click.option( "-d", "--device", type=int, metavar="SERIAL", help="Specify which YubiKey to interact with by serial number.", ) @click.option( "-r", "--reader", help="Use an external smart card reader. Conflicts with --device and list.", metavar="NAME", default=None, ) @click.option( "-l", "--log-level", default=None, type=click.Choice(ykman.logging_setup.LOG_LEVEL_NAMES, case_sensitive=False), 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( "--diagnose", is_flag=True, callback=print_diagnostics, expose_value=False, is_eager=True, help="Show diagnostics information useful for troubleshooting.", ) @click.option( "-v", "--version", is_flag=True, callback=print_version, expose_value=False, is_eager=True, help="Show version information about the app", ) @click.option( "--full-help", is_flag=True, expose_value=False, help="Show --help, including hidden commands, and exit.", ) @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) # Commands that don't directly act on a key if subcmd in (list_keys,): if device: ctx.fail("--device can't be used with this command.") if reader: ctx.fail("--reader can't be used with this command.") return # Commands which need a YubiKey to act on connections = getattr( subcmd, "connections", [SmartCardConnection, FidoConnection, OtpConnection] ) if connections: if connections == [FidoConnection] and WIN_CTAP_RESTRICTED: # FIDO-only command on Windows without Admin won't work. cli_fail("FIDO access on Windows requires running as Administrator.") def resolve(): items = getattr(resolve, "items", None) if not items: if device is not None: items = _run_cmd_for_serial(subcmd.name, connections, device) else: items = _run_cmd_for_single(ctx, subcmd.name, connections, reader) ctx.call_on_close(items[0].close) setattr(resolve, "items", items) return items ctx.obj.add_resolver("conn", lambda: resolve()[0]) ctx.obj.add_resolver("pid", lambda: resolve()[1].pid) ctx.obj.add_resolver("info", lambda: resolve()[2]) @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. """ if readers: for reader in list_readers(): click.echo(reader.name) ctx.exit() # List all attached devices pids = set() for dev, dev_info in list_all_devices(): if serials: if dev_info.serial: click.echo(dev_info.serial) else: if dev.pid is None: # Devices from list_all_devices should always have PID. raise AssertionError("PID is None") name = get_name(dev_info, dev.pid.get_type()) version = "%d.%d.%d" % dev_info.version if dev_info.version else "unknown" mode = dev.pid.name.split("_", 1)[1].replace("_", "+") click.echo( f"{name} ({version}) [{mode}]" + (f" Serial: {dev_info.serial}" if dev_info.serial else "") ) pids.add(dev.pid) # Look for FIDO devices that we can't access if not serials: devs, _ = scan_devices() for pid, count in devs.items(): if pid not in pids: for _ in range(count): name = pid.get_type().value mode = pid.name.split("_", 1)[1].replace("_", "+") click.echo(f"{name} [{mode}] ") COMMANDS = (list_keys, info, otp, openpgp, oath, piv, fido, config, apdu) for cmd in COMMANDS: cli.add_command(cmd) def main(): sys.argv = apply_aliases(sys.argv) try: # --full-help triggers --help, hidden commands will already have read it by now. sys.argv[sys.argv.index("--full-help")] = "--help" except ValueError: pass # No --full-help try: cli(obj={}) except ApplicationNotAvailableError as e: logger.error("Error", exc_info=e) cli_fail( "The functionality required for this command is not enabled or not " "available on this YubiKey." ) except ValueError as e: logger.error("Error", exc_info=e) cli_fail(str(e)) if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.1261084 yubikey-manager-4.0.7/ykman/cli/aliases.py0000644000000000000000000001262000000000000016624 0ustar0000000000000000# Copyright (c) 2020 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 click """ Command line aliases to support commands which have moved. """ ignore = None def replace(*args): def inner(argv, alias, match_at): return argv[:match_at] + list(args) + argv[match_at + len(alias) :] return inner def oath_access_remember(argv, alias, match_at): args = ["oath", "access"] for flag in ("-c", "--clear-all"): if flag in argv: argv.remove(flag) args.extend(["forget", "--all"]) break else: for flag in ("-F", "--forget"): if flag in argv: argv.remove(flag) args.append("forget") break else: args.append("remember") argv = argv[:match_at] + args + argv[match_at + len(alias) :] return argv _aliases = ( (["config", "mode"], ignore), # Avoid match on next line (["mode"], replace("config", "mode")), (["fido", "delete"], replace("fido", "credentials", "delete")), (["fido", "list"], replace("fido", "credentials", "list")), (["fido", "set-pin"], replace("fido", "access", "change-pin")), (["fido", "unlock"], replace("fido", "access", "verify-pin")), (["piv", "change-pin"], replace("piv", "access", "change-pin")), (["piv", "change-puk"], replace("piv", "access", "change-puk")), ( ["piv", "change-management-key"], replace("piv", "access", "change-management-key"), ), (["piv", "set-pin-retries"], replace("piv", "access", "set-retries")), (["piv", "unblock-pin"], replace("piv", "access", "unblock-pin")), (["piv", "attest"], replace("piv", "keys", "attest")), (["piv", "import-key"], replace("piv", "keys", "import")), (["piv", "generate-key"], replace("piv", "keys", "generate")), (["piv", "import-certificate"], replace("piv", "certificates", "import")), (["piv", "export-certificate"], replace("piv", "certificates", "export")), (["piv", "generate-certificate"], replace("piv", "certificates", "generate")), (["piv", "delete-certificate"], replace("piv", "certificates", "delete")), (["piv", "generate-csr"], replace("piv", "certificates", "request")), (["piv", "read-object"], replace("piv", "objects", "export")), (["piv", "write-object"], replace("piv", "objects", "import")), (["piv", "set-chuid"], replace("piv", "objects", "generate", "chuid")), (["piv", "set-ccc"], replace("piv", "objects", "generate", "ccc")), (["openpgp", "set-pin-retries"], replace("openpgp", "access", "set-retries")), (["openpgp", "import-certificate"], replace("openpgp", "certificates", "import")), (["openpgp", "export-certificate"], replace("openpgp", "certificates", "export")), (["openpgp", "delete-certificate"], replace("openpgp", "certificates", "delete")), (["openpgp", "attest"], replace("openpgp", "keys", "attest")), ( ["openpgp", "import-attestation-key"], replace("openpgp", "keys", "import", "att"), ), (["openpgp", "set-touch"], replace("openpgp", "keys", "set-touch")), (["oath", "add"], replace("oath", "accounts", "add")), (["oath", "code"], replace("oath", "accounts", "code")), (["oath", "delete"], replace("oath", "accounts", "delete")), (["oath", "list"], replace("oath", "accounts", "list")), (["oath", "uri"], replace("oath", "accounts", "uri")), (["oath", "set-password"], replace("oath", "access", "change")), (["oath", "remember-password"], oath_access_remember), ) def _find_match(data, selection): ln = len(selection) for i in range(0, len(data) - ln + 1): if data[i : i + ln] == selection: return i def apply_aliases(argv): for (alias, f) in _aliases: i = _find_match(argv, alias) if i is not None: if f: argv = f(argv, alias, i) click.echo( "WARNING: " "The use of this command is deprecated and will be removed!\n" "Replace with: ykman " + " ".join(argv[1:]) + "\n", err=True, ) break # Only handle first match return argv ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.1272597 yubikey-manager-4.0.7/ykman/cli/apdu.py0000644000000000000000000001365000000000000016140 0ustar0000000000000000# Copyright (c) 2020 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 binascii import a2b_hex from yubikit.core import AID from yubikit.core.smartcard import SmartCardConnection, SmartCardProtocol, ApduError, SW from .util import EnumChoice, ykman_command from typing import Tuple, Optional import re import sys import click import struct import logging logger = logging.getLogger(__name__) APDU_PATTERN = re.compile( r"^" r"(?P[0-9a-f]{2})?(?P[0-9a-f]{2})(?P[0-9a-f]{4})?" r"(?::(?P(?:[0-9a-f]{2})+))?" r"(?P=(?P[0-9a-f]{4})?)?" r"$", re.IGNORECASE, ) def _hex(data: bytes) -> str: return " ".join(f"{d:02X}" for d in data) def _parse_apdu(data: str) -> Tuple[Tuple[int, int, int, int, bytes], Optional[int]]: m = APDU_PATTERN.match(data) if not m: raise ValueError("Invalid APDU format: " + data) cla = int(m.group("cla") or "00", 16) ins = int(m.group("ins"), 16) params = int(m.group("params") or "0000", 16) body = a2b_hex(m.group("body") or "") if m.group("check"): sw: Optional[int] = int(m.group("sw") or "9000", 16) else: sw = None p1, p2 = params >> 8, params & 0xFF return (cla, ins, p1, p2, body), sw def _print_response(resp: bytes, sw: int, no_pretty: bool) -> None: click.echo(f"RECV (SW={sw:04X})" + (":" if resp else "")) if no_pretty: click.echo(resp.hex().upper()) else: for i in range(0, len(resp), 16): chunk = resp[i : i + 16] click.echo( " ".join(f"{c:02X}" for c in chunk).ljust(50) # Replace non-printable characters with a dot. + "".join(chr(c) if 31 < c < 127 else chr(183) for c in chunk) ) @ykman_command(SmartCardConnection, hidden="--full-help" not in sys.argv) @click.pass_context @click.option( "-x", "--no-pretty", is_flag=True, help="Print only the hex output of a response" ) @click.option( "-a", "--app", type=EnumChoice(AID), required=False, help="Select application", ) @click.argument("apdu", nargs=-1) @click.option("-s", "--send-apdu", multiple=True, help="Provide full APDUs") def apdu(ctx, no_pretty, app, apdu, send_apdu): """ Execute arbitary APDUs. Provide APDUs as a hex encoded, space-separated list using the following syntax: [CLA]INS[P1P2][:DATA][=EXPECTED_SW] If not provided CLA, P1 and P2 are all set to zero. Setting EXPECTED_SW will cause the command to check the response SW an fail if it differs. "=" can be used as shorthand for "=9000" (SW=OK). Examples: \b Select the OATH application, send a LIST instruction (0xA1), and make sure we get sw=9000 (these are equivalent): $ ykman apdu a40400:a000000527210101=9000 a1=9000 or $ ykman apdu -a oath a1= \b Factory reset the OATH application: $ ykman apdu -a oath 04dead or $ ykman apdu a40400:a000000527210101 04dead or (using full-apdu mode) $ ykman apdu -s 00a4040008a000000527210101 -s 0004dead """ if apdu and send_apdu: ctx.fail("Cannot mix positional APDUs and -s/--send-apdu.") elif not send_apdu: apdus = [_parse_apdu(data) for data in apdu] if not apdus and not app: ctx.fail("No commands provided.") protocol = SmartCardProtocol(ctx.obj["conn"]) is_first = True if app: is_first = False click.echo("SELECT AID: " + _hex(app)) resp = protocol.select(app) _print_response(resp, SW.OK, no_pretty) if send_apdu: # Compatibility mode (full APDUs) for apdu in send_apdu: if not is_first: click.echo() else: is_first = False apdu = a2b_hex(apdu) click.echo("SEND: " + _hex(apdu)) resp, sw = protocol.connection.send_and_receive(apdu) _print_response(resp, sw, no_pretty) else: # Standard mode for apdu, check in apdus: if not is_first: click.echo() else: is_first = False header, body = apdu[:4], apdu[4] req = _hex(struct.pack(">BBBB", *header)) if body: req += " -- " + _hex(body) click.echo("SEND: " + req) try: resp = protocol.send_apdu(*apdu) sw = SW.OK except ApduError as e: resp = e.data sw = e.sw _print_response(resp, sw, no_pretty) if check is not None and sw != check: click.echo(f"Aborted due to error (expected SW={check:04X}).") ctx.exit(1) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631098162.2496223 yubikey-manager-4.0.7/ykman/cli/config.py0000644000000000000000000004336400000000000016461 0ustar0000000000000000# 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 yubikit.core import TRANSPORT from yubikit.management import ( ManagementSession, DeviceConfig, CAPABILITY, USB_INTERFACE, DEVICE_FLAG, Mode, ) from .. import YUBIKEY from .util import ( click_postpone_execution, click_force_option, click_prompt, EnumChoice, cli_fail, ) import os import re import click import logging logger = logging.getLogger(__name__) CLEAR_LOCK_CODE = b"\0" * 16 def prompt_lock_code(): return click_prompt("Enter your lock code", hide_input=True) @click.group() @click.pass_context @click_postpone_execution def config(ctx): """ Enable or disable applications. The applications may be enabled and disabled independently over different transports (USB and NFC). The configuration may also be protected by a lock code. Examples: \b Disable PIV over NFC: $ 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 """ ctx.obj["controller"] = ManagementSession(ctx.obj["conn"]) def _require_config(ctx): info = ctx.obj["info"] if info.version < (5, 0, 0): cli_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. """ _require_config(ctx) info = ctx.obj["info"] app = ctx.obj["controller"] if sum(1 for arg in [new_lock_code, generate, clear] if arg) > 1: cli_fail( "Invalid options: Only one of --new-lock-code, --generate, " "and --clear may be used." ) # Get the new lock code to set if clear: set_code = CLEAR_LOCK_CODE elif generate: set_code = os.urandom(16) click.echo(f"Using a randomly generated lock code: {set_code.hex()}") force or click.confirm( "Lock configuration with this lock code?", abort=True, err=True ) else: if not new_lock_code: new_lock_code = click_prompt( "Enter your new lock code", hide_input=True, confirmation_prompt=True ) set_code = _parse_lock_code(ctx, new_lock_code) # Get the current lock code to use if info.is_locked: if not lock_code: lock_code = click_prompt("Enter your current lock code", hide_input=True) use_code = _parse_lock_code(ctx, lock_code) else: if lock_code: cli_fail("No lock code is currently set. Use --new-lock-code to set one.") use_code = None # Set new lock code try: app.write_device_config( None, False, use_code, set_code, ) except Exception as e: logger.error("Setting the lock code failed", exc_info=e) if info.is_locked: cli_fail("Failed to change the lock code. Wrong current code?") cli_fail("Failed to set the lock code.") @config.command() @click.pass_context @click_force_option @click.option( "-e", "--enable", multiple=True, type=EnumChoice(CAPABILITY), help="Enable applications.", ) @click.option( "-d", "--disable", multiple=True, type=EnumChoice(CAPABILITY), 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=None, 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=None, 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. """ _require_config(ctx) def ensure_not_all_disabled(ctx, usb_enabled): for app in CAPABILITY: 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.") info = ctx.obj["info"] usb_supported = info.supported_capabilities[TRANSPORT.USB] usb_enabled = info.config.enabled_capabilities[TRANSPORT.USB] usb_interfaces = USB_INTERFACE.for_capabilities(usb_enabled) flags = info.config.device_flags if enable_all: enable = [c for c in CAPABILITY if c in usb_supported] _ensure_not_invalid_options(ctx, enable, disable) if touch_eject and no_touch_eject: ctx.fail("Invalid options.") if not usb_supported: cli_fail("USB not supported on this YubiKey.") if list_enabled: _list_apps(ctx, usb_enabled) if touch_eject: flags |= DEVICE_FLAG.EJECT if no_touch_eject: flags &= ~DEVICE_FLAG.EJECT for app in enable: if app & usb_supported: usb_enabled |= app else: cli_fail(f"{app.name} not supported over USB on this YubiKey.") for app in disable: if app & usb_supported: usb_enabled &= ~app else: cli_fail(f"{app.name} not supported over USB on this YubiKey.") ensure_not_all_disabled(ctx, usb_enabled) reboot = usb_interfaces != USB_INTERFACE.for_capabilities(usb_enabled) f_confirm = "" if enable: f_confirm += f"Enable {', '.join(str(app) for app in enable)}.\n" if disable: f_confirm += f"Disable {', '.join(str(app) for app in disable)}.\n" if touch_eject: f_confirm += "Set touch eject.\n" elif no_touch_eject: f_confirm += "Disable touch eject.\n" if autoeject_timeout: f_confirm += f"Set autoeject timeout to {autoeject_timeout}.\n" if chalresp_timeout: f_confirm += f"Set challenge-response timeout to {chalresp_timeout}.\n" if reboot: f_confirm += "This will cause the YubiKey to reboot.\n" f_confirm += "Configure USB?" is_locked = info.is_locked if force and is_locked and not lock_code: cli_fail("Configuration is locked - please supply the --lock-code option.") if lock_code and not is_locked: cli_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) app = ctx.obj["controller"] try: app.write_device_config( DeviceConfig( {TRANSPORT.USB: usb_enabled}, autoeject_timeout, chalresp_timeout, flags, ), reboot, lock_code, ) except Exception as e: logger.error("Failed to write config", exc_info=e) cli_fail("Failed to configure USB applications.") @config.command() @click.pass_context @click_force_option @click.option( "-e", "--enable", multiple=True, type=EnumChoice(CAPABILITY), help="Enable applications.", ) @click.option( "-d", "--disable", multiple=True, type=EnumChoice(CAPABILITY), 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. """ _require_config(ctx) if not (list_enabled or enable_all or enable or disable_all or disable): ctx.fail("No configuration options chosen.") info = ctx.obj["info"] nfc_supported = info.supported_capabilities.get(TRANSPORT.NFC) nfc_enabled = info.config.enabled_capabilities.get(TRANSPORT.NFC) if enable_all: enable = [c for c in CAPABILITY if c in nfc_supported] if disable_all: disable = [c for c in CAPABILITY if c in nfc_enabled] _ensure_not_invalid_options(ctx, enable, disable) if not nfc_supported: cli_fail("NFC not available on this YubiKey.") if list_enabled: _list_apps(ctx, nfc_enabled) for app in enable: if app & nfc_supported: nfc_enabled |= app else: cli_fail(f"{app.name} not supported over NFC on this YubiKey.") for app in disable: if app & nfc_supported: nfc_enabled &= ~app else: cli_fail(f"{app.name} not supported over NFC on this YubiKey.") f_confirm = "" if enable: f_confirm += f"Enable {', '.join(str(app) for app in enable)}.\n" if disable: f_confirm += f"Disable {', '.join(str(app) for app in disable)}.\n" f_confirm += "Configure NFC?" is_locked = info.is_locked if force and is_locked and not lock_code: cli_fail("Configuration is locked - please supply the --lock-code option.") if lock_code and not is_locked: cli_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) app = ctx.obj["controller"] try: app.write_device_config( DeviceConfig({TRANSPORT.NFC: nfc_enabled}, None, None, None), False, # No need to reboot for NFC. lock_code, ) except Exception as e: logger.error("Failed to write config", exc_info=e) cli_fail("Failed to configure NFC applications.") def _list_apps(ctx, enabled): for app in CAPABILITY: 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 = bytes.fromhex(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.") # MODE def _parse_interface_string(interface): for iface in USB_INTERFACE: if iface.name.startswith(interface): return iface raise ValueError() def _parse_mode_string(ctx, param, mode): try: mode_int = int(mode) return Mode.from_code(mode_int) except IndexError: ctx.fail(f"Invalid mode: {mode_int}") except ValueError: pass # Not a numeric mode, parse string try: if mode[0] in ["+", "-"]: info = ctx.obj["info"] usb_enabled = info.config.enabled_capabilities[TRANSPORT.USB] interfaces = USB_INTERFACE.for_capabilities(usb_enabled) for mod in re.findall(r"[+-][A-Z]+", mode.upper()): interface = _parse_interface_string(mod[1:]) if mod.startswith("+"): interfaces |= interface else: interfaces ^= interface else: interfaces = USB_INTERFACE(0) for t in re.split(r"[+]+", mode.upper()): if t: interfaces |= _parse_interface_string(t) except ValueError: ctx.fail(f"Invalid mode string: {mode}") return Mode(interfaces) @config.command() @click.argument("mode", 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). This command is generaly used with YubiKeys prior to the 5 series. Use "ykman config usb" for more granular control on YubiKey 5 and later. 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 config mode OTP+FIDO \b Set the CCID only mode and use touch to eject the smart card: $ ykman config mode CCID --touch-eject """ info = ctx.obj["info"] mgmt = ctx.obj["controller"] usb_enabled = info.config.enabled_capabilities[TRANSPORT.USB] my_mode = Mode(USB_INTERFACE.for_capabilities(usb_enabled)) usb_supported = info.supported_capabilities[TRANSPORT.USB] interfaces_supported = USB_INTERFACE.for_capabilities(usb_supported) pid = ctx.obj["pid"] if pid: key_type = pid.get_type() else: key_type = None if autoeject_timeout: # autoeject implies touch eject touch_eject = True autoeject = autoeject_timeout if touch_eject else None if mode.interfaces != USB_INTERFACE.CCID: if touch_eject: ctx.fail("--touch-eject can only be used when setting CCID-only mode") if not force: if mode == my_mode: cli_fail(f"Mode is already {mode}, nothing to do...", 0) elif key_type in (YUBIKEY.YKS, YUBIKEY.YKP): cli_fail( "Mode switching is not supported on this YubiKey!\n" "Use --force to attempt to set it anyway." ) elif mode.interfaces not in interfaces_supported: cli_fail( f"Mode {mode} is not supported on this YubiKey!\n" + "Use --force to attempt to set it anyway." ) force or click.confirm(f"Set mode of YubiKey to {mode}?", abort=True, err=True) try: mgmt.set_mode(mode, chalresp_timeout, autoeject) click.echo( "Mode set! You must remove and re-insert your YubiKey " "for this change to take effect." ) except Exception 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." ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631098162.2506194 yubikey-manager-4.0.7/ykman/cli/fido.py0000755000000000000000000005747100000000000016144 0ustar0000000000000000# 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 fido2.ctap import CtapError from fido2.ctap1 import ApduError from fido2.ctap2 import ( Ctap2, ClientPin, CredentialManagement, FPBioEnrollment, CaptureError, ) from fido2.pcsc import CtapPcscDevice from yubikit.core.fido import FidoConnection from yubikit.core.smartcard import SW from time import sleep from .util import ( click_postpone_execution, click_prompt, click_force_option, ykman_group, prompt_timeout, ) from .util import cli_fail from ..fido import is_in_fips_mode, fips_reset, fips_change_pin, fips_verify_pin from ..hid import list_ctap_devices from ..device import is_fips_version from ..pcsc import list_devices as list_ccid from smartcard.Exceptions import NoCardException, CardConnectionException from typing import Optional import click import logging logger = logging.getLogger(__name__) FIPS_PIN_MIN_LENGTH = 6 PIN_MIN_LENGTH = 4 @ykman_group(FidoConnection) @click.pass_context @click_postpone_execution def fido(ctx): """ Manage the 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 access change-pin --pin 123456 --new-pin 654321 """ conn = ctx.obj["conn"] try: ctx.obj["ctap2"] = Ctap2(conn) except (ValueError, CtapError) as e: logger.info("FIDO device does not support CTAP2: %s", e) @fido.command() @click.pass_context def info(ctx): """ Display general status of the FIDO2 application. """ conn = ctx.obj["conn"] ctap2 = ctx.obj.get("ctap2") if is_fips_version(ctx.obj["info"].version): click.echo("FIPS Approved Mode: " + ("Yes" if is_in_fips_mode(conn) else "No")) elif ctap2: client_pin = ClientPin(ctap2) # N.B. All YubiKeys with CTAP2 support PIN. if ctap2.info.options["clientPin"]: if ctap2.info.force_pin_change: click.echo( "NOTE: The FIDO PID is disabled and must be changed before it can " "be used!" ) pin_retries, power_cycle = client_pin.get_pin_retries() if pin_retries: click.echo(f"PIN is set, with {pin_retries} attempt(s) remaining.") if power_cycle: click.echo( "PIN is temporarily blocked. " "Remove and re-insert the YubiKey to unblock." ) else: click.echo("PIN is set, but has been blocked.") else: click.echo("PIN is not set.") bio_enroll = ctap2.info.options.get("bioEnroll") if bio_enroll: uv_retries, _ = client_pin.get_uv_retries() if uv_retries: click.echo( f"Fingerprints registered, with {uv_retries} attempt(s) " "remaining." ) else: click.echo( "Fingerprints registered, but blocked until PIN is verified." ) elif bio_enroll is False: click.echo("No fingerprints have been registered.") always_uv = ctap2.info.options.get("alwaysUv") if always_uv is not None: click.echo( "Always Require User Verification is turned " + ("on." if always_uv else "off.") ) else: click.echo("PIN is not supported.") @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. """ conn = ctx.obj["conn"] if isinstance(conn, CtapPcscDevice): # NFC readers = list_ccid(conn._name) if not readers or readers[0].reader.name != conn._name: logger.error(f"Multiple readers matched: {readers}") cli_fail("Unable to isolate NFC reader.") dev = readers[0] logger.debug(f"use: {dev}") is_fips = False def prompt_re_insert(): click.echo( "Remove and re-place your YubiKey on the NFC reader to perform the " "reset..." ) removed = False while True: sleep(0.5) try: with dev.open_connection(FidoConnection): if removed: sleep(1.0) # Wait for the device to settle break except CardConnectionException: pass # Expected, ignore except NoCardException: removed = True return dev.open_connection(FidoConnection) else: # USB n_keys = len(list_ctap_devices()) if n_keys > 1: cli_fail("Only one YubiKey can be connected to perform a reset.") is_fips = is_fips_version(ctx.obj["info"].version) ctap2 = ctx.obj.get("ctap2") if not is_fips and not ctap2: cli_fail("This YubiKey does not support FIDO reset.") def prompt_re_insert(): click.echo("Remove and re-insert your YubiKey to perform the reset...") removed = False while True: sleep(0.5) keys = list_ctap_devices() if not keys: removed = True if removed and len(keys) == 1: return keys[0].open_connection(FidoConnection) 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() if is_fips: 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, ) if destroy_input != "OVERWRITE": cli_fail("Reset aborted by user.") conn = prompt_re_insert() try: with prompt_timeout(): if is_fips: fips_reset(conn) else: Ctap2(conn).reset() except CtapError as e: logger.error("Reset failed", exc_info=e) if e.code == CtapError.ERR.ACTION_TIMEOUT: cli_fail( "Reset failed. You need to touch your YubiKey to confirm the reset." ) elif e.code in (CtapError.ERR.NOT_ALLOWED, CtapError.ERR.PIN_AUTH_BLOCKED): cli_fail( "Reset failed. Reset must be triggered within 5 seconds after the " "YubiKey is inserted." ) else: cli_fail(f"Reset failed: {e.code.name}") except ApduError as e: # From fips_reset logger.error("Reset failed", exc_info=e) if e.code == SW.COMMAND_NOT_ALLOWED: cli_fail( "Reset failed. Reset must be triggered within 5 seconds after the " "YubiKey is inserted." ) else: cli_fail("Reset failed.") except Exception as e: logger.error(e) cli_fail("Reset failed.") def _fail_pin_error(ctx, e, other="%s"): if e.code == CtapError.ERR.PIN_INVALID: cli_fail("Wrong PIN.") elif e.code == CtapError.ERR.PIN_AUTH_BLOCKED: cli_fail( "PIN authentication is currently blocked. " "Remove and re-insert the YubiKey." ) elif e.code == CtapError.ERR.PIN_BLOCKED: cli_fail("PIN is blocked.") else: cli_fail(other % e.code) @fido.group("access") def access(): """ Manage the PIN for FIDO. """ @access.command("change-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 change_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. """ is_fips = is_fips_version(ctx.obj["info"].version) if is_fips and not u2f: cli_fail("This is a YubiKey FIPS. To set the U2F PIN, pass the --u2f option.") if u2f and not is_fips: cli_fail( "This is not a YubiKey FIPS, and therefore does not support a U2F PIN. " "To set the FIDO2 PIN, remove the --u2f option." ) if is_fips: conn = ctx.obj["conn"] else: ctap2 = ctx.obj.get("ctap2") if not ctap2: cli_fail("PIN is not supported on this YubiKey.") client_pin = ClientPin(ctap2) def prompt_new_pin(): return click_prompt( "Enter your new PIN", hide_input=True, confirmation_prompt=True, ) def change_pin(pin, new_pin): if pin is not None: _fail_if_not_valid_pin(ctx, pin, is_fips) try: if is_fips: try: # Failing this with empty current PIN does not cost a retry fips_change_pin(conn, pin or "", 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) fips_change_pin(conn, pin, new_pin) else: raise else: client_pin.change_pin(pin, new_pin) except CtapError as e: logger.error("Failed to change PIN", exc_info=e) if e.code == CtapError.ERR.PIN_POLICY_VIOLATION: cli_fail("New PIN doesn't meet policy requirements.") else: _fail_pin_error(ctx, e, "Failed to change PIN: %s") except ApduError as e: logger.error("Failed to change PIN", exc_info=e) if e.code == SW.VERIFY_FAIL_NO_RETRY: cli_fail("Wrong PIN.") elif e.code == SW.AUTH_METHOD_BLOCKED: cli_fail("PIN is blocked.") else: cli_fail(f"Failed to change PIN: SW={e.code:04x}") def set_pin(new_pin): _fail_if_not_valid_pin(ctx, new_pin, is_fips) try: client_pin.set_pin(new_pin) except CtapError as e: logger.error("Failed to set PIN", exc_info=e) if e.code == CtapError.ERR.PIN_POLICY_VIOLATION: cli_fail("PIN is too long.") else: cli_fail(f"Failed to set PIN: {e.code}") if not is_fips: if ctap2.info.options.get("clientPin"): if not pin: pin = _prompt_current_pin() else: if pin: cli_fail("There is no current PIN set. Use --new-pin to set one.") if not new_pin: new_pin = prompt_new_pin() if is_fips: _fail_if_not_valid_pin(ctx, new_pin, is_fips) change_pin(pin, new_pin) else: if len(new_pin) < ctap2.info.min_pin_length: cli_fail("New PIN is too short.") if ctap2.info.options.get("clientPin"): change_pin(pin, new_pin) else: set_pin(new_pin) def _require_pin(ctx, pin, feature="This feature"): ctap2 = ctx.obj.get("ctap2") if not ctap2: cli_fail(f"{feature} is not supported on this YubiKey.") if not ctap2.info.options.get("clientPin"): cli_fail(f"{feature} requires having a PIN. Set a PIN first.") if ctap2.info.force_pin_change: cli_fail("The FIDO PIN is blocked. Change the PIN first.") if pin is None: pin = _prompt_current_pin(prompt="Enter your PIN") return pin @access.command("verify-pin") @click.pass_context @click.option("-P", "--pin", help="Current PIN code.") def verify(ctx, pin): """ Verify the FIDO PIN against a YubiKey. For YubiKeys supporting FIDO2 this will reset the "retries" counter of the PIN. For YubiKey FIPS this will unlock the session, allowing U2F registration. """ ctap2 = ctx.obj.get("ctap2") if ctap2: pin = _require_pin(ctx, pin) client_pin = ClientPin(ctap2) try: # Get a PIN token to verify the PIN. client_pin.get_pin_token( pin, ClientPin.PERMISSION.GET_ASSERTION, "ykman.example.com" ) except CtapError as e: logger.error("PIN verification failed", exc_info=e) cli_fail(f"Error: {e}") elif is_fips_version(ctx.obj["info"].version): _fail_if_not_valid_pin(ctx, pin, True) try: fips_verify_pin(ctx.obj["conn"], pin) except ApduError as e: logger.error("PIN verification failed", exc_info=e) if e.code == SW.VERIFY_FAIL_NO_RETRY: cli_fail("Wrong PIN.") elif e.code == SW.AUTH_METHOD_BLOCKED: cli_fail("PIN is blocked.") elif e.code == SW.COMMAND_NOT_ALLOWED: cli_fail("PIN is not set.") else: cli_fail(f"PIN verification failed: {e.code.name}") else: cli_fail("This YubiKey does not support a FIDO PIN.") click.echo("PIN verified.") def _prompt_current_pin(prompt="Enter your current PIN"): return click_prompt(prompt, hide_input=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(f"PIN must be over {min_length} characters long") def _gen_creds(credman): data = credman.get_metadata() if data.get(CredentialManagement.RESULT.EXISTING_CRED_COUNT) == 0: return # No credentials for rp in credman.enumerate_rps(): for cred in credman.enumerate_creds(rp[CredentialManagement.RESULT.RP_ID_HASH]): yield ( rp[CredentialManagement.RESULT.RP]["id"], cred[CredentialManagement.RESULT.CREDENTIAL_ID], cred[CredentialManagement.RESULT.USER]["id"], cred[CredentialManagement.RESULT.USER]["name"], ) def _format_cred(rp_id, user_id, user_name): return f"{rp_id} {user_id.hex()} {user_name}" @fido.group("credentials") def creds(): """ Manage discoverable (resident) credentials. This command lets you manage credentials stored on your YubiKey. Credential management is only available when a FIDO PIN is set on the YubiKey. \b Examples: \b List credentials (providing PIN via argument): $ ykman fido credentials list --pin 123456 \b Delete a credential by user name (PIN will be prompted for): $ ykman fido credentials delete example_user """ def _init_credman(ctx, pin): pin = _require_pin(ctx, pin, "Credential Management") ctap2 = ctx.obj.get("ctap2") client_pin = ClientPin(ctap2) try: token = client_pin.get_pin_token(pin, ClientPin.PERMISSION.CREDENTIAL_MGMT) except CtapError as e: logger.error("Ctap error", exc_info=e) _fail_pin_error(ctx, e, "PIN error: %s") return CredentialManagement(ctap2, client_pin.protocol, token) @creds.command("list") @click.pass_context @click.option("-P", "--pin", help="PIN code.") def creds_list(ctx, pin): """ List credentials. """ creds = _init_credman(ctx, pin) for (rp_id, _, user_id, user_name) in _gen_creds(creds): click.echo(_format_cred(rp_id, user_id, user_name)) @creds.command("delete") @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 creds_delete(ctx, query, pin, force): """ Delete a credential. \b QUERY A unique substring match of a credentials RP ID, user ID (hex) or name, or credential ID. """ credman = _init_credman(ctx, pin) hits = [ (rp_id, cred_id, user_id, user_name) for (rp_id, cred_id, user_id, user_name) in _gen_creds(credman) if query.lower() in user_name.lower() or query.lower() in rp_id.lower() or user_id.hex().startswith(query.lower()) or query.lower() in _format_cred(rp_id, user_id, user_name) ] if len(hits) == 0: cli_fail("No matches, nothing to be done.") elif len(hits) == 1: (rp_id, cred_id, user_id, user_name) = hits[0] if force or click.confirm( f"Delete credential {_format_cred(rp_id, user_id, user_name)}?" ): try: credman.delete_cred(cred_id) except CtapError as e: logger.error("Failed to delete resident credential", exc_info=e) cli_fail("Failed to delete resident credential.") else: cli_fail("Multiple matches, make the query more specific.") @fido.group("fingerprints") def bio(): """ Manage fingerprints. Requires a YubiKey with fingerprint sensor. Fingerprint management is only available when a FIDO PIN is set on the YubiKey. \b Examples: \b Register a new fingerprint (providing PIN via argument): $ ykman fido fingerprints add "Left thumb" --pin 123456 \b List already stored fingerprints (providing PIN via argument): $ ykman fido fingerprints list --pin 123456 \b Delete a stored fingerprint with ID "f691" (PIN will be prompted for): $ ykman fido fingerprints delete f691 """ def _init_bio(ctx, pin): ctap2 = ctx.obj.get("ctap2") if not ctap2 or "bioEnroll" not in ctap2.info.options: cli_fail("Biometrics is not supported on this YubiKey.") pin = _require_pin(ctx, pin, "Biometrics") client_pin = ClientPin(ctap2) try: token = client_pin.get_pin_token(pin, ClientPin.PERMISSION.BIO_ENROLL) except CtapError as e: logger.error("Ctap error", exc_info=e) _fail_pin_error(ctx, e, "PIN error: %s") return FPBioEnrollment(ctap2, client_pin.protocol, token) def _format_fp(template_id, name): return f"{template_id.hex()}{f' ({name})' if name else ''}" @bio.command("list") @click.pass_context @click.option("-P", "--pin", help="PIN code.") def bio_list(ctx, pin): """ List registered fingerprint. Lists fingerprints by ID and (if available) label. """ bio = _init_bio(ctx, pin) for t_id, name in bio.enumerate_enrollments().items(): click.echo(f"ID: {_format_fp(t_id, name)}") @bio.command("add") @click.pass_context @click.argument("name") @click.option("-P", "--pin", help="PIN code.") def bio_enroll(ctx, name, pin): """ Add a new fingerprint. \b NAME A short readable name for the fingerprint (eg. "Left thumb"). """ if len(name.encode()) > 15: ctx.fail("Fingerprint name must be a maximum of 15 characters") bio = _init_bio(ctx, pin) enroller = bio.enroll() template_id = None while template_id is None: click.echo("Place your finger against the sensor now...") try: template_id = enroller.capture() remaining = enroller.remaining if remaining: click.echo(f"{remaining} more scans needed.") except CaptureError as e: logger.error(f"Capture error: {e.code}") click.echo("Capture failed. Re-center your finger, and try again.") except CtapError as e: logger.error("Failed to add fingerprint template", exc_info=e) if e.code == CtapError.ERR.FP_DATABASE_FULL: cli_fail( "Fingerprint storage full. " "Remove some fingerprints before adding new ones." ) elif e.code == CtapError.ERR.USER_ACTION_TIMEOUT: cli_fail("Failed to add fingerprint due to user inactivity.") cli_fail(f"Failed to add fingerprint: {e.code.name}") click.echo("Capture complete.") bio.set_name(template_id, name) @bio.command("rename") @click.pass_context @click.argument("template_id", metavar="ID") @click.argument("name") @click.option("-P", "--pin", help="PIN code.") def bio_rename(ctx, template_id, name, pin): """ Set the label for a fingerprint. \b ID The ID of the fingerprint to rename (as shown in "list"). NAME A short readable name for the fingerprint (eg. "Left thumb"). """ if len(name.encode()) >= 16: ctx.fail("Fingerprint name must be a maximum of 15 bytes") bio = _init_bio(ctx, pin) enrollments = bio.enumerate_enrollments() key = bytes.fromhex(template_id) if key not in enrollments: cli_fail(f"No fingerprint matching ID={template_id}.") bio.set_name(key, name) @bio.command("delete") @click.pass_context @click.argument("template_id", metavar="ID") @click.option("-P", "--pin", help="PIN code.") @click.option("-f", "--force", is_flag=True, help="Confirm deletion without prompting") def bio_delete(ctx, template_id, pin, force): """ Delete a fingerprint. Delete a fingerprint from the YubiKey by its ID, which can be seen by running the "list" subcommand. """ bio = _init_bio(ctx, pin) enrollments = bio.enumerate_enrollments() try: key: Optional[bytes] = bytes.fromhex(template_id) except ValueError: key = None if key not in enrollments: # Match using template_id as NAME matches = [k for k in enrollments if enrollments[k] == template_id] if len(matches) == 0: cli_fail(f"No fingerprint matching ID={template_id}") elif len(matches) > 1: cli_fail( f"Multiple matches for NAME={template_id}. " "Delete by template ID instead." ) key = matches[0] name = enrollments[key] if force or click.confirm(f"Delete fingerprint {_format_fp(key, name)}?"): try: bio.remove_enrollment(key) except CtapError as e: logger.error("Failed to delete fingerprint template", exc_info=e) cli_fail(f"Failed to delete fingerprint: {e.code.name}") ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1621233183.3186707 yubikey-manager-4.0.7/ykman/cli/info.py0000644000000000000000000001546300000000000016146 0ustar0000000000000000# 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 yubikit.core import TRANSPORT from yubikit.core.otp import OtpConnection from yubikit.core.fido import FidoConnection from yubikit.core.smartcard import SmartCardConnection from yubikit.management import CAPABILITY, USB_INTERFACE from yubikit.yubiotp import YubiOtpSession from yubikit.oath import OathSession from .util import cli_fail from ..device import is_fips_version, get_name, connect_to_device from ..otp import is_in_fips_mode as otp_in_fips_mode from ..oath import is_in_fips_mode as oath_in_fips_mode from ..fido import is_in_fips_mode as ctap_in_fips_mode from typing import List import click import logging logger = logging.getLogger(__name__) SHOWN_CAPABILITIES = set(CAPABILITY) def print_app_status_table(supported_apps, enabled_apps): usb_supported = supported_apps.get(TRANSPORT.USB, 0) usb_enabled = enabled_apps.get(TRANSPORT.USB, 0) nfc_supported = supported_apps.get(TRANSPORT.NFC, 0) nfc_enabled = enabled_apps.get(TRANSPORT.NFC, 0) rows = [] for app in SHOWN_CAPABILITIES: if app & usb_supported: if app & usb_enabled: usb_status = "Enabled" else: usb_status = "Disabled" else: usb_status = "Not available" if nfc_supported: if app & nfc_supported: if app & 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: List[int] = [] 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 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 += f"{c.ljust(column_l[idx])}\t" f_table += "\n" if nfc_supported: click.echo(f"{f_apps}\t{f_USB}\t{f_NFC}") else: click.echo(f"{f_apps}") click.echo(f_table, nl=False) def get_overall_fips_status(pid, info): statuses = {} usb_enabled = info.config.enabled_capabilities[TRANSPORT.USB] statuses["OTP"] = False if usb_enabled & CAPABILITY.OTP: with connect_to_device(info.serial, [OtpConnection])[0] as conn: otp_app = YubiOtpSession(conn) statuses["OTP"] = otp_in_fips_mode(otp_app) statuses["OATH"] = False if usb_enabled & CAPABILITY.OATH: with connect_to_device(info.serial, [SmartCardConnection])[0] as conn: oath_app = OathSession(conn) statuses["OATH"] = oath_in_fips_mode(oath_app) statuses["FIDO U2F"] = False if usb_enabled & CAPABILITY.U2F: with connect_to_device(info.serial, [FidoConnection])[0] as conn: statuses["FIDO U2F"] = ctap_in_fips_mode(conn) return statuses def _check_fips_status(pid, info): fips_status = get_overall_fips_status(pid, info) click.echo() click.echo(f"FIPS Approved Mode: {'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(f" {status_key}: {'Yes' if fips_status[status_key] else 'No'}") @click.option( "-c", "--check-fips", help="Check if YubiKey is in FIPS Approved mode (available on YubiKey 4 FIPS " "only).", 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, capabilities, etc. """ info = ctx.obj["info"] pid = ctx.obj["pid"] if pid is None: interfaces = None key_type = None else: interfaces = pid.get_interfaces() key_type = pid.get_type() device_name = get_name(info, key_type) click.echo(f"Device type: {device_name}") if info.serial: click.echo(f"Serial number: {info.serial}") if info.version: f_version = ".".join(str(x) for x in info.version) click.echo(f"Firmware version: {f_version}") else: click.echo( "Firmware version: Uncertain, re-run with only one YubiKey connected" ) if info.form_factor: click.echo(f"Form factor: {info.form_factor!s}") if interfaces: f_interfaces = ", ".join( t.name for t in USB_INTERFACE if t in USB_INTERFACE(interfaces) ) click.echo(f"Enabled USB interfaces: {f_interfaces}") if TRANSPORT.NFC in info.supported_capabilities: f_nfc = ( "enabled" if info.config.enabled_capabilities.get(TRANSPORT.NFC) else "disabled" ) click.echo(f"NFC transport is {f_nfc}.") if info.is_locked: click.echo("Configured capabilities are protected by a lock code.") click.echo() print_app_status_table( info.supported_capabilities, info.config.enabled_capabilities ) if check_fips: if is_fips_version(info.version): ctx.obj["conn"].close() _check_fips_status(pid, info) else: cli_fail("Unable to check FIPS Approved mode - Not a YubiKey 4 FIPS") ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1631098170.427506 yubikey-manager-4.0.7/ykman/cli/oath.py0000644000000000000000000005037200000000000016144 0ustar0000000000000000# 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 click import logging from .util import ( cli_fail, click_force_option, click_postpone_execution, click_callback, click_parse_b32_key, click_prompt, ykman_group, prompt_for_touch, prompt_timeout, EnumChoice, ) from yubikit.core.smartcard import ApduError, SW, SmartCardConnection from yubikit.oath import ( OathSession, CredentialData, OATH_TYPE, HASH_ALGORITHM, parse_b32_key, _format_cred_id, ) from ..oath import is_steam, calculate_steam, is_hidden from ..device import is_fips_version from ..settings import AppData logger = logging.getLogger(__name__) @ykman_group(SmartCardConnection) @click.pass_context @click_postpone_execution def oath(ctx): """ Manage the OATH application. Examples: \b Generate codes for accounts starting with 'yubi': $ ykman oath accounts code yubi \b Add an account with the secret key f5up4ub3dw and the name yubico, which requires touch: $ ykman oath accounts add yubico f5up4ub3dw --touch \b Set a password for the OATH application: $ ykman oath access change-password """ session = OathSession(ctx.obj["conn"]) ctx.obj["session"] = session ctx.obj["settings"] = AppData("oath") @oath.command() @click.pass_context def info(ctx): """ Display general status of the OATH application. """ session = ctx.obj["session"] version = session.version click.echo(f"OATH version: {version[0]}.{version[1]}.{version[2]}") click.echo("Password protection: " + ("enabled" if session.locked else "disabled")) keys = ctx.obj["settings"].get("keys", {}) if session.locked and session.device_id in keys: click.echo("The password for this YubiKey is remembered by ykman.") if is_fips_version(version): click.echo(f"FIPS Approved Mode: {'Yes' if session.locked else 'No'}") @oath.command() @click.pass_context @click.confirmation_option( "-f", "--force", prompt="WARNING! This will delete all stored OATH accounts and restore factory " "settings. Proceed?", ) def reset(ctx): """ Reset all OATH data. This action will delete all accounts and restore factory settings for the OATH application on the YubiKey. """ session = ctx.obj["session"] click.echo("Resetting OATH data...") old_id = session.device_id session.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 accounts have been deleted from the YubiKey.") click_password_option = click.option( "-p", "--password", help="Provide a password to unlock the YubiKey." ) def _validate(ctx, key, remember): try: session = ctx.obj["session"] session.validate(key) if remember: settings = ctx.obj["settings"] keys = settings.setdefault("keys", {}) keys[session.device_id] = key.hex() settings.write() click.echo("Password remembered.") except Exception: cli_fail("Authentication to the YubiKey failed. Wrong password?") def _init_session(ctx, password, remember, prompt="Enter the password"): session = ctx.obj["session"] settings = ctx.obj["settings"] keys = settings.setdefault("keys", {}) device_id = session.device_id if session.locked: if password: # If password argument given, use it key = session.derive_key(password) elif device_id in keys: # If remembered, use key key = bytes.fromhex(keys[device_id]) else: # Prompt for password password = click_prompt(prompt, hide_input=True) key = session.derive_key(password) _validate(ctx, key, remember) elif password: cli_fail("Password provided, but no password is set.") @oath.group() def access(): """Manage password protection for OATH.""" @access.command() @click.pass_context @click_password_option @click.option( "-c", "--clear", is_flag=True, help="Clear the current password.", ) @click.option("-n", "--new-password", help="Provide a new password as an argument.") def change(ctx, password, clear, new_password): """ Change the password used to protect OATH accounts. Allows you to set or change a password that will be required to access the OATH accounts stored on the YubiKey. """ if clear and new_password: ctx.fail("--clear cannot be combined with --new-password.") _init_session(ctx, password, False, prompt="Enter the current password") session = ctx.obj["session"] settings = ctx.obj["settings"] keys = settings.setdefault("keys", {}) device_id = session.device_id if clear: session.unset_key() if device_id in keys: del keys[device_id] settings.write() click.echo("Password cleared from YubiKey.") else: if not new_password: new_password = click_prompt( "Enter the new password", hide_input=True, confirmation_prompt=True ) key = session.derive_key(new_password) session.set_key(key) click.echo("Password updated.") if device_id in keys: keys[device_id] = key.hex() settings.write() click.echo("Password remembered.") @access.command() @click.pass_context @click_password_option def remember(ctx, password): """ Store the YubiKeys password on this computer to avoid having to enter it on each use. """ session = ctx.obj["session"] device_id = session.device_id settings = ctx.obj["settings"] keys = settings.setdefault("keys", {}) if not session.locked: if device_id in keys: del keys[session.device_id] settings.write() click.echo("This YubiKey is not password protected.") else: if not password: password = click_prompt("Enter the password", hide_input=True) key = session.derive_key(password) _validate(ctx, key, True) def _clear_all_passwords(ctx, param, value): if not value or ctx.resilient_parsing: return settings = AppData("oath") if "keys" in settings: del settings["keys"] settings.write() click.echo("All passwords have been forgotten.") ctx.exit() @access.command() @click.pass_context @click.option( "-a", "--all", is_flag=True, is_eager=True, expose_value=False, callback=_clear_all_passwords, help="Remove all stored passwords.", ) def forget(ctx): """ Remove a stored password from this computer. """ session = ctx.obj["session"] device_id = session.device_id settings = ctx.obj["settings"] keys = settings.setdefault("keys", {}) if device_id in keys: del keys[session.device_id] settings.write() click.echo("Password forgotten.") else: click.echo("No password stored for this YubiKey.") click_remember_option = click.option( "-r", "--remember", is_flag=True, help="Remember the password on this machine.", ) 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 accounts." ) def _string_id(credential): return credential.id.decode("utf-8") 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(_string_id(cred), err=True) ctx.exit(1) def _search(creds, query, show_hidden): hits = [] for c in creds: cred_id = _string_id(c) if not show_hidden and is_hidden(c): continue if cred_id == query: return [c] if query.lower() in cred_id.lower(): hits.append(c) return hits @oath.group() def accounts(): """Manage and use OATH accounts.""" @accounts.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) account.", 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(HASH_ALGORITHM), default=HASH_ALGORITHM.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 accounts.", ) @click.option("-i", "--issuer", help="Issuer of the account (optional).") @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_password_option @click_remember_option @click.pass_context def add( ctx, secret, name, issuer, period, oath_type, digits, touch, algorithm, counter, force, password, remember, ): """ Add a new account. This will add a new OATH account to the YubiKey. \b NAME Human readable name of the account, such as a username or e-mail address. SECRET Base32-encoded secret/key value provided by the server. """ digits = int(digits) if not secret: while True: secret = click_prompt("Enter a secret key (base32)") try: secret = parse_b32_key(secret) break except Exception as e: click.echo(e) _init_session(ctx, password, remember) _add_cred( ctx, CredentialData( name, oath_type, algorithm, secret, digits, period, counter, issuer ), touch, force, ) @click_callback() def click_parse_uri(ctx, param, val): try: return CredentialData.parse_uri(val) except ValueError: raise click.BadParameter("URI seems to have the wrong format.") @accounts.command() @click.argument("data", callback=click_parse_uri, required=False, metavar="URI") @click_touch_option @click_force_option @click_password_option @click_remember_option @click.pass_context def uri(ctx, data, touch, force, password, remember): """ Add a new account from an otpauth:// URI. Use a URI to add a new account to the YubiKey. """ if not data: while True: uri = click_prompt("Enter an OATH URI (otpauth://)") try: data = CredentialData.parse_uri(uri) break except Exception as e: click.echo(e) # Steam is a special case where we allow the otpauth # URI to contain a 'digits' value of '5'. if data.digits == 5 and is_steam(data): data.digits = 6 _init_session(ctx, password, remember) _add_cred(ctx, data, touch, force) def _add_cred(ctx, data, touch, force): session = ctx.obj["session"] version = session.version 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 touch and version < (4, 2, 6): cli_fail("Require touch is not supported on this YubiKey.") if data.counter and data.oath_type != OATH_TYPE.HOTP: ctx.fail("Counter only supported for HOTP accounts.") if data.hash_algorithm == HASH_ALGORITHM.SHA512 and ( version < (4, 3, 1) or is_fips_version(version) ): cli_fail("Algorithm SHA512 not supported on this YubiKey.") creds = session.list_credentials() cred_id = data.get_id() if not force and any(cred.id == cred_id for cred in creds): click.confirm( f"An account called {data.name} already exists on this YubiKey." " Do you want to overwrite it?", abort=True, err=True, ) firmware_overwrite_issue = (4, 0, 0) < version < (4, 3, 5) cred_is_subset = any( (cred.id.startswith(cred_id) and cred.id != cred_id) for cred in creds ) # YK4 has an issue with credential overwrite in firmware versions < 4.3.5 if firmware_overwrite_issue and cred_is_subset: cli_fail("Choose a name that is not a subset of an existing account.") try: session.put_credential(data, touch) except ApduError as e: if e.sw == SW.NO_SPACE: cli_fail("No space left on the YubiKey for OATH accounts.") elif e.sw == SW.COMMAND_ABORTED: # Some NEOs do not use the NO_SPACE error. cli_fail("The command failed. Is there enough space on the YubiKey?") else: raise @accounts.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.") @click_password_option @click_remember_option def list(ctx, show_hidden, oath_type, period, password, remember): """ List all accounts. List all accounts stored on the YubiKey. """ _init_session(ctx, password, remember) session = ctx.obj["session"] creds = [ cred for cred in session.list_credentials() if show_hidden or not is_hidden(cred) ] creds.sort() for cred in creds: click.echo(_string_id(cred), nl=False) if oath_type: click.echo(f", {cred.oath_type.name}", nl=False) if period: click.echo(f", {cred.period}", nl=False) click.echo() @accounts.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.", ) @click_password_option @click_remember_option def code(ctx, show_hidden, query, single, password, remember): """ Generate codes. Generate codes from OATH accounts stored on the YubiKey. Provide a query string to match one or more specific accounts. Accounts of type HOTP, or those that require touch, requre a single match to be triggered. """ _init_session(ctx, password, remember) session = ctx.obj["session"] entries = session.calculate_all() creds = _search(entries.keys(), query, show_hidden) if len(creds) == 1: cred = creds[0] code = entries[cred] if cred.touch_required: prompt_for_touch() try: if cred.oath_type == OATH_TYPE.HOTP: with prompt_timeout(): # HOTP might require touch, we don't know. # Assume yes after 500ms. code = session.calculate_code(cred) elif code is None: code = session.calculate_code(cred) except ApduError as e: if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED: cli_fail("Touch account timed out!") entries[cred] = code elif single and len(creds) > 1: _error_multiple_hits(ctx, creds) elif single and len(creds) == 0: cli_fail("No matching account found.") if single and creds: if is_steam(cred): click.echo(calculate_steam(session, cred)) else: click.echo(code.value) else: outputs = [] for cred in sorted(creds): code = entries[cred] if code: code = code.value elif cred.touch_required: code = "[Requires Touch]" elif cred.oath_type == OATH_TYPE.HOTP: code = "[HOTP Account]" elif is_steam(cred): code = calculate_steam(session, cred) else: code = "" outputs.append((_string_id(cred), code)) 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 = "{:<%d} {:>%d}" % (longest_name, longest_code) for name, result in outputs: click.echo(format_str.format(name, result)) @accounts.command() @click.pass_context @click.argument("query") @click.argument("name") @click.option("-f", "--force", is_flag=True, help="Confirm rename without prompting") @click_password_option @click_remember_option def rename(ctx, query, name, force, password, remember): """ Rename an account (Requires YubiKey 5.3 or later). \b QUERY A query to match a single account (as shown in "list"). NAME The name of the account (use ":" to specify issuer). """ _init_session(ctx, password, remember) session = ctx.obj["session"] creds = session.list_credentials() hits = _search(creds, query, True) if len(hits) == 0: click.echo("No matches, nothing to be done.") elif len(hits) == 1: cred = hits[0] if ":" in name: issuer, name = name.split(":", 1) else: issuer = None new_id = _format_cred_id(issuer, name, cred.oath_type, cred.period) if any(cred.id == new_id for cred in creds): cli_fail( f"Another account with ID {new_id.decode()} " "already exists on this YubiKey." ) if force or ( click.confirm( f"Rename account: {_string_id(cred)} ?", default=False, err=True, ) ): session.rename_credential(cred.id, name, issuer) click.echo(f"Renamed {_string_id(cred)} to {new_id.decode()}.") else: click.echo("Rename aborted by user.") else: _error_multiple_hits(ctx, hits) @accounts.command() @click.pass_context @click.argument("query") @click.option("-f", "--force", is_flag=True, help="Confirm deletion without prompting") @click_password_option @click_remember_option def delete(ctx, query, force, password, remember): """ Delete an account. Delete an account from the YubiKey. \b QUERY A query to match a single account (as shown in "list"). """ _init_session(ctx, password, remember) session = ctx.obj["session"] creds = session.list_credentials() hits = _search(creds, query, True) if len(hits) == 0: click.echo("No matches, nothing to be done.") elif len(hits) == 1: cred = hits[0] if force or ( click.confirm( f"Delete account: {_string_id(cred)} ?", default=False, err=True, ) ): session.delete_credential(cred.id) click.echo(f"Deleted {_string_id(cred)}.") else: click.echo("Deletion aborted by user.") else: _error_multiple_hits(ctx, hits) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631098162.2526157 yubikey-manager-4.0.7/ykman/cli/openpgp.py0000644000000000000000000003214100000000000016653 0ustar0000000000000000# 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 import click from ..util import parse_certificates, parse_private_key from ..openpgp import OpenPgpController, KEY_SLOT, TOUCH_MODE, get_openpgp_info from .util import ( cli_fail, click_force_option, click_format_option, click_postpone_execution, click_prompt, ykman_group, EnumChoice, ) from yubikit.core.smartcard import ApduError, SW, SmartCardConnection 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( f"Invalid value: {key}. Must be one of: {', '.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(f"Invalid value: {intval}. Must be in range {minval}-{maxval}") return inner @ykman_group(SmartCardConnection) @click.pass_context @click_postpone_execution def openpgp(ctx): """ Manage the OpenPGP application. Examples: \b Set the retries for PIN, Reset Code and Admin PIN to 10: $ ykman openpgp access set-retries 10 10 10 \b Require touch to use the authentication key: $ ykman openpgp keys set-touch aut on """ ctx.obj["controller"] = OpenPgpController(ctx.obj["conn"]) @openpgp.command() @click.pass_context def info(ctx): """ Display general status of the OpenPGP application. """ controller = ctx.obj["controller"] click.echo(get_openpgp_info(controller)) @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 all OpenPGP data. This action will wipe all OpenPGP data, and set all PINs to their default values. """ click.echo("Resetting OpenPGP data, don't remove the 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.group("access") def access(): """Manage PIN, Reset Code, and Admin PIN.""" @access.command("set-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) 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( f"Set PIN retry counters to: {pin_retries} {reset_code_retries} " f"{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.group("keys") def keys(): """Manage private keys.""" @keys.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 individually 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 deleting the private key Cached Touch required, cached for 15s after use Cached-Fixed Touch required, cached for 15s after use, can't be disabled without deleting the private key """ controller = ctx.obj["controller"] policy_name = policy.name.lower().replace("_", "-") if policy not in controller.supported_touch_policies: cli_fail(f"Touch policy {policy_name} not supported by this YubiKey.") if key == KEY_SLOT.ATT and not controller.supports_attestation: cli_fail("Attestation is not supported by this YubiKey.") if admin_pin is None: admin_pin = click_prompt("Enter Admin PIN", hide_input=True) prompt = f"Set touch policy of {key.value.lower()} key to {policy_name}?" if policy.is_fixed: prompt = ( "WARNING: This touch policy cannot be changed without deleting the " + "corresponding key slot!\n" + prompt ) if force or click.confirm(prompt, 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: cli_fail("Touch policy not allowed.") logger.debug("Failed to set touch policy", exc_info=e) cli_fail("Failed to set touch policy.") @keys.command("import") @click.option("-a", "--admin-pin", help="Admin PIN for OpenPGP.") @click.pass_context @click.argument("key", metavar="KEY", type=EnumChoice(KEY_SLOT)) @click.argument("private-key", type=click.File("rb"), metavar="PRIVATE-KEY") def import_key(ctx, key, private_key, admin_pin): """ Import a private key (ONLY SUPPORTS 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 key != KEY_SLOT.ATT: ctx.fail("Importing keys is only supported for the Attestation slot.") if admin_pin is None: admin_pin = click_prompt("Enter Admin PIN", hide_input=True) try: private_key = parse_private_key(private_key.read(), password=None) except Exception as e: logger.debug("Failed to parse", exc_info=e) cli_fail("Failed to parse private key.") try: controller.verify_admin(admin_pin) controller.import_key(key, private_key) except Exception as e: logger.debug("Failed to import", exc_info=e) cli_fail("Failed to import attestation key.") @keys.command() @click.pass_context @click.option("-P", "--pin", help="PIN code.") @click_format_option @click.argument("key", metavar="KEY", type=EnumChoice(KEY_SLOT, hidden=[KEY_SLOT.ATT])) @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", hide_input=True) try: cert = controller.read_certificate(key) except ValueError: cert = None if not cert or click.confirm( f"There is already data stored in the certificate slot for {key.value}, " "do you want to overwrite it?" ): touch_policy = controller.get_touch(KEY_SLOT.ATT) if touch_policy in [TOUCH_MODE.ON, TOUCH_MODE.FIXED]: click.echo("Touch the YubiKey sensor...") 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) cli_fail("Attestation failed") @openpgp.group("certificates") def certificates(): """ Manage certificates. """ @certificates.command("export") @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"] if controller.version < (5, 2, 0) and key != KEY_SLOT.AUT: cli_fail(f"Certificate slot {key.name} requires YubiKey 5.2.0 or later.") try: cert = controller.read_certificate(key) except ValueError: cli_fail(f"Failed to read certificate from {key.name}") certificate.write(cert.public_bytes(encoding=format)) @certificates.command("delete") @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 controller.version < (5, 2, 0) and key != KEY_SLOT.AUT: cli_fail(f"Certificate slot {key.name} requires YubiKey 5.2.0 or later.") if admin_pin is None: admin_pin = click_prompt("Enter Admin PIN", hide_input=True) try: controller.verify_admin(admin_pin) controller.delete_certificate(key) except Exception as e: logger.debug("Failed to delete ", exc_info=e) cli_fail("Failed to delete certificate.") @certificates.command("import") @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 controller.version < (5, 2, 0) and key != KEY_SLOT.AUT: cli_fail(f"Certificate slot {key.name} requires YubiKey 5.2.0 or later.") if admin_pin is None: admin_pin = click_prompt("Enter Admin PIN", hide_input=True) try: certs = parse_certificates(cert.read(), password=None) except Exception as e: logger.debug("Failed to parse", exc_info=e) cli_fail("Failed to parse certificate.") if len(certs) != 1: cli_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) cli_fail("Failed to import certificate") ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1631098162.253611 yubikey-manager-4.0.7/ykman/cli/otp.py0000644000000000000000000005553000000000000016014 0ustar0000000000000000# 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 base64 import b32encode from yubikit.yubiotp import ( SLOT, YubiOtpSession, YubiOtpSlotConfiguration, HmacSha1SlotConfiguration, StaticPasswordSlotConfiguration, HotpSlotConfiguration, UpdateConfiguration, ) from yubikit.core import TRANSPORT, CommandError from yubikit.core.otp import modhex_encode, modhex_decode, OtpConnection from .util import ( ykman_group, cli_fail, click_force_option, click_callback, click_parse_b32_key, click_postpone_execution, click_prompt, prompt_for_touch, EnumChoice, ) from .. import __version__ from ..device import is_fips_version from ..scancodes import encode, KEYBOARD_LAYOUT from ..otp import ( PrepareUploadFailed, prepare_upload_key, is_in_fips_mode, generate_static_pw, parse_oath_key, parse_b32_key, time_challenge, format_oath_code, ) from threading import Event from time import time 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 = bytes.fromhex(val) if len(val) != length: raise ValueError(f"Must be exactly {length} bytes.") return val return inner def parse_access_code_hex(access_code_hex): try: access_code = bytes.fromhex(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: SLOT(int(v)) ) def _failed_to_write_msg(ctx, exc_info): logger.error("Failed to write to device", exc_info=exc_info) cli_fail( "Failed to write to the YubiKey. Make sure the device does not " 'have restricted access (see "ykman otp --help" for more info).' ) def _confirm_slot_overwrite(slot_state, slot): if slot_state.is_configured(slot): click.confirm( f"Slot {slot} is already configured. Overwrite configuration?", abort=True, err=True, ) @ykman_group(OtpConnection) @click.pass_context @click_postpone_execution @click.option( "--access-code", required=False, metavar="HEX", help='A 6 byte access code. Use "-" as a value to prompt for input.', ) def otp(ctx, access_code): """ Manage the YubiOTP 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. To provide an access code to commands which require it, use the --access-code option. Note that this option must be given directly after the "otp" command, before any sub-command. 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 \b Remove a currently set access code from slot 2): $ ykman otp --access-code 0123456789ab settings 2 --delete-access-code """ ctx.obj["session"] = YubiOtpSession(ctx.obj["conn"]) if access_code is not None: if access_code == "-": access_code = click_prompt("Enter the access code", hide_input=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["access_code"] = access_code @otp.command() @click.pass_context def info(ctx): """ Display general status of the YubiKey OTP slots. """ session = ctx.obj["session"] state = session.get_config_state() slot1 = state.is_configured(1) slot2 = state.is_configured(2) click.echo(f"Slot 1: {slot1 and 'programmed' or 'empty'}") click.echo(f"Slot 2: {slot2 and 'programmed' or 'empty'}") if is_fips_version(session.version): click.echo(f"FIPS Approved Mode: {'Yes' if is_in_fips_mode(session) 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. """ session = ctx.obj["session"] click.echo("Swapping slots...") try: session.swap_slots() except CommandError 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): """ Configure a slot to be used over NDEF (NFC). The default prefix will be used if no prefix is specified: "https://my.yubico.com/yk/#" """ info = ctx.obj["info"] session = ctx.obj["session"] state = session.get_config_state() if not info.has_transport(TRANSPORT.NFC): cli_fail("This YubiKey does not support NFC.") if not state.is_configured(slot): cli_fail(f"Slot {slot} is empty.") try: session.set_ndef_configuration(slot, prefix, ctx.obj["access_code"]) except CommandError 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 stored in a slot. """ session = ctx.obj["session"] state = session.get_config_state() if not force and not state.is_configured(slot): cli_fail("Not possible to delete an empty slot.") force or click.confirm( f"Do you really want to delete the configuration of slot {slot}?", abort=True, err=True, ) click.echo(f"Deleting the configuration in slot {slot}...") try: session.delete_slot(slot, ctx.obj["access_code"]) except CommandError 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. """ info = ctx.obj["info"] session = ctx.obj["session"] 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: try: serial = session.get_serial() except CommandError: cli_fail("Serial number not set, public ID must be provided") public_id = modhex_encode(b"\xff\x00" + struct.pack(b">I", serial)) click.echo(f"Using YubiKey serial as public ID: {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") 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(f"Using a randomly generated private ID: {private_id.hex()}") 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") private_id = bytes.fromhex(private_id) if not key: if generate_key: key = os.urandom(16) click.echo(f"Using a randomly generated secret key: {key.hex()}") 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") key = bytes.fromhex(key) if not upload and not force: upload = click.confirm("Upload credential to YubiCloud?", abort=False, err=True) if upload: try: upload_url = prepare_upload_key( key, public_id, private_id, serial=info.serial, user_agent="ykman/" + __version__, ) click.echo("Upload to YubiCloud initiated successfully.") except PrepareUploadFailed as e: error_msg = "\n".join(e.messages()) cli_fail("Upload to YubiCloud failed.\n" + error_msg) force or click.confirm( f"Program a YubiOTP credential in slot {slot}?", abort=True, err=True ) try: session.put_configuration( slot, YubiOtpSlotConfiguration(public_id, private_id, key).append_cr( not no_enter ), ctx.obj["access_code"], ctx.obj["access_code"], ) except CommandError 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", metavar="LENGTH", type=click.IntRange(1, 38), default=38, show_default=True, 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 (upper and lower case) are allowed by default: cbdefghijklnrtuv Use the --keyboard-layout option to allow more characters based on preferred keyboard layout. """ session = ctx.obj["session"] 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") elif not password and generate: password = generate_static_pw(length, keyboard_layout) scan_codes = encode(password, keyboard_layout) if not force: _confirm_slot_overwrite(session.get_config_state(), slot) try: session.put_configuration( slot, StaticPasswordSlotConfiguration(scan_codes).append_cr(not no_enter), ctx.obj["access_code"], ctx.obj["access_code"], ) except CommandError 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 the YubiKey to generate a 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. """ session = ctx.obj["session"] if key: if generate: ctx.fail("Invalid options: --generate conflicts with KEY argument.") elif totp: key = parse_b32_key(key) else: key = parse_oath_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 generate: key = os.urandom(20) if totp: b32key = b32encode(key).decode() click.echo(f"Using a randomly generated key (Base32): {b32key}") else: click.echo(f"Using a randomly generated key: {key.hex()}") elif totp: while True: key = click_prompt("Enter a secret key (base32)") try: key = parse_b32_key(key) break except Exception as e: click.echo(e) else: key = click_prompt("Enter a secret key") key = parse_oath_key(key) cred_type = "TOTP" if totp else "challenge-response" force or click.confirm( f"Program a {cred_type} credential in slot {slot}?", abort=True, err=True, ) try: session.put_configuration( slot, HmacSha1SlotConfiguration(key).require_touch(touch), ctx.obj["access_code"], ctx.obj["access_code"], ) except CommandError 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 if challenge is omitted.", ) @click.option( "-d", "--digits", type=click.Choice(["6", "8"]), default="6", help="Number of digits in generated TOTP code (default: 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. """ session = ctx.obj["session"] if not challenge and not totp: challenge = click_prompt("Enter a challenge (hex)") # Check that slot is not empty if not session.get_config_state().is_configured(slot): cli_fail("Cannot perform challenge-response on an empty slot.") if totp: # Challenge omitted or timestamp if challenge is None: challenge = time_challenge(int(time())) else: try: challenge = time_challenge(int(challenge)) except Exception as e: logger.error("Error", exc_info=e) ctx.fail("Timestamp challenge for TOTP must be an integer.") else: # Challenge is hex challenge = bytes.fromhex(challenge) try: event = Event() def on_keepalive(status): if not hasattr(on_keepalive, "prompted") and status == 2: prompt_for_touch() setattr(on_keepalive, "prompted", True) response = session.calculate_hmac_sha1(slot, challenge, event, on_keepalive) if totp: value = format_oath_code(response, int(digits)) else: value = response.hex() click.echo(value) except CommandError as e: _failed_to_write_msg(ctx, e) @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. """ session = ctx.obj["session"] if not key: while True: key = click_prompt("Enter a secret key (base32)") try: key = parse_b32_key(key) break except Exception as e: click.echo(e) force or click.confirm( f"Program a HOTP credential in slot {slot}?", abort=True, err=True ) try: session.put_configuration( slot, HotpSlotConfiguration(key) .imf(counter) .digits8(int(digits) == 8) .append_cr(not no_enter), ctx.obj["access_code"], ctx.obj["access_code"], ) except CommandError 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. Use "-" as a value to 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. """ session = ctx.obj["session"] if new_access_code and delete_access_code: ctx.fail("--new-access-code conflicts with --delete-access-code.") if delete_access_code and not ctx.obj["access_code"]: cli_fail( "--delete-access-code used without providing an access code " '(see "ykman otp --help" for more info).' ) if not session.get_config_state().is_configured(slot): cli_fail("Not possible to update settings on an empty slot.") if new_access_code is None: if not delete_access_code: new_access_code = ctx.obj["access_code"] else: if new_access_code == "-": new_access_code = click_prompt( "Enter new access code", hide_input=True, confirmation_prompt=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( f"Update the settings for slot {slot}? " "All existing settings will be overwritten.", abort=True, err=True, ) click.echo(f"Updating settings for slot {slot}...") pacing_bits = int(pacing or "0") // 20 pacing_10ms = bool(pacing_bits & 1) pacing_20ms = bool(pacing_bits & 2) try: session.update_configuration( slot, UpdateConfiguration() .append_cr(enter) .use_numeric(use_numeric_keypad) .pacing(pacing_10ms, pacing_20ms), new_access_code, ctx.obj["access_code"], ) except CommandError as e: _failed_to_write_msg(ctx, e) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1621233183.3206673 yubikey-manager-4.0.7/ykman/cli/piv.py0000644000000000000000000010545300000000000016010 0ustar0000000000000000# 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 yubikit.core import NotSupportedError from yubikit.core.smartcard import SmartCardConnection from yubikit.piv import ( PivSession, InvalidPinError, KEY_TYPE, MANAGEMENT_KEY_TYPE, OBJECT_ID, SLOT, PIN_POLICY, TOUCH_POLICY, DEFAULT_MANAGEMENT_KEY, ) from yubikit.core.smartcard import ApduError, SW from ..util import ( get_leaf_certificates, parse_private_key, parse_certificates, InvalidPasswordError, ) from ..piv import ( get_piv_info, get_pivman_data, get_pivman_protected_data, pivman_set_mgm_key, pivman_change_pin, derive_management_key, generate_random_management_key, generate_chuid, generate_ccc, check_key, generate_self_signed_certificate, generate_csr, ) from .util import ( ykman_group, cli_fail, click_force_option, click_format_option, click_postpone_execution, click_callback, click_prompt, prompt_timeout, EnumChoice, ) from cryptography.hazmat.primitives import serialization, hashes from cryptography.hazmat.backends import default_backend import click import datetime import logging logger = logging.getLogger(__name__) @click_callback() def click_parse_piv_slot(ctx, param, val): try: return SLOT[val.upper().replace("-", "_")] except KeyError: try: return SLOT(int(val, 16)) except Exception: raise ValueError(val) @click_callback() def click_parse_piv_object(ctx, param, val): if val.upper() == "CCC": return OBJECT_ID.CAPABILITY try: return OBJECT_ID[val.upper().replace("-", "_")] except KeyError: try: return int(val, 16) except Exception: raise ValueError(val) @click_callback() def click_parse_management_key(ctx, param, val): try: key = bytes.fromhex(val) if key and len(key) not in (16, 24, 32): raise ValueError( "Management key must be exactly 16, 24, or 32 bytes " "(32, 48, or 64 hexadecimal digits) long." ) return key except Exception: raise ValueError(val) @click_callback() def click_parse_hash(ctx, param, val): try: return getattr(hashes, val) except AttributeError: raise ValueError(val) click_slot_argument = click.argument("slot", callback=click_parse_piv_slot) click_object_argument = click.argument( "object_id", callback=click_parse_piv_object, metavar="OBJECT" ) 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), default=PIN_POLICY.DEFAULT.name, help="PIN policy for slot.", ) click_touch_policy_option = click.option( "--touch-policy", type=EnumChoice(TOUCH_POLICY), default=TOUCH_POLICY.DEFAULT.name, help="Touch policy for slot.", ) click_hash_option = click.option( "-a", "--hash-algorithm", type=click.Choice(["SHA1", "SHA256", "SHA384", "SHA512"], case_sensitive=False), default="SHA256", show_default=True, help="Hash algorithm.", callback=click_parse_hash, ) @ykman_group(SmartCardConnection) @click.pass_context @click_postpone_execution def piv(ctx): """ Manage the PIV application. Examples: \b Generate an ECC P-256 private key and a self-signed certificate in slot 9a: $ ykman piv keys generate --algorithm ECCP256 9a pubkey.pem $ ykman piv certificates generate --subject "CN=yubico" 9a pubkey.pem \b Change the PIN from 123456 to 654321: $ ykman piv access change-pin --pin 123456 --new-pin 654321 \b Reset all PIV data and restore default settings: $ ykman piv reset """ session = PivSession(ctx.obj["conn"]) ctx.obj["session"] = session ctx.obj["pivman_data"] = get_pivman_data(session) @piv.command() @click.pass_context def info(ctx): """ Display general status of the PIV application. """ click.echo(get_piv_info(ctx.obj["session"])) @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 the YubiKey. """ click.echo("Resetting PIV data...") ctx.obj["session"].reset() click.echo("Success! All PIV data have been cleared from the 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.group() def access(): """Manage PIN, PUK, and Management Key.""" @access.command("set-retries") @click.pass_context @click.argument("pin-retries", type=click.IntRange(1, 255), metavar="PIN-RETRIES") @click.argument("puk-retries", type=click.IntRange(0, 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 retry attempts. NOTE: This will reset the PIN and PUK to their factory defaults. """ session = ctx.obj["session"] _ensure_authenticated( ctx, 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( f"Set the number of PIN and PUK retry attempts to: {pin_retries} " f"{puk_retries}?", abort=True, err=True, ) try: session.set_pin_attempts(pin_retries, puk_retries) click.echo("Default PINs are set:") click.echo("\tPIN:\t123456") click.echo("\tPUK:\t12345678") except Exception as e: logger.error("Failed to set PIN retries", exc_info=e) cli_fail("Setting pin retries failed.") @access.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 PINs are recommended. """ session = ctx.obj["session"] if not pin: pin = _prompt_pin("Enter the current PIN") if not new_pin: new_pin = click_prompt( "Enter the new PIN", default="", hide_input=True, show_default=False, confirmation_prompt=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: pivman_change_pin(session, pin, new_pin) click.echo("New PIN set.") except InvalidPinError as e: attempts = e.attempts_remaining if attempts: logger.debug( "Failed to change the PIN, %d tries left", attempts, exc_info=e ) cli_fail("PIN change failed - %d tries left." % attempts) else: logger.debug("PIN is blocked.", exc_info=e) cli_fail("PIN is blocked.") @access.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. """ session = ctx.obj["session"] if not puk: puk = _prompt_pin("Enter the current PUK") if not new_puk: new_puk = click_prompt( "Enter the new PUK", default="", hide_input=True, show_default=False, confirmation_prompt=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: session.change_puk(puk, new_puk) click.echo("New PUK set.") except InvalidPinError as e: attempts = e.attempts_remaining if attempts: logger.debug("Failed to change PUK, %d tries left", attempts, exc_info=e) cli_fail("PUK change failed - %d tries left." % attempts) else: logger.debug("PUK is blocked.", exc_info=e) cli_fail("PUK is blocked.") @access.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( "-a", "--algorithm", help="Management key algorithm.", type=EnumChoice(MANAGEMENT_KEY_TYPE), default=MANAGEMENT_KEY_TYPE.TDES.name, show_default=True, ) @click.option( "-p", "--protect", is_flag=True, help="Store new management key on the 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, algorithm, pin, new_management_key, touch, protect, generate, force, ): """ Change the management key. Management functionality is guarded by a 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. """ session = ctx.obj["session"] pivman = ctx.obj["pivman_data"] pin_verified = _ensure_authenticated( ctx, pin, management_key, require_pin_and_key=protect, mgm_key_prompt="Enter the current management key [blank to use default key]", no_prompt=force, ) # Can't combine new key with generate. if new_management_key and generate: ctx.fail("Invalid options: --new-management-key conflicts with --generate") # Touch not supported on NEO. if touch and session.version < (4, 0, 0): cli_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 pivman.has_stored_key: if pin: _verify_pin(ctx, session, pivman, 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: if protect or generate: new_management_key = generate_random_management_key(algorithm) if not protect: click.echo(f"Generated management key: {new_management_key.hex()}") 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: try: new_management_key = bytes.fromhex( click_prompt( "Enter the new management key", hide_input=True, confirmation_prompt=True, ) ) except Exception: ctx.fail("New management key has the wrong format.") if len(new_management_key) != algorithm.key_len: cli_fail( "Management key has the wrong length (expected %d bytes)" % algorithm.key_len ) try: pivman_set_mgm_key( session, new_management_key, algorithm, touch=touch, store_on_device=protect ) except ApduError as e: logger.error("Failed to change management key", exc_info=e) cli_fail("Changing the management key failed.") @access.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 (using PUK). """ session = ctx.obj["session"] if not puk: puk = click_prompt("Enter PUK", default="", show_default=False, hide_input=True) if not new_pin: new_pin = click_prompt( "Enter a new PIN", default="", show_default=False, hide_input=True ) try: session.unblock_pin(puk, new_pin) click.echo("PIN unblocked") except InvalidPinError as e: attempts = e.attempts_remaining if attempts: logger.debug("Failed to unblock PIN, %d tries left", attempts, exc_info=e) cli_fail("PIN unblock failed - %d tries left." % attempts) else: logger.debug("PUK is blocked.", exc_info=e) cli_fail("PUK is blocked.") @piv.group() def keys(): """ Manage private keys. """ @keys.command("generate") @click.pass_context @click_management_key_option @click_pin_option @click.option( "-a", "--algorithm", help="Algorithm to use in key generation.", type=EnumChoice(KEY_TYPE), default=KEY_TYPE.RSA2048.name, show_default=True, ) @click_format_option @click_pin_policy_option @click_touch_policy_option @click_slot_argument @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 of the private key. PUBLIC-KEY File containing the generated public key. Use '-' to use stdout. """ session = ctx.obj["session"] _ensure_authenticated(ctx, pin, management_key) public_key = session.generate_key(slot, algorithm, pin_policy, touch_policy) key_encoding = format public_key_output.write( public_key.public_bytes( encoding=key_encoding, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) ) @keys.command("import") @click.pass_context @click_pin_option @click_management_key_option @click_pin_policy_option @click_touch_policy_option @click_slot_argument @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, management_key, pin, slot, private_key, pin_policy, touch_policy, password ): """ Import a private key from file. Write a private key to one of the PIV slots on the YubiKey. \b SLOT PIV slot of the private key. PRIVATE-KEY File containing the private key. Use '-' to use stdin. """ session = ctx.obj["session"] data = private_key.read() while True: if password is not None: password = password.encode() try: private_key = parse_private_key(data, password) except InvalidPasswordError as e: logger.error("Error parsing key", exc_info=e) if password is None: password = click_prompt( "Enter password to decrypt key", default="", hide_input=True, show_default=False, ) continue else: password = None click.echo("Wrong password.") continue break _ensure_authenticated(ctx, pin, management_key) session.put_key(slot, private_key, pin_policy, touch_policy) @keys.command() @click.pass_context @click_format_option @click_slot_argument @click.argument("certificate", type=click.File("wb"), metavar="CERTIFICATE") def attest(ctx, slot, certificate, format): """ Generate an attestation certificate for a key pair. 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 of the private key. CERTIFICATE File to write attestation certificate to. Use '-' to use stdout. """ session = ctx.obj["session"] try: cert = session.attest_key(slot) except ApduError as e: logger.error("Attestation failed", exc_info=e) cli_fail("Attestation failed.") certificate.write(cert.public_bytes(encoding=format)) @keys.command() @click.pass_context @click_format_option @click_slot_argument @click.option( "-v", "--verify", is_flag=True, help="Verify that the public key matches the private key in the slot.", ) @click.option("-P", "--pin", help="PIN code (used for --verify).") @click.argument("public-key-output", type=click.File("wb"), metavar="PUBLIC-KEY") def export(ctx, slot, public_key_output, format, verify, pin): """ Export a public key corresponding to a stored private key. This command uses several different mechanisms for exporting the public key corresponding to a stored private key, which may fail. If a certificate is stored in the slot it is assumed to contain the correct public key. If this is not the case, the wrong public key will be returned. The --verify flag can be used to verify that the public key being returned matches the private key, by using the slot to create and verify a signature. This may require the PIN to be provided. \b SLOT PIV slot of the private key. PUBLIC-KEY File containing the generated public key. Use '-' to use stdout. """ session = ctx.obj["session"] try: # Prefer metadata if available public_key = session.get_slot_metadata(slot).public_key logger.debug("Public key read from YubiKey") except ApduError as e: if e.sw == SW.REFERENCE_DATA_NOT_FOUND: cli_fail(f"No key stored in slot {slot.name}.") except NotSupportedError: try: # Try attestation public_key = session.attest_key(slot).public_key() logger.debug("Public key read using attestation") except (NotSupportedError, ApduError): try: # Read from stored certificate public_key = session.get_certificate(slot).public_key() logger.debug("Public key read from stored certificate") if verify: # Only needed when read from certificate def do_verify(): with prompt_timeout(): if not check_key(session, slot, public_key): cli_fail( "This public key is not tied to the private key in " f"the {slot.name} slot." ) _verify_pin_if_needed(ctx, session, do_verify, pin) except ApduError: cli_fail(f"Unable to export public key from slot {slot.name}") key_encoding = format public_key_output.write( public_key.public_bytes( encoding=key_encoding, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) ) @piv.group("certificates") def cert(): """ Manage certificates. """ @cert.command("import") @click.pass_context @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_slot_argument @click.argument("cert", type=click.File("rb"), metavar="CERTIFICATE") def import_certificate(ctx, management_key, pin, slot, cert, password, verify): """ Import an X.509 certificate. Write a certificate to one of the PIV slots on the YubiKey. \b SLOT PIV slot of the certificate. CERTIFICATE File containing the certificate. Use '-' to use stdin. """ session = ctx.obj["session"] data = cert.read() while True: if password is not None: password = password.encode() try: certs = parse_certificates(data, password) except InvalidPasswordError as e: logger.error("Error parsing certificate", exc_info=e) if password is None: password = click_prompt( "Enter password to decrypt certificate", default="", hide_input=True, show_default=False, ) 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] _ensure_authenticated(ctx, pin, management_key) if verify: def do_verify(): with prompt_timeout(): if not check_key(session, slot, cert_to_import.public_key()): cli_fail( "This certificate is not tied to the private key in the " f"{slot.name} slot." ) _verify_pin_if_needed(ctx, session, do_verify, pin) session.put_certificate(slot, cert_to_import) session.put_object(OBJECT_ID.CHUID, generate_chuid()) @cert.command("export") @click.pass_context @click_format_option @click_slot_argument @click.argument("certificate", type=click.File("wb"), metavar="CERTIFICATE") def export_certificate(ctx, format, slot, certificate): """ Export an X.509 certificate. Reads a certificate from one of the PIV slots on the YubiKey. \b SLOT PIV slot of the certificate. CERTIFICATE File to write certificate to. Use '-' to use stdout. """ session = ctx.obj["session"] try: cert = session.get_certificate(slot) except ApduError as e: if e.sw == SW.FILE_NOT_FOUND: cli_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)) @cert.command("generate") @click.pass_context @click_management_key_option @click_pin_option @click_slot_argument @click.argument("public-key", type=click.File("rb"), metavar="PUBLIC-KEY") @click.option( "-s", "--subject", help="Subject for the certificate, as an RFC 4514 string.", required=True, ) @click.option( "-d", "--valid-days", help="Number of days until the certificate expires.", type=click.INT, default=365, show_default=True, ) @click_hash_option def generate_certificate( ctx, management_key, pin, slot, public_key, subject, valid_days, hash_algorithm ): """ 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 must already be present in the corresponding key slot. \b SLOT PIV slot of the certificate. PUBLIC-KEY File containing a public key. Use '-' to use stdin. """ session = ctx.obj["session"] _ensure_authenticated(ctx, 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) if "=" not in subject: # Old style, common name only. subject = "CN=" + subject try: with prompt_timeout(): cert = generate_self_signed_certificate( session, slot, public_key, subject, now, valid_to, hash_algorithm ) session.put_certificate(slot, cert) session.put_object(OBJECT_ID.CHUID, generate_chuid()) except ApduError as e: logger.error("Failed to generate certificate for slot %s", slot, exc_info=e) cli_fail("Certificate generation failed.") @cert.command("request") @click.pass_context @click_pin_option @click_slot_argument @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 for the requested certificate, as an RFC 4514 string.", required=True, ) @click_hash_option def generate_certificate_signing_request( ctx, pin, slot, public_key, csr_output, subject, hash_algorithm ): """ Generate a Certificate Signing Request (CSR). A private key must already be present in the corresponding key slot. \b SLOT PIV slot of the certificate. PUBLIC-KEY File containing a public key. Use '-' to use stdin. CSR File to write CSR to. Use '-' to use stdout. """ session = ctx.obj["session"] pivman = ctx.obj["pivman_data"] _verify_pin(ctx, session, pivman, pin) data = public_key.read() public_key = serialization.load_pem_public_key(data, default_backend()) if "=" not in subject: # Old style, common name only. subject = "CN=" + subject try: with prompt_timeout(): csr = generate_csr(session, slot, public_key, subject, hash_algorithm) except ApduError: cli_fail("Certificate Signing Request generation failed.") csr_output.write(csr.public_bytes(encoding=serialization.Encoding.PEM)) @cert.command("delete") @click.pass_context @click_management_key_option @click_pin_option @click_slot_argument def delete_certificate(ctx, management_key, pin, slot): """ Delete a certificate. Delete a certificate from a PIV slot on the YubiKey. \b SLOT PIV slot of the certificate. """ session = ctx.obj["session"] _ensure_authenticated(ctx, pin, management_key) session.delete_certificate(slot) session.put_object(OBJECT_ID.CHUID, generate_chuid()) @piv.group("objects") def objects(): """ Manage PIV data objects. Examples: \b Write the contents of a file to data object with ID: abc123: $ ykman piv objects import abc123 myfile.txt \b Read the contents of the data object with ID: abc123 into a file: $ ykman piv objects export abc123 myfile.txt \b Generate a random value for CHUID: $ ykman piv objects generate chuid """ @objects.command("export") @click_pin_option @click.pass_context @click_object_argument @click.argument("output", type=click.File("wb"), metavar="OUTPUT") def read_object(ctx, pin, object_id, output): """ Export an arbitrary PIV data object. \b OBJECT Name of PIV data object, or ID in HEX. OUTPUT File to write object to. Use '-' to use stdout. """ session = ctx.obj["session"] pivman = ctx.obj["pivman_data"] def do_read_object(retry=True): try: output.write(session.get_object(object_id)) except ApduError as e: if e.sw == SW.FILE_NOT_FOUND: cli_fail("No data found.") elif e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED: _verify_pin(ctx, session, pivman, pin) do_read_object(retry=False) else: raise do_read_object() @objects.command("import") @click_pin_option @click_management_key_option @click.pass_context @click_object_argument @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 Name of PIV data object, or ID in HEX. DATA File containing the data to be written. Use '-' to use stdin. """ session = ctx.obj["session"] _ensure_authenticated(ctx, pin, management_key) def do_write_object(): try: session.put_object(object_id, data.read()) except ApduError as e: logger.debug("Failed writing object", exc_info=e) if e.sw == SW.INCORRECT_PARAMETERS: cli_fail("Something went wrong, is the object id valid?") raise do_write_object() @objects.command("generate") @click_pin_option @click_management_key_option @click.pass_context @click_object_argument def generate_object(ctx, pin, management_key, object_id): """ Generate and write data for a supported data object. \b OBJECT Name of PIV data object, or ID in HEX. \b Supported data objects are: "CHUID" (Card Holder Unique ID) "CCC" (Card Capability Container) """ session = ctx.obj["session"] _ensure_authenticated(ctx, pin, management_key) if OBJECT_ID.CHUID == object_id: session.put_object(OBJECT_ID.CHUID, generate_chuid()) elif OBJECT_ID.CAPABILITY == object_id: session.put_object(OBJECT_ID.CAPABILITY, generate_ccc()) else: ctx.fail("Unsupported object ID for generate.") def _prompt_management_key(prompt="Enter a management key [blank to use default key]"): management_key = click_prompt( prompt, default="", hide_input=True, show_default=False ) if management_key == "": return DEFAULT_MANAGEMENT_KEY try: return bytes.fromhex(management_key) except Exception: cli_fail("Management key has the wrong format.") def _prompt_pin(prompt="Enter PIN"): return click_prompt(prompt, default="", hide_input=True, show_default=False) def _valid_pin_length(pin): return 6 <= len(pin) <= 8 def _ensure_authenticated( ctx, pin=None, management_key=None, require_pin_and_key=False, mgm_key_prompt=None, no_prompt=False, ): session = ctx.obj["session"] pivman = ctx.obj["pivman_data"] if pivman.has_protected_key and not management_key: _verify_pin(ctx, session, pivman, pin, no_prompt=no_prompt) return True _authenticate(ctx, session, management_key, mgm_key_prompt, no_prompt=no_prompt) if require_pin_and_key: # Ensure verify was the last thing we did _verify_pin(ctx, session, pivman, pin, no_prompt=no_prompt) return True def _verify_pin(ctx, session, pivman, pin, no_prompt=False): if not pin: if no_prompt: cli_fail("PIN required.") else: pin = _prompt_pin() try: session.verify_pin(pin) if pivman.has_derived_key: with prompt_timeout(): session.authenticate( MANAGEMENT_KEY_TYPE.TDES, derive_management_key(pin, pivman.salt) ) session.verify_pin(pin) # Ensure verify was the last thing we did elif pivman.has_stored_key: pivman_prot = get_pivman_protected_data(session) try: key_type = session.get_management_key_metadata().key_type except NotSupportedError: key_type = MANAGEMENT_KEY_TYPE.TDES with prompt_timeout(): session.authenticate(key_type, pivman_prot.key) session.verify_pin(pin) # Ensure verify was the last thing we did except InvalidPinError as e: attempts = e.attempts_remaining if attempts > 0: cli_fail(f"PIN verification failed, {attempts} tries left.") else: cli_fail("PIN is blocked.") except Exception: cli_fail("PIN verification failed.") def _verify_pin_if_needed(ctx, session, func, pin=None, no_prompt=False): try: return func() except ApduError as e: if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED: pivman = ctx.obj["pivman_data"] _verify_pin(ctx, session, pivman, pin, no_prompt) else: raise return func() def _authenticate(ctx, session, 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() else: management_key = _prompt_management_key(mgm_key_prompt) try: try: key_type = session.get_management_key_metadata().key_type except NotSupportedError: key_type = MANAGEMENT_KEY_TYPE.TDES with prompt_timeout(): session.authenticate(key_type, management_key) except Exception as e: logger.error("Authentication with management key failed.", exc_info=e) cli_fail("Authentication with management key failed.") ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1621233183.3216665 yubikey-manager-4.0.7/ykman/cli/util.py0000644000000000000000000001530100000000000016157 0ustar0000000000000000# 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 functools import click import sys from yubikit.core.otp import OtpConnection from yubikit.core.smartcard import SmartCardConnection from yubikit.core.fido import FidoConnection from yubikit.oath import parse_b32_key from collections import OrderedDict from collections.abc import MutableMapping from cryptography.hazmat.primitives import serialization from contextlib import contextmanager from threading import Timer class EnumChoice(click.Choice): """ 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, hidden=[]): super().__init__( [v.name.replace("_", "-") for v in choices_enum if v not in hidden], case_sensitive=False, ) self.choices_enum = choices_enum def convert(self, value, param, ctx): if isinstance(value, self.choices_enum): return value name = super().convert(value, param, ctx).replace("-", "_") return self.choices_enum[name] class _YkmanCommand(click.Command): def __init__(self, name=None, **attrs): self.interfaces = attrs.pop("interfaces", None) click.Command.__init__(self, name, **attrs) class _YkmanGroup(click.Group): """click.Group which returns commands before subgroups in list_commands.""" def __init__(self, name=None, commands=None, **attrs): self.connections = attrs.pop("connections", None) click.Group.__init__(self, name, commands, **attrs) def list_commands(self, ctx): return sorted( self.commands, key=lambda c: (isinstance(self.commands[c], click.Group), c) ) def ykman_group( connections=[SmartCardConnection, OtpConnection, FidoConnection], *args, **kwargs ): if not isinstance(connections, list): connections = [connections] # Single type return click.group( cls=_YkmanGroup, *args, connections=connections, **kwargs, ) # type: ignore def ykman_command(interfaces, *args, **kwargs): return click.command( cls=_YkmanCommand, *args, interfaces=interfaces, **kwargs, ) # type: ignore 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 ValueError as e: ctx.fail(f'Invalid value for "{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=click.Choice(["PEM", "DER"], case_sensitive=False), 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 click_prompt(prompt, err=True, **kwargs): """Replacement for click.prompt to better work when piping input to the command. Note that we change the default of err to be True, since that's how we typically use it. """ if not sys.stdin.isatty(): # Piped from stdin, see if there is data line = sys.stdin.readline() if line: return line.rstrip("\n") # No piped data, use standard prompt return click.prompt(prompt, err=err, **kwargs) def prompt_for_touch(): try: click.echo("Touch your YubiKey...", err=True) except Exception: sys.stderr.write("Touch your YubiKey...\n") @contextmanager def prompt_timeout(timeout=0.5): timer = Timer(timeout, prompt_for_touch) try: timer.start() yield None finally: timer.cancel() def cli_fail(message: str, code: int = 1): click.echo(f"Error: {message}", err=True) sys.exit(code) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631098170.4285038 yubikey-manager-4.0.7/ykman/device.py0000644000000000000000000004731500000000000015704 0ustar0000000000000000# Copyright (c) 2015-2020 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 yubikit.core import ( AID, TRANSPORT, Version, Connection, NotSupportedError, ApplicationNotAvailableError, ) from yubikit.core.otp import OtpConnection, CommandRejectedError from yubikit.core.fido import FidoConnection from yubikit.core.smartcard import ( SmartCardConnection, SmartCardProtocol, ) from yubikit.management import ( ManagementSession, DeviceInfo, DeviceConfig, USB_INTERFACE, CAPABILITY, FORM_FACTOR, DEVICE_FLAG, ) from yubikit.yubiotp import YubiOtpSession from .base import PID, YUBIKEY, YkmanDevice from .hid import ( list_otp_devices as _list_otp_devices, list_ctap_devices as _list_ctap_devices, ) from .pcsc import list_devices as _list_ccid_devices from smartcard.pcsc.PCSCExceptions import EstablishContextException from smartcard.Exceptions import NoCardException from time import sleep from collections import Counter from typing import Dict, Mapping, List, Tuple, Optional, Iterable, Type import sys import ctypes import logging logger = logging.getLogger(__name__) class ConnectionNotAvailableException(ValueError): def __init__(self, connection_types): super().__init__( f"No eligiable connections are available ({connection_types})." ) self.connection_types = connection_types def _warn_once(message, e_type=Exception): warned: List[bool] = [] def outer(f): def inner(): try: return f() except e_type: if not warned: print("WARNING:", message, file=sys.stderr) warned.append(True) raise return inner return outer @_warn_once( "PC/SC not available. Smart card protocols will not function.", EstablishContextException, ) def list_ccid_devices(): return _list_ccid_devices() @_warn_once("No CTAP HID backend available. FIDO protocols will not function.") def list_ctap_devices(): return _list_ctap_devices() @_warn_once("No OTP HID backend available. OTP protocols will not function.") def list_otp_devices(): return _list_otp_devices() def is_fips_version(version: Version) -> bool: """True if a given firmware version indicates a YubiKey (4) FIPS""" return (4, 4, 0) <= version < (4, 5, 0) BASE_NEO_APPS = CAPABILITY.OTP | CAPABILITY.OATH | CAPABILITY.PIV | CAPABILITY.OPENPGP CONNECTION_LIST_MAPPING = { SmartCardConnection: list_ccid_devices, OtpConnection: list_otp_devices, FidoConnection: list_ctap_devices, } def scan_devices() -> Tuple[Mapping[PID, int], int]: """Scan USB for attached YubiKeys, without opening any connections. Returns a dict mapping PID to device count, and a state object which can be used to detect changes in attached devices. """ fingerprints = set() merged: Dict[PID, int] = {} for list_devs in CONNECTION_LIST_MAPPING.values(): try: devs = list_devs() except Exception as e: logger.error("Unable to list devices for connection", exc_info=e) devs = [] merged.update(Counter(d.pid for d in devs if d.pid is not None)) fingerprints.update({d.fingerprint for d in devs}) if sys.platform == "win32" and not bool(ctypes.windll.shell32.IsUserAnAdmin()): from .hid.windows import list_paths counter = Counter() for pid, path in list_paths(): if pid not in merged: try: counter[PID(pid)] += 1 fingerprints.add(path) except ValueError: # Unsupported PID logger.debug(f"Unsupported Yubico device with PID: {pid:02x}") merged.update(counter) return merged, hash(tuple(fingerprints)) def list_all_devices() -> List[Tuple[YkmanDevice, DeviceInfo]]: """Connects to all attached YubiKeys and reads device info from them. Returns a list of (device, info) tuples for each connected device. """ handled_pids = set() pids: Dict[PID, bool] = {} devices = [] for connection_type, list_devs in CONNECTION_LIST_MAPPING.items(): try: devs = list_devs() except Exception as e: logger.error("Unable to list devices for connection", exc_info=e) devs = [] for dev in devs: if dev.pid not in handled_pids and pids.get(dev.pid, True): try: with dev.open_connection(connection_type) as conn: info = read_info(dev.pid, conn) pids[dev.pid] = True devices.append((dev, info)) except Exception as e: pids[dev.pid] = False logger.error("Failed opening device", exc_info=e) handled_pids.update({pid for pid, handled in pids.items() if handled}) return devices def connect_to_device( serial: Optional[int] = None, connection_types: Iterable[Type[Connection]] = CONNECTION_LIST_MAPPING.keys(), ) -> Tuple[Connection, YkmanDevice, DeviceInfo]: """Looks for a YubiKey to connect to. :param serial: Used to filter devices by serial number, if present. :param connection_types: Filter connection types. :return: An open connection to the device, the device reference, and the device information read from the device. """ failed_connections = set() retry_ccid = [] for connection_type in connection_types: try: devs = CONNECTION_LIST_MAPPING[connection_type]() except Exception as e: logger.error( f"Error listing connection of type {connection_type}", exc_info=e ) failed_connections.add(connection_type) continue for dev in devs: try: conn = dev.open_connection(connection_type) except NoCardException: retry_ccid.append(dev) logger.debug("CCID No card present, will retry") continue info = read_info(dev.pid, conn) if serial and info.serial != serial: conn.close() else: return conn, dev, info if set(connection_types) == failed_connections: raise ConnectionNotAvailableException(connection_types) # NEO ejects the card when other interfaces are used, and returns it after ~3s. for _ in range(6): if not retry_ccid: break sleep(0.5) for dev in retry_ccid[:]: try: conn = dev.open_connection(SmartCardConnection) except NoCardException: continue retry_ccid.remove(dev) info = read_info(dev.pid, conn) if serial and info.serial != serial: conn.close() else: return conn, dev, info if serial: raise ValueError("YubiKey with given serial not found") raise ValueError("No YubiKey found with the given interface(s)") def _otp_read_data(conn) -> Tuple[Version, Optional[int]]: otp = YubiOtpSession(conn) version = otp.version serial: Optional[int] = None try: serial = otp.get_serial() except Exception as e: logger.debug("Unable to read serial over OTP, no serial", exc_info=e) return version, serial AID_U2F_YUBICO = b"\xa0\x00\x00\x05\x27\x10\x02" # Old U2F AID SCAN_APPLETS = { # AID.OTP: CAPABILITY.OTP, # NB: OTP will be checked elsewhere AID.FIDO: CAPABILITY.U2F, AID_U2F_YUBICO: CAPABILITY.U2F, AID.PIV: CAPABILITY.PIV, AID.OPENPGP: CAPABILITY.OPENPGP, AID.OATH: CAPABILITY.OATH, } def _read_info_ccid(conn, key_type, interfaces): version: Optional[Version] = None try: mgmt = ManagementSession(conn) version = mgmt.version try: return mgmt.read_device_info() except NotSupportedError: # Workaround to "de-select" the Management Applet needed for NEO conn.send_and_receive(b"\xa4\x04\x00\x08") except ApplicationNotAvailableError: logger.debug("Unable to select Management application, use fallback.") # Synthesize data capabilities = CAPABILITY(0) # Try to read serial (and version if needed) from OTP application try: otp_version, serial = _otp_read_data(conn) capabilities |= CAPABILITY.OTP if version is None: version = otp_version except ApplicationNotAvailableError: logger.debug("Unable to select OTP application") serial = None if version is None: version = Version(3, 0, 0) # Guess, no way to know # Scan for remaining capabilities protocol = SmartCardProtocol(conn) for aid, code in SCAN_APPLETS.items(): try: logger.debug("Check for %s", code) protocol.select(aid) capabilities |= code logger.debug("Found applet: aid: %s, capability: %s", aid, code) except ApplicationNotAvailableError: logger.debug("Missing applet: aid: %s, capability: %s", aid, code) except Exception as e: logger.error( "Error selecting aid: %s, capability: %s", aid, code, exc_info=e, ) # Assume U2F on devices >= 3.3.0 if USB_INTERFACE.FIDO in interfaces or version >= (3, 3, 0): capabilities |= CAPABILITY.U2F return DeviceInfo( config=DeviceConfig( enabled_capabilities={}, # Populated later auto_eject_timeout=0, challenge_response_timeout=0, device_flags=DEVICE_FLAG(0), ), serial=serial, version=version, form_factor=FORM_FACTOR.UNKNOWN, supported_capabilities={ TRANSPORT.USB: capabilities, TRANSPORT.NFC: capabilities, }, is_locked=False, ) def _read_info_otp(conn, key_type, interfaces): otp = None serial = None try: mgmt = ManagementSession(conn) except ApplicationNotAvailableError: otp = YubiOtpSession(conn) # Retry during potential reclaim timeout period (~3s). for _ in range(8): try: if otp is None: try: return mgmt.read_device_info() # Rejected while reclaim except NotSupportedError: otp = YubiOtpSession(conn) serial = otp.get_serial() # Rejected if reclaim (or not API_SERIAL_VISIBLE) break except CommandRejectedError: sleep(0.5) # Potential reclaim else: otp = YubiOtpSession(conn) # Synthesize info logger.debug("Unable to get info via Management application, use fallback") version = otp.version if key_type == YUBIKEY.NEO: usb_supported = BASE_NEO_APPS if USB_INTERFACE.FIDO in interfaces or version >= (3, 3, 0): usb_supported |= CAPABILITY.U2F capabilities = { TRANSPORT.USB: usb_supported, TRANSPORT.NFC: usb_supported, } elif key_type == YUBIKEY.YKP: capabilities = { TRANSPORT.USB: CAPABILITY.OTP | CAPABILITY.U2F, } else: capabilities = { TRANSPORT.USB: CAPABILITY.OTP, } return DeviceInfo( config=DeviceConfig( enabled_capabilities={}, # Populated later auto_eject_timeout=0, challenge_response_timeout=0, device_flags=DEVICE_FLAG(0), ), serial=serial, version=version, form_factor=FORM_FACTOR.UNKNOWN, supported_capabilities=capabilities.copy(), is_locked=False, ) def _read_info_ctap(conn, key_type, interfaces): try: mgmt = ManagementSession(conn) return mgmt.read_device_info() except Exception: # SKY 1, NEO, or YKP # Best guess version if key_type == YUBIKEY.YKP: version = Version(4, 0, 0) else: version = Version(3, 0, 0) supported_apps = {TRANSPORT.USB: CAPABILITY.U2F} if key_type == YUBIKEY.NEO: supported_apps[TRANSPORT.USB] |= BASE_NEO_APPS supported_apps[TRANSPORT.NFC] = supported_apps[TRANSPORT.USB] return DeviceInfo( config=DeviceConfig( enabled_capabilities={}, # Populated later auto_eject_timeout=0, challenge_response_timeout=0, device_flags=DEVICE_FLAG(0), ), serial=None, version=version, form_factor=FORM_FACTOR.USB_A_KEYCHAIN, supported_capabilities=supported_apps, is_locked=False, ) def read_info(pid: Optional[PID], conn: Connection) -> DeviceInfo: """Read out a DeviceInfo object from a YubiKey, or attempt to synthesize one.""" if pid: key_type: Optional[YUBIKEY] = pid.get_type() interfaces = pid.get_interfaces() else: # No PID for NFC connections key_type = None interfaces = USB_INTERFACE(0) if isinstance(conn, SmartCardConnection): info = _read_info_ccid(conn, key_type, interfaces) elif isinstance(conn, OtpConnection): info = _read_info_otp(conn, key_type, interfaces) elif isinstance(conn, FidoConnection): info = _read_info_ctap(conn, key_type, interfaces) else: raise TypeError("Invalid connection type") logger.debug("Read info: %s", info) # Set usb_enabled if missing (pre YubiKey 5) if ( info.has_transport(TRANSPORT.USB) and TRANSPORT.USB not in info.config.enabled_capabilities ): usb_enabled = info.supported_capabilities[TRANSPORT.USB] if usb_enabled == (CAPABILITY.OTP | CAPABILITY.U2F | USB_INTERFACE.CCID): # YubiKey Edge, hide unusable CCID interface from supported # usb_enabled = CAPABILITY.OTP | CAPABILITY.U2F info.supported_capabilities = { TRANSPORT.USB: CAPABILITY.OTP | CAPABILITY.U2F } if USB_INTERFACE.OTP not in interfaces: usb_enabled &= ~CAPABILITY.OTP if USB_INTERFACE.FIDO not in interfaces: usb_enabled &= ~(CAPABILITY.U2F | CAPABILITY.FIDO2) if USB_INTERFACE.CCID not in interfaces: usb_enabled &= ~( USB_INTERFACE.CCID | CAPABILITY.OATH | CAPABILITY.OPENPGP | CAPABILITY.PIV ) info.config.enabled_capabilities[TRANSPORT.USB] = usb_enabled # YK4-based FIPS version if is_fips_version(info.version): info.is_fips = True # Set nfc_enabled if missing (pre YubiKey 5) if ( info.has_transport(TRANSPORT.NFC) and TRANSPORT.NFC not in info.config.enabled_capabilities ): info.config.enabled_capabilities[TRANSPORT.NFC] = info.supported_capabilities[ TRANSPORT.NFC ] # Workaround for invalid configurations. if info.version >= (4, 0, 0): if info.form_factor in ( FORM_FACTOR.USB_A_NANO, FORM_FACTOR.USB_C_NANO, FORM_FACTOR.USB_C_LIGHTNING, ) or ( info.form_factor is FORM_FACTOR.USB_C_KEYCHAIN and info.version < (5, 2, 4) ): # Known not to have NFC info.supported_capabilities.pop(TRANSPORT.NFC, None) info.config.enabled_capabilities.pop(TRANSPORT.NFC, None) return info def _fido_only(capabilities): return capabilities & ~(CAPABILITY.U2F | CAPABILITY.FIDO2) == 0 def _is_preview(version): _PREVIEW_RANGES = ( ((5, 0, 0), (5, 1, 0)), ((5, 2, 0), (5, 2, 3)), ((5, 5, 0), (5, 5, 2)), ) for start, end in _PREVIEW_RANGES: if start <= version < end: return True return False def get_name(info: DeviceInfo, key_type: Optional[YUBIKEY]) -> str: """Determine the product name of a YubiKey""" usb_supported = info.supported_capabilities[TRANSPORT.USB] if not key_type: if info.serial is None and _fido_only(usb_supported): key_type = YUBIKEY.SKY elif info.version[0] == 3: key_type = YUBIKEY.NEO else: key_type = YUBIKEY.YK4 device_name = key_type.value if key_type == YUBIKEY.SKY: if CAPABILITY.FIDO2 not in usb_supported: device_name = "FIDO U2F Security Key" # SKY 1 if info.has_transport(TRANSPORT.NFC): device_name = "Security Key NFC" elif key_type == YUBIKEY.YK4: major_version = info.version[0] if major_version < 4: if info.version[0] == 0: return "YubiKey (%d.%d.%d)" % info.version else: return "YubiKey" elif major_version == 4: if is_fips_version(info.version): # YK4 FIPS device_name = "YubiKey FIPS" elif usb_supported == CAPABILITY.OTP | CAPABILITY.U2F: device_name = "YubiKey Edge" else: device_name = "YubiKey 4" if _is_preview(info.version): device_name = "YubiKey Preview" elif info.version >= (5, 1, 0): is_nano = info.form_factor in ( FORM_FACTOR.USB_A_NANO, FORM_FACTOR.USB_C_NANO, ) is_bio = info.form_factor in (FORM_FACTOR.USB_A_BIO, FORM_FACTOR.USB_C_BIO) is_c = info.form_factor in ( # Does NOT include Ci FORM_FACTOR.USB_C_KEYCHAIN, FORM_FACTOR.USB_C_NANO, FORM_FACTOR.USB_C_BIO, ) if info.is_sky: name_parts = ["Security Key"] else: name_parts = ["YubiKey"] if not is_bio: name_parts.append("5") if is_c: name_parts.append("C") elif info.form_factor == FORM_FACTOR.USB_C_LIGHTNING: name_parts.append("Ci") if is_nano: name_parts.append("Nano") if info.has_transport(TRANSPORT.NFC): name_parts.append("NFC") elif info.form_factor == FORM_FACTOR.USB_A_KEYCHAIN: name_parts.append("A") # Only for non-NFC A Keychain. if is_bio: name_parts.append("Bio") if _fido_only(usb_supported): name_parts.append("- FIDO Edition") if info.is_fips: name_parts.append("FIPS") device_name = " ".join(name_parts).replace("5 C", "5C").replace("5 A", "5A") return device_name ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1621233183.323658 yubikey-manager-4.0.7/ykman/diagnostics.py0000644000000000000000000001434600000000000016752 0ustar0000000000000000from . import __version__ as ykman_version from .logging_setup import log_sys_info from .pcsc import list_readers, list_devices as list_ccid_devices from .hid import list_otp_devices, list_ctap_devices from .device import read_info, get_name from .piv import get_piv_info from .openpgp import OpenPgpController, get_openpgp_info from yubikit.core.smartcard import SmartCardConnection from yubikit.core.fido import FidoConnection from yubikit.core.otp import OtpConnection from yubikit.management import ManagementSession from yubikit.yubiotp import YubiOtpSession from yubikit.piv import PivSession from yubikit.oath import OathSession from fido2.ctap import CtapError from fido2.ctap2 import Ctap2, ClientPin def mgmt_info(pid, conn): lines = [] try: raw_info = ManagementSession(conn).backend.read_config() lines.append(f"\tRawInfo: {raw_info.hex()}") except Exception as e: lines.append(f"\tFailed to read device info via Management: {e!r}") try: info = read_info(pid, conn) lines.append(f"\t{info}") name = get_name(info, pid.get_type()) lines.append(f"\tDevice name: {name}") except Exception as e: lines.append(f"\tFailed to read device info: {e!r}") return lines def piv_info(conn): try: piv = PivSession(conn) return ["\tPIV"] + [f"\t\t{ln}" for ln in get_piv_info(piv).splitlines() if ln] except Exception as e: return [f"\tPIV not accessible {e!r}"] def openpgp_info(conn): try: openpgp = OpenPgpController(conn) return ["\tOpenPGP"] + [ f"\t\t{ln}" for ln in get_openpgp_info(openpgp).splitlines() if ln ] except Exception as e: return [f"\tOpenPGP not accessible {e!r}"] def oath_info(conn): try: oath = OathSession(conn) return [ "\tOATH", f"\t\tOath version: {'.'.join('%d' % d for d in oath.version)}", f"\t\tPassword protected: {oath.locked}", ] except Exception as e: return [f"\tOATH not accessible {e!r}"] def ccid_info(): lines = [] try: readers = list_readers() lines.append("Detected PC/SC readers:") for reader in readers: try: c = reader.createConnection() c.connect() c.disconnect() result = "Success" except Exception as e: result = e.__class__.__name__ lines.append(f"\t{reader.name} (connect: {result})") lines.append("") except Exception as e: return [ f"PC/SC failure: {e!r}", "", ] lines.append("Detected YubiKeys over PC/SC:") try: for dev in list_ccid_devices(): lines.append(f"\t{dev!r}") try: with dev.open_connection(SmartCardConnection) as conn: lines.extend(mgmt_info(dev.pid, conn)) lines.extend(piv_info(conn)) lines.extend(oath_info(conn)) lines.extend(openpgp_info(conn)) except Exception as e: lines.append(f"\tPC/SC connection failure: {e!r}") lines.append("") except Exception as e: return [ f"PC/SC failure: {e!r}", "", ] lines.append("") return lines def otp_info(): lines = [] lines.append("Detected YubiKeys over HID OTP:") try: for dev in list_otp_devices(): lines.append(f"\t{dev!r}") try: with dev.open_connection(OtpConnection) as conn: lines.extend(mgmt_info(dev.pid, conn)) otp = YubiOtpSession(conn) try: config = otp.get_config_state() lines.append(f"\tOTP: {config!r}") except ValueError as e: lines.append(f"\tCouldn't read OTP state: {e!r}") except Exception as e: lines.append(f"\tOTP connection failure: {e!r}") lines.append("") except Exception as e: lines.append(f"\tHID OTP backend failure: {e!r}") lines.append("") return lines def fido_info(): lines = [] lines.append("Detected YubiKeys over HID FIDO:") try: for dev in list_ctap_devices(): lines.append(f"\t{dev!r}") try: with dev.open_connection(FidoConnection) as conn: lines.append("CTAP device version: %d.%d.%d" % conn.device_version) lines.append(f"CTAPHID protocol version: {conn.version}") lines.append("Capabilities: %d" % conn.capabilities) lines.extend(mgmt_info(dev.pid, conn)) try: ctap2 = Ctap2(conn) lines.append(f"\tCtap2Info: {ctap2.info.data!r}") if ctap2.info.options.get("clientPin"): client_pin = ClientPin(ctap2) lines.append(f"PIN retries: {client_pin.get_pin_retries()}") bio_enroll = ctap2.info.options.get("bioEnroll") if bio_enroll: lines.append( "Fingerprint retries: " f"{client_pin.get_uv_retries()}" ) elif bio_enroll is False: lines.append("Fingerprints: Not configured") else: lines.append("PIN: Not configured") except (ValueError, CtapError) as e: lines.append(f"\tCouldn't get info: {e!r}") except Exception as e: lines.append(f"\tFIDO connection failure: {e!r}") lines.append("") except Exception as e: lines.append(f"\tHID FIDO backend failure: {e!r}") return lines def get_diagnostics(): lines = [] lines.append(f"ykman: {ykman_version}") log_sys_info(lines.append) lines.append("") lines.extend(ccid_info()) lines.extend(otp_info()) lines.extend(fido_info()) lines.append("End of diagnostics") return "\n".join(lines) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.1405075 yubikey-manager-4.0.7/ykman/fido.py0000644000000000000000000000674600000000000015371 0ustar0000000000000000# 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 time import struct from yubikit.core.fido import FidoConnection from yubikit.core.smartcard import SW from fido2.ctap1 import Ctap1, ApduError from typing import Optional U2F_VENDOR_FIRST = 0x40 # FIPS specific INS values INS_FIPS_VERIFY_PIN = U2F_VENDOR_FIRST + 3 INS_FIPS_SET_PIN = U2F_VENDOR_FIRST + 4 INS_FIPS_RESET = U2F_VENDOR_FIRST + 5 INS_FIPS_VERIFY_FIPS_MODE = U2F_VENDOR_FIRST + 6 def is_in_fips_mode(fido_connection: FidoConnection) -> bool: """Check if a YubiKey FIPS is in FIPS approved mode.""" try: ctap = Ctap1(fido_connection) ctap.send_apdu(ins=INS_FIPS_VERIFY_FIPS_MODE) return True except ApduError as e: # 0x6a81: Function not supported (PIN not set - not FIPS Mode) if e.code == SW.FUNCTION_NOT_SUPPORTED: return False raise def fips_change_pin( fido_connection: FidoConnection, old_pin: Optional[str], new_pin: str ): """Change the PIN on a YubiKey FIPS. If no PIN is set, pass None or an empty string as old_pin. """ ctap = Ctap1(fido_connection) old_pin_bytes = old_pin.encode() if old_pin else b"" new_pin_bytes = new_pin.encode() new_length = len(new_pin_bytes) data = struct.pack("B", new_length) + old_pin_bytes + new_pin_bytes ctap.send_apdu(ins=INS_FIPS_SET_PIN, data=data) def fips_verify_pin(fido_connection: FidoConnection, pin: str): """Unlock the YubiKey FIPS U2F module for credential creation.""" ctap = Ctap1(fido_connection) ctap.send_apdu(ins=INS_FIPS_VERIFY_PIN, data=pin.encode()) def fips_reset(fido_connection: FidoConnection): """Reset the FIDO module of a YubiKey FIPS. Note: This action is only permitted immediately after YubiKey FIPS power-up. It also requires the user to touch the flashing button on the YubiKey, and will halt until that happens, or the command times out. """ ctap = Ctap1(fido_connection) while True: try: ctap.send_apdu(ins=INS_FIPS_RESET) return except ApduError as e: if e.code == SW.CONDITIONS_NOT_SATISFIED: time.sleep(0.5) else: raise e ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1621233183.3246553 yubikey-manager-4.0.7/ykman/hid/__init__.py0000644000000000000000000000715700000000000016750 0ustar0000000000000000# Copyright (c) 2020 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 ..base import YkmanDevice, PID from .base import OtpYubiKeyDevice from yubikit.core import TRANSPORT from typing import List, Callable import sys import logging logger = logging.getLogger(__name__) if sys.platform.startswith("linux"): from . import linux as backend elif sys.platform.startswith("win32"): from . import windows as backend elif sys.platform.startswith("darwin"): from . import macos as backend else: class backend: @staticmethod def list_devices(): raise NotImplementedError( "OTP HID support is not implemented on this platform" ) list_otp_devices: Callable[[], List[OtpYubiKeyDevice]] = backend.list_devices try: from fido2.hid import list_descriptors, open_connection, CtapHidDevice class CtapYubiKeyDevice(YkmanDevice): """YubiKey FIDO USB HID device""" def __init__(self, descriptor): super(CtapYubiKeyDevice, self).__init__( TRANSPORT.USB, descriptor.path, PID(descriptor.pid) ) self.descriptor = descriptor def supports_connection(self, connection_type): return issubclass(CtapHidDevice, connection_type) def open_connection(self, connection_type): if self.supports_connection(connection_type): return CtapHidDevice(self.descriptor, open_connection(self.descriptor)) return super(CtapYubiKeyDevice, self).open_connection(connection_type) def list_ctap_devices() -> List[CtapYubiKeyDevice]: devs = [] for desc in list_descriptors(): if desc.vid == 0x1050: try: devs.append(CtapYubiKeyDevice(desc)) except ValueError: logger.debug(f"Unsupported Yubico device with PID: {desc.pid:02x}") return devs except Exception: # CTAP not supported on this platform class CtapYubiKeyDevice(YkmanDevice): # type: ignore def __init__(self, *args, **kwargs): raise NotImplementedError( "CTAP HID support is not implemented on this platform" ) def list_ctap_devices() -> List[CtapYubiKeyDevice]: raise NotImplementedError( "CTAP HID support is not implemented on this platform" ) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1618212188.143387 yubikey-manager-4.0.7/ykman/hid/base.py0000644000000000000000000000413000000000000016107 0ustar0000000000000000# Copyright (c) 2020 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 ..base import YkmanDevice, PID from yubikit.core import TRANSPORT YUBICO_VID = 0x1050 USAGE_FIDO = (0xF1D0, 1) USAGE_OTP = (1, 6) class OtpYubiKeyDevice(YkmanDevice): """YubiKey USB HID OTP device""" def __init__(self, path, pid, connection_cls): super(OtpYubiKeyDevice, self).__init__(TRANSPORT.USB, path, PID(pid)) self.path = path self._connection_cls = connection_cls def supports_connection(self, connection_type): return issubclass(self._connection_cls, connection_type) def open_connection(self, connection_type): if self.supports_connection(connection_type): return self._connection_cls(self.path) return super(OtpYubiKeyDevice, self).open_connection(connection_type) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.1445518 yubikey-manager-4.0.7/ykman/hid/linux.py0000644000000000000000000000715000000000000016341 0ustar0000000000000000# Copyright (c) 2020 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 yubikit.core.otp import OtpConnection from .base import OtpYubiKeyDevice, YUBICO_VID, USAGE_OTP import glob import fcntl import struct import logging logger = logging.getLogger(__name__) # usb_ioctl.h USB_GET_REPORT = 0xC0094807 USB_SET_REPORT = 0xC0094806 # hidraw.h HIDIOCGRAWINFO = 0x80084803 HIDIOCGRDESCSIZE = 0x80044801 HIDIOCGRDESC = 0x90044802 class HidrawConnection(OtpConnection): def __init__(self, path): self.handle = open(path, "wb") def close(self): self.handle.close() def receive(self): buf = bytearray(1 + 8) fcntl.ioctl(self.handle, USB_GET_REPORT, buf, True) return buf[1:] def send(self, data): buf = bytearray([0]) # Prepend the report ID. buf.extend(data) fcntl.ioctl(self.handle, USB_SET_REPORT, buf, True) def get_info(dev): buf = bytearray(4 + 2 + 2) fcntl.ioctl(dev, HIDIOCGRAWINFO, buf, True) return struct.unpack("q", timestamp // 30)) offset = resp[-1] & 0x0F code = struct.unpack(">I", resp[offset : offset + 4])[0] & 0x7FFFFFFF chars = [] for i in range(5): chars.append(STEAM_CHAR_TABLE[code % len(STEAM_CHAR_TABLE)]) code //= len(STEAM_CHAR_TABLE) return "".join(chars) def is_in_fips_mode(app): return app.locked ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631098162.2565107 yubikey-manager-4.0.7/ykman/openpgp.py0000755000000000000000000005064400000000000016117 0ustar0000000000000000# 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 yubikit.core import ( AID, Tlv, NotSupportedError, require_version, int2bytes, bytes2int, ) from yubikit.core.smartcard import SmartCardConnection, SmartCardProtocol, ApduError, SW from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.serialization import ( Encoding, PrivateFormat, NoEncryption, ) from cryptography.hazmat.primitives.asymmetric import rsa, ec from enum import Enum, IntEnum, unique from dataclasses import dataclass from typing import Optional, Tuple import time import struct import logging from typing import NamedTuple logger = logging.getLogger(__name__) class _KeySlot(NamedTuple): value: str indx: int key_id: int fingerprint: int gen_time: int uif: int # touch policy crt: bytes # 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 @property def is_fixed(self): return "FIXED" in self.name 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 class PinRetries(NamedTuple): pin: int reset: int admin: int 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 KDF = 0xF9 @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.RSAPrivateKeyWithSerialization): 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) values: Tuple[Tlv, ...] if isinstance(key, rsa.RSAPrivateKeyWithSerialization): rsa_numbers = key.private_numbers() ln = (key.key_size // 8) // 2 e = Tlv(0x91, b"\x01\x00\x01") # e=65537 p = Tlv(0x92, int2bytes(rsa_numbers.p, ln)) q = Tlv(0x93, int2bytes(rsa_numbers.q, ln)) values = (e, p, q) if crt: dp = Tlv(0x94, int2bytes(rsa_numbers.dmp1, ln)) dq = Tlv(0x95, int2bytes(rsa_numbers.dmq1, ln)) qinv = Tlv(0x96, int2bytes(rsa_numbers.iqmp, ln)) n = Tlv(0x97, int2bytes(rsa_numbers.public_numbers.n, 2 * ln)) values += (dp, dq, qinv, n) elif isinstance(key, ec.EllipticCurvePrivateKeyWithSerialization): ec_numbers = key.private_numbers() ln = key.key_size // 8 privkey = Tlv(0x92, int2bytes(ec_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)) @unique class HashAlgorithm(IntEnum): SHA256 = 0x08 SHA512 = 0x0A def create_digest(self): algorithm = getattr(hashes, self.name) return hashes.Hash(algorithm(), default_backend()) @unique class KdfAlgorithm(IntEnum): NONE = 0x00 KDF_ITERSALTED_S2K = 0x03 def _kdf_none(pin, salt, hash_algorithm, iteration_count): return pin def _kdf_itersalted_s2k(pin, salt, hash_algorithm, iteration_count): data = salt + pin digest = hash_algorithm.create_digest() # Although the field is called "iteration count", it's actually # the number of bytes to be passed to the hash function, which # is called only once. Go figure! data_count, trailing_bytes = divmod(iteration_count, len(data)) for _ in range(data_count): digest.update(data) digest.update(data[:trailing_bytes]) return digest.finalize() _KDFS = { KdfAlgorithm.NONE: _kdf_none, KdfAlgorithm.KDF_ITERSALTED_S2K: _kdf_itersalted_s2k, } def _parse_int(data, tag, func=lambda x: x, default=None): return func(int.from_bytes(data[tag], "big")) if tag in data else default @dataclass class KdfData: kdf_algorithm: KdfAlgorithm hash_algorithm: Optional[HashAlgorithm] iteration_count: Optional[int] pw1_salt_bytes: Optional[bytes] pw2_salt_bytes: Optional[bytes] pw3_salt_bytes: Optional[bytes] pw1_initial_hash: Optional[bytes] pw3_initial_hash: Optional[bytes] def process(self, pw, pin): kdf = _KDFS[self.kdf_algorithm] if pw == PW1: salt = self.pw1_salt_bytes elif pw == PW3: salt = self.pw3_salt_bytes or self.pw1_salt_bytes else: raise ValueError("Invalid value for pw") return kdf(pin, salt, self.hash_algorithm, self.iteration_count) @classmethod def parse(cls, data: bytes) -> "KdfData": fields = Tlv.parse_dict(data) return cls( _parse_int(fields, 0x81, KdfAlgorithm, KdfAlgorithm.NONE), _parse_int(fields, 0x82, HashAlgorithm), _parse_int(fields, 0x83), fields.get(0x84), fields.get(0x85), fields.get(0x86), fields.get(0x87), fields.get(0x88), ) class OpenPgpController(object): def __init__(self, connection: SmartCardConnection): protocol = SmartCardProtocol(connection) self._app = protocol try: protocol.select(AID.OPENPGP) except ApduError as e: if e.sw in (SW.NO_INPUT_DATA, SW.CONDITIONS_NOT_SATISFIED): protocol.send_apdu(0, INS.ACTIVATE, 0, 0) protocol.select(AID.OPENPGP) else: raise self._version = self._read_version() @property def version(self): return self._version def _get_data(self, do): return self._app.send_apdu(0, INS.GET_DATA, do >> 8, do & 0xFF) def _put_data(self, do, data): self._app.send_apdu(0, INS.PUT_DATA, do >> 8, do & 0xFF, data) def _select_certificate(self, key_slot): try: require_version(self.version, (5, 2, 0)) data: bytes = Tlv(0x60, Tlv(0x5C, b"\x7f\x21")) if self.version <= (5, 4, 3): # These use a non-standard byte in the command. data = b"\x06" + data # 6 is the length of the data. self._app.send_apdu( 0, INS.SELECT_DATA, 3 - key_slot.indx, 0x04, data, ) except NotSupportedError: if key_slot == KEY_SLOT.AUT: return # Older version still support AUT, which is the default slot. raise def _read_version(self): bcd_hex = self._app.send_apdu(0, INS.GET_VERSION, 0, 0).hex() 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 data[6], data[7] def get_remaining_pin_tries(self): data = self._get_data(DO.PW_STATUS) return PinRetries(*data[4:7]) def _block_pins(self): retries = self.get_remaining_pin_tries() for _ in range(retries.pin): try: self._app.send_apdu(0, INS.VERIFY, 0, PW1, INVALID_PIN) except ApduError: pass for _ in range(retries.admin): try: self._app.send_apdu(0, INS.VERIFY, 0, PW3, INVALID_PIN) except ApduError: pass 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._app.send_apdu(0, INS.TERMINATE, 0, 0) self._app.send_apdu(0, INS.ACTIVATE, 0, 0) def _get_kdf(self): try: data = self._get_data(DO.KDF) except ApduError: data = b"" return KdfData.parse(data) def _verify(self, pw, pin): try: pin = self._get_kdf().process(pw, pin.encode()) self._app.send_apdu(0, INS.VERIFY, 0, pw, pin) except ApduError: pw_remaining = self.get_remaining_pin_tries()[pw - PW1] raise ValueError(f"Invalid PIN, {pw_remaining} tries 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(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 (1, 0, 0) <= 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._app.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: require_version(self.version, (5, 2, 0)) 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: require_version(self.version, (5, 2, 0)) 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._app.send_apdu(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.""" if (4, 2, 0) <= self.version < (4, 3, 5): raise NotSupportedError("RSA key generation not supported on this YubiKey") 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._app.send_apdu(0, INS.GENERATE_ASYM, 0x80, 0x00, key_slot.crt) data = Tlv.parse_dict(Tlv.unpack(0x7F49, resp)) numbers = rsa.RSAPublicNumbers(bytes2int(data[0x82]), bytes2int(data[0x81])) 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): require_version(self.version, (5, 2, 0)) """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._app.send_apdu(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: require_version(self.version, (5, 2, 0)) 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.""" require_version(self.version, (5, 2, 0)) self._app.send_apdu(0x80, INS.GET_ATTESTATION, key_slot.indx, 0) return self.read_certificate(key_slot) def get_openpgp_info(controller: OpenPgpController) -> str: """Get human readable information about the OpenPGP configuration.""" lines = [] lines.append("OpenPGP version: %d.%d" % controller.get_openpgp_version()) lines.append("Application version: %d.%d.%d" % controller.version) lines.append("") retries = controller.get_remaining_pin_tries() lines.append(f"PIN tries remaining: {retries.pin}") lines.append(f"Reset code tries remaining: {retries.reset}") lines.append(f"Admin PIN tries remaining: {retries.admin}") # Touch only available on YK4 and later if controller.version >= (4, 2, 6): lines.append("") lines.append("Touch policies") lines.append(f"Signature key {controller.get_touch(KEY_SLOT.SIG)!s}") lines.append(f"Encryption key {controller.get_touch(KEY_SLOT.ENC)!s}") lines.append(f"Authentication key {controller.get_touch(KEY_SLOT.AUT)!s}") if controller.supports_attestation: lines.append( f"Attestation key {controller.get_touch(KEY_SLOT.ATT)!s}" ) return "\n".join(lines) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.1503043 yubikey-manager-4.0.7/ykman/otp.py0000644000000000000000000001513400000000000015241 0ustar0000000000000000# 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 . import __version__ from .scancodes import KEYBOARD_LAYOUT from yubikit.core.otp import modhex_encode from yubikit.yubiotp import YubiOtpSession from yubikit.oath import parse_b32_key from enum import Enum from http.client import HTTPSConnection from typing import Iterable import re import json import struct import random import logging logger = logging.getLogger(__name__) UPLOAD_HOST = "upload.yubico.com" UPLOAD_PATH = "/prepare" 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." # nosec SECRET_KEY_NOT_HEX = ( "Secret key must consist only of hex characters (0-9A-F)." # noqa: E501 # nosec ) SECRET_KEY_UNDEFINED = "Secret key is required." # nosec 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__( f"Upload to YubiCloud failed with status {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] def prepare_upload_key( key, public_id, private_id, serial=None, user_agent="python-yubikey-manager/" + __version__, ): modhex_public_id = modhex_encode(public_id) data = { "aes_key": key.hex(), "serial": serial or 0, "public_id": modhex_public_id, "private_id": private_id.hex(), } httpconn = HTTPSConnection(UPLOAD_HOST, timeout=1) # nosec 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 is_in_fips_mode(session: YubiOtpSession) -> bool: """Check if the OTP application of a FIPS YubiKey is in FIPS approved mode.""" return session.backend.send_and_receive(0x14, b"", 1) == b"\1" # type: ignore DEFAULT_PW_CHAR_BLOCKLIST = ["\t", "\n", " "] def generate_static_pw( length: int, keyboard_layout: KEYBOARD_LAYOUT = KEYBOARD_LAYOUT.MODHEX, blocklist: Iterable[str] = DEFAULT_PW_CHAR_BLOCKLIST, ) -> str: """Generate a random password.""" chars = [k for k in keyboard_layout.value.keys() if k not in blocklist] sr = random.SystemRandom() return "".join([sr.choice(chars) for _ in range(length)]) def parse_oath_key(val: str) -> bytes: """Parse a secret key encoded as either Hex or Base32.""" val = val.upper() if re.match(r"^([0-9A-F]{2})+$", val): # hex return bytes.fromhex(val) else: # Key should be b32 encoded return parse_b32_key(val) def format_oath_code(response: bytes, digits: int = 6) -> str: """Formats an OATH code from a hash response.""" offs = response[-1] & 0xF code = struct.unpack_from(">I", response[offs:])[0] & 0x7FFFFFFF return ("%%0%dd" % digits) % (code % 10 ** digits) def time_challenge(timestamp: int, period: int = 30) -> bytes: """Formats a HMAC-SHA1 challenge based on an OATH timestamp and period.""" return struct.pack(">q", int(timestamp // period)) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631098162.2575083 yubikey-manager-4.0.7/ykman/pcsc/__init__.py0000644000000000000000000001364700000000000017135 0ustar0000000000000000# Copyright (c) 2020 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 yubikit.core import TRANSPORT from yubikit.core.smartcard import SmartCardConnection from yubikit.management import USB_INTERFACE from ..base import YUBIKEY, YkmanDevice from smartcard import System from smartcard.Exceptions import CardConnectionException from smartcard.pcsc.PCSCExceptions import ListReadersException from smartcard.pcsc.PCSCContext import PCSCContext from fido2.pcsc import CtapPcscDevice from time import sleep import subprocess # nosec import logging logger = logging.getLogger(__name__) YK_READER_NAME = "yubico yubikey" # Figure out what the PID should be based on the reader name def _pid_from_name(name): if YK_READER_NAME not in name.lower(): return None interfaces = USB_INTERFACE(0) for iface in USB_INTERFACE: if iface.name in name: interfaces |= iface if "U2F" in name: interfaces |= USB_INTERFACE.FIDO key_type = YUBIKEY.NEO if "NEO" in name else YUBIKEY.YK4 return key_type.get_pid(interfaces) class ScardYubiKeyDevice(YkmanDevice): """YubiKey Smart card device""" def __init__(self, reader): # Base transport on reader name: NFC readers will have a different name if YK_READER_NAME in reader.name.lower(): transport = TRANSPORT.USB else: transport = TRANSPORT.NFC super(ScardYubiKeyDevice, self).__init__( transport, reader.name, _pid_from_name(reader.name) ) self.reader = reader def supports_connection(self, connection_type): if issubclass(CtapPcscDevice, connection_type): return self.transport == TRANSPORT.NFC return issubclass(ScardSmartCardConnection, connection_type) def open_connection(self, connection_type): if issubclass(ScardSmartCardConnection, connection_type): return self._open_smartcard_connection() elif issubclass(CtapPcscDevice, connection_type): if self.transport == TRANSPORT.NFC: return CtapPcscDevice(self.reader.createConnection(), self.reader.name) return super(ScardYubiKeyDevice, self).open_connection(connection_type) def _open_smartcard_connection(self) -> SmartCardConnection: try: return ScardSmartCardConnection(self.reader.createConnection()) except CardConnectionException as e: if kill_scdaemon(): return ScardSmartCardConnection(self.reader.createConnection()) raise e class ScardSmartCardConnection(SmartCardConnection): def __init__(self, connection): self.connection = connection connection.connect() atr = connection.getATR() self._transport = TRANSPORT.USB if atr[1] & 0xF0 == 0xF0 else TRANSPORT.NFC @property def transport(self): return self._transport def close(self): self.connection.disconnect() def send_and_receive(self, apdu): """Sends a command APDU and returns the response data and sw""" logger.debug("SEND: %s", apdu.hex()) data, sw1, sw2 = self.connection.transmit(list(apdu)) logger.debug("RECV: %s SW=%02x%02x", bytes(data).hex(), sw1, sw2) return bytes(data), sw1 << 8 | sw2 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. return_code = subprocess.call(["pkill", "-9", "scdaemon"]) # nosec if return_code == 0: killed = True if killed: 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 list_devices(name_filter=None): name_filter = YK_READER_NAME if name_filter is None else name_filter devices = [] for reader in list_readers(): if name_filter.lower() in reader.name.lower(): devices.append(ScardYubiKeyDevice(reader)) return devices ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1621233183.3266504 yubikey-manager-4.0.7/ykman/piv.py0000644000000000000000000005243500000000000015242 0ustar0000000000000000# 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 yubikit.core import Tlv, BadResponseError, NotSupportedError from yubikit.core.smartcard import ApduError, SW from yubikit.piv import ( PivSession, SLOT, OBJECT_ID, KEY_TYPE, MANAGEMENT_KEY_TYPE, ALGORITHM, TAG_LRC, ) from cryptography import x509 from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import hashes 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 datetime import datetime import logging import struct import os from typing import Union, Mapping, Optional, List, Type, cast logger = logging.getLogger(__name__) OBJECT_ID_PIVMAN_DATA = 0x5FFF00 OBJECT_ID_PIVMAN_PROTECTED_DATA = OBJECT_ID.PRINTED # Use slot for printed information. _NAME_ATTRIBUTES = { "CN": NameOID.COMMON_NAME, "L": NameOID.LOCALITY_NAME, "ST": NameOID.STATE_OR_PROVINCE_NAME, "O": NameOID.ORGANIZATION_NAME, "OU": NameOID.ORGANIZATIONAL_UNIT_NAME, "C": NameOID.COUNTRY_NAME, "STREET": NameOID.STREET_ADDRESS, "DC": NameOID.DOMAIN_COMPONENT, "UID": NameOID.USER_ID, } _ESCAPED = "\\\"+,'<> #=" def _parse(value: str) -> List[List[str]]: remaining = list(value) name = [] entry = [] buf = "" hexbuf = b"" while remaining: c = remaining.pop(0) if c == "\\": c1 = remaining.pop(0) if c1 in _ESCAPED: c = c1 else: c2 = remaining.pop(0) hexbuf += bytes.fromhex(c1 + c2) try: c = hexbuf.decode() hexbuf = b"" except UnicodeDecodeError: continue # Possibly multi-byte, expect more hex elif c in ",+": entry.append(buf) buf = "" if c == ",": name.append(entry) entry = [] continue if hexbuf: raise ValueError("Invalid UTF-8 data") buf += c entry.append(buf) name.append(entry) return name def parse_rfc4514_string(value: str) -> x509.Name: """Parses an RFC 4514 string into a x509.Name. See: https://tools.ietf.org/html/rfc4514.html """ name = _parse(value) attributes: List[x509.RelativeDistinguishedName] = [] for entry in name: parts = [] for part in entry: if "=" not in part: raise ValueError("Invalid RFC 4514 string") k, v = part.split("=", 1) if k not in _NAME_ATTRIBUTES: raise ValueError(f"Unsupported attribute: '{k}'") parts.append(x509.NameAttribute(_NAME_ATTRIBUTES[k], v)) attributes.insert(0, x509.RelativeDistinguishedName(parts)) return x509.Name(attributes) def _dummy_key(algorithm): if algorithm == KEY_TYPE.RSA1024: return rsa.generate_private_key(65537, 1024, default_backend()) # nosec if algorithm == KEY_TYPE.RSA2048: return rsa.generate_private_key(65537, 2048, default_backend()) if algorithm == KEY_TYPE.ECCP256: return ec.generate_private_key(ec.SECP256R1(), default_backend()) if algorithm == KEY_TYPE.ECCP384: return ec.generate_private_key(ec.SECP384R1(), default_backend()) raise ValueError("Invalid algorithm") def derive_management_key(pin: str, salt: bytes) -> bytes: """Derive a management key from the users PIN and a salt. NOTE: This method of derivation is deprecated! Protect the management key using PivmanProtectedData instead. """ kdf = PBKDF2HMAC(hashes.SHA1(), 24, salt, 10000, default_backend()) # nosec return kdf.derive(pin.encode("utf-8")) def generate_random_management_key(algorithm: MANAGEMENT_KEY_TYPE) -> bytes: """Generates a new random management key.""" return os.urandom(algorithm.key_len) class PivmanData: def __init__(self, raw_data: bytes = 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: int) -> bool: return bool((self._flags or 0) & mask) def _set_flag(self, mask: int, value: bool) -> None: if value: self._flags = (self._flags or 0) | mask elif self._flags is not None: self._flags &= ~mask @property def puk_blocked(self) -> bool: return self._get_flag(0x01) @puk_blocked.setter def puk_blocked(self, value: bool) -> None: self._set_flag(0x01, value) @property def mgm_key_protected(self) -> bool: return self._get_flag(0x02) @mgm_key_protected.setter def mgm_key_protected(self, value: bool) -> None: self._set_flag(0x02, value) @property def has_protected_key(self) -> bool: return self.has_derived_key or self.has_stored_key @property def has_derived_key(self) -> bool: return self.salt is not None @property def has_stored_key(self) -> bool: return self.mgm_key_protected def get_bytes(self) -> bytes: 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: def __init__(self, raw_data: bytes = Tlv(0x88)): data = Tlv.parse_dict(Tlv(raw_data).value) self.key = data.get(0x89) def get_bytes(self) -> bytes: data = b"" if self.key is not None: data += Tlv(0x89, self.key) return Tlv(0x88, data) def get_pivman_data(session: PivSession) -> PivmanData: """Reads out the Pivman data from a YubiKey.""" try: return PivmanData(session.get_object(OBJECT_ID_PIVMAN_DATA)) except ApduError as e: if e.sw == SW.FILE_NOT_FOUND: # No data there, initialise a new object. return PivmanData() raise def get_pivman_protected_data(session: PivSession) -> PivmanProtectedData: """Reads out the Pivman protected data from a YubiKey. This function requires PIN verification prior to being called. """ try: return PivmanProtectedData(session.get_object(OBJECT_ID_PIVMAN_PROTECTED_DATA)) except ApduError as e: if e.sw == SW.FILE_NOT_FOUND: # No data there, initialise a new object. return PivmanProtectedData() raise def pivman_set_mgm_key( session: PivSession, new_key: bytes, algorithm: MANAGEMENT_KEY_TYPE, touch: bool = False, store_on_device: bool = False, ) -> None: """Set a new management key, while keeping PivmanData in sync.""" pivman = get_pivman_data(session) if store_on_device or (not store_on_device and pivman.has_stored_key): # Ensure we have access to protected data before overwriting key try: pivman_prot = get_pivman_protected_data(session) 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 session.set_management_key(algorithm, new_key, touch) if pivman.has_derived_key: # Clear salt for old derived keys. pivman.salt = None # Set flag for stored or not stored key. pivman.mgm_key_protected = store_on_device # Update readable pivman data session.put_object(OBJECT_ID_PIVMAN_DATA, pivman.get_bytes()) if store_on_device: # Store key in protected pivman data pivman_prot.key = new_key session.put_object(OBJECT_ID_PIVMAN_PROTECTED_DATA, pivman_prot.get_bytes()) elif not store_on_device and pivman.has_stored_key: # If new key should not be stored and there is an old stored key, # try to clear it. try: pivman_prot.key = None session.put_object( OBJECT_ID_PIVMAN_PROTECTED_DATA, pivman_prot.get_bytes(), ) except ApduError as e: logger.debug("No PIN provided, can't clear key...", exc_info=e) def pivman_change_pin(session: PivSession, old_pin: str, new_pin: str) -> None: """Change the PIN, while keeping PivmanData in sync.""" session.change_pin(old_pin, new_pin) pivman = get_pivman_data(session) if pivman.has_derived_key: session.authenticate( MANAGEMENT_KEY_TYPE.TDES, derive_management_key(old_pin, cast(bytes, pivman.salt)), ) session.verify_pin(new_pin) new_salt = os.urandom(16) new_key = derive_management_key(new_pin, new_salt) session.set_management_key(MANAGEMENT_KEY_TYPE.TDES, new_key) pivman.salt = new_salt session.put_object(OBJECT_ID_PIVMAN_DATA, pivman.get_bytes()) def list_certificates(session: PivSession) -> Mapping[SLOT, Optional[x509.Certificate]]: """Reads out and parses stored certificates. Only certificates which are successfully parsed are returned. """ certs = OrderedDict() for slot in set(SLOT) - {SLOT.ATTESTATION}: try: certs[slot] = session.get_certificate(slot) except ApduError: pass except BadResponseError: certs[slot] = None # type: ignore return certs def check_key( session: PivSession, slot: SLOT, public_key: Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey], ) -> bool: """Check that a given public key corresponds to the private key in a slot. This will create a signature using the private key, so the PIN must be verified prior to calling this function if the PIN policy requires it. """ try: test_data = b"test" test_sig = session.sign( slot, KEY_TYPE.from_public_key(public_key), test_data, hashes.SHA256(), padding.PKCS1v15(), # Only used for RSA ) if isinstance(public_key, rsa.RSAPublicKey): public_key.verify( test_sig, test_data, padding.PKCS1v15(), hashes.SHA256(), ) 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)) return True except ApduError as e: if e.sw in (SW.INCORRECT_PARAMETERS, SW.WRONG_PARAMETERS_P1P2): return False raise except InvalidSignature: return False def generate_chuid() -> bytes: """Generates a CHUID (Cardholder Unique Identifier).""" # 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" return ( Tlv(0x30, FASC_N) + Tlv(0x34, os.urandom(16)) + Tlv(0x35, EXPIRY) + Tlv(0x3E) + Tlv(TAG_LRC) ) def generate_ccc() -> bytes: """Generates a CCC (Card Capability Container).""" return ( 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 get_piv_info(session: PivSession) -> str: """Get human readable information about the PIV configuration.""" pivman = get_pivman_data(session) lines = [] lines.append("PIV version: %d.%d.%d" % session.version) try: pin_data = session.get_pin_metadata() if pin_data.default_value: lines.append("WARNING: Using default PIN!") tries_str = "%d/%d" % (pin_data.attempts_remaining, pin_data.total_attempts) except NotSupportedError: # Largest possible number of PIN tries to get back is 15 tries = session.get_pin_attempts() tries_str = "15 or more." if tries == 15 else str(tries) lines.append(f"PIN tries remaining: {tries_str}") if pivman.puk_blocked: lines.append("PUK blocked.") try: metadata = session.get_management_key_metadata() if metadata.default_value: lines.append("WARNING: Using default Management key!") key_type = metadata.key_type except NotSupportedError: key_type = MANAGEMENT_KEY_TYPE.TDES lines.append(f"Management key algorithm: {key_type.name}") if pivman.has_derived_key: lines.append("Management key is derived from PIN.") if pivman.has_stored_key: lines.append("Management key is stored on the YubiKey, protected by PIN.") try: chuid = session.get_object(OBJECT_ID.CHUID).hex() except ApduError as e: if e.sw == SW.FILE_NOT_FOUND: chuid = "No data available." lines.append("CHUID:\t" + chuid) try: ccc = session.get_object(OBJECT_ID.CAPABILITY).hex() except ApduError as e: if e.sw == SW.FILE_NOT_FOUND: ccc = "No data available." lines.append("CCC: \t" + ccc) for (slot, cert) in list_certificates(session).items(): lines.append(f"Slot {slot:02x}:") 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") cn = cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME) subject_cn = cn[0].value if cn else "None" cn = cert.issuer.get_attributes_for_oid(x509.NameOID.COMMON_NAME) issuer_cn = cn[0].value if cn else "None" except ValueError as e: # Malformed certificates may throw ValueError logger.debug("Failed parsing certificate", exc_info=e) lines.append(f"\tMalformed certificate: {e}") continue fingerprint = cert.fingerprint(hashes.SHA256()).hex() try: key_algo = KEY_TYPE.from_public_key(cert.public_key()).name except ValueError: key_algo = "Unsupported" serial = cert.serial_number try: not_before: Optional[datetime] = cert.not_valid_before except ValueError as e: logger.debug("Failed reading not_valid_before", exc_info=e) not_before = None try: not_after: Optional[datetime] = cert.not_valid_after except ValueError as e: logger.debug("Failed reading not_valid_after", exc_info=e) not_after = None # Print out everything lines.append(f"\tAlgorithm:\t{key_algo}") if print_dn: lines.append(f"\tSubject DN:\t{subject_dn}") lines.append(f"\tIssuer DN:\t{issuer_dn}") else: lines.append(f"\tSubject CN:\t{subject_cn}") lines.append(f"\tIssuer CN:\t{issuer_cn}") lines.append(f"\tSerial:\t\t{serial}") lines.append(f"\tFingerprint:\t{fingerprint}") if not_before: lines.append(f"\tNot before:\t{not_before}") if not_after: lines.append(f"\tNot after:\t{not_after}") else: lines.append("\tError: Failed to parse certificate.") return "\n".join(lines) def sign_certificate_builder( session: PivSession, slot: SLOT, key_type: KEY_TYPE, builder: x509.CertificateBuilder, hash_algorithm: Type[hashes.HashAlgorithm] = hashes.SHA256, ) -> x509.Certificate: """Sign a Certificate.""" dummy_key = _dummy_key(key_type) cert = builder.sign(dummy_key, hash_algorithm(), default_backend()) sig = session.sign( slot, key_type, cert.tbs_certificate_bytes, hash_algorithm(), padding.PKCS1v15(), # Only used for RSA ) 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( session: PivSession, slot: SLOT, public_key: Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey], builder: x509.CertificateSigningRequestBuilder, hash_algorithm: Type[hashes.HashAlgorithm] = hashes.SHA256, ) -> x509.CertificateSigningRequest: """Sign a CSR.""" key_type = KEY_TYPE.from_public_key(public_key) dummy_key = _dummy_key(key_type) csr = builder.sign(dummy_key, hash_algorithm(), default_backend()) seq = Tlv.parse_list(Tlv.unpack(0x30, csr.public_bytes(Encoding.DER))) # Replace public key pub_format = ( PublicFormat.PKCS1 if key_type.algorithm == ALGORITHM.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] = Tlv(seq[0].replace(dummy_bytes, pub_bytes)) sig = session.sign( slot, key_type, seq[0], hash_algorithm(), padding.PKCS1v15(), # Only used for RSA ) # 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()) def generate_self_signed_certificate( session: PivSession, slot: SLOT, public_key: Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey], subject_str: str, valid_from: datetime, valid_to: datetime, hash_algorithm: Type[hashes.HashAlgorithm] = hashes.SHA256, ) -> x509.Certificate: """Generate a self-signed certificate using a private key in a slot.""" key_type = KEY_TYPE.from_public_key(public_key) subject = parse_rfc4514_string(subject_str) builder = ( x509.CertificateBuilder() .public_key(public_key) .subject_name(subject) .issuer_name(subject) # Same as subject on self-signed certificate. .serial_number(x509.random_serial_number()) .not_valid_before(valid_from) .not_valid_after(valid_to) ) try: return sign_certificate_builder( session, slot, key_type, builder, hash_algorithm ) except ApduError as e: logger.error("Failed to generate certificate for slot %s", slot, exc_info=e) raise def generate_csr( session: PivSession, slot: SLOT, public_key: Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey], subject_str: str, hash_algorithm: Type[hashes.HashAlgorithm] = hashes.SHA256, ) -> x509.CertificateSigningRequest: """Generate a CSR using a private key in a slot.""" builder = x509.CertificateSigningRequestBuilder().subject_name( parse_rfc4514_string(subject_str) ) try: return sign_csr_builder(session, slot, public_key, builder, hash_algorithm) except ApduError as e: logger.error( "Failed to generate Certificate Signing Request for slot %s", slot, exc_info=e, ) raise ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.1537561 yubikey-manager-4.0.7/ykman/scancodes/__init__.py0000644000000000000000000000356300000000000020143 0ustar0000000000000000# 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 enum import Enum from . import us, uk, de, fr, it, modhex, norman, bepo class KEYBOARD_LAYOUT(Enum): MODHEX = modhex.scancodes US = us.scancodes UK = uk.scancodes DE = de.scancodes FR = fr.scancodes IT = it.scancodes BEPO = bepo.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(f"Unsupported character: {e.args[0]}") ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1618212188.154908 yubikey-manager-4.0.7/ykman/scancodes/bepo.py0000644000000000000000000000651500000000000017331 0ustar0000000000000000# 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 BÉPO (fr dvorak) keyboard layout""" SHIFT = 0x80 scancodes = { "\t": 0x2B | SHIFT, "\n": 0x28 | SHIFT, " ": 0x2C, "!": 0x1C | SHIFT, '"': 0x1E, "#": 0x35 | SHIFT, "$": 0x35, "%": 0x2E, "'": 0x11, "(": 0x21, ")": 0x22, "*": 0x27, "+": 0x24, ",": 0x0A, "-": 0x25, ".": 0x19, "/": 0x26, "0": 0x27 | SHIFT, "1": 0x1E | SHIFT, "2": 0x1F | SHIFT, "3": 0x20 | SHIFT, "4": 0x21 | SHIFT, "5": 0x22 | SHIFT, "6": 0x23 | SHIFT, "7": 0x24 | SHIFT, "8": 0x25 | SHIFT, "9": 0x26 | SHIFT, ":": 0x19 | SHIFT, ";": 0x0A | SHIFT, "=": 0x2D, "?": 0x11 | SHIFT, "@": 0x23, "A": 0x04 | SHIFT, "B": 0x14 | SHIFT, "C": 0x0B | SHIFT, "D": 0x0C | SHIFT, "E": 0x09 | SHIFT, "F": 0x38 | SHIFT, "G": 0x36 | SHIFT, "H": 0x37 | SHIFT, "I": 0x07 | SHIFT, "J": 0x13 | SHIFT, "K": 0x05 | SHIFT, "L": 0x12 | SHIFT, "M": 0x34 | SHIFT, "N": 0x33 | SHIFT, "O": 0x15 | SHIFT, "P": 0x08 | SHIFT, "Q": 0x10 | SHIFT, "R": 0x0F | SHIFT, "S": 0x0E | SHIFT, "T": 0x0D | SHIFT, "U": 0x16 | SHIFT, "V": 0x18 | SHIFT, "W": 0x30 | SHIFT, "X": 0x06 | SHIFT, "Y": 0x1B | SHIFT, "Z": 0x2F | SHIFT, "`": 0x2E | SHIFT, "a": 0x04, "b": 0x14, "c": 0x0B, "d": 0x0C, "e": 0x09, "f": 0x38, "g": 0x36, "h": 0x37, "i": 0x07, "j": 0x13, "k": 0x05, "l": 0x12, "m": 0x34, "n": 0x33, "o": 0x15, "p": 0x08, "q": 0x10, "r": 0x0F, "s": 0x0E, "t": 0x0D, "u": 0x16, "v": 0x18, "w": 0x30, "x": 0x06, "y": 0x1B, "z": 0x2F, "\xa0": 0x2C | SHIFT, "«": 0x1F, "°": 0x2D | SHIFT, "»": 0x20, "À": 0x1D | SHIFT, "Ç": 0x31 | SHIFT, "È": 0x17 | SHIFT, "É": 0x1A | SHIFT, "Ê": 0x64 | SHIFT, "à": 0x1D, "ç": 0x31, "è": 0x17, "é": 0x1A, "ê": 0x64, } ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.1554842 yubikey-manager-4.0.7/ykman/scancodes/de.py0000644000000000000000000000641700000000000016775 0ustar0000000000000000# 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 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, } ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.1566393 yubikey-manager-4.0.7/ykman/scancodes/fr.py0000644000000000000000000000632700000000000017014 0ustar0000000000000000# 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 FR French (AZERTY) keyboard layout""" SHIFT = 0x80 scancodes = { "a": 0x14, "b": 0x05, "c": 0x06, "d": 0x07, "e": 0x08, "f": 0x09, "g": 0x0A, "h": 0x0B, "i": 0x0C, "j": 0x0D, "k": 0x0E, "l": 0x0F, "m": 0x33, "n": 0x11, "o": 0x12, "p": 0x13, "q": 0x04, "r": 0x15, "s": 0x16, "t": 0x17, "u": 0x18, "v": 0x19, "w": 0x1D, "x": 0x1B, "y": 0x1C, "z": 0x1A, "A": 0x14 | 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": 0x33 | SHIFT, "N": 0x11 | SHIFT, "O": 0x12 | SHIFT, "P": 0x13 | SHIFT, "Q": 0x04 | SHIFT, "R": 0x15 | SHIFT, "S": 0x16 | SHIFT, "T": 0x17 | SHIFT, "U": 0x18 | SHIFT, "V": 0x19 | SHIFT, "W": 0x1D | SHIFT, "X": 0x1B | SHIFT, "Y": 0x1C | SHIFT, "Z": 0x1A | SHIFT, "0": 0x27 | SHIFT, "1": 0x1E | SHIFT, "2": 0x1F | SHIFT, "3": 0x20 | SHIFT, "4": 0x21 | SHIFT, "5": 0x22 | SHIFT, "6": 0x23 | SHIFT, "7": 0x24 | SHIFT, "8": 0x25 | SHIFT, "9": 0x26 | SHIFT, "\t": 0x2B, "\n": 0x28, " ": 0x2C, "!": 0x38, '"': 0x20, "$": 0x30, "%": 0x34 | SHIFT, "&": 0x1E, "'": 0x21, "(": 0x22, ")": 0x2D, "*": 0x31, "+": 0x2E | SHIFT, ",": 0x10, "-": 0x23, ".": 0x36 | SHIFT, "/": 0x37 | SHIFT, ":": 0x37, ";": 0x36, "<": 0x64, "=": 0x2E, "_": 0x25, "\x7f": 0x2A, "£": 0x30 | SHIFT, "§": 0x38 | SHIFT, "°": 0x2D | SHIFT, "²": 0x35, "µ": 0x31 | SHIFT, "à": 0x27, "ç": 0x26, "è": 0x24, "é": 0x1F, "ù": 0x34, } ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1618212188.157787 yubikey-manager-4.0.7/ykman/scancodes/it.py0000644000000000000000000000642700000000000017022 0ustar0000000000000000# 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 IT Italian (AZERTY) keyboard layout""" SHIFT = 0x80 scancodes = { "\t": 0x2B, "\n": 0x28, " ": 0x2C, "!": 0x1E | SHIFT, '"': 0x1F | SHIFT, "#": 0x32, "$": 0x21 | SHIFT, "%": 0x22 | SHIFT, "&": 0x23 | SHIFT, "'": 0x2D, "(": 0x25 | SHIFT, ")": 0x26 | SHIFT, "*": 0x55, "+": 0x30, ",": 0x36, "-": 0x38, ".": 0x63, "/": 0x24 | SHIFT, "0": 0x27, "1": 0x1E, "2": 0x1F, "3": 0x20, "4": 0x21, "5": 0x22, "6": 0x23, "7": 0x24, "8": 0x25, "9": 0x26, ":": 0xB7, ";": 0xB6, "<": 0x64, "=": 0x27 | SHIFT, ">": 0x64 | SHIFT, "?": 0x2D | SHIFT, "@": 0x24, "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, "\\": 0x35, "^": 0xAE, "_": 0xB8, "`": 0x2D | SHIFT, "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, "|": 0xB5, "£": 0xA0, "§": 0xB2, "°": 0xB4, "ç": 0xB3, "è": 0x2F, "é": 0x2F | SHIFT, "à": 0x34, "ì": 0x2E, "ò": 0x33, "ù": 0x31, } ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.1583629 yubikey-manager-4.0.7/ykman/scancodes/modhex.py0000644000000000000000000000417400000000000017667 0ustar0000000000000000# 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, } ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.1595151 yubikey-manager-4.0.7/ykman/scancodes/norman.py0000644000000000000000000000636000000000000017674 0ustar0000000000000000# 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, } ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.1606734 yubikey-manager-4.0.7/ykman/scancodes/uk.py0000644000000000000000000000635200000000000017022 0ustar0000000000000000# 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 UK 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, } ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.1612432 yubikey-manager-4.0.7/ykman/scancodes/us.py0000644000000000000000000000635100000000000017031 0ustar0000000000000000# 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, } ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1618212188.162395 yubikey-manager-4.0.7/ykman/settings.py0000644000000000000000000000476500000000000016307 0ustar0000000000000000# 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. import os import json from pathlib import Path HOME_CONFIG = "~/.ykman" XDG_DATA_HOME = os.environ.get("XDG_DATA_HOME", "~/.local/share") + "/ykman" XDG_CONFIG_HOME = os.environ.get("XDG_CONFIG_HOME", "~/.config") + "/ykman" USE_XDG = "YKMAN_XDG_EXPERIMENTAL" in os.environ class Settings(dict): _config_dir = HOME_CONFIG def __init__(self, name): self.fname = Path(self._config_dir).expanduser().resolve() / (name + ".json") if self.fname.is_file(): with self.fname.open("r") as fd: self.update(json.load(fd)) def __eq__(self, other): return other is not None and self.fname == other.fname def __ne__(self, other): return other is None or self.fname != other.fname def write(self): conf_dir = self.fname.parent if not conf_dir.is_dir(): conf_dir.mkdir(0o700, parents=True) with self.fname.open("w") as fd: json.dump(self, fd, indent=2) __hash__ = None class Configuration(Settings): _config_dir = XDG_CONFIG_HOME if USE_XDG else HOME_CONFIG class AppData(Settings): _config_dir = XDG_DATA_HOME if USE_XDG else HOME_CONFIG ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1621233183.3266504 yubikey-manager-4.0.7/ykman/util.py0000644000000000000000000001633700000000000015422 0ustar0000000000000000# 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 yubikit.core import Tlv from cryptography.hazmat.primitives import serialization from cryptography.hazmat.backends import default_backend from cryptography import x509 from functools import partial from typing import Tuple import ctypes import logging logger = logging.getLogger(__name__) PEM_IDENTIFIER = b"-----BEGIN" class InvalidPasswordError(Exception): """Raised when parsing key/certificate and the password might be wrong/missing.""" def _parse_pkcs12_cryptography(pkcs12, data, password): try: key, cert, cas = pkcs12.load_key_and_certificates( data, password, default_backend() ) return key, [cert] + cas except ValueError as e: # cryptography raises ValueError on wrong password raise InvalidPasswordError(e) def _parse_pkcs12_pyopenssl(crypto, data, password): try: p12 = crypto.load_pkcs12(data, password) key_pem = crypto.dump_privatekey(crypto.FILETYPE_PEM, p12.get_privatekey()) key = serialization.load_pem_private_key( key_pem, password=None, backend=default_backend() ) certs = [p12.get_certificate()] cas = p12.get_ca_certificates() if cas: certs.extend(cas) certs_pem = [ crypto.dump_certificate(crypto.FILETYPE_PEM, cert) for cert in certs ] certs = [ x509.load_pem_x509_certificate(cert_pem, default_backend()) for cert_pem in certs_pem ] return key, certs except crypto.Error as e: raise InvalidPasswordError(e) def _parse_pkcs12_unsupported(data, password): raise ValueError("PKCS#12 support requires cryptography >= 2.5 or pyOpenSSL") try: # This requires cryptography 2.5. from cryptography.hazmat.primitives.serialization import pkcs12 _parse_pkcs12 = partial(_parse_pkcs12_cryptography, pkcs12) except ImportError: # Use pyOpenSSL as a backup try: from OpenSSL import crypto _parse_pkcs12 = partial(_parse_pkcs12_pyopenssl, crypto) except ImportError: # Can't support PKCS#12 _parse_pkcs12 = _parse_pkcs12_unsupported # type: ignore 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 InvalidPasswordError("No password provided for encrypted key.") try: return serialization.load_pem_private_key( data, password, backend=default_backend() ) except ValueError as e: # Cryptography raises ValueError if decryption fails. raise InvalidPasswordError(e) except Exception as e: logger.debug("Failed to parse PEM private key ", exc_info=e) # PKCS12 if is_pkcs12(data): return _parse_pkcs12(data, password)[0] # DER try: return serialization.load_der_private_key( data, password, backend=default_backend() ) except Exception as e: logger.debug("Failed to parse private key as DER", exc_info=e) # 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): if cert: try: certs.append( x509.load_pem_x509_certificate( PEM_IDENTIFIER + cert, default_backend() ) ) except Exception as e: logger.debug("Failed to parse PEM certificate", exc_info=e) # Could be valid PEM but not certificates. if not certs: raise ValueError("PEM file does not contain any certificate(s)") return certs # PKCS12 if is_pkcs12(data): return _parse_pkcs12(data, password)[1] # DER try: return [x509.load_der_x509_certificate(data, default_backend())] except Exception as e: logger.debug("Failed to parse certificate as DER", exc_info=e) 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 data and PEM_IDENTIFIER in data 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. """ try: header = Tlv.parse_from(Tlv.unpack(0x30, data))[0] return header.tag == 0x02 and header.value == b"\x03" except ValueError as e: logger.debug("Unable to parse TLV", exc_info=e) return False class OSVERSIONINFOW(ctypes.Structure): _fields_ = [ ("dwOSVersionInfoSize", ctypes.c_ulong), ("dwMajorVersion", ctypes.c_ulong), ("dwMinorVersion", ctypes.c_ulong), ("dwBuildNumber", ctypes.c_ulong), ("dwPlatformId", ctypes.c_ulong), ("szCSDVersion", ctypes.c_wchar * 128), ] def get_windows_version() -> Tuple[int, int, int]: """Get the true Windows version, since sys.getwindowsversion lies.""" osvi = OSVERSIONINFOW() osvi.dwOSVersionInfoSize = ctypes.sizeof(osvi) ctypes.windll.Ntdll.RtlGetVersion(ctypes.byref(osvi)) # type: ignore return osvi.dwMajorVersion, osvi.dwMinorVersion, osvi.dwBuildNumber ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1618212188.164123 yubikey-manager-4.0.7/yubikit/__init__.py0000644000000000000000000000253300000000000016536 0ustar0000000000000000# Copyright (c) 2020 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. ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1621233183.328645 yubikey-manager-4.0.7/yubikit/core/__init__.py0000644000000000000000000002147600000000000017475 0ustar0000000000000000# Copyright (c) 2020 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 enum import Enum, unique from typing import ( Type, List, Dict, Tuple, TypeVar, Union, Optional, Hashable, NamedTuple, ) import re import abc _VERSION_STRING_PATTERN = re.compile(r"\b(?P\d+).(?P\d).(?P\d)\b") class Version(NamedTuple): """3-digit version tuple.""" major: int minor: int patch: int @classmethod def from_bytes(cls, data: bytes) -> "Version": return cls(*data) @classmethod def from_string(cls, data: str) -> "Version": m = _VERSION_STRING_PATTERN.search(data) if m: return cls( int(m.group("major")), int(m.group("minor")), int(m.group("patch")) ) raise ValueError("No version found in string") @unique class TRANSPORT(str, Enum): """YubiKey physical connection transports.""" USB = "usb" NFC = "nfc" @unique class AID(bytes, Enum): """YubiKey Application smart card AID values.""" OTP = bytes.fromhex("a0000005272001") MANAGEMENT = bytes.fromhex("a000000527471117") OPENPGP = bytes.fromhex("d27600012401") OATH = bytes.fromhex("a0000005272101") PIV = bytes.fromhex("a000000308") FIDO = bytes.fromhex("a0000006472f0001") HSMAUTH = bytes.fromhex("a000000527210701") class Connection(abc.ABC): """A connection to a YubiKey""" def close(self) -> None: """Close the device, releasing any held resources.""" def __enter__(self): return self def __exit__(self, typ, value, traceback): self.close() T_Connection = TypeVar("T_Connection", bound=Connection) class YubiKeyDevice(abc.ABC): """YubiKey device reference""" def __init__(self, transport: TRANSPORT, fingerprint: Hashable): self._transport = transport self._fingerprint = fingerprint @property def transport(self) -> TRANSPORT: """Get the transport used to communicate with this YubiKey""" return self._transport def supports_connection(self, connection_type: Type[T_Connection]) -> bool: """Check if a YubiKeyDevice supports a specific Connection type""" return False def open_connection(self, connection_type: Type[T_Connection]) -> T_Connection: """Opens a connection to the YubiKey""" raise ValueError("Unsupported Connection type") @property def fingerprint(self) -> Hashable: """Used to identify that device references from different enumerations represent the same physical YubiKey. This fingerprint is not stable between sessions, or after un-plugging, and re-plugging a device.""" return self._fingerprint def __eq__(self, other): return isinstance(other, type(self)) and self.fingerprint == other.fingerprint def __hash__(self): return hash(self.fingerprint) def __repr__(self): return f"{type(self).__name__}(fingerprint={self.fingerprint!r})" class CommandError(Exception): """An error response from a YubiKey""" class BadResponseError(CommandError): """Invalid response data from the YubiKey""" class TimeoutError(CommandError): """An operation timed out waiting for something""" class ApplicationNotAvailableError(CommandError): """The application is either disabled or not supported on this YubiKey""" class NotSupportedError(ValueError): """Attempting an action that is not supported on this YubiKey""" def require_version( my_version: Version, min_version: Tuple[int, int, int], message=None ): """Ensure a version is at least min_version.""" # Skip version checks for major == 0, used for development builds. if my_version < min_version and my_version[0] != 0: if not message: message = "This action requires YubiKey %d.%d.%d or later" % min_version raise NotSupportedError(message) def int2bytes(value: int, min_len: int = 0) -> bytes: buf = [] while value > 0xFF: buf.append(value & 0xFF) value >>= 8 buf.append(value) return bytes(reversed(buf)).rjust(min_len, b"\0") def bytes2int(data: bytes) -> int: return int.from_bytes(data, "big") def _tlv_parse(data, offset=0): try: tag = data[offset] offset += 1 if tag & 0x1F == 0x1F: # Long form tag = tag << 8 | data[offset] offset += 1 while tag & 0x80 == 0x80: # Additional bytes tag = tag << 8 | data[offset] offset += 1 ln = data[offset] offset += 1 if ln == 0x80: # Indefinite length end = offset while data[end] or data[end + 1]: # Run until 0x0000 end = _tlv_parse(data, end)[3] # Skip over TLV ln = end - offset end += 2 # End after 0x0000 else: if ln > 0x80: # Length spans multiple bytes n_bytes = ln - 0x80 ln = bytes2int(data[offset : offset + n_bytes]) offset += n_bytes end = offset + ln return tag, offset, ln, end except IndexError: raise ValueError("Invalid encoding of tag/length") T_Tlv = TypeVar("T_Tlv", bound="Tlv") class Tlv(bytes): @property def tag(self) -> int: return self._tag @property def length(self) -> int: return self._value_ln @property def value(self) -> bytes: return self[self._value_offset : self._value_offset + self._value_ln] def __new__(cls, tag_or_data: Union[int, bytes], value: Optional[bytes] = None): """This allows creation by passing either binary data, or tag and value.""" if isinstance(tag_or_data, int): # Tag and (optional) value tag = tag_or_data # Pack into Tlv buf = bytearray() buf.extend(int2bytes(tag)) value = value or b"" length = len(value) if length < 0x80: buf.append(length) else: ln_bytes = int2bytes(length) buf.append(0x80 | len(ln_bytes)) buf.extend(ln_bytes) buf.extend(value) data = bytes(buf) else: # Binary TLV data if value is not None: raise ValueError("value can only be provided if tag_or_data is a tag") data = tag_or_data # mypy thinks this is wrong return super(Tlv, cls).__new__(cls, data) # type: ignore def __init__(self, tag_or_data: Union[int, bytes], value: Optional[bytes] = None): self._tag, self._value_offset, self._value_ln, end = _tlv_parse(self) if len(self) != end: raise ValueError("Incorrect TLV length") def __repr__(self): return f"Tlv(tag=0x{self.tag:02x}, value={self.value.hex()})" @classmethod def parse_from(cls: Type[T_Tlv], data: bytes) -> Tuple[T_Tlv, bytes]: tag, offs, ln, end = _tlv_parse(data) return cls(data[:end]), data[end:] @classmethod def parse_list(cls: Type[T_Tlv], data: bytes) -> List[T_Tlv]: res = [] while data: tlv, data = cls.parse_from(data) res.append(tlv) return res @classmethod def parse_dict(cls: Type[T_Tlv], data: bytes) -> Dict[int, bytes]: return dict((tlv.tag, tlv.value) for tlv in cls.parse_list(data)) @classmethod def unpack(cls: Type[T_Tlv], tag: int, data: bytes) -> bytes: tlv = cls(data) if tlv.tag != tag: raise ValueError(f"Wrong tag, got 0x{tlv.tag:02x} expected 0x{tag:02x}") return tlv.value ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1618212188.167581 yubikey-manager-4.0.7/yubikit/core/fido.py0000644000000000000000000000277000000000000016653 0ustar0000000000000000# Copyright (c) 2020 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 . import Connection from fido2.ctap import CtapDevice # Make CtapDevice a Connection FidoConnection = CtapDevice Connection.register(FidoConnection) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.1687326 yubikey-manager-4.0.7/yubikit/core/otp.py0000644000000000000000000002225200000000000016531 0ustar0000000000000000# Copyright (c) 2020 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 . import Connection, CommandError, TimeoutError, Version from time import sleep from threading import Event from typing import Optional, Callable import abc import struct import logging logger = logging.getLogger(__name__) class CommandRejectedError(CommandError): """The issues command was rejected by the YubiKey""" class OtpConnection(Connection, metaclass=abc.ABCMeta): @abc.abstractmethod def receive(self) -> bytes: """Reads an 8 byte feature report""" @abc.abstractmethod def send(self, data: bytes) -> None: """Writes an 8 byte feature report""" CRC_OK_RESIDUAL = 0xF0B8 def calculate_crc(data: bytes) -> int: crc = 0xFFFF for index in range(len(data)): crc ^= data[index] for i in range(8): j = crc & 1 crc >>= 1 if j == 1: crc ^= 0x8408 return crc & 0xFFFF def check_crc(data: bytes) -> bool: return calculate_crc(data) == CRC_OK_RESIDUAL _MODHEX = "cbdefghijklnrtuv" def modhex_encode(data: bytes) -> str: """Encode a bytes-like object using Modhex (modified hexadecimal) encoding.""" return "".join(_MODHEX[b >> 4] + _MODHEX[b & 0xF] for b in data) def modhex_decode(string: str) -> bytes: """Decode the Modhex (modified hexadecimal) string.""" return bytes( _MODHEX.index(string[i]) << 4 | _MODHEX.index(string[i + 1]) for i in range(0, len(string), 2) ) FEATURE_RPT_SIZE = 8 FEATURE_RPT_DATA_SIZE = FEATURE_RPT_SIZE - 1 SLOT_DATA_SIZE = 64 FRAME_SIZE = SLOT_DATA_SIZE + 6 RESP_PENDING_FLAG = 0x40 # Response pending flag SLOT_WRITE_FLAG = 0x80 # Write flag - set by app - cleared by device RESP_TIMEOUT_WAIT_FLAG = 0x20 # Waiting for timeout operation DUMMY_REPORT_WRITE = 0x8F # Write a dummy report to force update or abort SEQUENCE_MASK = 0x1F STATUS_OFFSET_PROG_SEQ = 0x4 STATUS_OFFSET_TOUCH_LOW = 0x5 CONFIG_STATUS_MASK = 0x1F STATUS_PROCESSING = 1 STATUS_UPNEEDED = 2 def _should_send(packet, seq): """All-zero packets are skipped, except for the very first and last packets""" return seq in (0, 9) or any(packet) def _format_frame(slot, payload): return payload + struct.pack(" None: self.connection.close() def send_and_receive( self, slot: int, data: Optional[bytes] = None, event: Optional[Event] = None, on_keepalive: Optional[Callable[[int], None]] = None, ) -> bytes: """Sends a command to the YubiKey, and reads the response. If the command results in a configuration update, the programming sequence number is verified and the updated status bytes are returned. @param slot the slot to send to @param data the data payload to send @param state optional CommandState for listening for user presence requirement and for cancelling a command. @return response data (including CRC) in the case of data, or an updated status struct """ payload = (data or b"").ljust(SLOT_DATA_SIZE, b"\0") if len(payload) > SLOT_DATA_SIZE: raise ValueError("Payload too large for HID frame") if not on_keepalive: on_keepalive = lambda x: None # noqa frame = _format_frame(slot, payload) logger.debug("SEND: %s", frame.hex()) response = self._read_frame( self._send_frame(frame), event or Event(), on_keepalive ) logger.debug("RECV: %s", response.hex()) return response def _receive(self): report = self.connection.receive() if len(report) != FEATURE_RPT_SIZE: raise Exception( f"Incorrect reature report size (was {len(report)}, " f"expected {FEATURE_RPT_SIZE})" ) return report def read_status(self) -> bytes: """Receive status bytes from YubiKey @return status bytes (first 3 bytes are the firmware version) @throws IOException in case of communication error """ return self._receive()[1:-1] def _await_ready_to_write(self): """Sleep for up to ~1s waiting for the WRITE flag to be unset""" for _ in range(20): if (self._receive()[FEATURE_RPT_DATA_SIZE] & SLOT_WRITE_FLAG) == 0: return sleep(0.05) raise Exception("Timeout waiting for YubiKey to become ready to receive") def _send_frame(self, buf): """Sends a 70 byte frame""" prog_seq = self._receive()[STATUS_OFFSET_PROG_SEQ] seq = 0 while buf: report, buf = buf[:FEATURE_RPT_DATA_SIZE], buf[FEATURE_RPT_DATA_SIZE:] if _should_send(report, seq): report += struct.pack(">B", 0x80 | seq) self._await_ready_to_write() self.connection.send(report) seq += 1 return prog_seq def _read_frame(self, prog_seq, event, on_keepalive): """Reads one frame""" response = b"" seq = 0 needs_touch = False try: while True: report = self._receive() status_byte = report[FEATURE_RPT_DATA_SIZE] if (status_byte & RESP_PENDING_FLAG) != 0: # Response packet if seq == (status_byte & SEQUENCE_MASK): # Correct sequence response += report[:FEATURE_RPT_DATA_SIZE] seq += 1 elif 0 == (status_byte & SEQUENCE_MASK): # Transmission complete self._reset_state() return response elif status_byte == 0: # Status response next_prog_seq = report[STATUS_OFFSET_PROG_SEQ] if response: raise Exception("Incomplete transfer") elif next_prog_seq == prog_seq + 1 or ( prog_seq > 0 and next_prog_seq == 0 and report[STATUS_OFFSET_TOUCH_LOW] & CONFIG_STATUS_MASK == 0 ): # Note: If no valid configurations exist, prog_seq resets to 0. # Sequence updated, return status. return report[1:-1] elif needs_touch: raise TimeoutError("Timed out waiting for touch") else: raise CommandRejectedError("No data") else: # Need to wait if (status_byte & RESP_TIMEOUT_WAIT_FLAG) != 0: on_keepalive(STATUS_UPNEEDED) needs_touch = True timeout = 0.1 else: on_keepalive(STATUS_PROCESSING) timeout = 0.02 sleep(timeout) if event.wait(timeout): self._reset_state() raise TimeoutError("Command cancelled by Event") except KeyboardInterrupt: logger.debug("Keyboard interrupt, reset state...") self._reset_state() raise def _reset_state(self): """Reset the state of YubiKey from reading""" self.connection.send(b"\xff".rjust(FEATURE_RPT_SIZE, b"\0")) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1618212188.1693125 yubikey-manager-4.0.7/yubikit/core/smartcard.py0000644000000000000000000001403100000000000017703 0ustar0000000000000000# Copyright (c) 2020 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 . import Version, TRANSPORT, Connection, CommandError, ApplicationNotAvailableError from time import time from enum import Enum, IntEnum, unique from typing import Tuple import abc import struct class SmartCardConnection(Connection, metaclass=abc.ABCMeta): @property @abc.abstractmethod def transport(self) -> TRANSPORT: """Get the transport type of the connection (USB or NFC)""" @abc.abstractmethod def send_and_receive(self, apdu: bytes) -> Tuple[bytes, int]: """Sends a command APDU and returns the response""" class ApduError(CommandError): """Thrown when an APDU response has the wrong SW code""" def __init__(self, data: bytes, sw: int): self.data = data self.sw = sw def __str__(self): return f"APDU error: SW=0x{self.sw:04x}" @unique class ApduFormat(str, Enum): """APDU encoding format""" SHORT = "short" EXTENDED = "extended" @unique class SW(IntEnum): 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 FUNCTION_NOT_SUPPORTED = 0x6A81 FILE_NOT_FOUND = 0x6A82 NO_SPACE = 0x6A84 REFERENCE_DATA_NOT_FOUND = 0x6A88 WRONG_PARAMETERS_P1P2 = 0x6B00 INVALID_INSTRUCTION = 0x6D00 COMMAND_ABORTED = 0x6F00 OK = 0x9000 INS_SELECT = 0xA4 P1_SELECT = 0x04 P2_SELECT = 0x00 INS_SEND_REMAINING = 0xC0 SW1_HAS_MORE_DATA = 0x61 SHORT_APDU_MAX_CHUNK = 0xFF def _encode_short_apdu(cla, ins, p1, p2, data): return struct.pack(">BBBBB", cla, ins, p1, p2, len(data)) + data def _encode_extended_apdu(cla, ins, p1, p2, data): return struct.pack(">BBBBBH", cla, ins, p1, p2, 0, len(data)) + data class SmartCardProtocol: def __init__( self, smartcard_connection: SmartCardConnection, ins_send_remaining: int = INS_SEND_REMAINING, ): self.apdu_format = ApduFormat.SHORT self.connection = smartcard_connection self._ins_send_remaining = ins_send_remaining self._touch_workaround = False self._last_long_resp = 0.0 def close(self) -> None: self.connection.close() def enable_touch_workaround(self, version: Version) -> None: self._touch_workaround = self.connection.transport == TRANSPORT.USB and ( (4, 2, 0) <= version <= (4, 2, 6) ) def select(self, aid: bytes) -> bytes: try: return self.send_apdu(0, INS_SELECT, P1_SELECT, P2_SELECT, aid) except ApduError as e: if e.sw in ( SW.FILE_NOT_FOUND, SW.INVALID_INSTRUCTION, SW.WRONG_PARAMETERS_P1P2, ): raise ApplicationNotAvailableError() raise def send_apdu( self, cla: int, ins: int, p1: int, p2: int, data: bytes = b"" ) -> bytes: if ( self._touch_workaround and self._last_long_resp > 0 and time() - self._last_long_resp < 2 ): self.connection.send_and_receive( _encode_short_apdu(0, 0, 0, 0, b"") ) # Dummy APDU, returns error self._last_long_resp = 0 if self.apdu_format is ApduFormat.SHORT: while len(data) > SHORT_APDU_MAX_CHUNK: chunk, data = data[:SHORT_APDU_MAX_CHUNK], data[SHORT_APDU_MAX_CHUNK:] response, sw = self.connection.send_and_receive( _encode_short_apdu(0x10 | cla, ins, p1, p2, chunk) ) if sw != SW.OK: raise ApduError(response, sw) response, sw = self.connection.send_and_receive( _encode_short_apdu(cla, ins, p1, p2, data) ) get_data = _encode_short_apdu(0, self._ins_send_remaining, 0, 0, b"") elif self.apdu_format is ApduFormat.EXTENDED: response, sw = self.connection.send_and_receive( _encode_extended_apdu(cla, ins, p1, p2, data) ) get_data = _encode_extended_apdu(0, self._ins_send_remaining, 0, 0, b"") else: raise TypeError("Invalid ApduFormat set") # Read chained response buf = b"" while sw >> 8 == SW1_HAS_MORE_DATA: buf += response response, sw = self.connection.send_and_receive(get_data) if sw != SW.OK: raise ApduError(response, sw) buf += response if self._touch_workaround and len(buf) > 54: self._last_long_resp = time() else: self._last_long_resp = 0 return buf ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631098162.2585056 yubikey-manager-4.0.7/yubikit/management.py0000644000000000000000000004053500000000000017117 0ustar0000000000000000# Copyright (c) 2020 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 .core import ( bytes2int, int2bytes, require_version, Version, Tlv, AID, TRANSPORT, NotSupportedError, BadResponseError, ApplicationNotAvailableError, ) from .core.otp import ( check_crc, OtpConnection, OtpProtocol, STATUS_OFFSET_PROG_SEQ, CommandRejectedError, ) from .core.fido import FidoConnection from .core.smartcard import SmartCardConnection, SmartCardProtocol from fido2.hid import CAPABILITY as CTAP_CAPABILITY from enum import IntEnum, IntFlag, unique from dataclasses import dataclass from typing import Optional, Union, Mapping import abc import struct @unique class CAPABILITY(IntFlag): """YubiKey Application identifiers.""" OTP = 0x01 U2F = 0x02 FIDO2 = 0x200 OATH = 0x20 PIV = 0x10 OPENPGP = 0x08 HSMAUTH = 0x100 def __str__(self): if self == CAPABILITY.U2F: return "FIDO U2F" elif self == CAPABILITY.OPENPGP: return "OpenPGP" elif self == CAPABILITY.HSMAUTH: return "YubiHSM Auth" else: return getattr(self, "name", super().__str__()) @unique class USB_INTERFACE(IntFlag): """YubiKey USB interface identifiers.""" OTP = 0x01 FIDO = 0x02 CCID = 0x04 def supports_connection(self, connection_type) -> bool: if issubclass(connection_type, SmartCardConnection): return USB_INTERFACE.CCID in self if issubclass(connection_type, FidoConnection): return USB_INTERFACE.FIDO in self if issubclass(connection_type, OtpConnection): return USB_INTERFACE.OTP in self return False @staticmethod def for_capabilities(capabilities: CAPABILITY) -> "USB_INTERFACE": ifaces = USB_INTERFACE(0) if capabilities & CAPABILITY.OTP: ifaces |= USB_INTERFACE.OTP if capabilities & (CAPABILITY.U2F | CAPABILITY.FIDO2): ifaces |= USB_INTERFACE.FIDO if capabilities & ( CAPABILITY.OATH | CAPABILITY.PIV | CAPABILITY.OPENPGP | CAPABILITY.HSMAUTH ): ifaces |= USB_INTERFACE.CCID return ifaces @unique class FORM_FACTOR(IntEnum): """YubiKey device form factors.""" UNKNOWN = 0x00 USB_A_KEYCHAIN = 0x01 USB_A_NANO = 0x02 USB_C_KEYCHAIN = 0x03 USB_C_NANO = 0x04 USB_C_LIGHTNING = 0x05 USB_A_BIO = 0x06 USB_C_BIO = 0x07 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.USB_A_BIO: return "Bio (USB-A)" elif self == FORM_FACTOR.USB_C_BIO: return "Bio (USB-C)" else: return "Unknown" @classmethod def from_code(cls, code: int) -> "FORM_FACTOR": if code and not isinstance(code, int): raise ValueError(f"Invalid form factor code: {code}") code &= 0xF return cls(code) if code in cls.__members__.values() else cls.UNKNOWN @unique class DEVICE_FLAG(IntFlag): """Configuration flags.""" REMOTE_WAKEUP = 0x40 EJECT = 0x80 TAG_USB_SUPPORTED = 0x01 TAG_SERIAL = 0x02 TAG_USB_ENABLED = 0x03 TAG_FORM_FACTOR = 0x04 TAG_VERSION = 0x05 TAG_AUTO_EJECT_TIMEOUT = 0x06 TAG_CHALRESP_TIMEOUT = 0x07 TAG_DEVICE_FLAGS = 0x08 TAG_APP_VERSIONS = 0x09 TAG_CONFIG_LOCK = 0x0A TAG_UNLOCK = 0x0B TAG_REBOOT = 0x0C TAG_NFC_SUPPORTED = 0x0D TAG_NFC_ENABLED = 0x0E @dataclass class DeviceConfig: """Management settings for YubiKey which can be configured by the user.""" enabled_capabilities: Mapping[TRANSPORT, CAPABILITY] auto_eject_timeout: Optional[int] challenge_response_timeout: Optional[int] device_flags: Optional[DEVICE_FLAG] def get_bytes( self, reboot: bool, cur_lock_code: Optional[bytes] = None, new_lock_code: Optional[bytes] = None, ) -> bytes: buf = b"" if reboot: buf += Tlv(TAG_REBOOT) if cur_lock_code: buf += Tlv(TAG_UNLOCK, cur_lock_code) usb_enabled = self.enabled_capabilities.get(TRANSPORT.USB) if usb_enabled is not None: buf += Tlv(TAG_USB_ENABLED, int2bytes(usb_enabled, 2)) nfc_enabled = self.enabled_capabilities.get(TRANSPORT.NFC) if nfc_enabled is not None: buf += Tlv(TAG_NFC_ENABLED, int2bytes(nfc_enabled, 2)) if self.auto_eject_timeout is not None: buf += Tlv(TAG_AUTO_EJECT_TIMEOUT, int2bytes(self.auto_eject_timeout, 2)) if self.challenge_response_timeout is not None: buf += Tlv(TAG_CHALRESP_TIMEOUT, int2bytes(self.challenge_response_timeout)) if self.device_flags is not None: buf += Tlv(TAG_DEVICE_FLAGS, int2bytes(self.device_flags)) if new_lock_code: buf += Tlv(TAG_CONFIG_LOCK, new_lock_code) if len(buf) > 0xFF: raise NotSupportedError("DeviceConfiguration too large") return int2bytes(len(buf)) + buf @dataclass class DeviceInfo: """Information about a YubiKey readable using the ManagementSession.""" config: DeviceConfig serial: Optional[int] version: Version form_factor: FORM_FACTOR supported_capabilities: Mapping[TRANSPORT, CAPABILITY] is_locked: bool is_fips: bool = False is_sky: bool = False def has_transport(self, transport: TRANSPORT) -> bool: return transport in self.supported_capabilities @classmethod def parse(cls, encoded: bytes, default_version: Version) -> "DeviceInfo": if len(encoded) - 1 != encoded[0]: raise BadResponseError("Invalid length") data = Tlv.parse_dict(encoded[1:]) locked = data.get(TAG_CONFIG_LOCK) == b"\1" serial = bytes2int(data.get(TAG_SERIAL, b"\0")) or None ff_value = bytes2int(data.get(TAG_FORM_FACTOR, b"\0")) form_factor = FORM_FACTOR.from_code(ff_value) fips = bool(ff_value & 0x80) sky = bool(ff_value & 0x40) if TAG_VERSION in data: version = Version.from_bytes(data[TAG_VERSION]) else: version = default_version auto_eject_to = bytes2int(data.get(TAG_AUTO_EJECT_TIMEOUT, b"\0")) chal_resp_to = bytes2int(data.get(TAG_CHALRESP_TIMEOUT, b"\0")) flags = DEVICE_FLAG(bytes2int(data.get(TAG_DEVICE_FLAGS, b"\0"))) supported = {} enabled = {} if version == (4, 2, 4): # Doesn't report correctly supported[TRANSPORT.USB] = CAPABILITY(0x3F) else: supported[TRANSPORT.USB] = CAPABILITY(bytes2int(data[TAG_USB_SUPPORTED])) if TAG_USB_ENABLED in data: # From YK 5.0.0 if not ((4, 0, 0) <= version < (5, 0, 0)): # Broken on YK4 enabled[TRANSPORT.USB] = CAPABILITY(bytes2int(data[TAG_USB_ENABLED])) if TAG_NFC_SUPPORTED in data: # YK with NFC supported[TRANSPORT.NFC] = CAPABILITY(bytes2int(data[TAG_NFC_SUPPORTED])) enabled[TRANSPORT.NFC] = CAPABILITY(bytes2int(data[TAG_NFC_ENABLED])) return cls( DeviceConfig(enabled, auto_eject_to, chal_resp_to, flags), serial, version, form_factor, supported, locked, fips, sky, ) _MODES = [ USB_INTERFACE.OTP, # 0x00 USB_INTERFACE.CCID, # 0x01 USB_INTERFACE.OTP | USB_INTERFACE.CCID, # 0x02 USB_INTERFACE.FIDO, # 0x03 USB_INTERFACE.OTP | USB_INTERFACE.FIDO, # 0x04 USB_INTERFACE.FIDO | USB_INTERFACE.CCID, # 0x05 USB_INTERFACE.OTP | USB_INTERFACE.FIDO | USB_INTERFACE.CCID, # 0x06 ] @dataclass(init=False, repr=False) class Mode: """YubiKey USB Mode configuration for use with YubiKey NEO and 4.""" code: int interfaces: USB_INTERFACE def __init__(self, interfaces: USB_INTERFACE): try: self.code = _MODES.index(interfaces) self.interfaces = USB_INTERFACE(interfaces) except ValueError: raise ValueError("Invalid mode!") def __repr__(self): return "+".join(t.name for t in USB_INTERFACE if t in self.interfaces) @classmethod def from_code(cls, code: int) -> "Mode": code = code & 0b00000111 return cls(_MODES[code]) SLOT_DEVICE_CONFIG = 0x11 SLOT_YK4_CAPABILITIES = 0x13 SLOT_YK4_SET_DEVICE_INFO = 0x15 class _Backend(abc.ABC): version: Version @abc.abstractmethod def close(self) -> None: ... @abc.abstractmethod def set_mode(self, data: bytes) -> None: ... @abc.abstractmethod def read_config(self) -> bytes: ... @abc.abstractmethod def write_config(self, config: bytes) -> None: ... class _ManagementOtpBackend(_Backend): def __init__(self, otp_connection): self.protocol = OtpProtocol(otp_connection) self.version = self.protocol.version if (1, 0, 0) <= self.version < (3, 0, 0): raise ApplicationNotAvailableError() def close(self): self.protocol.close() def set_mode(self, data): empty = self.protocol.read_status()[STATUS_OFFSET_PROG_SEQ] == 0 try: self.protocol.send_and_receive(SLOT_DEVICE_CONFIG, data) except CommandRejectedError: if empty: return # ProgSeq isn't updated by set mode when empty raise def read_config(self): response = self.protocol.send_and_receive(SLOT_YK4_CAPABILITIES) r_len = response[0] if check_crc(response[: r_len + 1 + 2]): return response[: r_len + 1] raise BadResponseError("Invalid checksum") def write_config(self, config): self.protocol.send_and_receive(SLOT_YK4_SET_DEVICE_INFO, config) INS_READ_CONFIG = 0x1D INS_WRITE_CONFIG = 0x1C INS_SET_MODE = 0x16 P1_DEVICE_CONFIG = 0x11 class _ManagementSmartCardBackend(_Backend): def __init__(self, smartcard_connection): self.protocol = SmartCardProtocol(smartcard_connection) select_bytes = self.protocol.select(AID.MANAGEMENT) if select_bytes[-2:] == b"\x90\x00": # YubiKey Edge incorrectly appends SW twice. select_bytes = select_bytes[:-2] select_str = select_bytes.decode() self.version = Version.from_string(select_str) # For YubiKey NEO, we use the OTP application for further commands if self.version[0] == 3: # Workaround to "de-select" on NEO, otherwise it gets stuck. self.protocol.connection.send_and_receive(b"\xa4\x04\x00\x08") self.protocol.select(AID.OTP) def close(self): self.protocol.close() def set_mode(self, data): if self.version[0] == 3: # Using the OTP application self.protocol.send_apdu(0, 0x01, SLOT_DEVICE_CONFIG, 0, data) else: self.protocol.send_apdu(0, INS_SET_MODE, P1_DEVICE_CONFIG, 0, data) def read_config(self): return self.protocol.send_apdu(0, INS_READ_CONFIG, 0, 0) def write_config(self, config): self.protocol.send_apdu(0, INS_WRITE_CONFIG, 0, 0, config) CTAP_VENDOR_FIRST = 0x40 CTAP_YUBIKEY_DEVICE_CONFIG = CTAP_VENDOR_FIRST CTAP_READ_CONFIG = CTAP_VENDOR_FIRST + 2 CTAP_WRITE_CONFIG = CTAP_VENDOR_FIRST + 3 class _ManagementCtapBackend(_Backend): def __init__(self, fido_connection): self.ctap = fido_connection version = fido_connection.device_version if version[0] < 4: # Prior to YK4 this was not firmware version if not ( version[0] == 0 and fido_connection.capabilities & CTAP_CAPABILITY.CBOR ): version = (3, 0, 0) # Guess that it's a NEO self.version = Version(*version) def close(self): self.ctap.close() def set_mode(self, data): self.ctap.call(CTAP_YUBIKEY_DEVICE_CONFIG, data) def read_config(self): return self.ctap.call(CTAP_READ_CONFIG) def write_config(self, config): self.ctap.call(CTAP_WRITE_CONFIG, config) class ManagementSession: def __init__( self, connection: Union[OtpConnection, SmartCardConnection, FidoConnection] ): if isinstance(connection, OtpConnection): self.backend: _Backend = _ManagementOtpBackend(connection) elif isinstance(connection, SmartCardConnection): self.backend = _ManagementSmartCardBackend(connection) elif isinstance(connection, FidoConnection): self.backend = _ManagementCtapBackend(connection) else: raise TypeError("Unsupported connection type") def close(self) -> None: self.backend.close() @property def version(self) -> Version: return self.backend.version def read_device_info(self) -> DeviceInfo: require_version(self.version, (4, 1, 0)) return DeviceInfo.parse(self.backend.read_config(), self.version) def write_device_config( self, config: Optional[DeviceConfig] = None, reboot: bool = False, cur_lock_code: Optional[bytes] = None, new_lock_code: Optional[bytes] = None, ) -> None: require_version(self.version, (5, 0, 0)) if cur_lock_code is not None and len(cur_lock_code) != 16: raise ValueError("Lock code must be 16 bytes") if new_lock_code is not None and len(new_lock_code) != 16: raise ValueError("Lock code must be 16 bytes") config = config or DeviceConfig({}, None, None, None) self.backend.write_config( config.get_bytes(reboot, cur_lock_code, new_lock_code) ) def set_mode( self, mode: Mode, chalresp_timeout: int = 0, auto_eject_timeout: Optional[int] = None, ) -> None: if self.version >= (5, 0, 0): # Translate into DeviceConfig usb_enabled = CAPABILITY(0) if USB_INTERFACE.OTP in mode.interfaces: usb_enabled |= CAPABILITY.OTP if USB_INTERFACE.CCID in mode.interfaces: usb_enabled |= CAPABILITY.OATH | CAPABILITY.PIV | CAPABILITY.OPENPGP if USB_INTERFACE.FIDO in mode.interfaces: usb_enabled |= CAPABILITY.U2F | CAPABILITY.FIDO2 self.write_device_config( DeviceConfig( {TRANSPORT.USB: usb_enabled}, auto_eject_timeout, chalresp_timeout, None, ) ) else: code = mode.code if auto_eject_timeout is not None: if mode.interfaces == USB_INTERFACE.CCID: code |= DEVICE_FLAG.EJECT else: raise ValueError("Touch-eject only applicable for mode: CCID") self.backend.set_mode( # N.B. This is little endian! struct.pack(" "CredentialData": parsed = urlparse(uri.strip()) if parsed.scheme != "otpauth": raise ValueError("Invalid URI scheme") if parsed.hostname is None: raise ValueError("Missing OATH type") oath_type = OATH_TYPE[parsed.hostname.upper()] 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( name=name, oath_type=oath_type, hash_algorithm=HASH_ALGORITHM[params.get("algorithm", "SHA1").upper()], secret=parse_b32_key(params["secret"]), digits=int(params.get("digits", DEFAULT_DIGITS)), period=int(params.get("period", DEFAULT_PERIOD)), counter=int(params.get("counter", DEFAULT_IMF)), issuer=params.get("issuer", issuer), ) def get_id(self) -> bytes: return _format_cred_id(self.issuer, self.name, self.oath_type, self.period) @dataclass class Code: value: str valid_from: int valid_to: int @total_ordering @dataclass(order=False, frozen=True) class Credential: device_id: str id: bytes issuer: Optional[str] name: str oath_type: OATH_TYPE period: int touch_required: Optional[bool] 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 def __eq__(self, other): return ( isinstance(other, type(self)) and self.device_id == other.device_id and self.id == other.id ) def __hash__(self): return hash((self.device_id, self.id)) def _format_cred_id(issuer, name, oath_type, period=DEFAULT_PERIOD): cred_id = "" if oath_type == OATH_TYPE.TOTP and period != DEFAULT_PERIOD: cred_id += "%d/" % period if issuer: cred_id += issuer + ":" cred_id += name return cred_id.encode() def _parse_cred_id(cred_id, oath_type): data = cred_id.decode() if oath_type == OATH_TYPE.TOTP: match = TOTP_ID_PATTERN.match(data) if match: period_str = match.group(2) return ( match.group(4), match.group(5), int(period_str) if period_str else DEFAULT_PERIOD, ) else: return None, data, DEFAULT_PERIOD else: if ":" in data: issuer, data = data.split(":", 1) else: issuer = None return issuer, data, None def _get_device_id(salt): d = hashlib.sha256(salt).digest()[:16] return b64encode(d).replace(b"=", b"").decode() def _hmac_sha1(key, message): return hmac.new(key, message, "sha1").digest() def _derive_key(salt, passphrase): return hashlib.pbkdf2_hmac("sha1", passphrase.encode(), salt, 1000, 16) def _hmac_shorten_key(key, algo): h = hashlib.new(algo.name) if len(key) > h.block_size: h.update(key) key = h.digest() return key def _get_challenge(timestamp, period): time_step = timestamp // period return struct.pack(">q", time_step) def _format_code(credential, timestamp, truncated): if credential.oath_type == OATH_TYPE.TOTP: time_step = timestamp // credential.period valid_from = time_step * credential.period valid_to = (time_step + 1) * credential.period else: # HOTP valid_from = timestamp valid_to = float("Inf") digits = truncated[0] return Code( str((bytes2int(truncated[1:]) & 0x7FFFFFFF) % 10 ** digits).rjust(digits, "0"), valid_from, valid_to, ) class OathSession: def __init__(self, connection: SmartCardConnection): self.protocol = SmartCardProtocol(connection, INS_SEND_REMAINING) self._version, self._salt, self._challenge = _parse_select( self.protocol.select(AID.OATH) ) self._has_key = self._challenge is not None self._device_id = _get_device_id(self._salt) self.protocol.enable_touch_workaround(self._version) @property def version(self) -> Version: return self._version @property def device_id(self) -> str: return self._device_id @property def has_key(self) -> bool: return self._has_key @property def locked(self) -> bool: return self._challenge is not None def reset(self) -> None: self.protocol.send_apdu(0, INS_RESET, 0xDE, 0xAD) _, self._salt, self._challenge = _parse_select(self.protocol.select(AID.OATH)) self._has_key = False self._device_id = _get_device_id(self._salt) def derive_key(self, password: str) -> bytes: return _derive_key(self._salt, password) def validate(self, key: bytes) -> None: response = _hmac_sha1(key, self._challenge) challenge = os.urandom(8) data = Tlv(TAG_RESPONSE, response) + Tlv(TAG_CHALLENGE, challenge) resp = self.protocol.send_apdu(0, INS_VALIDATE, 0, 0, data) verification = _hmac_sha1(key, challenge) if not hmac.compare_digest(Tlv.unpack(TAG_RESPONSE, resp), verification): raise BadResponseError( "Response from validation does not match verification!" ) self._challenge = None def set_key(self, key: bytes) -> None: challenge = os.urandom(8) response = _hmac_sha1(key, challenge) self.protocol.send_apdu( 0, INS_SET_CODE, 0, 0, ( Tlv(TAG_KEY, int2bytes(OATH_TYPE.TOTP | HASH_ALGORITHM.SHA1) + key) + Tlv(TAG_CHALLENGE, challenge) + Tlv(TAG_RESPONSE, response) ), ) self._has_key = True def unset_key(self) -> None: self.protocol.send_apdu(0, INS_SET_CODE, 0, 0, Tlv(TAG_KEY)) self._has_key = False def put_credential( self, credential_data: CredentialData, touch_required: bool = False ) -> Credential: d = credential_data cred_id = d.get_id() secret = _hmac_shorten_key(d.secret, d.hash_algorithm) secret = secret.ljust(HMAC_MINIMUM_KEY_SIZE, b"\0") data = Tlv(TAG_NAME, cred_id) + Tlv( TAG_KEY, struct.pack("BB", TAG_PROPERTY, PROP_REQUIRE_TOUCH) if d.counter > 0: data += Tlv(TAG_IMF, struct.pack(">I", d.counter)) self.protocol.send_apdu(0, INS_PUT, 0, 0, data) return Credential( self.device_id, cred_id, d.issuer, d.name, d.oath_type, d.period, touch_required, ) def rename_credential( self, credential_id: bytes, name: str, issuer: Optional[str] = None ) -> bytes: require_version(self.version, (5, 3, 1)) _, _, period = _parse_cred_id(credential_id, OATH_TYPE.TOTP) new_id = _format_cred_id(issuer, name, OATH_TYPE.TOTP, period) self.protocol.send_apdu( 0, INS_RENAME, 0, 0, Tlv(TAG_NAME, credential_id) + Tlv(TAG_NAME, new_id) ) return new_id def list_credentials(self) -> List[Credential]: creds = [] for tlv in Tlv.parse_list(self.protocol.send_apdu(0, INS_LIST, 0, 0)): data = Tlv.unpack(TAG_NAME_LIST, tlv) oath_type = OATH_TYPE(MASK_TYPE & data[0]) cred_id = data[1:] issuer, name, period = _parse_cred_id(cred_id, oath_type) creds.append( Credential( self.device_id, cred_id, issuer, name, oath_type, period, None ) ) return creds def calculate(self, credential_id: bytes, challenge: bytes) -> bytes: resp = Tlv.unpack( TAG_RESPONSE, self.protocol.send_apdu( 0, INS_CALCULATE, 0, 0, Tlv(TAG_NAME, credential_id) + Tlv(TAG_CHALLENGE, challenge), ), ) return resp[1:] def delete_credential(self, credential_id: bytes) -> None: self.protocol.send_apdu(0, INS_DELETE, 0, 0, Tlv(TAG_NAME, credential_id)) def calculate_all( self, timestamp: Optional[int] = None ) -> Mapping[Credential, Optional[Code]]: timestamp = int(timestamp or time()) challenge = _get_challenge(timestamp, DEFAULT_PERIOD) entries = {} data = Tlv.parse_list( self.protocol.send_apdu( 0, INS_CALCULATE_ALL, 0, 1, Tlv(TAG_CHALLENGE, challenge) ) ) while data: cred_id = Tlv.unpack(TAG_NAME, data.pop(0)) tlv = data.pop(0) resp_tag = tlv.tag oath_type = OATH_TYPE.HOTP if resp_tag == TAG_HOTP else OATH_TYPE.TOTP touch = resp_tag == TAG_TOUCH issuer, name, period = _parse_cred_id(cred_id, oath_type) credential = Credential( self.device_id, cred_id, issuer, name, oath_type, period, touch ) code = None # Will be None for HOTP and touch if resp_tag == TAG_TRUNCATED: # Only TOTP, no-touch here if period == DEFAULT_PERIOD: code = _format_code(credential, timestamp, tlv.value) else: # Non-standard period, recalculate code = self.calculate_code(credential, timestamp) entries[credential] = code return entries def calculate_code( self, credential: Credential, timestamp: Optional[int] = None ) -> Code: if credential.device_id != self.device_id: raise ValueError("Credential does not belong to this YubiKey") timestamp = int(timestamp or time()) if credential.oath_type == OATH_TYPE.TOTP: challenge = _get_challenge(timestamp, credential.period) else: # HOTP challenge = b"" response = Tlv.unpack( TAG_TRUNCATED, self.protocol.send_apdu( 0, INS_CALCULATE, 0, 0x01, # Truncate Tlv(TAG_NAME, credential.id) + Tlv(TAG_CHALLENGE, challenge), ), ) return _format_code(credential, timestamp, response) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631098162.2595024 yubikey-manager-4.0.7/yubikit/piv.py0000755000000000000000000006315000000000000015602 0ustar0000000000000000# Copyright (c) 2020 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 .core import ( require_version as _require_version, int2bytes, bytes2int, Version, Tlv, AID, CommandError, NotSupportedError, BadResponseError, ) from .core.smartcard import ( SmartCardConnection, SmartCardProtocol, ApduError, SW, ApduFormat, ) from cryptography import x509 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.serialization import Encoding, PublicFormat from cryptography.hazmat.primitives.asymmetric import rsa, ec from cryptography.hazmat.primitives.asymmetric.padding import AsymmetricPadding from cryptography.hazmat.backends import default_backend from dataclasses import dataclass from enum import Enum, IntEnum, unique from typing import Optional, Union, Type, cast import logging import os import re logger = logging.getLogger(__name__) @unique class ALGORITHM(str, Enum): EC = "ec" RSA = "rsa" # Don't treat pre 1.0 versions as "developer builds". def require_version(my_version: Version, *args, **kwargs): if my_version <= (0, 1, 4): # Last pre 1.0 release of ykneo-piv my_version = Version(1, 0, 0) _require_version(my_version, *args, **kwargs) @unique class KEY_TYPE(IntEnum): RSA1024 = 0x06 RSA2048 = 0x07 ECCP256 = 0x11 ECCP384 = 0x14 @property def algorithm(self): return ALGORITHM.EC if self.name.startswith("ECC") else ALGORITHM.RSA @property def bit_len(self): match = re.search(r"\d+$", self.name) if match: return int(match.group()) raise ValueError("No bit_len") @classmethod def from_public_key(cls, key): if isinstance(key, rsa.RSAPublicKey): try: return getattr(cls, "RSA%d" % key.key_size) except AttributeError: raise ValueError("Unsupported RSA key size: %d" % key.key_size) pass # Fall through to ValueError 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 ValueError(f"Unsupported EC curve: {curve_name}") raise ValueError(f"Unsupported key type: {type(key).__name__}") @unique class MANAGEMENT_KEY_TYPE(IntEnum): TDES = 0x03 AES128 = 0x08 AES192 = 0x0A AES256 = 0x0C @property def key_len(self): if self.name == "TDES": return 24 # AES return int(self.name[3:]) // 8 @property def challenge_len(self): if self.name == "TDES": return 8 return 16 def _parse_management_key(key_type, management_key): if key_type == MANAGEMENT_KEY_TYPE.TDES: return algorithms.TripleDES(management_key) else: return algorithms.AES(management_key) # The card management slot is special, we don't include it in SLOT below SLOT_CARD_MANAGEMENT = 0x9B @unique class SLOT(IntEnum): AUTHENTICATION = 0x9A 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 OBJECT_ID(IntEnum): CAPABILITY = 0x5FC107 CHUID = 0x5FC102 AUTHENTICATION = 0x5FC105 # cert for 9a key FINGERPRINTS = 0x5FC103 SECURITY = 0x5FC106 FACIAL = 0x5FC108 PRINTED = 0x5FC109 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 ATTESTATION = 0x5FFF01 @classmethod def from_slot(cls, slot): return getattr(cls, SLOT(slot).name) @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 # 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" ) PIN_LEN = 8 # Instruction set INS_VERIFY = 0x20 INS_CHANGE_REFERENCE = 0x24 INS_RESET_RETRY = 0x2C INS_GENERATE_ASYMMETRIC = 0x47 INS_AUTHENTICATE = 0x87 INS_GET_DATA = 0xCB INS_PUT_DATA = 0xDB INS_GET_METADATA = 0xF7 INS_ATTEST = 0xF9 INS_SET_PIN_RETRIES = 0xFA INS_RESET = 0xFB INS_GET_VERSION = 0xFD INS_IMPORT_KEY = 0xFE INS_SET_MGMKEY = 0xFF # Tags for parsing responses and preparing requests TAG_AUTH_WITNESS = 0x80 TAG_AUTH_CHALLENGE = 0x81 TAG_AUTH_RESPONSE = 0x82 TAG_AUTH_EXPONENTIATION = 0x85 TAG_GEN_ALGORITHM = 0x80 TAG_OBJ_DATA = 0x53 TAG_OBJ_ID = 0x5C TAG_CERTIFICATE = 0x70 TAG_CERT_INFO = 0x71 TAG_DYN_AUTH = 0x7C TAG_LRC = 0xFE TAG_PIN_POLICY = 0xAA TAG_TOUCH_POLICY = 0xAB # Metadata tags TAG_METADATA_ALGO = 0x01 TAG_METADATA_POLICY = 0x02 TAG_METADATA_ORIGIN = 0x03 TAG_METADATA_PUBLIC_KEY = 0x04 TAG_METADATA_IS_DEFAULT = 0x05 TAG_METADATA_RETRIES = 0x06 ORIGIN_GENERATED = 1 ORIGIN_IMPORTED = 2 INDEX_PIN_POLICY = 0 INDEX_TOUCH_POLICY = 1 INDEX_RETRIES_TOTAL = 0 INDEX_RETRIES_REMAINING = 1 PIN_P2 = 0x80 PUK_P2 = 0x81 class InvalidPinError(CommandError): def __init__(self, attempts_remaining): super(InvalidPinError, self).__init__( "Invalid PIN/PUK. Remaining attempts: %d" % attempts_remaining ) self.attempts_remaining = attempts_remaining def _pin_bytes(pin): pin = pin.encode() if len(pin) > PIN_LEN: raise ValueError("PIN/PUK must be no longer than 8 bytes") return pin.ljust(PIN_LEN, b"\xff") def _retries_from_sw(version, sw): if sw == SW.AUTH_METHOD_BLOCKED: return 0 if version < (1, 0, 4): if 0x6300 <= sw <= 0x63FF: return sw & 0xFF else: if 0x63C0 <= sw <= 0x63CF: return sw & 0x0F return None @dataclass class PinMetadata: default_value: bool total_attempts: int attempts_remaining: int @dataclass class ManagementKeyMetadata: key_type: MANAGEMENT_KEY_TYPE default_value: bool touch_policy: TOUCH_POLICY @dataclass class SlotMetadata: key_type: KEY_TYPE pin_policy: PIN_POLICY touch_policy: TOUCH_POLICY generated: bool public_key_encoded: bytes @property def public_key(self): return _parse_device_public_key(self.key_type, self.public_key_encoded) def _pad_message(key_type, message, hash_algorithm, padding): if key_type.algorithm == ALGORITHM.EC: h = hashes.Hash(hash_algorithm, default_backend()) h.update(message) hashed = h.finalize() byte_len = key_type.bit_len // 8 if len(hashed) < byte_len: return hashed.rjust(byte_len // 8, b"\0") return hashed[:byte_len] elif key_type.algorithm == ALGORITHM.RSA: # Sign with a dummy key, then encrypt the signature to get the padded message e = 65537 dummy = rsa.generate_private_key(e, key_type.bit_len, default_backend()) signature = dummy.sign(message, padding, hash_algorithm) # Raw (textbook) RSA encrypt n = dummy.public_key().public_numbers().n return int2bytes(pow(bytes2int(signature), e, n), key_type.bit_len // 8) def _unpad_message(padded, padding): e = 65537 dummy = rsa.generate_private_key(e, len(padded) * 8, default_backend()) # Raw (textbook) RSA encrypt n = dummy.public_key().public_numbers().n encrypted = int2bytes(pow(bytes2int(padded), e, n), len(padded)) return dummy.decrypt(encrypted, padding) def check_key_support( version: Version, key_type: KEY_TYPE, pin_policy: PIN_POLICY, touch_policy: TOUCH_POLICY, generate: bool = True, ) -> None: """Check if a key type is supported by a specific YubiKey firmware version. This method will return None if the key (with PIN and touch policies) is supported, or it will raise a NotSupportedError if it is not. """ if version[0] == 0 and version > (0, 1, 3): return # Development build, skip version checks if version < (4, 0, 0): if key_type == KEY_TYPE.ECCP384: raise NotSupportedError("ECCP384 requires YubiKey 4 or later") if touch_policy != TOUCH_POLICY.DEFAULT or pin_policy != PIN_POLICY.DEFAULT: raise NotSupportedError("PIN/Touch policy requires YubiKey 4 or later") if version < (4, 3, 0) and touch_policy == TOUCH_POLICY.CACHED: raise NotSupportedError("Cached touch policy requires YubiKey 4.3 or later") # ROCA if (4, 2, 0) <= version < (4, 3, 5): if generate and key_type.algorithm == ALGORITHM.RSA: raise NotSupportedError("RSA key generation not supported on this YubiKey") # FIPS if (4, 4, 0) <= version < (4, 5, 0): if key_type == KEY_TYPE.RSA1024: raise NotSupportedError("RSA 1024 not supported on YubiKey FIPS") if pin_policy == PIN_POLICY.NEVER: raise NotSupportedError("PIN_POLICY.NEVER not allowed on YubiKey FIPS") def _parse_device_public_key(key_type, encoded): data = Tlv.parse_dict(encoded) if key_type.algorithm == ALGORITHM.RSA: modulus = bytes2int(data[0x81]) exponent = bytes2int(data[0x82]) return rsa.RSAPublicNumbers(exponent, modulus).public_key(default_backend()) else: if key_type == KEY_TYPE.ECCP256: curve: Type[ec.EllipticCurve] = ec.SECP256R1 else: curve = ec.SECP384R1 try: # Added in cryptography 2.5 return ec.EllipticCurvePublicKey.from_encoded_point(curve(), data[0x86]) except AttributeError: return ec.EllipticCurvePublicNumbers.from_encoded_point( curve(), data[0x86] ).public_key(default_backend()) class PivSession: def __init__(self, connection: SmartCardConnection): self.protocol = SmartCardProtocol(connection) self.protocol.select(AID.PIV) self._version = Version.from_bytes( self.protocol.send_apdu(0, INS_GET_VERSION, 0, 0) ) self.protocol.enable_touch_workaround(self.version) if self.version >= (4, 0, 0): self.protocol.apdu_format = ApduFormat.EXTENDED self._current_pin_retries = 3 self._max_pin_retries = 3 @property def version(self) -> Version: return self._version def reset(self) -> None: # Block PIN counter = self.get_pin_attempts() while counter > 0: try: self.verify_pin("") except InvalidPinError as e: counter = e.attempts_remaining # Block PUK counter = 1 while counter > 0: try: self._change_reference(INS_RESET_RETRY, PIN_P2, "", "") except InvalidPinError as e: counter = e.attempts_remaining # Reset self.protocol.send_apdu(0, INS_RESET, 0, 0) self._current_pin_retries = 3 self._max_pin_retries = 3 def authenticate( self, key_type: MANAGEMENT_KEY_TYPE, management_key: bytes ) -> None: key_type = MANAGEMENT_KEY_TYPE(key_type) response = self.protocol.send_apdu( 0, INS_AUTHENTICATE, key_type, SLOT_CARD_MANAGEMENT, Tlv(TAG_DYN_AUTH, Tlv(TAG_AUTH_WITNESS)), ) witness = Tlv.unpack(TAG_AUTH_WITNESS, Tlv.unpack(TAG_DYN_AUTH, response)) challenge = os.urandom(key_type.challenge_len) backend = default_backend() cipher_key = _parse_management_key(key_type, management_key) cipher = Cipher(cipher_key, modes.ECB(), backend) # nosec decryptor = cipher.decryptor() decrypted = decryptor.update(witness) + decryptor.finalize() response = self.protocol.send_apdu( 0, INS_AUTHENTICATE, key_type, SLOT_CARD_MANAGEMENT, Tlv( TAG_DYN_AUTH, Tlv(TAG_AUTH_WITNESS, decrypted) + Tlv(TAG_AUTH_CHALLENGE, challenge), ), ) encrypted = Tlv.unpack(TAG_AUTH_RESPONSE, Tlv.unpack(TAG_DYN_AUTH, response)) encryptor = cipher.encryptor() expected = encryptor.update(challenge) + encryptor.finalize() if not bytes_eq(expected, encrypted): raise BadResponseError("Device response is incorrect") def set_management_key( self, key_type: MANAGEMENT_KEY_TYPE, management_key: bytes, require_touch: bool = False, ) -> None: key_type = MANAGEMENT_KEY_TYPE(key_type) if key_type != MANAGEMENT_KEY_TYPE.TDES: require_version(self.version, (5, 4, 0)) if len(management_key) != key_type.key_len: raise ValueError("Management key must be %d bytes" % key_type.key_len) self.protocol.send_apdu( 0, INS_SET_MGMKEY, 0xFF, 0xFE if require_touch else 0xFF, int2bytes(key_type) + Tlv(SLOT_CARD_MANAGEMENT, management_key), ) def verify_pin(self, pin: str) -> None: try: self.protocol.send_apdu(0, INS_VERIFY, 0, PIN_P2, _pin_bytes(pin)) self._current_pin_retries = self._max_pin_retries except ApduError as e: retries = _retries_from_sw(self.version, e.sw) if retries is None: raise self._current_pin_retries = retries raise InvalidPinError(retries) def get_pin_attempts(self) -> int: try: return self.get_pin_metadata().attempts_remaining except NotSupportedError: try: self.protocol.send_apdu(0, INS_VERIFY, 0, PIN_P2) # Already verified, no way to know true count return self._current_pin_retries except ApduError as e: retries = _retries_from_sw(self.version, e.sw) if retries is None: raise self._current_pin_retries = retries return retries def change_pin(self, old_pin: str, new_pin: str) -> None: self._change_reference(INS_CHANGE_REFERENCE, PIN_P2, old_pin, new_pin) def change_puk(self, old_puk: str, new_puk: str) -> None: self._change_reference(INS_CHANGE_REFERENCE, PUK_P2, old_puk, new_puk) def unblock_pin(self, puk: str, new_pin: str) -> None: self._change_reference(INS_RESET_RETRY, PIN_P2, puk, new_pin) def set_pin_attempts(self, pin_attempts: int, puk_attempts: int) -> None: self.protocol.send_apdu(0, INS_SET_PIN_RETRIES, pin_attempts, puk_attempts) self._max_pin_retries = pin_attempts self._current_pin_retries = pin_attempts def get_pin_metadata(self) -> PinMetadata: return self._get_pin_puk_metadata(PIN_P2) def get_puk_metadata(self) -> PinMetadata: return self._get_pin_puk_metadata(PUK_P2) def get_management_key_metadata(self) -> ManagementKeyMetadata: require_version(self.version, (5, 3, 0)) data = Tlv.parse_dict( self.protocol.send_apdu(0, INS_GET_METADATA, 0, SLOT_CARD_MANAGEMENT) ) policy = data[TAG_METADATA_POLICY] return ManagementKeyMetadata( MANAGEMENT_KEY_TYPE(data.get(TAG_METADATA_ALGO, b"\x03")[0]), data[TAG_METADATA_IS_DEFAULT] != b"\0", TOUCH_POLICY(policy[INDEX_TOUCH_POLICY]), ) def get_slot_metadata(self, slot: SLOT) -> SlotMetadata: require_version(self.version, (5, 3, 0)) data = Tlv.parse_dict(self.protocol.send_apdu(0, INS_GET_METADATA, 0, slot)) policy = data[TAG_METADATA_POLICY] return SlotMetadata( KEY_TYPE(data[TAG_METADATA_ALGO][0]), PIN_POLICY(policy[INDEX_PIN_POLICY]), TOUCH_POLICY(policy[INDEX_TOUCH_POLICY]), data[TAG_METADATA_ORIGIN][0] == ORIGIN_GENERATED, data[TAG_METADATA_PUBLIC_KEY], ) def sign( self, slot: SLOT, key_type: KEY_TYPE, message: bytes, hash_algorithm: hashes.HashAlgorithm, padding: Optional[AsymmetricPadding] = None, ) -> bytes: key_type = KEY_TYPE(key_type) padded = _pad_message(key_type, message, hash_algorithm, padding) return self._use_private_key(slot, key_type, padded, False) def decrypt( self, slot: SLOT, cipher_text: bytes, padding: AsymmetricPadding ) -> bytes: if len(cipher_text) == 1024 // 8: key_type = KEY_TYPE.RSA1024 elif len(cipher_text) == 2048 // 8: key_type = KEY_TYPE.RSA2048 else: raise ValueError("Invalid length of ciphertext") padded = self._use_private_key(slot, key_type, cipher_text, False) return _unpad_message(padded, padding) def calculate_secret( self, slot: SLOT, peer_public_key: ec.EllipticCurvePublicKey ) -> bytes: key_type = KEY_TYPE.from_public_key(peer_public_key) if key_type.algorithm != ALGORITHM.EC: raise ValueError("Unsupported key type") data = peer_public_key.public_bytes( Encoding.X962, PublicFormat.UncompressedPoint ) return self._use_private_key(slot, key_type, data, True) def get_object(self, object_id: int) -> bytes: if object_id == OBJECT_ID.DISCOVERY: expected: int = OBJECT_ID.DISCOVERY else: expected = TAG_OBJ_DATA try: return Tlv.unpack( expected, self.protocol.send_apdu( 0, INS_GET_DATA, 0x3F, 0xFF, Tlv(TAG_OBJ_ID, int2bytes(object_id)), ), ) except ValueError as e: raise BadResponseError("Malformed object data", e) def put_object(self, object_id: int, data: Optional[bytes] = None) -> None: self.protocol.send_apdu( 0, INS_PUT_DATA, 0x3F, 0xFF, Tlv(TAG_OBJ_ID, int2bytes(object_id)) + Tlv(TAG_OBJ_DATA, data or b""), ) def get_certificate(self, slot: SLOT) -> x509.Certificate: try: data = Tlv.parse_dict(self.get_object(OBJECT_ID.from_slot(slot))) except ValueError: raise BadResponseError("Malformed certificate data object") cert_info = data.get(TAG_CERT_INFO) if cert_info and cert_info[0] != 0: raise NotSupportedError("Compressed certificates are not supported") try: return x509.load_der_x509_certificate( data[TAG_CERTIFICATE], default_backend() ) except Exception as e: raise BadResponseError("Invalid certificate", e) def put_certificate(self, slot: SLOT, certificate: x509.Certificate) -> None: cert_data = certificate.public_bytes(Encoding.DER) data = ( Tlv(TAG_CERTIFICATE, cert_data) + Tlv(TAG_CERT_INFO, b"\0") + Tlv(TAG_LRC) ) self.put_object(OBJECT_ID.from_slot(slot), data) def delete_certificate(self, slot: SLOT) -> None: self.put_object(OBJECT_ID.from_slot(slot)) def put_key( self, slot: SLOT, private_key: Union[ rsa.RSAPrivateKeyWithSerialization, ec.EllipticCurvePrivateKeyWithSerialization, ], pin_policy: PIN_POLICY = PIN_POLICY.DEFAULT, touch_policy: TOUCH_POLICY = TOUCH_POLICY.DEFAULT, ) -> None: key_type = KEY_TYPE.from_public_key(private_key.public_key()) check_key_support(self.version, key_type, pin_policy, touch_policy, False) ln = key_type.bit_len // 8 numbers = private_key.private_numbers() if key_type.algorithm == ALGORITHM.RSA: numbers = cast(rsa.RSAPrivateNumbers, numbers) if numbers.public_numbers.e != 65537: raise NotSupportedError("RSA exponent must be 65537") ln //= 2 data = ( Tlv(0x01, int2bytes(numbers.p, ln)) + Tlv(0x02, int2bytes(numbers.q, ln)) + Tlv(0x03, int2bytes(numbers.dmp1, ln)) + Tlv(0x04, int2bytes(numbers.dmq1, ln)) + Tlv(0x05, int2bytes(numbers.iqmp, ln)) ) else: numbers = cast(ec.EllipticCurvePrivateNumbers, numbers) data = Tlv(0x06, int2bytes(numbers.private_value, ln)) if pin_policy: data += Tlv(TAG_PIN_POLICY, int2bytes(pin_policy)) if touch_policy: data += Tlv(TAG_TOUCH_POLICY, int2bytes(touch_policy)) self.protocol.send_apdu(0, INS_IMPORT_KEY, key_type, slot, data) return key_type def generate_key( self, slot: SLOT, key_type: KEY_TYPE, pin_policy: PIN_POLICY = PIN_POLICY.DEFAULT, touch_policy: TOUCH_POLICY = TOUCH_POLICY.DEFAULT, ) -> Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey]: key_type = KEY_TYPE(key_type) check_key_support(self.version, key_type, pin_policy, touch_policy, True) data: bytes = Tlv(TAG_GEN_ALGORITHM, int2bytes(key_type)) if pin_policy: data += Tlv(TAG_PIN_POLICY, int2bytes(pin_policy)) if touch_policy: data += Tlv(TAG_TOUCH_POLICY, int2bytes(touch_policy)) response = self.protocol.send_apdu( 0, INS_GENERATE_ASYMMETRIC, 0, slot, Tlv(0xAC, data) ) return _parse_device_public_key(key_type, Tlv.unpack(0x7F49, response)) def attest_key(self, slot: SLOT) -> x509.Certificate: require_version(self.version, (4, 3, 0)) response = self.protocol.send_apdu(0, INS_ATTEST, slot, 0) return x509.load_der_x509_certificate(response, default_backend()) def _change_reference(self, ins, p2, value1, value2): try: self.protocol.send_apdu( 0, ins, 0, p2, _pin_bytes(value1) + _pin_bytes(value2) ) except ApduError as e: retries = _retries_from_sw(self.version, e.sw) if retries is None: raise if p2 == PIN_P2: self._current_pin_retries = retries raise InvalidPinError(retries) def _get_pin_puk_metadata(self, p2): require_version(self.version, (5, 3, 0)) data = Tlv.parse_dict(self.protocol.send_apdu(0, INS_GET_METADATA, 0, p2)) attempts = data[TAG_METADATA_RETRIES] return PinMetadata( data[TAG_METADATA_IS_DEFAULT] != b"\0", attempts[INDEX_RETRIES_TOTAL], attempts[INDEX_RETRIES_REMAINING], ) def _use_private_key(self, slot, key_type, message, exponentiation): try: response = self.protocol.send_apdu( 0, INS_AUTHENTICATE, key_type, slot, Tlv( TAG_DYN_AUTH, Tlv(TAG_AUTH_RESPONSE) + Tlv( TAG_AUTH_EXPONENTIATION if exponentiation else TAG_AUTH_CHALLENGE, message, ), ), ) return Tlv.unpack( TAG_AUTH_RESPONSE, Tlv.unpack( TAG_DYN_AUTH, response, ), ) except ApduError as e: if e.sw == SW.INCORRECT_PARAMETERS: raise e # TODO: Different error, No key? raise ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631098162.2604997 yubikey-manager-4.0.7/yubikit/yubiotp.py0000644000000000000000000006604400000000000016501 0ustar0000000000000000# Copyright (c) 2020 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 .core import ( AID, TRANSPORT, Version, bytes2int, require_version, NotSupportedError, BadResponseError, ) from .core import ApplicationNotAvailableError from .core.otp import ( check_crc, calculate_crc, OtpConnection, OtpProtocol, CommandRejectedError, ) from .core.smartcard import SmartCardConnection, SmartCardProtocol import abc import struct from hashlib import sha1 from threading import Event from enum import unique, IntEnum, IntFlag from typing import TypeVar, Optional, Union, Callable T = TypeVar("T") @unique class SLOT(IntEnum): ONE = 1 TWO = 2 @staticmethod def map(slot: "SLOT", one: T, two: T) -> T: if slot == 1: return one elif slot == 2: return two raise ValueError("Invalid slot (must be 1 or 2)") @unique class CONFIG_SLOT(IntEnum): CONFIG_1 = 1 # First (default / V1) configuration NAV = 2 # V1 only CONFIG_2 = 3 # Second (V2) configuration UPDATE_1 = 4 # Update slot 1 UPDATE_2 = 5 # Update slot 2 SWAP = 6 # Swap slot 1 and 2 NDEF_1 = 8 # Write NDEF record NDEF_2 = 9 # Write NDEF record for slot 2 DEVICE_SERIAL = 0x10 # Device serial number DEVICE_CONFIG = 0x11 # Write device configuration record SCAN_MAP = 0x12 # Write scancode map YK4_CAPABILITIES = 0x13 # Read YK4 capabilities (device info) list YK4_SET_DEVICE_INFO = 0x15 # Write device info CHAL_OTP_1 = 0x20 # Write 6 byte challenge to slot 1, get Yubico OTP response CHAL_OTP_2 = 0x28 # Write 6 byte challenge to slot 2, get Yubico OTP response CHAL_HMAC_1 = 0x30 # Write 64 byte challenge to slot 1, get HMAC-SHA1 response CHAL_HMAC_2 = 0x38 # Write 64 byte challenge to slot 2, get HMAC-SHA1 response class TKTFLAG(IntFlag): # Yubikey 1 and above TAB_FIRST = 0x01 # Send TAB before first part APPEND_TAB1 = 0x02 # Send TAB after first part APPEND_TAB2 = 0x04 # Send TAB after second part APPEND_DELAY1 = 0x08 # Add 0.5s delay after first part APPEND_DELAY2 = 0x10 # Add 0.5s delay after second part APPEND_CR = 0x20 # Append CR as final character # Yubikey 2 and above PROTECT_CFG2 = 0x80 # Block update of config 2 unless config 2 is configured and has this bit set # Yubikey 2.1 and above OATH_HOTP = 0x40 # OATH HOTP mode # Yubikey 2.2 and above CHAL_RESP = 0x40 # Challenge-response enabled (both must be set) class CFGFLAG(IntFlag): # Yubikey 1 and above SEND_REF = 0x01 # Send reference string (0..F) before data PACING_10MS = 0x04 # Add 10ms intra-key pacing PACING_20MS = 0x08 # Add 20ms intra-key pacing STATIC_TICKET = 0x20 # Static ticket generation # Yubikey 1 only TICKET_FIRST = 0x02 # Send ticket first (default is fixed part) ALLOW_HIDTRIG = 0x10 # Allow trigger through HID/keyboard # Yubikey 2 and above SHORT_TICKET = 0x02 # Send truncated ticket (half length) STRONG_PW1 = 0x10 # Strong password policy flag #1 (mixed case) STRONG_PW2 = 0x40 # Strong password policy flag #2 (subtitute 0..7 to digits) MAN_UPDATE = 0x80 # Allow manual (local) update of static OTP # Yubikey 2.1 and above OATH_HOTP8 = 0x02 # Generate 8 digits HOTP rather than 6 digits OATH_FIXED_MODHEX1 = 0x10 # First byte in fixed part sent as modhex OATH_FIXED_MODHEX2 = 0x40 # First two bytes in fixed part sent as modhex OATH_FIXED_MODHEX = 0x50 # Fixed part sent as modhex OATH_FIXED_MASK = 0x50 # Mask to get out fixed flags # Yubikey 2.2 and above CHAL_YUBICO = 0x20 # Challenge-response enabled - Yubico OTP mode CHAL_HMAC = 0x22 # Challenge-response enabled - HMAC-SHA1 HMAC_LT64 = 0x04 # Set when HMAC message is less than 64 bytes CHAL_BTN_TRIG = 0x08 # Challenge-response operation requires button press class EXTFLAG(IntFlag): SERIAL_BTN_VISIBLE = 0x01 # Serial number visible at startup (button press) SERIAL_USB_VISIBLE = 0x02 # Serial number visible in USB iSerial field SERIAL_API_VISIBLE = 0x04 # Serial number visible via API call # V2.3 flags only USE_NUMERIC_KEYPAD = 0x08 # Use numeric keypad for digits FAST_TRIG = 0x10 # Use fast trig if only cfg1 set ALLOW_UPDATE = 0x20 # Allow update of existing configuration (selected flags + access code) DORMANT = 0x40 # Dormant config (woken up, flag removed, requires update flag) # V2.4/3.1 flags only LED_INV = 0x80 # LED idle state is off rather than on # Flags valid for update TKTFLAG_UPDATE_MASK = ( TKTFLAG.TAB_FIRST | TKTFLAG.APPEND_TAB1 | TKTFLAG.APPEND_TAB2 | TKTFLAG.APPEND_DELAY1 | TKTFLAG.APPEND_DELAY2 | TKTFLAG.APPEND_CR ) CFGFLAG_UPDATE_MASK = CFGFLAG.PACING_10MS | CFGFLAG.PACING_20MS EXTFLAG_UPDATE_MASK = ( EXTFLAG.SERIAL_BTN_VISIBLE | EXTFLAG.SERIAL_USB_VISIBLE | EXTFLAG.SERIAL_API_VISIBLE | EXTFLAG.USE_NUMERIC_KEYPAD | EXTFLAG.FAST_TRIG | EXTFLAG.ALLOW_UPDATE | EXTFLAG.DORMANT | EXTFLAG.LED_INV ) # Data sizes FIXED_SIZE = 16 UID_SIZE = 6 KEY_SIZE = 16 ACC_CODE_SIZE = 6 CONFIG_SIZE = 52 NDEF_DATA_SIZE = 54 HMAC_KEY_SIZE = 20 HMAC_CHALLENGE_SIZE = 64 HMAC_RESPONSE_SIZE = 20 SCAN_CODES_SIZE = FIXED_SIZE + UID_SIZE + KEY_SIZE SHA1_BLOCK_SIZE = 64 DEFAULT_NDEF_URI = "https://my.yubico.com/yk/#" NDEF_URL_PREFIXES = ( "http://www.", "https://www.", "http://", "https://", "tel:", "mailto:", "ftp://anonymous:anonymous@", "ftp://ftp.", "ftps://", "sftp://", "smb://", "nfs://", "ftp://", "dav://", "news:", "telnet://", "imap:", "rtsp://", "urn:", "pop:", "sip:", "sips:", "tftp:", "btspp://", "btl2cap://", "btgoep://", "tcpobex://", "irdaobex://", "file://", "urn:epc:id:", "urn:epc:tag:", "urn:epc:pat:", "urn:epc:raw:", "urn:epc:", "urn:nfc:", ) def _build_config(fixed, uid, key, ext, tkt, cfg, acc_code=None): buf = ( fixed.ljust(FIXED_SIZE, b"\0") + uid + key + (acc_code or b"\0" * ACC_CODE_SIZE) + struct.pack(">BBBB", len(fixed), ext, tkt, cfg) + b"\0\0" # RFU ) return buf + struct.pack(" NDEF_DATA_SIZE: raise ValueError("URI payload too large") return struct.pack(" bytes: if len(key) > SHA1_BLOCK_SIZE: key = sha1(key).digest() # nosec elif len(key) > HMAC_KEY_SIZE: raise NotSupportedError(f"Key lengths > {HMAC_KEY_SIZE} bytes not supported") return key Cfg = TypeVar("Cfg", bound="SlotConfiguration") class SlotConfiguration: def __init__(self): self._fixed = b"" self._uid = b"\0" * UID_SIZE self._key = b"\0" * KEY_SIZE self._flags = {} self._update_flags(EXTFLAG.SERIAL_API_VISIBLE, True) self._update_flags(EXTFLAG.ALLOW_UPDATE, True) def _update_flags(self, flag: IntFlag, value: bool) -> None: flag_key = type(flag) flags = self._flags.get(flag_key, 0) self._flags[flag_key] = flags | flag if value else flags & ~flag def is_supported_by(self, version: Version) -> bool: return True def get_config(self, acc_code: Optional[bytes] = None) -> bytes: return _build_config( self._fixed, self._uid, self._key, self._flags.get(EXTFLAG, 0), self._flags.get(TKTFLAG, 0), self._flags.get(CFGFLAG, 0), acc_code, ) def serial_api_visible(self: Cfg, value: bool) -> Cfg: self._update_flags(EXTFLAG.SERIAL_API_VISIBLE, value) return self def serial_usb_visible(self: Cfg, value: bool) -> Cfg: self._update_flags(EXTFLAG.SERIAL_USB_VISIBLE, value) return self def allow_update(self: Cfg, value: bool) -> Cfg: self._update_flags(EXTFLAG.ALLOW_UPDATE, value) return self def dormant(self: Cfg, value: bool) -> Cfg: self._update_flags(EXTFLAG.DORMANT, value) return self def invert_led(self: Cfg, value: bool) -> Cfg: self._update_flags(EXTFLAG.LED_INV, value) return self def protect_slot2(self: Cfg, value: bool) -> Cfg: self._update_flags(TKTFLAG.PROTECT_CFG2, value) return self class HmacSha1SlotConfiguration(SlotConfiguration): def __init__(self, key: bytes): super(HmacSha1SlotConfiguration, self).__init__() key = _shorten_hmac_key(key) # Key is packed into key and uid self._key = key[:KEY_SIZE].ljust(KEY_SIZE, b"\0") self._uid = key[KEY_SIZE:].ljust(UID_SIZE, b"\0") self._update_flags(TKTFLAG.CHAL_RESP, True) self._update_flags(CFGFLAG.CHAL_HMAC, True) self._update_flags(CFGFLAG.HMAC_LT64, True) def is_supported_by(self, version): return version >= (2, 2, 0) or version[0] == 0 def require_touch(self: Cfg, value: bool) -> Cfg: self._update_flags(CFGFLAG.CHAL_BTN_TRIG, value) return self def lt64(self: Cfg, value: bool) -> Cfg: self._update_flags(CFGFLAG.HMAC_LT64, value) return self class KeyboardSlotConfiguration(SlotConfiguration): def __init__(self): super(KeyboardSlotConfiguration, self).__init__() self._update_flags(TKTFLAG.APPEND_CR, True) self._update_flags(EXTFLAG.FAST_TRIG, True) def append_cr(self: Cfg, value: bool) -> Cfg: self._update_flags(TKTFLAG.APPEND_CR, value) return self def fast_trigger(self: Cfg, value: bool) -> Cfg: self._update_flags(EXTFLAG.FAST_TRIG, value) return self def pacing(self: Cfg, pacing_10ms: bool = False, pacing_20ms: bool = False) -> Cfg: self._update_flags(CFGFLAG.PACING_10MS, pacing_10ms) self._update_flags(CFGFLAG.PACING_20MS, pacing_20ms) return self def use_numeric(self: Cfg, value: bool) -> Cfg: self._update_flags(EXTFLAG.USE_NUMERIC_KEYPAD, value) return self class HotpSlotConfiguration(KeyboardSlotConfiguration): def __init__(self, key: bytes): super(HotpSlotConfiguration, self).__init__() key = _shorten_hmac_key(key) # Key is packed into key and uid self._key = key[:KEY_SIZE].ljust(KEY_SIZE, b"\0") self._uid = key[KEY_SIZE:].ljust(UID_SIZE, b"\0") self._update_flags(TKTFLAG.OATH_HOTP, True) self._update_flags(CFGFLAG.OATH_FIXED_MODHEX2, True) def is_supported_by(self, version): return version >= (2, 2, 0) or version[0] == 0 def digits8(self: Cfg, value: bool) -> Cfg: self._update_flags(CFGFLAG.OATH_HOTP8, value) return self def token_id( self: Cfg, token_id: bytes, fixed_modhex1: bool = False, fixed_modhex2: bool = True, ) -> Cfg: if len(token_id) > FIXED_SIZE: raise ValueError(f"token_id must be <= {FIXED_SIZE} bytes") self._fixed = token_id self._update_flags(CFGFLAG.OATH_FIXED_MODHEX1, fixed_modhex1) self._update_flags(CFGFLAG.OATH_FIXED_MODHEX2, fixed_modhex2) return self def imf(self: Cfg, imf: int) -> Cfg: if not (imf % 16 == 0 and 0 <= imf <= 0xFFFF0): raise ValueError( f"imf should be between {0} and {1048560}, evenly dividable by 16" ) self._uid = self._uid[:4] + struct.pack(">H", imf >> 4) return self class StaticPasswordSlotConfiguration(KeyboardSlotConfiguration): def __init__(self, scan_codes: bytes): super(StaticPasswordSlotConfiguration, self).__init__() if len(scan_codes) > SCAN_CODES_SIZE: raise NotSupportedError("Password is too long") # Scan codes are packed into fixed, uid, and key scan_codes = scan_codes.ljust(SCAN_CODES_SIZE, b"\0") self._fixed = scan_codes[:FIXED_SIZE] self._uid = scan_codes[FIXED_SIZE : FIXED_SIZE + UID_SIZE] self._key = scan_codes[FIXED_SIZE + UID_SIZE :] self._update_flags(CFGFLAG.SHORT_TICKET, True) def is_supported_by(self, version): return version >= (2, 2, 0) or version[0] == 0 class YubiOtpSlotConfiguration(KeyboardSlotConfiguration): def __init__(self, fixed: bytes, uid: bytes, key: bytes): super(YubiOtpSlotConfiguration, self).__init__() if len(fixed) > FIXED_SIZE: raise ValueError(f"fixed must be <= {FIXED_SIZE} bytes") if len(uid) != UID_SIZE: raise ValueError(f"uid must be {UID_SIZE} bytes") if len(key) != KEY_SIZE: raise ValueError(f"key must be {KEY_SIZE} bytes") self._fixed = fixed self._uid = uid self._key = key def tabs( self: Cfg, before: bool = False, after_first: bool = False, after_second: bool = False, ) -> Cfg: self._update_flags(TKTFLAG.TAB_FIRST, before) self._update_flags(TKTFLAG.APPEND_TAB1, after_first) self._update_flags(TKTFLAG.APPEND_TAB2, after_second) return self def delay(self: Cfg, after_first: bool = False, after_second: bool = False) -> Cfg: self._update_flags(TKTFLAG.APPEND_DELAY1, after_first) self._update_flags(TKTFLAG.APPEND_DELAY2, after_second) return self def send_reference(self: Cfg, value: bool) -> Cfg: self._update_flags(CFGFLAG.SEND_REF, value) return self class StaticTicketSlotConfiguration(KeyboardSlotConfiguration): def __init__(self, fixed: bytes, uid: bytes, key: bytes): super(StaticTicketSlotConfiguration, self).__init__() if len(fixed) > FIXED_SIZE: raise ValueError(f"fixed must be <= {FIXED_SIZE} bytes") if len(uid) != UID_SIZE: raise ValueError(f"uid must be {UID_SIZE} bytes") if len(key) != KEY_SIZE: raise ValueError(f"key must be {KEY_SIZE} bytes") self._fixed = fixed self._uid = uid self._key = key self._update_flags(CFGFLAG.STATIC_TICKET, True) def short_ticket(self: Cfg, value: bool) -> Cfg: self._update_flags(CFGFLAG.SHORT_TICKET, value) return self def strong_password( self: Cfg, upper_case: bool = False, digit: bool = False, special: bool = False ) -> Cfg: self._update_flags(CFGFLAG.STRONG_PW1, upper_case) self._update_flags(CFGFLAG.STRONG_PW2, digit or special) self._update_flags(CFGFLAG.SEND_REF, special) return self def manual_update(self: Cfg, value: bool) -> Cfg: self._update_flags(CFGFLAG.MAN_UPDATE, value) return self class UpdateConfiguration(KeyboardSlotConfiguration): def __init__(self): super(UpdateConfiguration, self).__init__() self._fixed = b"\0" * FIXED_SIZE self._uid = b"\0" * UID_SIZE self._key = b"\0" * KEY_SIZE def is_supported_by(self, version): return version >= (2, 2, 0) or version[0] == 0 def _update_flags(self, flag, value): # NB: All EXT flags are allowed if isinstance(flag, TKTFLAG): if not TKTFLAG_UPDATE_MASK & flag: raise ValueError("Unsupported TKT flag for update") elif isinstance(flag, CFGFLAG): if not CFGFLAG_UPDATE_MASK & flag: raise ValueError("Unsupported CFG flag for update") super(UpdateConfiguration, self)._update_flags(flag, value) def protect_slot2(self: Cfg, value): raise ValueError("protect_slot2 cannot be applied to UpdateConfiguration") def tabs( self: Cfg, before: bool = False, after_first: bool = False, after_second: bool = False, ) -> Cfg: self._update_flags(TKTFLAG.TAB_FIRST, before) self._update_flags(TKTFLAG.APPEND_TAB1, after_first) self._update_flags(TKTFLAG.APPEND_TAB2, after_second) return self def delay(self: Cfg, after_first: bool = False, after_second: bool = False) -> Cfg: self._update_flags(TKTFLAG.APPEND_DELAY1, after_first) self._update_flags(TKTFLAG.APPEND_DELAY2, after_second) return self class ConfigState: """The confgiuration state of the YubiOTP application.""" def __init__(self, version: Version, touch_level: int): self.version = version self.flags = sum(CFGSTATE) & touch_level def is_configured(self, slot: SLOT) -> bool: """Checks of a slot is programmed, or empty""" require_version(self.version, (2, 1, 0)) return self.flags & (CFGSTATE.SLOT1_VALID, CFGSTATE.SLOT2_VALID)[slot - 1] != 0 def is_touch_triggered(self, slot: SLOT) -> bool: """Checks if a (programmed) state is triggered by touch (not challenge-response) Requires YubiKey 3 or later. """ require_version(self.version, (3, 0, 0)) return self.flags & (CFGSTATE.SLOT1_TOUCH, CFGSTATE.SLOT2_TOUCH)[slot - 1] != 0 def is_led_inverted(self) -> bool: """Checks if the LED behavior is inverted.""" return self.flags & CFGSTATE.LED_INV != 0 def __repr__(self): return "ConfigState(configured: %s, touch_triggered: %s, led_inverted: %s)" % ( (self.is_configured(SLOT.ONE), self.is_configured(SLOT.TWO)), (self.is_touch_triggered(SLOT.ONE), self.is_touch_triggered(SLOT.TWO)) if self.version[0] >= 3 else None, self.is_led_inverted(), ) class _Backend(abc.ABC): version: Version @abc.abstractmethod def close(self) -> None: ... @abc.abstractmethod def write_update(self, slot: CONFIG_SLOT, data: bytes) -> bytes: ... @abc.abstractmethod def send_and_receive( self, slot: CONFIG_SLOT, data: bytes, expected_len: int, event: Optional[Event] = None, on_keepalive: Optional[Callable[[int], None]] = None, ) -> bytes: ... class _YubiOtpOtpBackend(_Backend): def __init__(self, protocol): self.protocol = protocol def close(self): self.protocol.close() def write_update(self, slot, data): return self.protocol.send_and_receive(slot, data) def send_and_receive(self, slot, data, expected_len, event=None, on_keepalive=None): response = self.protocol.send_and_receive(slot, data, event, on_keepalive) if check_crc(response[: expected_len + 2]): return response[:expected_len] raise BadResponseError("Invalid CRC") INS_CONFIG = 0x01 class _YubiOtpSmartCardBackend(_Backend): def __init__(self, protocol, version, prog_seq): self.protocol = protocol self._version = version self._prog_seq = prog_seq def close(self): self.protocol.close() def write_update(self, slot, data): status = self.protocol.send_apdu(0, INS_CONFIG, slot, 0, data) prev_prog_seq, self._prog_seq = self._prog_seq, status[3] if self._prog_seq == prev_prog_seq + 1: return status if self._prog_seq == 0 and prev_prog_seq > 0: version = Version.from_bytes(status[:3]) if (4, 0) <= version < (5, 5): # Programming state does not update return status if status[4] & 0x1F == 0: return status raise CommandRejectedError("Not updated") def send_and_receive(self, slot, data, expected_len, event=None, on_keepalive=None): response = self.protocol.send_apdu(0, INS_CONFIG, slot, 0, data) if expected_len == len(response): return response raise BadResponseError("Unexpected response length") class YubiOtpSession: def __init__(self, connection: Union[OtpConnection, SmartCardConnection]): if isinstance(connection, OtpConnection): otp_protocol = OtpProtocol(connection) self._status = otp_protocol.read_status() self._version = otp_protocol.version self.backend: _Backend = _YubiOtpOtpBackend(otp_protocol) elif isinstance(connection, SmartCardConnection): card_protocol = SmartCardProtocol(connection) mgmt_version = None if connection.transport == TRANSPORT.NFC: # This version is more reliable over NFC try: card_protocol.select(AID.MANAGEMENT) select_str = card_protocol.select(AID.MANAGEMENT).decode() mgmt_version = Version.from_string(select_str) except ApplicationNotAvailableError: pass # Not available (probably NEO), get version from status self._status = card_protocol.select(AID.OTP) otp_version = Version.from_bytes(self._status[:3]) if mgmt_version and mgmt_version[0] == 3: # NEO reports the highest of these two self._version = max(mgmt_version, otp_version) else: self._version = mgmt_version or otp_version card_protocol.enable_touch_workaround(self._version) self.backend = _YubiOtpSmartCardBackend( card_protocol, self._version, self._status[3] ) else: raise TypeError("Unsupported connection type") def close(self) -> None: self.backend.close() @property def version(self) -> Version: return self._version def get_serial(self) -> int: return bytes2int( self.backend.send_and_receive(CONFIG_SLOT.DEVICE_SERIAL, b"", 4) ) def get_config_state(self) -> ConfigState: return ConfigState(self.version, struct.unpack(" None: if not configuration.is_supported_by(self.version): raise NotSupportedError( "This configuration is not supported on this YubiKey version" ) self._write_config( SLOT.map(slot, CONFIG_SLOT.CONFIG_1, CONFIG_SLOT.CONFIG_2), configuration.get_config(acc_code), cur_acc_code, ) def update_configuration( self, slot: SLOT, configuration: SlotConfiguration, acc_code: Optional[bytes] = None, cur_acc_code: Optional[bytes] = None, ) -> None: if not configuration.is_supported_by(self.version): raise NotSupportedError( "This configuration is not supported on this YubiKey version" ) if acc_code != cur_acc_code and (4, 3, 2) <= self.version < (4, 3, 6): raise NotSupportedError( "The access code cannot be updated on this YubiKey. " "Instead, delete the slot and configure it anew." ) self._write_config( SLOT.map(slot, CONFIG_SLOT.UPDATE_1, CONFIG_SLOT.UPDATE_2), configuration.get_config(acc_code), cur_acc_code, ) def swap_slots(self) -> None: self._write_config(CONFIG_SLOT.SWAP, b"", None) def delete_slot(self, slot: SLOT, cur_acc_code: Optional[bytes] = None) -> None: self._write_config( SLOT.map(slot, CONFIG_SLOT.CONFIG_1, CONFIG_SLOT.CONFIG_2), b"\0" * CONFIG_SIZE, cur_acc_code, ) def set_scan_map( self, scan_map: bytes, cur_acc_code: Optional[bytes] = None ) -> None: self._write_config(CONFIG_SLOT.SCAN_MAP, scan_map, cur_acc_code) def set_ndef_configuration( self, slot: SLOT, uri: Optional[str] = None, cur_acc_code: Optional[bytes] = None, ) -> None: self._write_config( SLOT.map(slot, CONFIG_SLOT.NDEF_1, CONFIG_SLOT.NDEF_2), _build_ndef_config(uri), cur_acc_code, ) def calculate_hmac_sha1( self, slot: SLOT, challenge: bytes, event: Optional[Event] = None, on_keepalive: Optional[Callable[[int], None]] = None, ) -> bytes: require_version(self.version, (2, 2, 0)) # Pad challenge with byte different from last challenge = challenge.ljust( HMAC_CHALLENGE_SIZE, b"\1" if challenge.endswith(b"\0") else b"\0" ) return self.backend.send_and_receive( SLOT.map(slot, CONFIG_SLOT.CHAL_HMAC_1, CONFIG_SLOT.CHAL_HMAC_2), challenge, HMAC_RESPONSE_SIZE, event, on_keepalive, ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631098193.5987191 yubikey-manager-4.0.7/setup.py0000644000000000000000000000210600000000000014453 0ustar0000000000000000# -*- coding: utf-8 -*- from setuptools import setup packages = \ ['ykman', 'ykman.cli', 'ykman.hid', 'ykman.pcsc', 'ykman.scancodes', 'yubikit', 'yubikit.core'] package_data = \ {'': ['*']} install_requires = \ ['click>=6.0,<9.0', 'cryptography>=2.1,<4.0', 'fido2>=0.9,<1.0', 'pyscard>=1.9,<3.0'] extras_require = \ {':python_version < "3.7"': ['dataclasses>=0.8,<0.9'], ':sys_platform == "win32"': ['pywin32>=223']} entry_points = \ {'console_scripts': ['ykman = ykman.cli.__main__:main']} setup_kwargs = { 'name': 'yubikey-manager', 'version': '4.0.7', 'description': 'Tool for managing your YubiKey configuration.', 'long_description': None, 'author': 'Dain Nilsson', 'author_email': 'dain@yubico.com', 'maintainer': None, 'maintainer_email': None, 'url': 'https://github.com/Yubico/yubikey-manager', 'packages': packages, 'package_data': package_data, 'install_requires': install_requires, 'extras_require': extras_require, 'entry_points': entry_points, 'python_requires': '>=3.6,<4.0', } setup(**setup_kwargs) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631098193.5989997 yubikey-manager-4.0.7/PKG-INFO0000644000000000000000000000216700000000000014045 0ustar0000000000000000Metadata-Version: 2.1 Name: yubikey-manager Version: 4.0.7 Summary: Tool for managing your YubiKey configuration. Home-page: https://github.com/Yubico/yubikey-manager License: BSD Keywords: yubikey,yubiotp,piv,fido Author: Dain Nilsson Author-email: dain@yubico.com Requires-Python: >=3.6,<4.0 Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: End Users/Desktop Classifier: License :: Other/Proprietary License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Security :: Cryptography Classifier: Topic :: Utilities Requires-Dist: click (>=6.0,<9.0) Requires-Dist: cryptography (>=2.1,<4.0) Requires-Dist: dataclasses (>=0.8,<0.9); python_version < "3.7" Requires-Dist: fido2 (>=0.9,<1.0) Requires-Dist: pyOpenSSL (>=0.15.1) Requires-Dist: pyscard (>=1.9,<3.0) Requires-Dist: pywin32 (>=223); sys_platform == "win32" Project-URL: Repository, https://github.com/Yubico/yubikey-manager