[Pkg-auth-maintainers] Bug#911921: yubikey-manager: diff for NMU version 2.0.0-0.1
Afif Elghraoui
afif at debian.org
Sun Feb 3 03:27:33 GMT 2019
Control: tags 911921 + patch
Control: tags 911921 + pending
Dear maintainer,
I've prepared an NMU for yubikey-manager (versioned as 2.0.0-0.1) and
uploaded it to DELAYED/7. Please feel free to tell me if I
should delay it longer.
My changes are also pushed to my fork on salsa:
https://salsa.debian.org/afif/yubikey-manager
regards
Afif
diff -Nru yubikey-manager-0.7.1/debian/changelog yubikey-manager-2.0.0/debian/changelog
--- yubikey-manager-0.7.1/debian/changelog 2018-08-02 12:12:42.000000000 -0400
+++ yubikey-manager-2.0.0/debian/changelog 2019-02-02 22:12:48.000000000 -0500
@@ -1,3 +1,13 @@
+yubikey-manager (2.0.0-0.1) unstable; urgency=medium
+
+ * Non-maintainer upload.
+ * New upstream version (Closes: #911921)
+ * d/control: add VCS URLs
+ * Add missing dependency on python3-pkg-resources for yubikey-manager
+ * enable build-time test suite
+
+ -- Afif Elghraoui <afif at debian.org> Sat, 02 Feb 2019 22:12:48 -0500
+
yubikey-manager (0.7.1-1) unstable; urgency=low
* Initial package (upstream release 2018-07-09)
diff -Nru yubikey-manager-0.7.1/debian/control yubikey-manager-2.0.0/debian/control
--- yubikey-manager-0.7.1/debian/control 2018-08-02 12:12:42.000000000 -0400
+++ yubikey-manager-2.0.0/debian/control 2019-02-02 22:12:36.000000000 -0500
@@ -16,6 +16,8 @@
python3-usb,
python3-fido2
Homepage: https://developers.yubico.com/yubikey-manager/
+Vcs-Git: https://salsa.debian.org/auth-team/yubikey-manager.git
+Vcs-Browser: https://salsa.debian.org/auth-team/yubikey-manager
Package: python3-yubikey-manager
Architecture: all
@@ -37,6 +39,7 @@
Depends: ${misc:Depends},
${python3:Depends},
python3-click,
+ python3-pkg-resources,
python3-yubikey-manager (= ${binary:Version}),
pcscd,
Description: Python library and command line tool for configuring a YubiKey
diff -Nru yubikey-manager-0.7.1/debian/rules yubikey-manager-2.0.0/debian/rules
--- yubikey-manager-0.7.1/debian/rules 2018-08-01 02:00:16.000000000 -0400
+++ yubikey-manager-2.0.0/debian/rules 2019-02-02 22:06:55.000000000 -0500
@@ -1,5 +1,4 @@
#!/usr/bin/make -f
-export PYBUILD_DISABLE=test
%:
dh $@ --with python3 --buildsystem=pybuild
diff -Nru yubikey-manager-0.7.1/doc/development.adoc yubikey-manager-2.0.0/doc/development.adoc
--- yubikey-manager-0.7.1/doc/development.adoc 2018-07-02 02:27:29.000000000 -0400
+++ yubikey-manager-2.0.0/doc/development.adoc 2018-11-08 06:33:42.000000000 -0500
@@ -1,29 +1,47 @@
-== Setting up a development environment on Ubuntu 16.04 (Xenial)
+== Working with the code
-Install development dependencies:
+=== Install dependencies
- $ sudo apt-get install python-pip python-pyscard libykpers-1-1 libu2f-host0
+It's assumed a Python environment with pip is installed.
-Setup the repository:
+==== Windows
+Make sure the http://www.swig.org/[swig] executable is in your PATH. Add http://libusb.info/[libusb]
+and https://developers.yubico.com/yubikey-personalization/[ykpers] DLLs to the root of the repository.
+
+==== macOS
+
+ $ brew install swig ykpers libusb
+
+==== Linux (Debian-based distributions)
+
+ $ sudo apt install swig libykpers-1-1 libu2f-udev pcscd libpcsclite-dev
+
+=== Install yubikey-manager from source
+
+Clone the repository:
$ git clone https://github.com/Yubico/yubikey-manager.git
$ cd yubikey-manager
-Install in editable mode with pip (from root of repository):
+Install in editable mode with pip:
- $ sudo pip install -e .
+ $ pip install -e .
-Run the app:
+Show available commands:
$ ykman --help
-To update once installed, just make sure the repo is up to date:
+Show information about inserted YubiKey:
+
+ $ ykman info
+
+Run ykman in DEBUG mode:
- $ git pull
+ $ ykman --log-level DEBUG info
To uninstall, run:
- $ sudo pip uninstall yubikey-manager
+ $ pip uninstall yubikey-manager
=== Code Style
@@ -46,27 +64,3 @@
To run integration tests, indicate the serial number (given by `ykman list`) of the YubiKey to test with:
$ DESTRUCTIVE_TEST_YUBIKEY_SERIAL=123456 python setup.py test
-
-== Using vagrant for development
-
-A Vagrantfile with a development environment based on Ubuntu 16.04 is included in the repository.
-Modify the Vagrantfile to set up a USB filter to capture the device with VirtualBox.
-
-
-== Publishing to Ubuntu PPA
-
- 1. Update version number and signoff in `debian/changelog`.
- 2. Build and upload package.
-
-For (2) you can use the Vagrant VM in `vagrant/ppa`. You'll need to set up the
-VM to capture the YubiKey containing your signing key. If you use VirtualBox,
-you can do this by uncommenting the USB filter included in the `Vagrantfile`.
-Then:
-
- alice at work $ cd yubikey-manager/vagrant/ppa
- alice at work $ vagrant up
- alice at work $ vagrant ssh
- ubuntu at ubuntu-xenial $ gpg2 --recv-keys ABCDEF78
- ubuntu at ubuntu-xenial $ gpg2 --card-status
- ubuntu at ubuntu-xenial $ cd yubikey-manager
- ubuntu at ubuntu-xenial $ ~/scripts/make-ppa -k ABCDEF78 -p gpg2
diff -Nru yubikey-manager-0.7.1/MANIFEST.in yubikey-manager-2.0.0/MANIFEST.in
--- yubikey-manager-0.7.1/MANIFEST.in 2018-07-09 03:18:05.000000000 -0400
+++ yubikey-manager-2.0.0/MANIFEST.in 2019-01-07 05:25:27.000000000 -0500
@@ -3,7 +3,6 @@
include ChangeLog
include resources/*
include doc/*.adoc
-include test/**/**/**
-
+recursive-include test *
include README.adoc
include ykman/VERSION
diff -Nru yubikey-manager-0.7.1/NEWS yubikey-manager-2.0.0/NEWS
--- yubikey-manager-0.7.1/NEWS 2018-07-09 03:01:59.000000000 -0400
+++ yubikey-manager-2.0.0/NEWS 2019-01-08 02:53:33.000000000 -0500
@@ -1,3 +1,29 @@
+* Version 2.0.0 (unreleased)
+ ** 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.
@@ -33,7 +59,7 @@
** 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 dymanically spaced depending on contents
+ *** 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:
@@ -64,10 +90,10 @@
** 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 compability, for adding new credentials.
+ ** 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 compability, when used as a library.
+ ** OATH: Fix issue with backwards compatibility, when used as a library.
* Version 0.4.2 (released 2017-09-05)
** OATH: Support 7 digit credentials.
@@ -76,7 +102,7 @@
* Version 0.4.1 (released 2017-08-10)
** PIV: Dropped support for deriving a management key from PIN.
- ** PIV: Addded support for generating a random management key and storing it on the device protected by the 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.
diff -Nru yubikey-manager-0.7.1/PKG-INFO yubikey-manager-2.0.0/PKG-INFO
--- yubikey-manager-0.7.1/PKG-INFO 2018-07-09 03:19:36.000000000 -0400
+++ yubikey-manager-2.0.0/PKG-INFO 2019-01-08 03:00:36.000000000 -0500
@@ -1,6 +1,6 @@
Metadata-Version: 1.2
Name: yubikey-manager
-Version: 0.7.1
+Version: 2.0.0
Summary: Tool for managing your YubiKey configuration.
Home-page: https://github.com/Yubico/yubikey-manager
Author: Dain Nilsson
diff -Nru yubikey-manager-0.7.1/README.adoc yubikey-manager-2.0.0/README.adoc
--- yubikey-manager-0.7.1/README.adoc 2018-07-02 02:27:29.000000000 -0400
+++ yubikey-manager-2.0.0/README.adoc 2018-11-08 06:33:42.000000000 -0500
@@ -5,6 +5,7 @@
Python library and command line tool for configuring a YubiKey. If you're looking for the full graphical application, which also includes the command line tool, it's https://developers.yubico.com/yubikey-manager-qt/[here].
=== Usage
+For more usage information and examples, see the https://support.yubico.com/support/solutions/articles/15000012643-yubikey-manager-cli-ykman-user-guide[YubiKey Manager CLI User Manual].
....
Usage: ykman [OPTIONS] COMMAND [ARGS]...
@@ -14,20 +15,20 @@
-v, --version
-d, --device SERIAL
-l, --log-level [DEBUG|INFO|WARNING|ERROR|CRITICAL]
- Enable logging at given verbosity level
- --log-file FILE Write logs to the given FILE instead of standard error;
- ignored unless --log-level is also set
+ 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.
-h, --help Show this message and exit.
Commands:
+ config Enable/Disable applications.
fido Manage FIDO applications.
info Show general information.
list List connected YubiKeys.
mode Manage connection modes (USB Interfaces).
- oath Manage OATH application.
- openpgp Manage OpenPGP application.
+ oath Manage OATH Application.
+ openpgp Manage OpenPGP Application.
otp Manage OTP Application.
- piv Manage PIV application.
+ piv Manage PIV Application.
....
=== Installation
@@ -42,11 +43,6 @@
$ brew install ykman
-Or from source:
-
- $ brew install swig ykpers libusb
- $ pip install --user yubikey-manager
-
==== Windows
The command line tool is installed together with the GUI version of https://developers.yubico.com/yubikey-manager-qt/[YubiKey Manager].
@@ -58,6 +54,9 @@
In order for the pip package to work, https://developers.yubico.com/yubikey-personalization/[ykpers] and http://libusb.info/[libusb] need to be installed on your system as well.
https://pyscard.sourceforge.io/[Pyscard] is also needed in some form, and if it's not installed pip builds it using http://www.swig.org/[swig] and potentially https://pcsclite.alioth.debian.org/pcsclite.html[PCSC lite].
+==== Source
+To install from source, see the https://github.com/Yubico/yubikey-manager/blob/master/doc/development.adoc[development] instructions.
+
=== Bash completion
Experimental Bash completion for the command line tool is available, but not
diff -Nru yubikey-manager-0.7.1/test/files/rsa_1024_key.pem yubikey-manager-2.0.0/test/files/rsa_1024_key.pem
--- yubikey-manager-0.7.1/test/files/rsa_1024_key.pem 1969-12-31 19:00:00.000000000 -0500
+++ yubikey-manager-2.0.0/test/files/rsa_1024_key.pem 2018-11-08 06:33:42.000000000 -0500
@@ -0,0 +1,15 @@
+-----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-----
Binary files /tmp/dkFm3sZ4QE/yubikey-manager-0.7.1/test/files/rsa_2048_cert.der and /tmp/2VDZFninfB/yubikey-manager-2.0.0/test/files/rsa_2048_cert.der differ
diff -Nru yubikey-manager-0.7.1/test/files/rsa_2048_cert_metadata.pem yubikey-manager-2.0.0/test/files/rsa_2048_cert_metadata.pem
--- yubikey-manager-0.7.1/test/files/rsa_2048_cert_metadata.pem 1969-12-31 19:00:00.000000000 -0500
+++ yubikey-manager-2.0.0/test/files/rsa_2048_cert_metadata.pem 2018-12-20 08:58:43.000000000 -0500
@@ -0,0 +1,20 @@
+Subject: 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-----
diff -Nru yubikey-manager-0.7.1/test/files/rsa_2048_cert.pem yubikey-manager-2.0.0/test/files/rsa_2048_cert.pem
--- yubikey-manager-0.7.1/test/files/rsa_2048_cert.pem 1969-12-31 19:00:00.000000000 -0500
+++ yubikey-manager-2.0.0/test/files/rsa_2048_cert.pem 2018-11-08 06:33:42.000000000 -0500
@@ -0,0 +1,18 @@
+-----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-----
Binary files /tmp/dkFm3sZ4QE/yubikey-manager-0.7.1/test/files/rsa_2048_key_cert_encrypted.pfx and /tmp/2VDZFninfB/yubikey-manager-2.0.0/test/files/rsa_2048_key_cert_encrypted.pfx differ
Binary files /tmp/dkFm3sZ4QE/yubikey-manager-0.7.1/test/files/rsa_2048_key_cert.pfx and /tmp/2VDZFninfB/yubikey-manager-2.0.0/test/files/rsa_2048_key_cert.pfx differ
diff -Nru yubikey-manager-0.7.1/test/files/rsa_2048_key_encrypted.pem yubikey-manager-2.0.0/test/files/rsa_2048_key_encrypted.pem
--- yubikey-manager-0.7.1/test/files/rsa_2048_key_encrypted.pem 1969-12-31 19:00:00.000000000 -0500
+++ yubikey-manager-2.0.0/test/files/rsa_2048_key_encrypted.pem 2018-11-08 06:33:42.000000000 -0500
@@ -0,0 +1,30 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,579D3F4E703C6287
+
+lYbBuzBurcdWdxmt+cEZNYsKtIpmkPUtHZ+/WuP7CSiINuIx3fxZyitIKAMiwuYM
+404t+/iR5jmzJWPEwWkj+XXCPN5vJDbf4euLY+d++3VSbXWas2fxutfzIPRRXdoQ
+WvjnR4vQDIUE3vHYcDyS1j2gwmbyeBaZBFlJ8Qyloy1W3rLTn3nxizfK7KqkGSaO
+lm1eSfTw8wj2M6nryPPBDHq9PS3Ic+mJDL0gIXPq8D+UcnTbVCXluguErn+4y7LJ
+f0RX2sJ8KaR5CmRpfHL7kzVc+oXI5CoWqOFZpjNojpvDqSrh97DpIArOoYrlX6dR
+pXlRdXhhXdzQ4CpGLcAthyAhuaSzSKF3+X1S/oRXM1TtkBvAGxw6b1x10hOu/7Cn
+/tzFcpz1E2LYL67+jGJ9IpEZkT2e8inVeBBq2nb8qN214XNVpdia4Xm5rV0qVMpB
+E8hLmkmzZyOJGKWJbwgjAjCv9C7E4urgKySjBjWdBmpQnqTJoiAE60qjlyuC6VT6
+BOr5rq1qIpqy3lJZulANABPMAdj7rLSdlKhncp0WLlBjMpQhpyF0SEiiEULDvbIp
+jwMBfP8K1gsAJRsIUvNrWC6PaD0plNAPn1+yMps0cNddMALFhddXuLau86QrPpOT
+BZkS4zqUKDOOGxqs5aVjyaK66ilf79Ga09vjvU5d2A6fysVs5w58LJbz8za/zyt5
+qst9Tg8kUXs4aRHeABV7v2/F8CxDoUdFYH49Ga8IJQn2nGX0zqF7lOBVv5LZlTDy
+/2g3Q7yqCObfHofKIBt7U266IrHbg2Sn5px2suyAjKxgbAE+GXamiLrfpwZ4ao0G
+5BAkIc65JE795JfEXuWc1ZwC7yvfF1W2e0UTjj1HHSWZQzcO8W6nlNGSKl21b1Ez
+wdjDXnKxi1OGVN0qjEVJxB62GoLatVMtl3diivE7gdg8TRDZtGbEBbRNG/Wwa6LT
+phKO3H66bjyQJmldK5eYRQwToAIKJ3lyZH/gnGV/jrOZ2FuAR6CTAOdUTmFD6Hww
++eRomTvawWrqXOut8LS9uWixzEANnBTZ7iR3bL6ux8Auv4utNbaLtAfwpAHGHrOD
+ULW6zums2qNb2J1K1f36L/Y7GESCOcgeZ4I1yHaKAcdyJmoEg4tYTMGmMUNl3w1U
+47mBpQu0Sg8PtFCt3hp79qaRNOMIn6MEkmTeIyootjt7KxtD5BMYxy0DxpTBhAuj
+Z2gxUSR4Ozm1vHbcOi87ZW2nPA8L5C2/SD03DsJPf4BbnVLuP7swLlLPDs4Ra6nL
+qrm6devSZjeuXj1+jqLoqR5L5QBAzFOw5p5YfoPkVu4TrNM44Pv1tdkfYE/SX2tZ
+UkLPLwfhDa2Q3zqvuzaTAQTZ4PjQTfB4c/eVC4c/463aTywri963WqSg1kKESAMw
+O7XFSvUhUDljR1CGAOAYZWQ8cQ9A1kHinooJLHBpzWeoVAjajJrgusv1mryQzcjs
+TVL75mveOGetHrcPcR1aUXiFDcYZp5OYo23A2z34wYagORSvdg0UWgtDj+9EPwRk
+plMZ77cnkLkd6x7cQCobj2yHRH0foN1L1ntKLxO0joo85UQUsSoba9nCoeS2Hvlj
+-----END RSA PRIVATE KEY-----
diff -Nru yubikey-manager-0.7.1/test/files/rsa_2048_key.pem yubikey-manager-2.0.0/test/files/rsa_2048_key.pem
--- yubikey-manager-0.7.1/test/files/rsa_2048_key.pem 1969-12-31 19:00:00.000000000 -0500
+++ yubikey-manager-2.0.0/test/files/rsa_2048_key.pem 2018-11-08 06:33:42.000000000 -0500
@@ -0,0 +1,27 @@
+-----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-----
diff -Nru yubikey-manager-0.7.1/test/on_yubikey/cli_piv/test_key_management.py yubikey-manager-2.0.0/test/on_yubikey/cli_piv/test_key_management.py
--- yubikey-manager-0.7.1/test/on_yubikey/cli_piv/test_key_management.py 2018-07-04 06:48:30.000000000 -0400
+++ yubikey-manager-2.0.0/test/on_yubikey/cli_piv/test_key_management.py 2018-12-20 08:58:43.000000000 -0500
@@ -107,6 +107,84 @@
csr = x509.load_pem_x509_csr(output.encode(), default_backend())
self.assertTrue(csr.is_signature_valid)
+ def test_import_correct_cert_succeeds_with_pin(self):
+ # Set up a key in the slot and create a certificate for it
+ public_key_pem = ykman_cli(
+ 'piv', 'generate-key', '9a', '-a', 'ECCP256', '-m',
+ DEFAULT_MANAGEMENT_KEY, '--pin-policy', 'ALWAYS', '-')
+
+ ykman_cli(
+ 'piv', 'generate-certificate', '9a', '-',
+ '-m', DEFAULT_MANAGEMENT_KEY, '-P', DEFAULT_PIN, '-s', 'test',
+ input=public_key_pem)
+
+ ykman_cli('piv', 'export-certificate', '9a', '/tmp/test-pub-key.pem')
+
+ with self.assertRaises(SystemExit):
+ ykman_cli(
+ 'piv', 'import-certificate', '9a', '/tmp/test-pub-key.pem',
+ '-m', DEFAULT_MANAGEMENT_KEY)
+
+ ykman_cli(
+ 'piv', 'import-certificate', '9a', '/tmp/test-pub-key.pem',
+ '-m', DEFAULT_MANAGEMENT_KEY, '-P', DEFAULT_PIN)
+ ykman_cli(
+ 'piv', 'import-certificate', '9a', '/tmp/test-pub-key.pem',
+ '-m', DEFAULT_MANAGEMENT_KEY, input=DEFAULT_PIN)
+
+ def test_import_wrong_cert_fails(self):
+ # Set up a key in the slot and create a certificate for it
+ public_key_pem = ykman_cli(
+ 'piv', 'generate-key', '9a', '-a', 'ECCP256', '-m',
+ DEFAULT_MANAGEMENT_KEY, '--pin-policy', 'ALWAYS', '-')
+
+ ykman_cli(
+ 'piv', 'generate-certificate', '9a', '-',
+ '-m', DEFAULT_MANAGEMENT_KEY, '-P', DEFAULT_PIN, '-s', 'test',
+ input=public_key_pem)
+
+ cert_pem = ykman_cli('piv', 'export-certificate', '9a', '-')
+
+ # Overwrite the key with a new one
+ ykman_cli(
+ 'piv', 'generate-key', '9a', '-a', 'ECCP256', '-m',
+ DEFAULT_MANAGEMENT_KEY, '--pin-policy', 'ALWAYS', '-',
+ input=public_key_pem)
+
+ with self.assertRaises(SystemExit):
+ ykman_cli(
+ 'piv', 'import-certificate', '9a', '-',
+ '-m', DEFAULT_MANAGEMENT_KEY, '-P', DEFAULT_PIN, input=cert_pem)
+
+ def test_import_wrong_cert_can_be_forced(self):
+ # Set up a key in the slot and create a certificate for it
+ public_key_pem = ykman_cli(
+ 'piv', 'generate-key', '9a', '-a', 'ECCP256', '-m',
+ DEFAULT_MANAGEMENT_KEY, '--pin-policy', 'ALWAYS', '-')
+
+ ykman_cli(
+ 'piv', 'generate-certificate', '9a', '-',
+ '-m', DEFAULT_MANAGEMENT_KEY, '-P', DEFAULT_PIN, '-s', 'test',
+ input=public_key_pem)
+
+ cert_pem = ykman_cli('piv', 'export-certificate', '9a', '-')
+
+ # Overwrite the key with a new one
+ ykman_cli(
+ 'piv', 'generate-key', '9a', '-a', 'ECCP256', '-m',
+ DEFAULT_MANAGEMENT_KEY, '--pin-policy', 'ALWAYS', '-',
+ input=public_key_pem)
+
+ with self.assertRaises(SystemExit):
+ ykman_cli(
+ 'piv', 'import-certificate', '9a', '-',
+ '-m', DEFAULT_MANAGEMENT_KEY, '-P', DEFAULT_PIN, input=cert_pem)
+
+ ykman_cli(
+ 'piv', 'import-certificate', '9a', '-',
+ '-m', DEFAULT_MANAGEMENT_KEY, '-P', DEFAULT_PIN, '--no-verify',
+ input=cert_pem)
+
@unittest.skipIf(*no_attestation)
def test_export_attestation_certificate(self):
output = ykman_cli('piv', 'export-certificate', 'f9', '-')
diff -Nru yubikey-manager-0.7.1/test/on_yubikey/test_cli_config.py yubikey-manager-2.0.0/test/on_yubikey/test_cli_config.py
--- yubikey-manager-0.7.1/test/on_yubikey/test_cli_config.py 1969-12-31 19:00:00.000000000 -0500
+++ yubikey-manager-2.0.0/test/on_yubikey/test_cli_config.py 2018-11-08 06:33:42.000000000 -0500
@@ -0,0 +1,171 @@
+import unittest
+from .util import (DestructiveYubikeyTestCase, ykman_cli, can_write_config)
+
+
+VALID_LOCK_CODE = 'a' * 32
+INVALID_LOCK_CODE_NON_HEX = 'z' * 32
+
+
+ at unittest.skipIf(not can_write_config(), 'Device can not write config')
+class TestConfigUSB(DestructiveYubikeyTestCase):
+
+ def setUp(self):
+ ykman_cli('config', 'usb', '--enable-all', '-f')
+
+ def tearDown(self):
+ ykman_cli('config', 'usb', '--enable-all', '-f')
+
+ def test_disable_otp(self):
+ ykman_cli('config', 'usb', '--disable', 'OTP', '-f')
+ output = ykman_cli('config', 'usb', '--list')
+ self.assertNotIn('OTP', output)
+
+ def test_disable_u2f(self):
+ ykman_cli('config', 'usb', '--disable', 'U2F', '-f')
+ output = ykman_cli('config', 'usb', '--list')
+ self.assertNotIn('FIDO U2F', output)
+
+ def test_disable_openpgp(self):
+ ykman_cli('config', 'usb', '--disable', 'OPGP', '-f')
+ output = ykman_cli('config', 'usb', '--list')
+ self.assertNotIn('OpenPGP', output)
+
+ def test_disable_piv(self):
+ ykman_cli('config', 'usb', '--disable', 'PIV', '-f')
+ output = ykman_cli('config', 'usb', '--list')
+ self.assertNotIn('PIV', output)
+
+ def test_disable_oath(self):
+ ykman_cli('config', 'usb', '--disable', 'OATH', '-f')
+ output = ykman_cli('config', 'usb', '--list')
+ self.assertNotIn('OATH', output)
+
+ def test_disable_fido2(self):
+ ykman_cli('config', 'usb', '--disable', 'FIDO2', '-f')
+ output = ykman_cli('config', 'usb', '--list')
+ self.assertNotIn('FIDO2', output)
+
+ def test_disable_and_enable(self):
+ with self.assertRaises(SystemExit):
+ ykman_cli(
+ 'config', 'usb', '--disable', 'FIDO2', '--enable',
+ 'FIDO2', '-f')
+ with self.assertRaises(SystemExit):
+ ykman_cli(
+ 'config', 'usb', '--enable-all', '--disable', 'FIDO2', '-f')
+
+ def test_disable_all(self):
+ with self.assertRaises(SystemExit):
+ ykman_cli(
+ 'config', 'usb', '-d', 'FIDO2', '-d', 'U2F', '-d',
+ 'OATH', '-d', 'OPGP', 'PIV', '-d', 'OTP')
+
+ def test_mode_command(self):
+ ykman_cli('mode', 'ccid', '-f')
+ output = ykman_cli('config', 'usb', '--list')
+ self.assertNotIn('FIDO U2F', output)
+ self.assertNotIn('FIDO2', output)
+ self.assertNotIn('OTP', output)
+
+ ykman_cli('mode', 'otp', '-f')
+ output = ykman_cli('config', 'usb', '--list')
+ self.assertNotIn('FIDO U2F', output)
+ self.assertNotIn('FIDO2', output)
+ self.assertNotIn('OpenPGP', output)
+ self.assertNotIn('PIV', output)
+ self.assertNotIn('OATH', output)
+
+ ykman_cli('mode', 'fido', '-f')
+ output = ykman_cli('config', 'usb', '--list')
+ self.assertNotIn('OTP', output)
+ self.assertNotIn('OATH', output)
+ self.assertNotIn('PIV', output)
+ self.assertNotIn('OpenPGP', output)
+
+
+ at unittest.skipIf(not can_write_config(), 'Device can not write config')
+class TestConfigNFC(DestructiveYubikeyTestCase):
+
+ def setUp(self):
+ ykman_cli('config', 'nfc', '--enable-all', '-f')
+
+ def tearDown(self):
+ ykman_cli('config', 'nfc', '--enable-all', '-f')
+
+ def test_disable_otp(self):
+ ykman_cli('config', 'nfc', '--disable', 'OTP', '-f')
+ output = ykman_cli('config', 'nfc', '--list')
+ self.assertNotIn('OTP', output)
+
+ def test_disable_u2f(self):
+ ykman_cli('config', 'nfc', '--disable', 'U2F', '-f')
+ output = ykman_cli('config', 'nfc', '--list')
+ self.assertNotIn('FIDO U2F', output)
+
+ def test_disable_openpgp(self):
+ ykman_cli('config', 'nfc', '--disable', 'OPGP', '-f')
+ output = ykman_cli('config', 'nfc', '--list')
+ self.assertNotIn('OpenPGP', output)
+
+ def test_disable_piv(self):
+ ykman_cli('config', 'nfc', '--disable', 'PIV', '-f')
+ output = ykman_cli('config', 'nfc', '--list')
+ self.assertNotIn('PIV', output)
+
+ def test_disable_oath(self):
+ ykman_cli('config', 'nfc', '--disable', 'OATH', '-f')
+ output = ykman_cli('config', 'nfc', '--list')
+ self.assertNotIn('OATH', output)
+
+ def test_disable_fido2(self):
+ ykman_cli('config', 'nfc', '--disable', 'FIDO2', '-f')
+ output = ykman_cli('config', 'nfc', '--list')
+ self.assertNotIn('FIDO2', output)
+
+ def test_disable_all(self):
+ ykman_cli('config', 'nfc', '--disable-all', '-f')
+ output = ykman_cli('config', 'nfc', '--list')
+ self.assertFalse(output)
+
+ def test_disable_and_enable(self):
+ with self.assertRaises(SystemExit):
+ ykman_cli(
+ 'config', 'nfc', '--disable', 'FIDO2',
+ '--enable', 'FIDO2', '-f')
+ with self.assertRaises(SystemExit):
+ ykman_cli(
+ 'config', 'nfc', '--disable-all', '--enable', 'FIDO2', '-f')
+ with self.assertRaises(SystemExit):
+ ykman_cli(
+ 'config', 'nfc', '--enable-all', '--disable', 'FIDO2', '-f')
+ with self.assertRaises(SystemExit):
+ ykman_cli(
+ 'config', 'nfc', '--enable-all', '--disable-all', 'FIDO2', '-f')
+
+
+ at unittest.skipIf(not can_write_config(), 'Device can not write config')
+class TestConfigLockCode(DestructiveYubikeyTestCase):
+
+ def test_set_lock_code(self):
+ ykman_cli(
+ 'config', 'set-lock-code', '--new-lock-code', VALID_LOCK_CODE)
+ output = ykman_cli('info')
+ self.assertIn(
+ 'Configured applications are protected by a lock code', output)
+ ykman_cli(
+ 'config', 'set-lock-code', '-l', VALID_LOCK_CODE, '--clear')
+ output = ykman_cli('info')
+ self.assertNotIn(
+ 'Configured applications are protected by a lock code', output)
+
+ def test_set_invalid_lock_code(self):
+
+ with self.assertRaises(SystemExit):
+ ykman_cli(
+ 'config', 'set-lock-code',
+ '--new-lock-code', 'aaaa')
+
+ with self.assertRaises(SystemExit):
+ ykman_cli(
+ 'config', 'set-lock-code',
+ '--new-lock-code', INVALID_LOCK_CODE_NON_HEX)
diff -Nru yubikey-manager-0.7.1/test/on_yubikey/test_cli_misc.py yubikey-manager-2.0.0/test/on_yubikey/test_cli_misc.py
--- yubikey-manager-0.7.1/test/on_yubikey/test_cli_misc.py 1969-12-31 19:00:00.000000000 -0500
+++ yubikey-manager-2.0.0/test/on_yubikey/test_cli_misc.py 2018-11-08 06:33:42.000000000 -0500
@@ -0,0 +1,25 @@
+import unittest
+
+from .util import (DestructiveYubikeyTestCase, is_fips, ykman_cli)
+
+
+class TestYkmanInfo(DestructiveYubikeyTestCase):
+
+ def test_ykman_info(self):
+ info = ykman_cli('info')
+ self.assertIn('Device type:', info)
+ self.assertIn('Serial number:', info)
+ self.assertIn('Firmware version:', info)
+
+ @unittest.skipIf(is_fips(), 'Not applicable to YubiKey FIPS.')
+ def test_ykman_info_does_not_report_fips_for_non_fips_device(self):
+ info = ykman_cli('info')
+ self.assertNotIn('FIPS', info)
+
+ @unittest.skipIf(not is_fips(), 'YubiKey FIPS required.')
+ def test_ykman_info_reports_fips_status(self):
+ info = ykman_cli('info')
+ self.assertIn('FIPS Approved Mode:', info)
+ self.assertIn(' FIDO U2F:', info)
+ self.assertIn(' OATH:', info)
+ self.assertIn(' OTP:', info)
diff -Nru yubikey-manager-0.7.1/test/on_yubikey/test_cli_oath.py yubikey-manager-2.0.0/test/on_yubikey/test_cli_oath.py
--- yubikey-manager-0.7.1/test/on_yubikey/test_cli_oath.py 1969-12-31 19:00:00.000000000 -0500
+++ yubikey-manager-2.0.0/test/on_yubikey/test_cli_oath.py 2018-11-08 06:33:42.000000000 -0500
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+
+import unittest
+from ykman.util import TRANSPORT
+from .util import (DestructiveYubikeyTestCase, missing_mode, ykman_cli,
+ get_version, is_fips)
+
+
+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'
+
+
+ at unittest.skipIf(*missing_mode(TRANSPORT.CCID))
+class TestOATH(DestructiveYubikeyTestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ ykman_cli('oath', 'reset', '-f')
+
+ def test_oath_info(self):
+ output = ykman_cli('oath', 'info')
+ self.assertIn('version:', output)
+
+ @unittest.skipIf(is_fips(), 'Not applicable to YubiKey FIPS.')
+ def test_info_does_not_indicate_fips_mode_for_non_fips_key(self):
+ info = ykman_cli('oath', 'info')
+ self.assertNotIn('FIPS:', info)
+
+ def test_oath_add_credential(self):
+ ykman_cli('oath', 'add', 'test-name', 'abba')
+ creds = ykman_cli('oath', 'list')
+ self.assertIn('test-name', creds)
+
+ def test_oath_add_credential_prompt(self):
+ ykman_cli('oath', 'add', 'test-name-2', input='abba')
+ creds = ykman_cli('oath', 'list')
+ self.assertIn('test-name-2', creds)
+
+ def test_oath_add_credential_with_space(self):
+ ykman_cli('oath', 'add', 'test-name-space', 'ab ba')
+ creds = ykman_cli('oath', 'list')
+ self.assertIn('test-name-space', creds)
+
+ def test_oath_hidden_cred(self):
+ ykman_cli('oath', 'add', '_hidden:name', 'abba')
+ creds = ykman_cli('oath', 'code')
+ self.assertNotIn('_hidden:name', creds)
+ creds = ykman_cli('oath', 'code', '-H')
+ self.assertIn('_hidden:name', creds)
+
+ def test_oath_add_uri_hotp(self):
+ ykman_cli('oath', 'uri', URI_HOTP_EXAMPLE)
+ creds = ykman_cli('oath', 'list')
+ self.assertIn('Example:demo', creds)
+
+ def test_oath_add_uri_totp(self):
+ ykman_cli('oath', 'uri', URI_TOTP_EXAMPLE)
+ creds = ykman_cli('oath', 'list')
+ self.assertIn('john.doe', creds)
+
+ def test_oath_add_uri_totp_extra_parameter(self):
+ ykman_cli('oath', 'uri', URI_TOTP_EXAMPLE_EXTRA_PARAMETER)
+ creds = ykman_cli('oath', 'list')
+ self.assertIn('john.doe.extra', creds)
+
+ def test_oath_add_uri_totp_prompt(self):
+ ykman_cli('oath', 'uri', input=URI_TOTP_EXAMPLE_B)
+ creds = ykman_cli('oath', 'list')
+ self.assertIn('john.doe', creds)
+
+ def test_oath_code(self):
+ ykman_cli('oath', 'add', 'test-name2', 'abba')
+ creds = ykman_cli('oath', 'code')
+ self.assertIn('test-name2', creds)
+
+ def test_oath_code_query(self):
+ ykman_cli('oath', 'add', 'query-me', 'abba')
+ creds = ykman_cli('oath', 'code', 'query-me')
+ self.assertIn('query-me', creds)
+
+ def test_oath_reset(self):
+ output = ykman_cli('oath', 'reset', '-f')
+ self.assertIn('Success! All OATH credentials have been cleared from '
+ 'your YubiKey', output)
+
+ def test_oath_hotp_code(self):
+ ykman_cli('oath', 'add', '-o', 'HOTP', 'hotp-cred', 'abba')
+ cred = ykman_cli('oath', 'code', 'hotp-cred')
+ self.assertIn('659165', cred)
+
+ def test_oath_hotp_steam_code(self):
+ ykman_cli('oath', 'add', '-o', 'HOTP', 'Steam:steam-cred', 'abba')
+ cred = ykman_cli('oath', 'code', 'steam-cred')
+ self.assertIn('CGC3K', cred)
+
+ def test_oath_delete(self):
+ ykman_cli('oath', 'add', 'delete-me', 'abba')
+ ykman_cli('oath', 'delete', 'delete-me', '-f')
+ self.assertNotIn('delete-me', ykman_cli('oath', 'list'))
+
+ def test_oath_unicode(self):
+ ykman_cli('oath', 'add', '๐', 'abba')
+ ykman_cli('oath', 'code')
+ ykman_cli('oath', 'list')
+ ykman_cli('oath', 'delete', '๐', '-f')
+
+ @unittest.skipIf(is_fips(), 'Not applicable to YubiKey FIPS.')
+ def test_oath_sha512(self):
+ if get_version() < (4, 3, 1):
+ self.skipTest('Not applicable to YubiKey versions before 4.3.1')
+
+ ykman_cli('oath', 'add', 'abba', 'abba', '--algorithm', 'SHA512')
+ ykman_cli('oath', 'delete', 'abba', '-f')
+
+
+ at unittest.skipIf(not is_fips(), 'Only applicable to YubiKey FIPS.')
+ at unittest.skipIf(*missing_mode(TRANSPORT.CCID))
+class TestOathFips(DestructiveYubikeyTestCase):
+
+ def setUp(self):
+ ykman_cli('oath', 'reset', '-f')
+
+ @classmethod
+ def tearDownClass(cls):
+ ykman_cli('oath', 'reset', '-f')
+
+ def test_no_fips_mode_without_password(self):
+ output = ykman_cli('oath', 'info')
+ self.assertIn('FIPS Approved Mode: No', output)
+
+ def test_fips_mode_with_password(self):
+ ykman_cli('oath', 'set-password', '-n', PASSWORD)
+ output = ykman_cli('oath', 'info')
+ self.assertIn('FIPS Approved Mode: Yes', output)
+
+ def test_sha512_not_supported(self):
+ with self.assertRaises(SystemExit):
+ ykman_cli('oath', 'add', 'abba', 'abba', '--algorithm', 'SHA512')
diff -Nru yubikey-manager-0.7.1/test/on_yubikey/test_cli_openpgp.py yubikey-manager-2.0.0/test/on_yubikey/test_cli_openpgp.py
--- yubikey-manager-0.7.1/test/on_yubikey/test_cli_openpgp.py 1969-12-31 19:00:00.000000000 -0500
+++ yubikey-manager-2.0.0/test/on_yubikey/test_cli_openpgp.py 2018-11-08 06:33:42.000000000 -0500
@@ -0,0 +1,17 @@
+import unittest
+from ykman.util import TRANSPORT
+from .util import (DestructiveYubikeyTestCase, missing_mode, ykman_cli)
+
+
+ at unittest.skipIf(*missing_mode(TRANSPORT.CCID))
+class TestOpenPGP(DestructiveYubikeyTestCase):
+
+ def test_openpgp_info(self):
+ output = ykman_cli('openpgp', 'info')
+ self.assertIn('OpenPGP version:', output)
+
+ def test_openpgp_reset(self):
+ output = ykman_cli('openpgp', 'reset', '-f')
+ self.assertIn(
+ 'Success! All data has been cleared and default PINs are set.',
+ output)
diff -Nru yubikey-manager-0.7.1/test/on_yubikey/test_cli_otp.py yubikey-manager-2.0.0/test/on_yubikey/test_cli_otp.py
--- yubikey-manager-0.7.1/test/on_yubikey/test_cli_otp.py 1969-12-31 19:00:00.000000000 -0500
+++ yubikey-manager-2.0.0/test/on_yubikey/test_cli_otp.py 2018-11-08 06:33:42.000000000 -0500
@@ -0,0 +1,492 @@
+# vim: set fileencoding=utf-8 :
+
+# Copyright (c) 2018 Yubico AB
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+from ykman.util import TRANSPORT
+from .util import (DestructiveYubikeyTestCase, get_version, is_fips,
+ missing_mode, ykman_cli)
+
+
+ at unittest.skipIf(*missing_mode(TRANSPORT.OTP))
+class TestSlotStatus(DestructiveYubikeyTestCase):
+
+ def test_ykman_otp_info(self):
+ info = ykman_cli('otp', 'info')
+ self.assertIn('Slot 1:', info)
+ self.assertIn('Slot 2:', info)
+
+ def test_ykman_swap_slots(self):
+ output = ykman_cli('otp', 'swap', '-f')
+ self.assertIn('Swapping slots...', output)
+ output = ykman_cli('otp', 'swap', '-f')
+ self.assertIn('Swapping slots...', output)
+
+ @unittest.skipIf(is_fips(), 'Not applicable to YubiKey FIPS.')
+ def test_ykman_otp_info_does_not_indicate_fips_mode_for_non_fips_key(self):
+ info = ykman_cli('otp', 'info')
+ self.assertNotIn('FIPS Approved Mode:', info)
+
+
+ at unittest.skipIf(*missing_mode(TRANSPORT.OTP))
+class TestSlotStaticPassword(DestructiveYubikeyTestCase):
+
+ def setUp(self):
+ ykman_cli('otp', 'delete', '2', '-f')
+
+ def tearDown(self):
+ ykman_cli('otp', 'delete', '2', '-f')
+
+ def test_too_long(self):
+ with self.assertRaises(SystemExit):
+ ykman_cli(
+ 'otp', 'static', '2',
+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
+
+ def test_unsupported_chars(self):
+ with self.assertRaises(ValueError):
+ ykman_cli('otp', 'static', '2', 'รถ')
+ with self.assertRaises(ValueError):
+ ykman_cli('otp', 'static', '2', '@')
+
+ def test_provide_valid_pw(self):
+ ykman_cli(
+ 'otp', 'static', '2',
+ 'higngdukgerjktbbikrhirngtlkkttkb')
+ self.assertIn('Slot 2: programmed', ykman_cli('otp', 'info'))
+
+ def test_provide_valid_pw_prompt(self):
+ ykman_cli(
+ 'otp', 'static', '2',
+ input='higngdukgerjktbbikrhirngtlkkttkb\ny\n')
+ self.assertIn('Slot 2: programmed', ykman_cli('otp', 'info'))
+
+ def test_generate_pw_too_long(self):
+ with self.assertRaises(SystemExit):
+ ykman_cli('otp', 'static', '2', '--generate', '--length', '39')
+
+ def test_generate_pw_no_length(self):
+ with self.assertRaises(SystemExit):
+ ykman_cli('otp', 'static', '2', '--generate', '--length')
+ with self.assertRaises(SystemExit):
+ ykman_cli('otp', 'static', '2', '--generate')
+
+ def test_generate_zero_length(self):
+ with self.assertRaises(SystemExit):
+ ykman_cli('otp', 'static', '2', '--generate', '--length', '0')
+
+ def test_generate_pw(self):
+ ykman_cli('otp', 'static', '2', '--generate', '--length', '38')
+ self.assertIn('Slot 2: programmed', ykman_cli('otp', 'info'))
+
+ def test_us_scancodes(self):
+ ykman_cli('otp', 'static', '2', 'abcABC123', '--keyboard-layout', 'US')
+ ykman_cli('otp', 'static', '2', '@!)', '-f', '--keyboard-layout', 'US')
+
+ def test_de_scancodes(self):
+ ykman_cli('otp', 'static', '2', 'abcABC123', '--keyboard-layout', 'DE')
+ ykman_cli('otp', 'static', '2', 'รรรถ', '-f', '--keyboard-layout', 'DE')
+
+ def test_overwrite_prompt(self):
+ ykman_cli('otp', 'static', '2', 'bbb')
+ with self.assertRaises(SystemExit):
+ ykman_cli('otp', 'static', '2', 'ccc')
+ ykman_cli('otp', 'static', '2', 'ddd', '-f')
+ self.assertIn('Slot 2: programmed', ykman_cli('otp', 'info'))
+
+
+ at unittest.skipIf(*missing_mode(TRANSPORT.OTP))
+class TestSlotProgramming(DestructiveYubikeyTestCase):
+
+ def setUp(self):
+ ykman_cli('otp', 'delete', '2', '-f')
+
+ def tearDown(self):
+ ykman_cli('otp', 'delete', '2', '-f')
+
+ def _require_version_between(self, min_exclusive, max_exclusive):
+ if not min_exclusive < get_version() < max_exclusive:
+ self.skipTest('Requires version {} < v < {}'.format(
+ min_exclusive, max_exclusive))
+
+ def _require_version_not_between(self, min_exclusive, max_exclusive):
+ if min_exclusive < get_version() < max_exclusive:
+ self.skipTest('Requires version not {} < v < {}'.format(
+ min_exclusive, max_exclusive))
+
+ def test_ykman_program_otp_slot_2(self):
+ ykman_cli(
+ 'otp', 'yubiotp', '2', '--public-id', 'vvccccfiluij',
+ '--private-id', '267e0a88949b',
+ '--key', 'b8e31ab90bb8830e3c1fe1b483a8e0d4', '-f')
+ self._check_slot_2_programmed()
+
+ def test_ykman_program_otp_slot_2_prompt(self):
+ ykman_cli(
+ 'otp', 'yubiotp', '2', input='vvccccfiluij\n'
+ '267e0a88949b\n'
+ 'b8e31ab90bb8830e3c1fe1b483a8e0d4\n'
+ 'y\n')
+ self._check_slot_2_programmed()
+
+ def test_ykman_program_otp_slot_2_options(self):
+ output = ykman_cli(
+ 'otp', 'yubiotp', '2', '--public-id', 'vvccccfiluij',
+ '--private-id', '267e0a88949b',
+ '--key', 'b8e31ab90bb8830e3c1fe1b483a8e0d4', '-f')
+ self.assertEqual('', output)
+ self._check_slot_2_programmed()
+
+ def test_ykman_program_otp_slot_2_generated_all(self):
+ output = ykman_cli('otp', 'yubiotp', '2', '-f', '--serial-public-id',
+ '--generate-private-id', '--generate-key')
+ self.assertIn('Using YubiKey serial as public ID', output)
+ self.assertIn('Using a randomly generated private ID', output)
+ self.assertIn('Using a randomly generated secret key', output)
+ self._check_slot_2_programmed()
+
+ def test_ykman_program_otp_slot_2_serial_public_id(self):
+ output = ykman_cli(
+ 'otp', 'yubiotp', '2', '--serial-public-id',
+ '--private-id', '267e0a88949b',
+ '--key', 'b8e31ab90bb8830e3c1fe1b483a8e0d4', '-f')
+ self.assertIn('Using YubiKey serial as public ID', output)
+ self.assertNotIn('generated private ID', output)
+ self.assertNotIn('generated secret key', output)
+ self._check_slot_2_programmed()
+
+ def test_invalid_public_id(self):
+ with self.assertRaises(SystemExit):
+ ykman_cli('otp', 'yubiotp', '-P', 'imnotmodhex!')
+
+ def test_ykman_program_otp_slot_2_generated_private_id(self):
+ output = ykman_cli(
+ 'otp', 'yubiotp', '2', '--public-id', 'vvccccfiluij',
+ '--generate-private-id',
+ '--key', 'b8e31ab90bb8830e3c1fe1b483a8e0d4', '-f')
+ self.assertNotIn('serial as public ID', output)
+ self.assertIn('Using a randomly generated private ID', output)
+ self.assertNotIn('generated secret key', output)
+ self._check_slot_2_programmed()
+
+ def test_ykman_program_otp_slot_2_generated_secret_key(self):
+ output = ykman_cli(
+ 'otp', 'yubiotp', '2', '--public-id', 'vvccccfiluij',
+ '--private-id', '267e0a88949b', '--generate-key', '-f')
+ self.assertNotIn('serial as public ID', output)
+ self.assertNotIn('generated private ID', output)
+ self.assertIn('Using a randomly generated secret key', output)
+ self._check_slot_2_programmed()
+
+ def test_ykman_program_otp_slot_2_serial_id_conflicts_public_id(self):
+ with self.assertRaises(SystemExit):
+ ykman_cli('otp', 'yubiotp', '2', '-f', '--serial-public-id',
+ '--public-id', 'vvccccfiluij',
+ '--generate-private-id', '--generate-key')
+ self._check_slot_2_not_programmed()
+
+ def test_ykman_program_otp_slot_2_generate_id_conflicts_private_id(self):
+ with self.assertRaises(SystemExit):
+ ykman_cli('otp', 'yubiotp', '2', '-f', '--serial-public-id',
+ '--generate-private-id', '--private-id', '267e0a88949b',
+ '--generate-key')
+ self._check_slot_2_not_programmed()
+
+ def test_ykman_program_otp_slot_2_generate_key_conflicts_key(self):
+ with self.assertRaises(SystemExit):
+ ykman_cli('otp', 'yubiotp', '2', '-f', '--serial-public-id',
+ '--generate-private-id',
+ '--generate-key',
+ '--key', 'b8e31ab90bb8830e3c1fe1b483a8e0d4')
+ self._check_slot_2_not_programmed()
+
+ def test_ykman_program_chalresp_slot_2(self):
+ ykman_cli('otp', 'chalresp', '2', 'abba', '-f')
+ self._check_slot_2_programmed()
+ ykman_cli('otp', 'chalresp', '2', '--totp', 'abba', '-f')
+ self._check_slot_2_programmed()
+ ykman_cli('otp', 'chalresp', '2', '--touch', 'abba', '-f')
+ self._check_slot_2_programmed()
+
+ def test_ykman_program_chalresp_slot_2_force_fails_without_key(self):
+ with self.assertRaises(SystemExit):
+ ykman_cli('otp', 'chalresp', '2', '-f')
+ self._check_slot_2_not_programmed()
+
+ def test_ykman_program_chalresp_slot_2_generated(self):
+ output = ykman_cli('otp', 'chalresp', '2', '-f', '-g')
+ self.assertRegex(output,
+ 'Using a randomly generated key: [0-9a-f]{40}$')
+ self._check_slot_2_programmed()
+
+ def test_ykman_program_chalresp_slot_2_generated_fails_if_also_given(self):
+ with self.assertRaises(SystemExit):
+ ykman_cli('otp', 'chalresp', '2', '-f', '-g', 'abababab')
+
+ def test_ykman_program_chalresp_slot_2_prompt(self):
+ ykman_cli('otp', 'chalresp', '2', input='abba\ny\n')
+ self._check_slot_2_programmed()
+
+ def test_ykman_program_hotp_slot_2(self):
+ ykman_cli(
+ 'otp', 'hotp', '2',
+ '27KIZZE3SD7GE2FVJJBAXEI3I6RRTPGM', '-f')
+ self._check_slot_2_programmed()
+
+ def test_ykman_program_hotp_slot_2_prompt(self):
+ ykman_cli('otp', 'hotp', '2', input='abba\ny\n')
+ self._check_slot_2_programmed()
+
+ def test_update_settings_enter_slot_2(self):
+ ykman_cli('otp', 'static', '2', '-f', '-g', '-l', '20')
+ output = ykman_cli('otp', 'settings', '2', '-f', '--no-enter')
+ self.assertIn('Updating settings for slot', output)
+
+ def test_delete_slot_2(self):
+ ykman_cli('otp', 'static', '2', '-f', '-g', '-l', '20')
+ output = ykman_cli('otp', 'delete', '2', '-f')
+ self.assertIn('Deleting the configuration', output)
+ status = ykman_cli('otp', 'info')
+ self.assertIn('Slot 2: empty', status)
+
+ def test_access_code_slot_2(self):
+ ykman_cli(
+ 'otp', '--access-code', '111111111111', 'static', '2',
+ '--generate', '--length', '10')
+ self._check_slot_2_programmed()
+ self._check_slot_2_has_access_code()
+ ykman_cli('otp', '--access-code', '111111111111', 'delete', '2', '-f')
+ status = ykman_cli('otp', 'info')
+ self.assertIn('Slot 2: empty', status)
+
+ def test_update_access_code_fails_on_yk_432_to_435(self):
+ self._require_version_between((4, 3, 1), (4, 3, 6))
+
+ ykman_cli('otp', 'static', '2', '--generate', '--length', '10')
+
+ self._check_slot_2_programmed()
+
+ with self.assertRaises(SystemExit):
+ ykman_cli('otp', 'settings', '--new-access-code', '111111111111',
+ '2', '-f')
+
+ ykman_cli('otp', '--access-code', '111111111111', 'static', '2', '-f',
+ '--generate', '--length', '10')
+
+ with self.assertRaises(SystemExit):
+ ykman_cli('otp', 'delete', '2', '-f')
+
+ with self.assertRaises(SystemExit):
+ ykman_cli('otp', '--access-code', '111111111111', 'settings',
+ '--new-access-code', '222222222222', '2', '-f')
+
+ ykman_cli('otp', '--access-code', '111111111111', 'delete', '2', '-f')
+
+ def test_delete_access_code_fails_on_yk_432_to_435(self):
+ self._require_version_between((4, 3, 1), (4, 3, 6))
+
+ ykman_cli('otp', '--access-code', '111111111111', 'static', '2',
+ '--generate', '--length', '10')
+
+ self._check_slot_2_programmed()
+
+ with self.assertRaises(SystemExit):
+ ykman_cli('otp', '--access-code', '111111111111', 'settings',
+ '--delete-access-code', '2', '-f')
+
+ with self.assertRaises(SystemExit):
+ ykman_cli('otp', 'delete', '2', '-f')
+
+ ykman_cli('otp', '--access-code', '111111111111', 'delete', '2', '-f')
+
+ def test_update_access_code_slot_2(self):
+ self._require_version_not_between((4, 3, 1), (4, 3, 6))
+
+ ykman_cli('otp', 'static', '2', '--generate', '--length', '10')
+
+ self._check_slot_2_programmed()
+ self._check_slot_2_does_not_have_access_code()
+
+ ykman_cli('otp', 'settings', '--new-access-code', '111111111111', '2',
+ '-f')
+ self._check_slot_2_has_access_code()
+
+ ykman_cli('otp', '--access-code', '111111111111', 'settings',
+ '--delete-access-code', '2', '-f')
+ self._check_slot_2_does_not_have_access_code()
+
+ ykman_cli('otp', 'delete', '2', '-f')
+
+ def test_update_access_code_prompt_slot_2(self):
+ self._require_version_not_between((4, 3, 1), (4, 3, 6))
+
+ ykman_cli('otp', 'static', '2', '--generate', '--length', '10')
+
+ self._check_slot_2_programmed()
+ self._check_slot_2_does_not_have_access_code()
+
+ ykman_cli('otp', 'settings', '--new-access-code', '', '2',
+ '-f', input='111111111111')
+ self._check_slot_2_has_access_code()
+
+ ykman_cli('otp', '--access-code', '', 'settings',
+ '--delete-access-code', '2', '-f', input='111111111111')
+ self._check_slot_2_does_not_have_access_code()
+
+ ykman_cli('otp', 'delete', '2', '-f')
+
+ def test_new_access_code_conflicts_with_delete_access_code(self):
+ self._require_version_not_between((4, 3, 1), (4, 3, 6))
+
+ ykman_cli('otp', 'static', '2', '--generate', '--length', '10')
+
+ self._check_slot_2_programmed()
+ self._check_slot_2_does_not_have_access_code()
+
+ with self.assertRaises(SystemExit):
+ ykman_cli('otp', 'settings', '--delete-access-code',
+ '--new-access-code', '111111111111', '2', '-f')
+ self._check_slot_2_does_not_have_access_code()
+
+ ykman_cli('otp', 'settings', '--new-access-code', '111111111111', '2',
+ '-f')
+
+ with self.assertRaises(SystemExit):
+ ykman_cli('otp', 'settings', '--delete-access-code',
+ '--new-access-code', '111111111111', '2', '-f')
+ self._check_slot_2_has_access_code()
+
+ ykman_cli('otp', '--access-code', '111111111111', 'delete', '2', '-f')
+
+ def _check_slot_2_programmed(self):
+ status = ykman_cli('otp', 'info')
+ self.assertIn('Slot 2: programmed', status)
+
+ def _check_slot_2_not_programmed(self):
+ status = ykman_cli('otp', 'info')
+ self.assertIn('Slot 2: empty', status)
+
+ def _check_slot_2_has_access_code(self):
+ with self.assertRaises(SystemExit):
+ ykman_cli('otp', 'settings', '--pacing', '0', '2', '-f')
+
+ ykman_cli('otp', '--access-code', '111111111111', 'settings',
+ '--pacing', '0', '2', '-f')
+
+ def _check_slot_2_does_not_have_access_code(self):
+ ykman_cli('otp', 'settings', '--pacing', '0', '2', '-f')
+
+
+ at unittest.skipIf(*missing_mode(TRANSPORT.OTP))
+class TestSlotCalculate(DestructiveYubikeyTestCase):
+
+ def test_calculate_hex(self):
+ ykman_cli('otp', 'delete', '2', '-f')
+ ykman_cli('otp', 'chalresp', '2', 'abba', '-f')
+ output = ykman_cli('otp', 'calculate', '2', 'abba')
+ self.assertIn('f8de2586056d89d8b961a072d1245a495d2155e1', output)
+
+ def test_calculate_totp(self):
+ ykman_cli('otp', 'delete', '2', '-f')
+ ykman_cli('otp', 'chalresp', '2', 'abba', '-f')
+ output = ykman_cli('otp', 'calculate', '2', '999', '-T')
+ self.assertEqual('533486', output.strip())
+ output = ykman_cli('otp', 'calculate', '2', '999', '-T', '-d', '8')
+ self.assertEqual('04533486', output.strip())
+ output = ykman_cli('otp', 'calculate', '2', '-T')
+ self.assertEqual(6, len(output.strip()))
+ output = ykman_cli('otp', 'calculate', '2', '-T', '-d', '8')
+ self.assertEqual(8, len(output.strip()))
+
+
+ at unittest.skipIf(not is_fips(), 'Only applicable to YubiKey FIPS.')
+class TestFipsMode(DestructiveYubikeyTestCase):
+
+ def tearDown(self):
+ ykman_cli('otp', '--access-code', '111111111111', 'delete', '1', '-f')
+ ykman_cli('otp', '--access-code', '111111111111', 'delete', '2', '-f')
+
+ def test_not_fips_mode_if_no_slot_programmed(self):
+ ykman_cli('otp', 'delete', '1', '-f')
+ ykman_cli('otp', 'delete', '2', '-f')
+
+ info = ykman_cli('otp', 'info')
+ self.assertIn('FIPS Approved Mode: No', info)
+
+ def test_not_fips_mode_if_slot_1_not_programmed(self):
+ ykman_cli('otp', 'delete', '1', '-f')
+ ykman_cli('otp', 'static', '2', '--generate', '--length', '10')
+
+ info = ykman_cli('otp', 'info')
+ self.assertIn('FIPS Approved Mode: No', info)
+
+ def test_not_fips_mode_if_slot_2_not_programmed(self):
+ ykman_cli('otp', 'static', '1', '--generate', '--length', '10')
+ ykman_cli('otp', 'delete', '2', '-f')
+
+ info = ykman_cli('otp', 'info')
+ self.assertIn('FIPS Approved Mode: No', info)
+
+ def test_not_fips_mode_if_no_slot_has_access_code(self):
+ ykman_cli('otp', 'static', '1', '--generate', '--length', '10')
+ ykman_cli('otp', 'static', '2', '--generate', '--length', '10')
+
+ info = ykman_cli('otp', 'info')
+ self.assertIn('FIPS Approved Mode: No', info)
+
+ def test_not_fips_mode_if_only_slot_1_has_access_code(self):
+ ykman_cli('otp', 'static', '1', '--generate', '--length', '10')
+ ykman_cli('otp', 'static', '2', '--generate', '--length', '10')
+
+ ykman_cli('otp', 'settings', '--new-access-code', '111111111111', '1',
+ '-f')
+
+ info = ykman_cli('otp', 'info')
+ self.assertIn('FIPS Approved Mode: No', info)
+
+ def test_not_fips_mode_if_only_slot_2_has_access_code(self):
+ ykman_cli('otp', 'static', '1', '--generate', '--length', '10')
+ ykman_cli('otp', 'static', '2', '--generate', '--length', '10')
+
+ ykman_cli('otp', 'settings', '--new-access-code', '111111111111', '2',
+ '-f')
+
+ info = ykman_cli('otp', 'info')
+ self.assertIn('FIPS Approved Mode: No', info)
+
+ def test_fips_mode_if_both_slots_have_access_code(self):
+ ykman_cli('otp', 'static', '1', '--generate', '--length', '10', '-f')
+ ykman_cli('otp', 'static', '2', '--generate', '--length', '10', '-f')
+
+ ykman_cli('otp', 'settings', '--new-access-code', '111111111111', '1',
+ '-f')
+ ykman_cli('otp', 'settings', '--new-access-code', '111111111111', '2',
+ '-f')
+
+ info = ykman_cli('otp', 'info')
+ self.assertIn('FIPS Approved Mode: Yes', info)
diff -Nru yubikey-manager-0.7.1/test/on_yubikey/test_fips_u2f_commands.py yubikey-manager-2.0.0/test/on_yubikey/test_fips_u2f_commands.py
--- yubikey-manager-0.7.1/test/on_yubikey/test_fips_u2f_commands.py 1969-12-31 19:00:00.000000000 -0500
+++ yubikey-manager-2.0.0/test/on_yubikey/test_fips_u2f_commands.py 2018-11-08 06:33:42.000000000 -0500
@@ -0,0 +1,110 @@
+import struct
+import unittest
+
+from fido2.hid import (CTAPHID)
+from ykman.util import (TRANSPORT)
+from ykman.driver_fido import (FIPS_U2F_CMD)
+from .util import (DestructiveYubikeyTestCase, is_fips, open_device)
+
+
+HID_CMD = 0x03
+P1 = 0
+P2 = 0
+
+
+ at unittest.skipIf(not is_fips(), 'YubiKey FIPS required.')
+class TestFipsU2fCommands(DestructiveYubikeyTestCase):
+
+ def test_echo_command(self):
+ dev = open_device(transports=TRANSPORT.FIDO)
+
+ res = dev.driver._dev.call(
+ CTAPHID.MSG,
+ struct.pack(
+ '>HBBBH6s',
+ FIPS_U2F_CMD.ECHO, P1, P2, 0, 6, b'012345'
+ ))
+
+ self.assertEqual(res, b'012345\x90\x00')
+
+ def test_pin_commands(self):
+ # Assumes PIN is 012345 or not set at beginning of test
+ # Sets PIN to 012345
+
+ dev = open_device(transports=TRANSPORT.FIDO)
+
+ verify_res1 = dev.driver._dev.call(
+ CTAPHID.MSG,
+ struct.pack(
+ '>HBBBH6s',
+ FIPS_U2F_CMD.VERIFY_PIN, P1, P2, 0, 6, b'012345'
+ ))
+
+ if verify_res1 == b'\x63\xc0':
+ self.skipTest('PIN set to something other than 012345')
+
+ if verify_res1 == b'\x69\x83':
+ self.skipTest('PIN blocked')
+
+ if verify_res1 == b'\x90\x00':
+ res = dev.driver._dev.call(
+ CTAPHID.MSG,
+ struct.pack(
+ '>HBBBHB6s6s',
+ FIPS_U2F_CMD.SET_PIN, P1, P2, 0, 13, 6, b'012345', b'012345'
+ ))
+ else:
+ res = dev.driver._dev.call(
+ CTAPHID.MSG,
+ struct.pack(
+ '>HBBBHB6s',
+ FIPS_U2F_CMD.SET_PIN, P1, P2, 0, 7, 6, b'012345'
+ ))
+
+ verify_res2 = dev.driver._dev.call(
+ CTAPHID.MSG,
+ struct.pack(
+ '>HBBBH6s',
+ FIPS_U2F_CMD.VERIFY_PIN, P1, P2, 0, 6, b'543210'
+ ))
+
+ verify_res3 = dev.driver._dev.call(
+ CTAPHID.MSG,
+ struct.pack(
+ '>HBBBH6s',
+ FIPS_U2F_CMD.VERIFY_PIN, P1, P2, 0, 6, b'012345'
+ ))
+
+ self.assertIn(verify_res1, [b'\x90\x00', b'\x69\x86']) # OK / not set
+ self.assertEqual(res, b'\x90\x00') # Success
+ self.assertEqual(verify_res2, b'\x63\xc0') # Incorrect PIN
+ self.assertEqual(verify_res3, b'\x90\x00') # Success
+
+ def test_reset_command(self):
+ dev = open_device(transports=TRANSPORT.FIDO)
+
+ res = dev.driver._dev.call(
+ CTAPHID.MSG,
+ struct.pack(
+ '>HBB',
+ FIPS_U2F_CMD.RESET, P1, P2
+ ))
+
+ # 0x6985: Touch required
+ # 0x6986: Power cycle required
+ # 0x9000: Success
+ self.assertIn(res, [b'\x69\x85', b'\x69\x86', b'\x90\x00'])
+
+ def test_verify_fips_mode_command(self):
+ dev = open_device(transports=TRANSPORT.FIDO)
+
+ res = dev.driver._dev.call(
+ CTAPHID.MSG,
+ struct.pack(
+ '>HBB',
+ FIPS_U2F_CMD.VERIFY_FIPS_MODE, P1, P2
+ ))
+
+ # 0x6a81: Function not supported (PIN not set - not FIPS Approved Mode)
+ # 0x9000: Success (PIN set - FIPS Approved Mode)
+ self.assertIn(res, [b'\x6a\x81', b'\x90\x00'])
diff -Nru yubikey-manager-0.7.1/test/on_yubikey/test_interfaces.py yubikey-manager-2.0.0/test/on_yubikey/test_interfaces.py
--- yubikey-manager-0.7.1/test/on_yubikey/test_interfaces.py 1969-12-31 19:00:00.000000000 -0500
+++ yubikey-manager-2.0.0/test/on_yubikey/test_interfaces.py 2018-11-08 06:33:42.000000000 -0500
@@ -0,0 +1,19 @@
+from .util import DestructiveYubikeyTestCase
+from ykman import driver_fido, driver_otp, driver_ccid
+
+
+class TestInterfaces(DestructiveYubikeyTestCase):
+
+ def test_switch_interfaces(self):
+ next(driver_fido.open_devices()).read_config()
+ next(driver_otp.open_devices()).read_config()
+ next(driver_fido.open_devices()).read_config()
+ next(driver_ccid.open_devices()).read_config()
+ next(driver_otp.open_devices()).read_config()
+ next(driver_ccid.open_devices()).read_config()
+ next(driver_otp.open_devices()).read_config()
+ next(driver_fido.open_devices()).read_config()
+ next(driver_ccid.open_devices()).read_config()
+ next(driver_fido.open_devices()).read_config()
+ next(driver_ccid.open_devices()).read_config()
+ next(driver_otp.open_devices()).read_config()
diff -Nru yubikey-manager-0.7.1/test/on_yubikey/test_piv.py yubikey-manager-2.0.0/test/on_yubikey/test_piv.py
--- yubikey-manager-0.7.1/test/on_yubikey/test_piv.py 1969-12-31 19:00:00.000000000 -0500
+++ yubikey-manager-2.0.0/test/on_yubikey/test_piv.py 2018-12-20 08:58:43.000000000 -0500
@@ -0,0 +1,495 @@
+from __future__ import unicode_literals
+
+import datetime
+import random
+import unittest
+from binascii import a2b_hex
+from cryptography import x509
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import ec
+from ykman.driver_ccid import APDUError
+from ykman.piv import (ALGO, PIN_POLICY, PivController, SLOT, TOUCH_POLICY)
+from ykman.piv import (
+ AuthenticationBlocked, AuthenticationFailed, WrongPuk, KeypairMismatch)
+from ykman.util import TRANSPORT, parse_certificates, parse_private_key
+from .util import (
+ DestructiveYubikeyTestCase, missing_mode, open_device, get_version, is_fips)
+from ..util import open_file
+
+
+DEFAULT_PIN = '123456'
+NON_DEFAULT_PIN = '654321'
+DEFAULT_PUK = '12345678'
+NON_DEFAULT_PUK = '87654321'
+DEFAULT_MANAGEMENT_KEY = a2b_hex('010203040506070801020304050607080102030405060708') # noqa: E501
+NON_DEFAULT_MANAGEMENT_KEY = a2b_hex('010103040506070801020304050607080102030405060708') # noqa: E501
+
+
+now = datetime.datetime.now
+
+no_pin_policy = (get_version() is not None and get_version() < (4, 0, 0),
+ 'PIN policies not supported.')
+
+
+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)
+
+
+ at unittest.skipIf(*missing_mode(TRANSPORT.CCID))
+class PivTestCase(DestructiveYubikeyTestCase):
+
+ def setUp(self):
+ self.dev = open_device(transports=TRANSPORT.CCID)
+ self.controller = PivController(self.dev.driver)
+
+ def tearDown(self):
+ self.dev.driver.close()
+
+ def assertMgmKeyIs(self, key):
+ self.controller.authenticate(key)
+
+ def assertMgmKeyIsNot(self, key):
+ with self.assertRaises(AuthenticationFailed):
+ self.controller.authenticate(key)
+
+ def assertStoredMgmKeyEquals(self, key):
+ self.assertEqual(self.controller._pivman_protected_data.key, key)
+
+ def assertStoredMgmKeyNotEquals(self, key):
+ self.assertNotEqual(self.controller._pivman_protected_data.key, key)
+
+ def reconnect(self):
+ self.dev.driver.close()
+ self.dev = open_device(transports=TRANSPORT.CCID)
+ self.controller = PivController(self.dev.driver)
+
+
+class KeyManagement(PivTestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ with open_device(transports=TRANSPORT.CCID) as dev:
+ controller = PivController(dev.driver)
+ controller.reset()
+
+ def generate_key(self, slot, alg=ALGO.ECCP256, pin_policy=None):
+ self.controller.authenticate(DEFAULT_MANAGEMENT_KEY)
+ public_key = self.controller.generate_key(
+ slot, alg, pin_policy=pin_policy,
+ touch_policy=TOUCH_POLICY.NEVER)
+ self.reconnect()
+ return public_key
+
+ def test_delete_certificate_requires_authentication(self):
+ self.generate_key(SLOT.AUTHENTICATION)
+
+ with self.assertRaises(APDUError):
+ self.controller.delete_certificate(SLOT.AUTHENTICATION)
+
+ self.controller.authenticate(DEFAULT_MANAGEMENT_KEY)
+ self.controller.delete_certificate(SLOT.AUTHENTICATION)
+
+ def test_generate_csr_works(self):
+ public_key = self.generate_key(SLOT.AUTHENTICATION)
+ if get_version() < (4, 0, 0):
+ # NEO always has PIN policy "ONCE"
+ self.controller.verify(DEFAULT_PIN)
+
+ self.controller.verify(DEFAULT_PIN)
+ csr = self.controller.generate_certificate_signing_request(
+ SLOT.AUTHENTICATION, public_key, 'alice')
+
+ self.assertEqual(
+ csr.public_key().public_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PublicFormat.SubjectPublicKeyInfo),
+ public_key.public_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PublicFormat.SubjectPublicKeyInfo),
+ )
+ self.assertEqual(
+ csr.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0].value, # noqa: E501
+ 'alice'
+ )
+
+ def test_generate_self_signed_certificate_requires_authentication(self):
+ public_key = self.generate_key(SLOT.AUTHENTICATION)
+ if get_version() < (4, 0, 0):
+ # NEO always has PIN policy "ONCE"
+ self.controller.verify(DEFAULT_PIN)
+
+ with self.assertRaises(APDUError):
+ self.controller.generate_self_signed_certificate(
+ SLOT.AUTHENTICATION, public_key, 'alice', now(), now())
+
+ self.controller.authenticate(DEFAULT_MANAGEMENT_KEY)
+ self.controller.verify(DEFAULT_PIN)
+ self.controller.generate_self_signed_certificate(
+ SLOT.AUTHENTICATION, public_key, 'alice', now(), now())
+
+ def _test_generate_self_signed_certificate(self, slot):
+ public_key = self.generate_key(slot)
+ self.controller.authenticate(DEFAULT_MANAGEMENT_KEY)
+ self.controller.verify(DEFAULT_PIN)
+ self.controller.generate_self_signed_certificate(
+ slot, public_key, 'alice', now(), now())
+
+ cert = self.controller.read_certificate(slot)
+
+ self.assertEqual(
+ cert.public_key().public_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PublicFormat.SubjectPublicKeyInfo),
+ public_key.public_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PublicFormat.SubjectPublicKeyInfo),
+ )
+ self.assertEqual(
+ cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0].value, # noqa: E501
+ 'alice'
+ )
+
+ def test_generate_self_signed_certificate_slot_9a_works(self):
+ self._test_generate_self_signed_certificate(SLOT.AUTHENTICATION)
+
+ def test_generate_self_signed_certificate_slot_9c_works(self):
+ self._test_generate_self_signed_certificate(SLOT.SIGNATURE)
+
+ def test_generate_key_requires_authentication(self):
+ with self.assertRaises(APDUError):
+ self.controller.generate_key(SLOT.AUTHENTICATION, ALGO.ECCP256,
+ touch_policy=TOUCH_POLICY.NEVER)
+
+ self.controller.authenticate(DEFAULT_MANAGEMENT_KEY)
+ self.controller.generate_key(SLOT.AUTHENTICATION, ALGO.ECCP256,
+ touch_policy=TOUCH_POLICY.NEVER)
+
+ def test_import_certificate_requires_authentication(self):
+ cert = get_test_cert()
+ with self.assertRaises(APDUError):
+ self.controller.import_certificate(SLOT.AUTHENTICATION, cert,
+ verify=False)
+
+ self.controller.authenticate(DEFAULT_MANAGEMENT_KEY)
+ self.controller.import_certificate(SLOT.AUTHENTICATION, cert,
+ verify=False)
+
+ def _test_import_key_pairing(self, alg1, alg2):
+ # Set up a key in the slot and create a certificate for it
+ public_key = self.generate_key(
+ SLOT.AUTHENTICATION, alg=alg1, pin_policy=PIN_POLICY.NEVER)
+ self.controller.authenticate(DEFAULT_MANAGEMENT_KEY)
+ self.controller.generate_self_signed_certificate(
+ SLOT.AUTHENTICATION, public_key, 'test', datetime.datetime.now(),
+ datetime.datetime.now())
+ cert = self.controller.read_certificate(SLOT.AUTHENTICATION)
+ self.controller.delete_certificate(SLOT.AUTHENTICATION)
+
+ # Importing the correct certificate should work
+ self.controller.import_certificate(SLOT.AUTHENTICATION, cert,
+ verify=True)
+
+ # Overwrite the key with one of the same type
+ self.generate_key(
+ SLOT.AUTHENTICATION, alg=alg1, pin_policy=PIN_POLICY.NEVER)
+ # Importing the same certificate should not work with the new key
+ self.controller.authenticate(DEFAULT_MANAGEMENT_KEY)
+ with self.assertRaises(KeypairMismatch):
+ self.controller.import_certificate(SLOT.AUTHENTICATION, cert,
+ verify=True)
+
+ # Overwrite the key with one of a different type
+ self.generate_key(
+ SLOT.AUTHENTICATION, alg=alg2, pin_policy=PIN_POLICY.NEVER)
+ # Importing the same certificate should not work with the new key
+ self.controller.authenticate(DEFAULT_MANAGEMENT_KEY)
+ with self.assertRaises(KeypairMismatch):
+ self.controller.import_certificate(SLOT.AUTHENTICATION, cert,
+ verify=True)
+
+ @unittest.skipIf(is_fips(), 'Not applicable to YubiKey FIPS.')
+ def test_import_certificate_verifies_key_pairing_rsa1024(self):
+ self._test_import_key_pairing(ALGO.RSA1024, ALGO.ECCP256)
+
+ def test_import_certificate_verifies_key_pairing_rsa2048(self):
+ self._test_import_key_pairing(ALGO.RSA2048, ALGO.ECCP256)
+
+ def test_import_certificate_verifies_key_pairing_eccp256(self):
+ self._test_import_key_pairing(ALGO.ECCP256, ALGO.ECCP384)
+
+ def test_import_certificate_verifies_key_pairing_eccp384(self):
+ self._test_import_key_pairing(ALGO.ECCP384, ALGO.ECCP256)
+
+ def test_import_key_requires_authentication(self):
+ private_key = get_test_key()
+ with self.assertRaises(APDUError):
+ self.controller.import_key(SLOT.AUTHENTICATION, private_key)
+
+ self.controller.authenticate(DEFAULT_MANAGEMENT_KEY)
+ self.controller.import_key(SLOT.AUTHENTICATION, private_key)
+
+ def test_read_certificate_does_not_require_authentication(self):
+ cert = get_test_cert()
+ self.controller.authenticate(DEFAULT_MANAGEMENT_KEY)
+ self.controller.import_certificate(SLOT.AUTHENTICATION, cert,
+ verify=False)
+
+ self.reconnect()
+
+ cert = self.controller.read_certificate(SLOT.AUTHENTICATION)
+ self.assertIsNotNone(cert)
+
+
+class ManagementKeyReadOnly(PivTestCase):
+ """
+ Tests after which the management key is always the default management key.
+ Placing compatible tests here reduces the amount of slow reset calls needed.
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ with open_device(transports=TRANSPORT.CCID) as dev:
+ PivController(dev.driver).reset()
+
+ def test_authenticate_twice_does_not_throw(self):
+ self.controller.authenticate(DEFAULT_MANAGEMENT_KEY)
+ self.controller.authenticate(DEFAULT_MANAGEMENT_KEY)
+
+ def test_reset_resets_has_stored_key_flag(self):
+ self.assertFalse(self.controller.has_stored_key)
+
+ self.controller.verify(DEFAULT_PIN)
+ self.controller.authenticate(DEFAULT_MANAGEMENT_KEY)
+ self.controller.set_mgm_key(None, store_on_device=True)
+
+ self.assertTrue(self.controller.has_stored_key)
+
+ self.reconnect()
+ self.controller.reset()
+
+ self.assertFalse(self.controller.has_stored_key)
+
+ def test_reset_while_verified_throws_nice_ValueError(self):
+ self.controller.verify(DEFAULT_PIN)
+ with self.assertRaises(ValueError) as cm:
+ self.controller.reset()
+ self.assertTrue(
+ 'Cannot read remaining tries from status word: 9000'
+ in str(cm.exception))
+
+ def test_set_mgm_key_does_not_change_key_if_not_authenticated(self):
+ with self.assertRaises(APDUError):
+ self.controller.set_mgm_key(NON_DEFAULT_MANAGEMENT_KEY)
+ self.assertMgmKeyIs(DEFAULT_MANAGEMENT_KEY)
+
+ @unittest.skipIf(get_version() is not None and get_version() < (3, 5, 0),
+ 'Known fixed bug')
+ def test_set_stored_mgm_key_does_not_destroy_key_if_pin_not_verified(self):
+ self.controller.authenticate(DEFAULT_MANAGEMENT_KEY)
+ with self.assertRaises(APDUError):
+ self.controller.set_mgm_key(None, store_on_device=True)
+
+ self.assertMgmKeyIs(DEFAULT_MANAGEMENT_KEY)
+
+
+class ManagementKeyReadWrite(PivTestCase):
+ """
+ Tests after which the management key may not be the default management key.
+ """
+
+ def setUp(self):
+ PivTestCase.setUp(self)
+ self.controller.reset()
+
+ def test_set_mgm_key_changes_mgm_key(self):
+ self.controller.authenticate(DEFAULT_MANAGEMENT_KEY)
+ self.controller.set_mgm_key(NON_DEFAULT_MANAGEMENT_KEY)
+
+ self.assertMgmKeyIsNot(DEFAULT_MANAGEMENT_KEY)
+ self.assertMgmKeyIs(NON_DEFAULT_MANAGEMENT_KEY)
+
+ def test_set_stored_mgm_key_succeeds_if_pin_is_verified(self):
+ self.controller.verify(DEFAULT_PIN)
+ self.controller.authenticate(DEFAULT_MANAGEMENT_KEY)
+ self.controller.set_mgm_key(NON_DEFAULT_MANAGEMENT_KEY,
+ store_on_device=True)
+
+ self.assertMgmKeyIsNot(DEFAULT_MANAGEMENT_KEY)
+ self.assertMgmKeyIs(NON_DEFAULT_MANAGEMENT_KEY)
+ self.assertStoredMgmKeyEquals(NON_DEFAULT_MANAGEMENT_KEY)
+ self.assertMgmKeyIs(self.controller._pivman_protected_data.key)
+
+ def test_set_stored_random_mgm_key_succeeds_if_pin_is_verified(self):
+ self.controller.verify(DEFAULT_PIN)
+ self.controller.authenticate(DEFAULT_MANAGEMENT_KEY)
+ self.controller.set_mgm_key(None, store_on_device=True)
+
+ self.assertMgmKeyIsNot(DEFAULT_MANAGEMENT_KEY)
+ self.assertMgmKeyIsNot(NON_DEFAULT_MANAGEMENT_KEY)
+ self.assertMgmKeyIs(self.controller._pivman_protected_data.key)
+ self.assertStoredMgmKeyNotEquals(DEFAULT_MANAGEMENT_KEY)
+ self.assertStoredMgmKeyNotEquals(NON_DEFAULT_MANAGEMENT_KEY)
+
+
+class Operations(PivTestCase):
+
+ def setUp(self):
+ PivTestCase.setUp(self)
+ self.controller.reset()
+
+ def generate_key(self, pin_policy=None):
+ self.controller.authenticate(DEFAULT_MANAGEMENT_KEY)
+ public_key = self.controller.generate_key(
+ SLOT.AUTHENTICATION, ALGO.ECCP256, pin_policy=pin_policy,
+ touch_policy=TOUCH_POLICY.NEVER)
+ self.reconnect()
+ return public_key
+
+ @unittest.skipIf(*no_pin_policy)
+ def test_sign_with_pin_policy_always_requires_pin_every_time(self):
+ self.generate_key(pin_policy=PIN_POLICY.ALWAYS)
+
+ with self.assertRaises(APDUError):
+ self.controller.sign(SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo')
+
+ self.controller.verify(DEFAULT_PIN)
+ sig = self.controller.sign(SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo')
+ self.assertIsNotNone(sig)
+
+ with self.assertRaises(APDUError):
+ self.controller.sign(SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo')
+
+ self.controller.verify(DEFAULT_PIN)
+ sig = self.controller.sign(SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo')
+ self.assertIsNotNone(sig)
+
+ @unittest.skipIf(is_fips(), 'Not applicable to YubiKey FIPS.')
+ @unittest.skipIf(*no_pin_policy)
+ def test_sign_with_pin_policy_never_does_not_require_pin(self):
+ self.generate_key(pin_policy=PIN_POLICY.NEVER)
+ sig = self.controller.sign(SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo')
+ self.assertIsNotNone(sig)
+
+ @unittest.skipIf(not is_fips(), 'YubiKey FIPS required.')
+ def test_pin_policy_never_blocked_on_fips(self):
+ with self.assertRaises(APDUError):
+ self.generate_key(pin_policy=PIN_POLICY.NEVER)
+
+ def test_sign_with_pin_policy_once_requires_pin_once_per_session(self):
+ self.generate_key(pin_policy=PIN_POLICY.ONCE)
+
+ with self.assertRaises(APDUError):
+ self.controller.sign(SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo')
+
+ self.controller.verify(DEFAULT_PIN)
+ sig = self.controller.sign(SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo')
+ self.assertIsNotNone(sig)
+
+ sig = self.controller.sign(SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo')
+ self.assertIsNotNone(sig)
+
+ self.reconnect()
+
+ with self.assertRaises(APDUError):
+ self.controller.sign(SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo')
+
+ self.controller.verify(DEFAULT_PIN)
+ sig = self.controller.sign(SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo')
+ self.assertIsNotNone(sig)
+
+ sig = self.controller.sign(SLOT.AUTHENTICATION, ALGO.ECCP256, b'foo')
+ self.assertIsNotNone(sig)
+
+ def test_signature_can_be_verified_by_public_key(self):
+ public_key = self.generate_key(pin_policy=PIN_POLICY.ONCE)
+
+ signed_data = bytes(random.randint(0, 255) for i in range(32))
+
+ self.controller.verify(DEFAULT_PIN)
+ sig = self.controller.sign(
+ SLOT.AUTHENTICATION, ALGO.ECCP256, signed_data)
+ self.assertIsNotNone(sig)
+
+ public_key.verify(
+ sig, signed_data,
+ ec.ECDSA(hashes.SHA256()))
+
+
+class UnblockPin(PivTestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ with open_device(transports=TRANSPORT.CCID) as dev:
+ controller = PivController(dev.driver)
+ controller.reset()
+
+ def block_pin(self):
+ while self.controller.get_pin_tries() > 0:
+ try:
+ self.controller.verify(NON_DEFAULT_PIN)
+ except Exception:
+ pass
+
+ def test_unblock_pin_requires_no_previous_authentication(self):
+ self.controller.unblock_pin(DEFAULT_PUK, NON_DEFAULT_PIN)
+
+ def test_unblock_pin_with_wrong_puk_throws_WrongPuk(self):
+ with self.assertRaises(WrongPuk):
+ self.controller.unblock_pin(NON_DEFAULT_PUK, NON_DEFAULT_PIN)
+
+ def test_unblock_pin_resets_pin_and_retries(self):
+ self.controller.reset()
+ self.reconnect()
+
+ self.controller.verify(DEFAULT_PIN, NON_DEFAULT_PIN)
+ self.reconnect()
+
+ self.block_pin()
+
+ with self.assertRaises(AuthenticationBlocked):
+ self.controller.verify(DEFAULT_PIN)
+
+ self.controller.unblock_pin(DEFAULT_PUK, NON_DEFAULT_PIN)
+
+ self.assertEqual(self.controller.get_pin_tries(), 3)
+ self.controller.verify(NON_DEFAULT_PIN)
+
+ def test_set_pin_retries_requires_pin_and_mgm_key(self):
+ # Fails with no authentication
+ with self.assertRaises(APDUError):
+ self.controller.set_pin_retries(4, 4)
+
+ # Fails with only PIN
+ self.controller.verify(DEFAULT_PIN)
+ with self.assertRaises(APDUError):
+ self.controller.set_pin_retries(4, 4)
+
+ self.reconnect()
+
+ # Fails with only management key
+ self.controller.authenticate(DEFAULT_MANAGEMENT_KEY)
+ with self.assertRaises(APDUError):
+ self.controller.set_pin_retries(4, 4)
+
+ # Succeeds with both PIN and management key
+ self.controller.verify(DEFAULT_PIN)
+ self.controller.set_pin_retries(4, 4)
+
+ def test_set_pin_retries_sets_pin_and_puk_tries(self):
+ pin_tries = 9
+ puk_tries = 7
+
+ self.controller.verify(DEFAULT_PIN)
+ self.controller.authenticate(DEFAULT_MANAGEMENT_KEY)
+ self.controller.set_pin_retries(pin_tries, puk_tries)
+
+ self.reconnect()
+
+ self.assertEqual(self.controller.get_pin_tries(), pin_tries)
+ self.assertEqual(self.controller._get_puk_tries(), puk_tries - 1)
diff -Nru yubikey-manager-0.7.1/test/on_yubikey/util.py yubikey-manager-2.0.0/test/on_yubikey/util.py
--- yubikey-manager-0.7.1/test/on_yubikey/util.py 1969-12-31 19:00:00.000000000 -0500
+++ yubikey-manager-2.0.0/test/on_yubikey/util.py 2018-12-20 08:58:43.000000000 -0500
@@ -0,0 +1,131 @@
+from __future__ import print_function
+import click
+import os
+import sys
+import unittest
+import ykman.descriptor
+from ykman.util import (
+ is_cve201715361_vulnerable_firmware_version, TRANSPORT)
+import test.util
+
+
+_one_yubikey = False
+_the_yubikey = None
+_skip = True
+
+_test_serial = os.environ.get('DESTRUCTIVE_TEST_YUBIKEY_SERIAL')
+_no_prompt = os.environ.get('DESTRUCTIVE_TEST_DO_NOT_PROMPT') == 'TRUE'
+
+if _test_serial is not None:
+ _one_yubikey = len(ykman.descriptor.get_descriptors()) == 1
+
+ _skip = False
+
+ if (_one_yubikey):
+ if not _no_prompt:
+ click.confirm(
+ 'Run integration tests? This will erase data on the YubiKey'
+ ' with serial number: %s. Make sure it is a key used for'
+ ' development.'
+ % _test_serial,
+ abort=True)
+ try:
+ _the_yubikey = ykman.descriptor.open_device(
+ serial=int(_test_serial), attempts=2)
+
+ except Exception:
+ print('Failed to open device. Please make sure you have connected'
+ ' the YubiKey with serial number: {}'.format(_test_serial),
+ file=sys.stderr)
+ sys.exit(1)
+
+
+def _missing_mode(mode):
+ if not _one_yubikey:
+ return False
+ return not _the_yubikey.mode.has_transport(mode)
+
+
+def get_version():
+ if not _one_yubikey:
+ return None
+ return _the_yubikey.version
+
+
+def can_write_config():
+ if _one_yubikey:
+ return _the_yubikey.can_write_config
+ else:
+ return False
+
+
+def is_NEO():
+ if _one_yubikey:
+ return get_version() < (4, 0, 0)
+ else:
+ return False
+
+
+def is_fips():
+ if _one_yubikey:
+ return _the_yubikey.is_fips
+ else:
+ return False
+
+
+def _no_attestation():
+ if _one_yubikey:
+ return get_version() < (4, 3, 0)
+ else:
+ return False
+
+
+def _is_cve201715361_vulnerable_yubikey():
+ if _one_yubikey:
+ return is_cve201715361_vulnerable_firmware_version(get_version())
+ else:
+ return False
+
+
+def ykman_cli(*args, **kwargs):
+ return test.util.ykman_cli(
+ '--device', _test_serial,
+ *args, **kwargs
+ )
+
+
+def ykman_cli_raw(*args, **kwargs):
+ return test.util.ykman_cli_raw(
+ '--device', _test_serial,
+ *args, **kwargs
+ )
+
+
+def open_device(transports=sum(TRANSPORT)):
+ return ykman.descriptor.open_device(transports=transports,
+ serial=int(_test_serial))
+
+
+def missing_mode(transport):
+ return (_missing_mode(transport), transport.name + ' needs to be enabled')
+
+
+not_one_yubikey = (not _one_yubikey, 'A single YubiKey needs to be connected.')
+
+destructive_tests_not_activated = (
+ _skip, 'DESTRUCTIVE_TEST_YUBIKEY_SERIAL == None')
+
+no_attestation = (_no_attestation(), 'Attestation not available.')
+
+skip_roca = (
+ _is_cve201715361_vulnerable_yubikey(),
+ 'Not applicable to CVE-2017-15361 affected YubiKey.')
+skip_not_roca = (
+ not _is_cve201715361_vulnerable_yubikey(),
+ 'Applicable only to CVE-2017-15361 affected YubiKey.')
+
+
+ at unittest.skipIf(*destructive_tests_not_activated)
+ at unittest.skipIf(*not_one_yubikey)
+class DestructiveYubikeyTestCase(unittest.TestCase):
+ pass
diff -Nru yubikey-manager-0.7.1/test/test_oath.py yubikey-manager-2.0.0/test/test_oath.py
--- yubikey-manager-0.7.1/test/test_oath.py 2018-07-02 02:27:29.000000000 -0400
+++ yubikey-manager-2.0.0/test/test_oath.py 2018-11-21 08:29:41.000000000 -0500
@@ -12,10 +12,10 @@
self.assertEqual('Issuer', issuer)
self.assertEqual('name', name)
- def test_credential_parse_wierd_issuer_and_name(self):
- issuer, name, period = Credential.parse_key(b'wierd/Issuer:name')
+ def test_credential_parse_weird_issuer_and_name(self):
+ issuer, name, period = Credential.parse_key(b'weird/Issuer:name')
self.assertEqual(30, period)
- self.assertEqual('wierd/Issuer', issuer)
+ self.assertEqual('weird/Issuer', issuer)
self.assertEqual('name', name)
def test_credential_parse_issuer_and_name(self):
diff -Nru yubikey-manager-0.7.1/test/test_piv.py yubikey-manager-2.0.0/test/test_piv.py
--- yubikey-manager-0.7.1/test/test_piv.py 2018-07-02 02:27:29.000000000 -0400
+++ yubikey-manager-2.0.0/test/test_piv.py 2018-12-20 08:58:43.000000000 -0500
@@ -3,6 +3,17 @@
import ykman.piv as piv
import unittest
+from ykman.piv import ALGO
+
+
+class FakeController(object):
+ def __init__(self, version):
+ self.version = version
+
+ @property
+ def is_fips(self):
+ return piv.PivController.is_fips.fget(self)
+
class TestPivFunctions(unittest.TestCase):
@@ -12,3 +23,27 @@
self.assertIsInstance(output1, bytes)
self.assertIsInstance(output2, bytes)
self.assertNotEqual(output1, output2)
+
+ def test_supported_algorithms(self):
+ neo_supported = piv.PivController.supported_algorithms.fget(
+ FakeController((3, 1, 1)))
+ self.assertNotIn(ALGO.TDES, neo_supported)
+ self.assertNotIn(ALGO.ECCP384, neo_supported)
+
+ fips_supported = piv.PivController.supported_algorithms.fget(
+ FakeController((4, 4, 1)))
+ self.assertNotIn(ALGO.TDES, fips_supported)
+ self.assertNotIn(ALGO.RSA1024, fips_supported)
+
+ roca_supported = piv.PivController.supported_algorithms.fget(
+ FakeController((4, 3, 4)))
+ self.assertNotIn(ALGO.TDES, roca_supported)
+ self.assertNotIn(ALGO.RSA1024, roca_supported)
+ self.assertNotIn(ALGO.RSA2048, roca_supported)
+
+ yk5_supported = piv.PivController.supported_algorithms.fget(
+ FakeController((5, 1, 0)))
+ self.assertEqual(
+ set(yk5_supported),
+ set([a for a in ALGO if a != ALGO.TDES]),
+ )
diff -Nru yubikey-manager-0.7.1/test/test_scancodes.py yubikey-manager-2.0.0/test/test_scancodes.py
--- yubikey-manager-0.7.1/test/test_scancodes.py 2018-07-02 02:27:29.000000000 -0400
+++ yubikey-manager-2.0.0/test/test_scancodes.py 2018-11-08 06:33:42.000000000 -0500
@@ -115,6 +115,7 @@
self.assertEqual(b'\xa7', encode(')', KEYBOARD_LAYOUT.US))
self.assertEqual(b'\xa5', encode('*', KEYBOARD_LAYOUT.US))
self.assertEqual(b'\xae', encode('+', KEYBOARD_LAYOUT.US))
+ self.assertEqual(b'\x35', encode('`', KEYBOARD_LAYOUT.US))
self.assertEqual(b'\x36', encode(',', KEYBOARD_LAYOUT.US))
self.assertEqual(b'\x2d', encode('-', KEYBOARD_LAYOUT.US))
self.assertEqual(b'\x37', encode('.', KEYBOARD_LAYOUT.US))
diff -Nru yubikey-manager-0.7.1/test/test_util.py yubikey-manager-2.0.0/test/test_util.py
--- yubikey-manager-0.7.1/test/test_util.py 2018-07-02 02:27:29.000000000 -0400
+++ yubikey-manager-2.0.0/test/test_util.py 2018-12-20 08:58:43.000000000 -0500
@@ -3,7 +3,7 @@
from ykman.util import (bytes2int, format_code, generate_static_pw,
hmac_shorten_key, modhex_decode, modhex_encode,
parse_tlvs, parse_truncated, time_challenge, Tlv,
- is_pkcs12, FORM_FACTOR)
+ is_pkcs12, is_pem, FORM_FACTOR)
from .util import open_file
import unittest
@@ -131,6 +131,31 @@
rsa_2048_key_cert_encrypted_pfx:
self.assertTrue(is_pkcs12(rsa_2048_key_cert_encrypted_pfx.read()))
+ def test_is_pem(self):
+ self.assertFalse(is_pem(b'just a byte string'))
+ self.assertFalse(is_pem(None))
+
+ with open_file('rsa_2048_key.pem') as rsa_2048_key_pem:
+ self.assertTrue(is_pem(rsa_2048_key_pem.read()))
+
+ with open_file('rsa_2048_key_encrypted.pem') as f:
+ self.assertTrue(is_pem(f.read()))
+
+ with open_file('rsa_2048_cert.pem') as rsa_2048_cert_pem:
+ self.assertTrue(is_pem(rsa_2048_cert_pem.read()))
+
+ with open_file('rsa_2048_key_cert.pfx') as rsa_2048_key_cert_pfx:
+ self.assertFalse(is_pem(rsa_2048_key_cert_pfx.read()))
+
+ with open_file('rsa_2048_cert_metadata.pem') as rsa_2048_cert_metadata_pem:
+ self.assertTrue(is_pem(rsa_2048_cert_metadata_pem.read()))
+
+ with open_file(
+ 'rsa_2048_key_cert_encrypted.pfx') as \
+ rsa_2048_key_cert_encrypted_pfx:
+ self.assertFalse(is_pem(rsa_2048_key_cert_encrypted_pfx.read()))
+
+
def test_form_factor_from_code(self):
self.assertEqual(FORM_FACTOR.UNKNOWN, FORM_FACTOR.from_code(None))
with self.assertRaises(ValueError):
diff -Nru yubikey-manager-0.7.1/test/util.py yubikey-manager-2.0.0/test/util.py
--- yubikey-manager-0.7.1/test/util.py 1969-12-31 19:00:00.000000000 -0500
+++ yubikey-manager-2.0.0/test/util.py 2018-12-20 08:58:43.000000000 -0500
@@ -0,0 +1,23 @@
+from click.testing import CliRunner
+from ykman.cli.__main__ import cli
+import os
+
+
+PKG_DIR = os.path.dirname(os.path.abspath(__file__))
+
+
+def open_file(*relative_path):
+ return open(os.path.join(PKG_DIR, 'files', *relative_path), 'rb')
+
+
+def ykman_cli(*argv, **kwargs):
+ result = ykman_cli_raw(*argv, **kwargs)
+ if result.exit_code != 0:
+ raise result.exception
+ return result.output
+
+
+def ykman_cli_raw(*argv, **kwargs):
+ runner = CliRunner()
+ result = runner.invoke(cli, list(argv), obj={}, **kwargs)
+ return result
diff -Nru yubikey-manager-0.7.1/ykman/cli/config.py yubikey-manager-2.0.0/ykman/cli/config.py
--- yubikey-manager-0.7.1/ykman/cli/config.py 2018-07-06 04:10:26.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/cli/config.py 2019-01-02 03:11:40.000000000 -0500
@@ -27,10 +27,11 @@
from __future__ import absolute_import
-from .util import click_skip_on_help, click_force_option, UpperCaseChoice
+from .util import click_postpone_execution, click_force_option, UpperCaseChoice
from ..device import device_config, FLAGS
from ..util import APPLICATION
-from binascii import a2b_hex
+from binascii import a2b_hex, b2a_hex
+import os
import logging
import click
@@ -41,9 +42,14 @@
CLEAR_LOCK_CODE = '0' * 32
+def prompt_lock_code(prompt='Enter your lock code'):
+ return click.prompt(
+ prompt, default='', hide_input=True, show_default=False, err=True)
+
+
@click.group()
@click.pass_context
- at click_skip_on_help
+ at click_postpone_execution
def config(ctx):
"""
Enable/Disable applications.
@@ -51,6 +57,20 @@
The applications may be enabled and disabled independently
over different interfaces (USB and NFC). The configuration may
also be protected by a lock code.
+
+ Examples:
+
+ \b
+ Disable PIV over the NFC interface:
+ $ ykman config nfc --disable PIV
+
+ \b
+ Enable all applications over USB:
+ $ ykman config usb --enable-all
+
+ \b
+ Generate and set a random application lock code:
+ $ ykman config set-lock-code --generate
"""
dev = ctx.obj['dev']
if not dev.can_write_config:
@@ -60,27 +80,30 @@
@config.command('set-lock-code')
@click.pass_context
- at click.option('-l', '--lock-code', help='Current lock code.')
- at click.option('-n', '--new-lock-code', help='New lock code.')
+ at click_force_option
+ at click.option('-l', '--lock-code', metavar='HEX', help='Current lock code.')
+ at 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.')
-def set_lock_code(ctx, lock_code, new_lock_code, clear):
+ at 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 16 byte lock code may be used to protect the application configuration.
+ A lock code may be used to protect the application configuration.
+ The lock code must be a 32 characters (16 bytes) hex value.
"""
dev = ctx.obj['dev']
def prompt_new_lock_code():
- return click.prompt(
- 'Enter your new lock code', default='', hide_input=True,
- show_default=False, confirmation_prompt=True)
+ return prompt_lock_code(prompt='Enter your new lock code')
def prompt_current_lock_code():
- return click.prompt(
- 'Enter your current lock code', default='', hide_input=True,
- show_default=False)
+ return prompt_lock_code(prompt='Enter your current lock code')
def change_lock_code(lock_code, new_lock_code):
lock_code = _parse_lock_code(ctx, lock_code)
@@ -106,9 +129,19 @@
logger.error('Setting the lock code failed', exc_info=e)
ctx.fail('Failed to set the lock code.')
+ if generate and new_lock_code:
+ ctx.fail('Invalid options: --new-lock-code conflicts with --generate.')
+
if clear:
new_lock_code = CLEAR_LOCK_CODE
+ if generate:
+ new_lock_code = b2a_hex(os.urandom(16)).decode('utf-8')
+ click.echo(
+ 'Using a randomly generated lock code: {}'.format(new_lock_code))
+ force or click.confirm(
+ 'Lock configuration with this lock code?', abort=True, err=True)
+
if dev.config.configuration_locked:
if lock_code:
if new_lock_code:
@@ -128,7 +161,7 @@
if lock_code:
ctx.fail(
'There is no current lock code set. '
- 'Use -n/--new-lock-code to set one.')
+ 'Use --new-lock-code to set one.')
else:
if new_lock_code:
set_lock_code(new_lock_code)
@@ -150,8 +183,8 @@
@click.option(
'-a', '--enable-all', is_flag=True, help='Enable all applications.')
@click.option(
- '-L', '--lock-code',
- help='A 16 byte lock code used to protect the application configuration.')
+ '-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).')
@@ -212,9 +245,6 @@
if no_touch_eject:
flags &= ~FLAGS.MODE_FLAG_EJECT
- if lock_code:
- lock_code = _parse_lock_code(ctx, lock_code)
-
for app in enable:
if APPLICATION[app] & usb_supported:
usb_enabled |= APPLICATION[app]
@@ -242,7 +272,23 @@
'Set challenge-response timeout to {}.\n'.format(
chalresp_timeout) if chalresp_timeout else '')
- force or click.confirm(f_confirm, abort=True)
+ is_locked = dev.config.configuration_locked
+
+ if force and is_locked and not lock_code:
+ ctx.fail('Configuration is locked - please supply the --lock-code '
+ 'option.')
+ if lock_code and not is_locked:
+ ctx.fail('Configuration is not locked - please remove the '
+ '--lock-code option.')
+
+ force or click.confirm(f_confirm, abort=True, err=True)
+
+ if is_locked and not lock_code:
+ lock_code = prompt_lock_code()
+
+ if lock_code:
+ lock_code = _parse_lock_code(ctx, lock_code)
+
try:
dev.write_config(
device_config(
@@ -272,8 +318,8 @@
'-D', '--disable-all', is_flag=True, help='Disable all applications')
@click.option('-l', '--list', is_flag=True, help='List enabled applications')
@click.option(
- '-L', '--lock-code',
- help='A 16 byte lock code used to protect the application configuration.')
+ '-L', '--lock-code', metavar='HEX',
+ help='Current application configuration lock code.')
def nfc(ctx, enable, disable, enable_all, disable_all, list, lock_code, force):
"""
Enable or disable applications over NFC.
@@ -290,9 +336,6 @@
_ensure_not_invalid_options(ctx, enable, disable)
- if lock_code:
- lock_code = _parse_lock_code(ctx, lock_code)
-
dev = ctx.obj['dev']
nfc_supported = dev.config.nfc_supported
nfc_enabled = dev.config.nfc_enabled
@@ -322,7 +365,23 @@
', '.join(
[str(APPLICATION[app]) for app in disable])) if disable else '')
- force or click.confirm(f_confirm, abort=True)
+ is_locked = dev.config.configuration_locked
+
+ if force and is_locked and not lock_code:
+ ctx.fail('Configuration is locked - please supply the --lock-code '
+ 'option.')
+ if lock_code and not is_locked:
+ ctx.fail('Configuration is not locked - please remove the '
+ '--lock-code option.')
+
+ force or click.confirm(f_confirm, abort=True, err=True)
+
+ if is_locked and not lock_code:
+ lock_code = prompt_lock_code()
+
+ if lock_code:
+ lock_code = _parse_lock_code(ctx, lock_code)
+
try:
dev.write_config(
device_config(
diff -Nru yubikey-manager-0.7.1/ykman/cli/fido.py yubikey-manager-2.0.0/ykman/cli/fido.py
--- yubikey-manager-0.7.1/ykman/cli/fido.py 2018-07-06 02:37:03.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/cli/fido.py 2019-01-02 03:11:40.000000000 -0500
@@ -31,10 +31,8 @@
from fido2.ctap1 import ApduError
from fido2.ctap import CtapError
from time import sleep
-from .util import click_skip_on_help, prompt_for_touch, click_force_option
-from ..driver_ccid import (
- SW_COMMAND_NOT_ALLOWED, SW_VERIFY_FAIL_NO_RETRY,
- SW_AUTH_METHOD_BLOCKED, SW_WRONG_LENGTH)
+from .util import click_postpone_execution, prompt_for_touch, click_force_option
+from ..driver_ccid import SW
from ..util import TRANSPORT
from ..fido import Fido2Controller, FipsU2fController
from ..descriptor import get_descriptors
@@ -49,20 +47,32 @@
@click.group()
@click.pass_context
- at click_skip_on_help
+ at click_postpone_execution
def fido(ctx):
"""
Manage FIDO applications.
+
+ Examples:
+
+ \b
+ Reset the FIDO (FIDO2 and U2F) applications:
+ $ ykman fido reset
+
+ \b
+ Change the FIDO2 PIN from 123456 to 654321:
+ $ ykman fido set-pin --pin 123456 --new-pin 654321
+
"""
- if ctx.obj['dev'].is_fips:
+ dev = ctx.obj['dev']
+ if dev.is_fips:
try:
- ctx.obj['controller'] = FipsU2fController(ctx.obj['dev'].driver)
+ ctx.obj['controller'] = FipsU2fController(dev.driver)
except Exception as e:
logger.debug('Failed to load FipsU2fController', exc_info=e)
ctx.fail('Failed to load FIDO Application.')
else:
try:
- ctx.obj['controller'] = Fido2Controller(ctx.obj['dev'].driver)
+ ctx.obj['controller'] = Fido2Controller(dev.driver)
except Exception as e:
logger.debug('Failed to load Fido2Controller', exc_info=e)
ctx.fail('Failed to load FIDO 2 Application.')
@@ -123,7 +133,7 @@
def prompt_new_pin():
return click.prompt(
'Enter your new PIN', default='', hide_input=True,
- show_default=False, confirmation_prompt=True)
+ show_default=False, confirmation_prompt=True, err=True)
def change_pin(pin, new_pin):
if pin is not None:
@@ -135,7 +145,7 @@
# Failing this with empty current PIN does not cost a retry
controller.change_pin(old_pin=pin or '', new_pin=new_pin)
except ApduError as e:
- if e.code == SW_WRONG_LENGTH:
+ if e.code == SW.WRONG_LENGTH:
pin = _prompt_current_pin()
_fail_if_not_valid_pin(ctx, pin, is_fips)
controller.change_pin(old_pin=pin, new_pin=new_pin)
@@ -154,14 +164,16 @@
'Remove and re-insert the YubiKey.')
if e.code == CtapError.ERR.PIN_BLOCKED:
ctx.fail('PIN is blocked.')
+ if e.code == CtapError.ERR.PIN_POLICY_VIOLATION:
+ ctx.fail('New PIN is too long.')
logger.error('Failed to change PIN', exc_info=e)
ctx.fail('Failed to change PIN.')
except ApduError as e:
- if e.code == SW_VERIFY_FAIL_NO_RETRY:
+ if e.code == SW.VERIFY_FAIL_NO_RETRY:
ctx.fail('Wrong PIN.')
- if e.code == SW_AUTH_METHOD_BLOCKED:
+ if e.code == SW.AUTH_METHOD_BLOCKED:
ctx.fail('PIN is blocked.')
logger.error('Failed to change PIN', exc_info=e)
@@ -169,10 +181,16 @@
def set_pin(new_pin):
_fail_if_not_valid_pin(ctx, new_pin, is_fips)
- controller.set_pin(new_pin)
+ try:
+ controller.set_pin(new_pin)
+ except CtapError as e:
+ if e.code == CtapError.ERR.PIN_POLICY_VIOLATION:
+ ctx.fail('PIN is too long.')
+ logger.error('Failed to set PIN', exc_info=e)
+ ctx.fail('Failed to set PIN')
if pin and not controller.has_pin:
- ctx.fail('There is no current PIN set. Use -n/--new-pin to set one.')
+ ctx.fail('There is no current PIN set. Use --new-pin to set one.')
if controller.has_pin and pin is None and not is_fips:
pin = _prompt_current_pin()
@@ -186,12 +204,8 @@
set_pin(new_pin)
- at click_force_option
@fido.command('reset')
- at click.confirmation_option(
- '-f', '--force', prompt='WARNING! This will delete '
- 'all FIDO credentials, including FIDO U2F credentials,'
- ' and restore factory settings. Proceed?')
+ at click_force_option
@click.pass_context
def reset(ctx, force):
"""
@@ -204,6 +218,17 @@
inserted, and requires a touch on the YubiKey.
"""
+ n_keys = len(list(get_descriptors()))
+ if n_keys > 1:
+ ctx.fail('Only one YubiKey can be connected to perform a reset.')
+
+ if not force:
+ if not click.confirm('WARNING! This will delete all FIDO credentials, '
+ 'including FIDO U2F credentials, and restore '
+ 'factory settings. Proceed?',
+ err=True):
+ ctx.abort()
+
def prompt_re_insert_key():
click.echo('Remove and re-insert your YubiKey to perform the reset...')
@@ -235,7 +260,8 @@
'device.\n'
'To proceed, please enter the text "OVERWRITE"',
default='',
- show_default=False
+ show_default=False,
+ err=True
)
if destroy_input != 'OVERWRITE':
ctx.fail('Reset aborted by user.')
@@ -244,7 +270,7 @@
try_reset(FipsU2fController)
except ApduError as e:
- if e.code == SW_COMMAND_NOT_ALLOWED:
+ if e.code == SW.COMMAND_NOT_ALLOWED:
ctx.fail(
'Reset failed. Reset must be triggered within 5 seconds'
' after the YubiKey is inserted.')
@@ -289,7 +315,7 @@
controller = ctx.obj['controller']
if not controller.is_fips:
ctx.fail('This is not a YubiKey FIPS, and therefore'
- 'does not support a U2F PIN.')
+ ' does not support a U2F PIN.')
if pin is None:
pin = _prompt_current_pin('Enter your PIN')
@@ -298,11 +324,11 @@
try:
controller.verify_pin(pin)
except ApduError as e:
- if e.code == SW_VERIFY_FAIL_NO_RETRY:
+ if e.code == SW.VERIFY_FAIL_NO_RETRY:
ctx.fail('Wrong PIN.')
- if e.code == SW_AUTH_METHOD_BLOCKED:
+ if e.code == SW.AUTH_METHOD_BLOCKED:
ctx.fail('PIN is blocked.')
- if e.code == SW_COMMAND_NOT_ALLOWED:
+ if e.code == SW.COMMAND_NOT_ALLOWED:
ctx.fail('PIN is not set.')
logger.error('PIN verification failed', exc_info=e)
@@ -310,15 +336,15 @@
def _prompt_current_pin(prompt='Enter your current PIN'):
- return click.prompt(prompt, default='', hide_input=True, show_default=False)
+ return click.prompt(
+ prompt, default='', hide_input=True, show_default=False, err=True)
def _fail_if_not_valid_pin(ctx, pin=None, is_fips=False):
min_length = FIPS_PIN_MIN_LENGTH \
if is_fips else PIN_MIN_LENGTH
- if not pin or len(pin) < min_length or len(pin.encode('utf-8')) > 128:
- ctx.fail('PIN must be over {} characters long and under 128 bytes.'
- .format(min_length))
+ if not pin or len(pin) < min_length:
+ ctx.fail('PIN must be over {} characters long'.format(min_length))
fido.transports = TRANSPORT.FIDO
diff -Nru yubikey-manager-0.7.1/ykman/cli/__main__.py yubikey-manager-2.0.0/ykman/cli/__main__.py
--- yubikey-manager-0.7.1/ykman/cli/__main__.py 2018-07-02 02:27:29.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/cli/__main__.py 2018-12-11 06:33:30.000000000 -0500
@@ -32,9 +32,11 @@
from ..util import TRANSPORT, Cve201715361VulnerableError, YUBIKEY
from ..native.pyusb import get_usb_backend_version
from ..driver_otp import libversion as ykpers_version
+from ..driver_ccid import open_devices as open_ccid
+from ..device import YubiKey
from ..descriptor import (get_descriptors, list_devices, open_device,
- FailedOpeningDeviceException)
-from .util import click_skip_on_help, UpperCaseChoice
+ FailedOpeningDeviceException, Descriptor)
+from .util import UpperCaseChoice, YkmanContextObject
from .info import info
from .mode import mode
from .otp import otp
@@ -98,7 +100,18 @@
.format(serial))
-def _run_cmd_for_single(ctx, cmd, transports):
+def _run_cmd_for_single(ctx, cmd, transports, reader=None):
+ if reader:
+ if TRANSPORT.has(transports, TRANSPORT.CCID):
+ readers = list(open_ccid(reader))
+ if len(readers) == 1:
+ return YubiKey(Descriptor.from_driver(readers[0]), readers[0])
+ elif len(readers) > 1:
+ ctx.fail('Multiple YubiKeys on external readers detected.')
+ else:
+ ctx.fail('No YubiKey found on external reader.')
+ else:
+ ctx.fail('Not a CCID command.')
try:
descriptors = get_descriptors()
except usb.core.NoBackendError:
@@ -125,36 +138,57 @@
@click.option('-d', '--device', type=int, metavar='SERIAL')
@click.option('-l', '--log-level', default=None,
type=UpperCaseChoice(ykman.logging_setup.LOG_LEVEL_NAMES),
- help='Enable logging at given verbosity level',
+ 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',
+ 'ignored unless --log-level is also set.',
)
+ at click.option(
+ '-r', '--reader',
+ help='Use an external smart card reader. Conflicts with --device and '
+ 'list.',
+ metavar='NAME', default=None)
@click.pass_context
- at click_skip_on_help
-def cli(ctx, device, log_level, log_file):
+def cli(ctx, device, log_level, log_file, reader):
"""
Configure your YubiKey via the command line.
+
+ Examples:
+
+ \b
+ List connected YubiKeys, only output serial number:
+ $ ykman list --serials
+
+ \b
+ Show information about YubiKey with serial number 0123456:
+ $ ykman --device 0123456 info
"""
+ ctx.obj = YkmanContextObject()
if log_level:
ykman.logging_setup.setup(log_level, log_file=log_file)
+ if reader and device:
+ ctx.fail('--reader and --device options can\'t be combined.')
+
subcmd = next(c for c in COMMANDS if c.name == ctx.invoked_subcommand)
if subcmd == list_keys:
+ if reader:
+ ctx.fail('--reader and list command can\'t be combined.')
return
transports = getattr(subcmd, 'transports', TRANSPORT.usb_transports())
if transports:
- if device is not None:
- dev = _run_cmd_for_serial(ctx, subcmd.name, transports, device)
- else:
- dev = _run_cmd_for_single(ctx, subcmd.name, transports)
-
- ctx.obj['dev'] = dev
- ctx.call_on_close(dev.close)
+ def resolve_device():
+ if device is not None:
+ dev = _run_cmd_for_serial(ctx, subcmd.name, transports, device)
+ else:
+ dev = _run_cmd_for_single(ctx, subcmd.name, transports, reader)
+ ctx.call_on_close(dev.close)
+ return dev
+ ctx.obj.add_resolver('dev', resolve_device)
@cli.command('list')
@@ -212,12 +246,12 @@
cli(obj={})
except ValueError as e:
logger.error('Error', exc_info=e)
- print('Error:', e)
+ click.echo('Error: ' + str(e))
return 1
except Cve201715361VulnerableError as err:
logger.error('Error', exc_info=err)
- print('Error:', err)
+ click.echo('Error: ' + str(err))
return 2
diff -Nru yubikey-manager-0.7.1/ykman/cli/mode.py yubikey-manager-2.0.0/ykman/cli/mode.py
--- yubikey-manager-0.7.1/ykman/cli/mode.py 2018-07-02 02:27:29.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/cli/mode.py 2019-01-02 03:11:40.000000000 -0500
@@ -99,6 +99,16 @@
MODE can be a string, such as "OTP+FIDO+CCID", or a shortened form: "o+f+c".
It can also be a mode number.
+
+ Examples:
+
+ \b
+ Set the OTP and FIDO mode:
+ $ ykman mode OTP+FIDO
+
+ \b
+ Set the CCID only mode and use touch to eject the smart card:
+ $ ykman mode CCID --touch-eject
"""
dev = ctx.obj['dev']
if autoeject_timeout:
@@ -121,7 +131,7 @@
.format(mode))
ctx.fail('Use --force to attempt to set it anyway.')
force or click.confirm('Set mode of YubiKey to {}?'.format(mode),
- abort=True)
+ abort=True, err=True)
try:
dev.set_mode(mode, chalresp_timeout, autoeject)
diff -Nru yubikey-manager-0.7.1/ykman/cli/oath.py yubikey-manager-2.0.0/ykman/cli/oath.py
--- yubikey-manager-0.7.1/ykman/cli/oath.py 2018-07-04 06:48:30.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/cli/oath.py 2019-01-02 03:11:40.000000000 -0500
@@ -31,12 +31,14 @@
from threading import Timer
from binascii import b2a_hex, a2b_hex
from .util import (
- click_force_option, click_skip_on_help,
- click_callback, click_parse_b32_key,
- prompt_for_touch, UpperCaseChoice)
-from ..driver_ccid import APDUError, SW_APPLICATION_NOT_FOUND
+ click_force_option, click_postpone_execution, click_callback,
+ click_parse_b32_key, prompt_for_touch, UpperCaseChoice
+)
+from ..driver_ccid import (
+ APDUError, SW
+)
from ..util import TRANSPORT, parse_b32_key
-from ..oath import OathController, SW, CredentialData, OATH_TYPE, ALGO
+from ..oath import OathController, CredentialData, OATH_TYPE, ALGO
from ..settings import Settings
@@ -79,19 +81,33 @@
@click.group()
@click.pass_context
- at click_skip_on_help
+ at click_postpone_execution
@click.option('-p', '--password', help='Provide a password to unlock the '
'YubiKey.')
def oath(ctx, password):
"""
- Manage OATH application.
+ Manage OATH Application.
+
+ Examples:
+
+ \b
+ Generate codes for credentials starting with 'yubi':
+ $ ykman oath code yubi
+
+ \b
+ Add a touch credential with the secret key f5up4ub3dw and the name yubico:
+ $ ykman oath add yubico f5up4ub3dw --touch
+
+ \b
+ Set a password for the OATH application:
+ $ ykman oath set-password
"""
try:
controller = OathController(ctx.obj['dev'].driver)
ctx.obj['controller'] = controller
ctx.obj['settings'] = Settings('oath')
except APDUError as e:
- if e.sw == SW_APPLICATION_NOT_FOUND:
+ if e.sw == SW.NOT_FOUND:
ctx.fail("The OATH application can't be found on this YubiKey.")
raise
@@ -134,14 +150,16 @@
the OATH application on the YubiKey.
"""
+ controller = ctx.obj['controller']
click.echo('Resetting OATH data...')
- old_id = ctx.obj['controller'].id
- ctx.obj['controller'].reset()
+ old_id = controller.id
+ controller.reset()
- keys = ctx.obj['settings'].setdefault('keys', {})
+ settings = ctx.obj['settings']
+ keys = settings.setdefault('keys', {})
if old_id in keys:
del keys[old_id]
- ctx.obj['settings'].write()
+ settings.write()
click.echo(
'Success! All OATH credentials have been cleared from your YubiKey.')
@@ -185,13 +203,12 @@
if not secret:
while True:
- secret = click.prompt('Enter a secret key (base32)')
+ secret = click.prompt('Enter a secret key (base32)', err=True)
try:
secret = parse_b32_key(secret)
break
except Exception as e:
click.echo(e)
- pass
ensure_validated(ctx)
@@ -213,13 +230,12 @@
if not uri:
while True:
- uri = click.prompt('Enter an OATH URI')
+ uri = click.prompt('Enter an OATH URI', err=True)
try:
uri = CredentialData.from_uri(uri)
break
except Exception as e:
click.echo(e)
- pass
ensure_validated(ctx)
data = uri
@@ -235,7 +251,6 @@
def _add_cred(ctx, data, force):
-
controller = ctx.obj['controller']
if not (0 < len(data.name) <= 64):
@@ -258,7 +273,8 @@
if not force and any(cred.key == key for cred in controller.list()):
click.confirm(
'A credential called {} already exists on this YubiKey.'
- ' Do you want to overwrite it?'.format(data.name), abort=True)
+ ' Do you want to overwrite it?'.format(data.name), abort=True,
+ err=True)
firmware_overwrite_issue = (4, 0, 0) < controller.version < (4, 3, 5)
cred_is_subset = any(
@@ -275,7 +291,8 @@
except APDUError as e:
if e.sw == SW.NO_SPACE:
ctx.fail('No space left on your YubiKey for OATH credentials.')
- elif e.sw == SW.COMMAND_ABORTED: # NEO Doesn't use the NO_SPACE errpr.
+ elif e.sw == SW.COMMAND_ABORTED:
+ # Some NEOs do not use the NO_SPACE error.
ctx.fail(
'The command failed. Is there enough space on your YubiKey?')
else:
@@ -303,7 +320,7 @@
for cred in creds:
click.echo(cred.printable_key, nl=False)
if oath_type:
- click.echo(', {}'.format(cred.oath_type.name), nl=False)
+ click.echo(u', {}'.format(cred.oath_type.name), nl=False)
if period:
click.echo(', {}'.format(cred.period), nl=False)
click.echo()
@@ -338,15 +355,20 @@
cred, code = creds[0]
if cred.touch:
prompt_for_touch()
- if cred.oath_type == OATH_TYPE.HOTP:
- # HOTP might require touch, we don't know.
- # Assume yes after 500ms.
- hotp_touch_timer = Timer(0.500, prompt_for_touch)
- hotp_touch_timer.start()
- creds = [(cred, controller.calculate(cred))]
- hotp_touch_timer.cancel()
- elif code is None:
- creds = [(cred, controller.calculate(cred))]
+ try:
+ if cred.oath_type == OATH_TYPE.HOTP:
+ # HOTP might require touch, we don't know.
+ # Assume yes after 500ms.
+ hotp_touch_timer = Timer(0.500, prompt_for_touch)
+ hotp_touch_timer.start()
+ creds = [(cred, controller.calculate(cred))]
+ hotp_touch_timer.cancel()
+ elif code is None:
+ creds = [(cred, controller.calculate(cred))]
+ except APDUError as e:
+ if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED:
+ ctx.fail('Touch credential timed out!')
+
elif single:
_error_multiple_hits(ctx, [cr for cr, c in creds])
@@ -367,7 +389,7 @@
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)
+ format_str = u'{:<%d} {:>%d}' % (longest_name, longest_code)
for name, result in outputs:
click.echo(format_str.format(name, result))
@@ -395,11 +417,11 @@
elif len(hits) == 1:
cred = hits[0]
if force or (click.confirm(
- 'Delete credential: {} ?'.format(cred.printable_key),
- default=False
+ u'Delete credential: {} ?'.format(cred.printable_key),
+ default=False, err=True
)):
controller.delete(cred)
- click.echo('Deleted {}.'.format(cred.printable_key))
+ click.echo(u'Deleted {}.'.format(cred.printable_key))
else:
click.echo('Deletion aborted by user.')
@@ -429,7 +451,8 @@
new_password = click.prompt(
'Enter your new password',
hide_input=True,
- confirmation_prompt=True)
+ confirmation_prompt=True,
+ err=True)
controller = ctx.obj['controller']
settings = ctx.obj['settings']
@@ -491,7 +514,7 @@
del keys[controller.id]
# Prompt for password
- password = click.prompt(prompt, hide_input=True)
+ password = click.prompt(prompt, hide_input=True, err=True)
key = controller.derive_key(password)
_validate(ctx, key, remember)
diff -Nru yubikey-manager-0.7.1/ykman/cli/opgp.py yubikey-manager-2.0.0/ykman/cli/opgp.py
--- yubikey-manager-0.7.1/ykman/cli/opgp.py 2018-07-02 02:27:29.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/cli/opgp.py 2019-01-02 03:11:40.000000000 -0500
@@ -31,8 +31,8 @@
import click
from ..util import TRANSPORT
from ..opgp import OpgpController, KEY_SLOT, TOUCH_MODE
-from ..driver_ccid import APDUError, SW_APPLICATION_NOT_FOUND
-from .util import click_force_option, click_skip_on_help
+from ..driver_ccid import APDUError, SW
+from .util import click_force_option, click_postpone_execution
logger = logging.getLogger(__name__)
@@ -79,16 +79,25 @@
@click.group()
@click.pass_context
- at click_skip_on_help
+ at click_postpone_execution
def openpgp(ctx):
"""
- Manage OpenPGP application.
+ Manage OpenPGP Application.
+
+ Examples:
+
+ \b
+ Set the retries for PIN, Reset Code and Admin PIN to 10:
+ $ ykman openpgp set-retries 10 10 10
+
+ \b
+ Require touch to use the authentication key:
+ $ ykman openpgp touch aut on
"""
try:
- controller = OpgpController(ctx.obj['dev'].driver)
- ctx.obj['controller'] = controller
+ ctx.obj['controller'] = OpgpController(ctx.obj['dev'].driver)
except APDUError as e:
- if e.sw == SW_APPLICATION_NOT_FOUND:
+ if e.sw == SW.NOT_FOUND:
ctx.fail("The OpenPGP application can't be found on this "
'YubiKey.')
logger.debug('Failed to load OpenPGP Application', exc_info=e)
@@ -161,9 +170,9 @@
ctx.fail('A FIXED policy cannot be changed!')
force or click.confirm('Set touch policy of {.name} key to {.name}?'.format(
- key, policy), abort=True)
+ key, policy), abort=True, err=True)
if admin_pin is None:
- admin_pin = click.prompt('Enter admin PIN', hide_input=True)
+ admin_pin = click.prompt('Enter admin PIN', hide_input=True, err=True)
controller.set_touch(key, policy, admin_pin.encode('utf8'))
click.echo('Touch policy successfully set.')
@@ -189,7 +198,7 @@
click.echo('WARNING: Setting PIN retries will reset the values for all '
'3 PINs!')
force or click.confirm('Set PIN retry counters to: {} {} {}?'.format(
- *pw_attempts), abort=True)
+ *pw_attempts), abort=True, err=True)
controller.set_pin_retries(*(pw_attempts + (admin_pin.encode('utf8'),)))
click.echo('PIN retries successfully set.')
if resets_pins:
diff -Nru yubikey-manager-0.7.1/ykman/cli/otp.py yubikey-manager-2.0.0/ykman/cli/otp.py
--- yubikey-manager-0.7.1/ykman/cli/otp.py 2018-07-04 06:48:30.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/cli/otp.py 2019-01-02 03:11:40.000000000 -0500
@@ -29,7 +29,7 @@
from .util import (
click_force_option, click_callback, click_parse_b32_key,
- click_skip_on_help, prompt_for_touch, UpperCaseChoice)
+ click_postpone_execution, prompt_for_touch, UpperCaseChoice)
from ..util import (
TRANSPORT, generate_static_pw, modhex_decode,
modhex_encode, parse_key, parse_b32_key)
@@ -82,16 +82,16 @@
if slot == 1 and slot1:
click.confirm(
'Slot 1 is already configured. Overwrite configuration?',
- abort=True)
+ abort=True, err=True)
if slot == 2 and slot2:
click.confirm(
'Slot 2 is already configured. Overwrite configuration?',
- abort=True)
+ abort=True, err=True)
@click.group()
@click.pass_context
- at click_skip_on_help
+ at click_postpone_execution
@click.option(
'--access-code', required=False, metavar='HEX',
help='A 6 byte access code. Set to empty to use a prompt for input.')
@@ -106,12 +106,31 @@
prevents the configuration to be overwritten without the access code
provided. Mode switching the YubiKey is not possible when a slot is
configured with an access code.
+
+ Examples:
+
+ \b
+ Swap the configurations between the two slots:
+ $ ykman otp swap
+
+ \b
+ Program a random challenge-response credential to slot 2:
+ $ ykman otp chalresp --generate 2
+
+ \b
+ Program a Yubico OTP credential to slot 2, using the serial as public id:
+ $ ykman otp yubiotp 1 --serial-public-id
+
+ \b
+ Program a random 38 characters long static password to slot 2:
+ $ ykman otp static --generate 2 --length 38
"""
ctx.obj['controller'] = OtpController(ctx.obj['dev'].driver)
if access_code is not None:
if access_code == '':
- access_code = click.prompt('Enter access code', show_default=False)
+ access_code = click.prompt(
+ 'Enter access code', show_default=False, err=True)
try:
access_code = parse_access_code_hex(access_code)
@@ -158,15 +177,27 @@
@otp.command()
@click_slot_argument
@click.pass_context
-def ndef(ctx, slot):
+ at click.option(
+ '-p', '--prefix', help='Added before the NDEF payload. Typically a URI.')
+def ndef(ctx, slot, prefix):
"""
Select slot configuration to use for NDEF.
+
+ The default prefix will be used if no prefix is specified.
"""
+ dev = ctx.obj['dev']
controller = ctx.obj['controller']
+ if not dev.config.nfc_supported:
+ ctx.fail('NFC interface not available.')
+
if not controller.slot_status[slot - 1]:
ctx.fail('Slot {} is empty.'.format(slot))
+
try:
- controller.configure_ndef_slot(slot)
+ if prefix:
+ controller.configure_ndef_slot(slot, prefix)
+ else:
+ controller.configure_ndef_slot(slot)
except YkpersError as e:
_failed_to_write_msg(ctx, e)
@@ -184,7 +215,7 @@
ctx.fail('Not possible to delete an empty slot.')
force or click.confirm(
'Do you really want to delete'
- ' the configuration of slot {}?'.format(slot), abort=True)
+ ' the configuration of slot {}?'.format(slot), abort=True, err=True)
click.echo('Deleting the configuration of slot {}...'.format(slot))
try:
controller.zap_slot(slot)
@@ -214,7 +245,8 @@
@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):
+ serial_public_id, generate_private_id,
+ generate_key):
"""
Program a Yubico OTP credential.
@@ -247,7 +279,7 @@
'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')
+ public_id = click.prompt('Enter public ID', err=True)
try:
public_id = modhex_decode(public_id)
@@ -265,7 +297,7 @@
'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 = click.prompt('Enter private ID', err=True)
private_id = a2b_hex(private_id)
if not key:
@@ -278,11 +310,11 @@
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 = click.prompt('Enter secret key', err=True)
key = a2b_hex(key)
force or click.confirm('Program an OTP credential in slot {}?'.format(slot),
- abort=True)
+ abort=True, err=True)
try:
controller.program_otp(slot, key, public_id, private_id, not no_enter)
except YkpersError as e:
@@ -319,6 +351,7 @@
preferred keyboard layout.
"""
+ controller = ctx.obj['controller']
keyboard_layout = KEYBOARD_LAYOUT[keyboard_layout]
if password and len(password) > 38:
@@ -327,12 +360,10 @@
ctx.fail('Provide a length for the generated password.')
if not password and not generate:
- password = click.prompt('Enter a static password')
+ password = click.prompt('Enter a static password', err=True)
elif not password and generate:
password = generate_static_pw(length, keyboard_layout).decode()
- controller = ctx.obj['controller']
-
if not force:
_confirm_slot_overwrite(controller, slot)
try:
@@ -377,25 +408,24 @@
'set the KEY argument or set the --generate flag.')
elif totp:
while True:
- key = click.prompt('Enter a secret key (base32)')
+ key = click.prompt('Enter a secret key (base32)', err=True)
try:
key = parse_b32_key(key)
break
except Exception as e:
click.echo(e)
- pass
else:
if generate:
key = os.urandom(20)
click.echo('Using a randomly generated key: {}'.format(
b2a_hex(key).decode('ascii')))
else:
- key = click.prompt('Enter a secret key')
+ key = click.prompt('Enter a secret key', err=True)
key = parse_key(key)
cred_type = 'TOTP' if totp else 'challenge-response'
force or click.confirm('Program a {} credential in slot {}?'
- .format(cred_type, slot), abort=True)
+ .format(cred_type, slot), abort=True, err=True)
try:
controller.program_chalresp(slot, key, touch)
except YkpersError as e:
@@ -417,7 +447,7 @@
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.
+ credential, and read the response. Supports output as a OATH-TOTP code.
"""
controller = ctx.obj['controller']
if not challenge and not totp:
@@ -477,16 +507,16 @@
controller = ctx.obj['controller']
if not key:
while True:
- key = click.prompt('Enter a secret key (base32)')
+ key = click.prompt('Enter a secret key (base32)', err=True)
try:
key = parse_b32_key(key)
break
except Exception as e:
click.echo(e)
- pass
force or click.confirm(
- 'Program a HOTP credential in slot {}?'.format(slot), abort=True)
+ 'Program a HOTP credential in slot {}?'.format(slot), abort=True,
+ err=True)
try:
controller.program_hotp(
slot, key, counter, int(digits) == 8, not no_enter)
@@ -523,7 +553,7 @@
controller = ctx.obj['controller']
if (new_access_code is not None) and delete_access_code:
- ctx.fail('-A/--new-access-code conflicts with --delete-access-code.')
+ ctx.fail('--new-access-code conflicts with --delete-access-code.')
if not controller.slot_status[slot - 1]:
ctx.fail('Not possible to update settings on an empty slot.')
@@ -531,7 +561,7 @@
if new_access_code is not None:
if new_access_code == '':
new_access_code = click.prompt(
- 'Enter new access code', show_default=False)
+ 'Enter new access code', show_default=False, err=True)
try:
new_access_code = parse_access_code_hex(new_access_code)
@@ -540,7 +570,8 @@
force or click.confirm(
'Update the settings for slot {}? '
- 'All existing settings will be overwritten.'.format(slot), abort=True)
+ 'All existing settings will be overwritten.'.format(slot), abort=True,
+ err=True)
click.echo('Updating settings for slot {}...'.format(slot))
if pacing is not None:
diff -Nru yubikey-manager-0.7.1/ykman/cli/piv.py yubikey-manager-2.0.0/ykman/cli/piv.py
--- yubikey-manager-0.7.1/ykman/cli/piv.py 2018-07-03 06:29:24.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/cli/piv.py 2019-01-02 03:11:40.000000000 -0500
@@ -27,16 +27,19 @@
from __future__ import absolute_import
-from ..util import TRANSPORT, parse_private_key, parse_certificate
+from ..util import (
+ TRANSPORT, get_leaf_certificates, parse_private_key, parse_certificates)
from ..piv import (
- PivController, ALGO, OBJ, SW, SLOT, PIN_POLICY, TOUCH_POLICY,
+ PivController, ALGO, OBJ, SLOT, PIN_POLICY, TOUCH_POLICY,
DEFAULT_MANAGEMENT_KEY, generate_random_management_key)
-from ..driver_ccid import APDUError, SW_APPLICATION_NOT_FOUND
+from ..piv import (
+ AuthenticationBlocked, AuthenticationFailed, KeypairMismatch,
+ UnsupportedAlgorithm, WrongPin, WrongPuk)
+from ..driver_ccid import APDUError, SW
from .util import (
- click_force_option, click_skip_on_help, click_callback, prompt_for_touch,
- UpperCaseChoice)
+ click_force_option, click_postpone_execution, click_callback,
+ prompt_for_touch, UpperCaseChoice)
from cryptography import x509
-from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.backends import default_backend
from binascii import b2a_hex, a2b_hex
@@ -71,11 +74,11 @@
try:
key = a2b_hex(val)
if key and len(key) != 24:
- return ValueError('Management key must be exactly 24 bytes '
- '(48 hexadecimal digits) long.')
+ raise ValueError('Management key must be exactly 24 bytes '
+ '(48 hexadecimal digits) long.')
return key
except Exception:
- return ValueError(val)
+ raise ValueError(val)
click_slot_argument = click.argument('slot', callback=click_parse_piv_slot)
@@ -101,16 +104,31 @@
@click.group()
@click.pass_context
- at click_skip_on_help
+ at click_postpone_execution
def piv(ctx):
"""
- Manage PIV application.
+ Manage PIV Application.
+
+ Examples:
+
+ \b
+ Generate an ECC P-256 private key and a self-signed certificate in
+ slot 9a:
+ $ ykman piv generate-key --algorithm ECCP256 9a pubkey.pem
+ $ ykman piv generate-certificate --subject "yubico" 9a pubkey.pem
+
+ \b
+ Change the PIN from 123456 to 654321:
+ $ ykman piv change-pin --pin 123456 --new-pin 654321
+
+ \b
+ Reset all PIV data and restore default settings:
+ $ ykman piv reset
"""
try:
- controller = PivController(ctx.obj['dev'].driver)
- ctx.obj['controller'] = controller
+ ctx.obj['controller'] = PivController(ctx.obj['dev'].driver)
except APDUError as e:
- if e.sw == SW_APPLICATION_NOT_FOUND:
+ if e.sw == SW.NOT_FOUND:
ctx.fail("The PIV application can't be found on this YubiKey.")
raise
@@ -217,26 +235,30 @@
PUBLIC-KEY File containing the generated public key. Use '-' to use stdout.
"""
+ dev = ctx.obj['dev']
controller = ctx.obj['controller']
_ensure_authenticated(ctx, controller, pin, management_key)
- algorithm = ALGO.from_string(algorithm)
+ algorithm_id = ALGO.from_string(algorithm)
if pin_policy:
pin_policy = PIN_POLICY.from_string(pin_policy)
if touch_policy:
touch_policy = TOUCH_POLICY.from_string(touch_policy)
- _check_algorithm(ctx, controller, algorithm)
- _check_pin_policy(ctx, controller, pin_policy)
+ _check_pin_policy(ctx, dev, controller, pin_policy)
_check_touch_policy(ctx, controller, touch_policy)
- public_key = controller.generate_key(
- slot,
- algorithm,
- pin_policy,
- touch_policy)
+ try:
+ public_key = controller.generate_key(
+ slot,
+ algorithm_id,
+ pin_policy,
+ touch_policy)
+ except UnsupportedAlgorithm:
+ ctx.fail('Algorithm {} is not supported by this '
+ 'YubiKey.'.format(algorithm))
key_encoding = format
public_key_output.write(public_key.public_bytes(
@@ -251,9 +273,14 @@
@click_pin_option
@click.option(
'-p', '--password', help='A password may be needed to decrypt the data.')
+ at click.option(
+ '--no-verify', 'verify', is_flag=True, default=False,
+ callback=lambda ctx, param, value: not value,
+ help='Skip verifying that the certificate matches the private key in the '
+ 'slot.')
@click.argument('cert', type=click.File('rb'), metavar='CERTIFICATE')
def import_certificate(
- ctx, slot, management_key, pin, cert, password):
+ ctx, slot, management_key, pin, cert, password, verify):
"""
Import a X.509 certificate.
@@ -272,13 +299,14 @@
if password is not None:
password = password.encode()
try:
- cert = parse_certificate(data, password)
+ certs = parse_certificates(data, password)
except (ValueError, TypeError):
if password is None:
password = click.prompt(
'Enter password to decrypt certificate',
default='', hide_input=True,
- show_default=False)
+ show_default=False,
+ err=True)
continue
else:
password = None
@@ -286,7 +314,30 @@
continue
break
- controller.import_certificate(slot, cert)
+ if len(certs) > 1:
+ # If multiple certs, only import leaf.
+ # Leaf is the cert with a subject that is not an issuer in the chain.
+ leafs = get_leaf_certificates(certs)
+ cert_to_import = leafs[0]
+ else:
+ cert_to_import = certs[0]
+
+ def do_import(retry=True):
+ try:
+ controller.import_certificate(slot, cert_to_import, verify=verify)
+
+ except KeypairMismatch:
+ ctx.fail('This certificate is not tied to the private key in the '
+ '{} slot.'.format(slot.name))
+
+ except APDUError as e:
+ if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED and retry:
+ _verify_pin(ctx, controller, pin)
+ do_import(retry=False)
+ else:
+ raise
+
+ do_import()
@piv.command('import-key')
@@ -311,6 +362,7 @@
SLOT PIV slot to import the private key to.
PRIVATE-KEY File containing the private key. Use '-' to use stdin.
"""
+ dev = ctx.obj['dev']
controller = ctx.obj['controller']
_ensure_authenticated(ctx, controller, pin, management_key)
@@ -326,7 +378,8 @@
password = click.prompt(
'Enter password to decrypt key',
default='', hide_input=True,
- show_default=False)
+ show_default=False,
+ err=True)
continue
else:
password = None
@@ -339,9 +392,9 @@
if touch_policy:
touch_policy = TOUCH_POLICY.from_string(touch_policy)
- _check_pin_policy(ctx, controller, pin_policy)
+ _check_pin_policy(ctx, dev, controller, pin_policy)
_check_touch_policy(ctx, controller, touch_policy)
- _check_key_size(ctx, private_key)
+ _check_key_size(ctx, controller, private_key)
controller.import_key(
slot,
@@ -449,7 +502,7 @@
click.echo('WARNING: This will reset the PIN and PUK to the factory '
'defaults!')
force or click.confirm('Set PIN and PUK retry counters to: {} {}?'.format(
- pin_retries, puk_retries), abort=True)
+ pin_retries, puk_retries), abort=True, err=True)
try:
controller.set_pin_retries(pin_retries, puk_retries)
click.echo('Default PINs are set.')
@@ -506,9 +559,6 @@
exc_info=e)
ctx.fail('Certificate generation failed.')
- except InvalidSignature:
- ctx.fail('Invalid signature, certificate not imported.')
-
@piv.command('generate-csr')
@click.pass_context
@@ -582,13 +632,19 @@
if not new_pin:
new_pin = click.prompt(
'Enter your new PIN', default='', hide_input=True,
- show_default=False, confirmation_prompt=True)
+ show_default=False, confirmation_prompt=True, err=True)
try:
controller.change_pin(pin, new_pin)
- except APDUError as e:
- logger.error('Failed to change PIN', exc_info=e)
- ctx.fail('Changing the PIN failed.')
- click.echo('New PIN set.')
+ click.echo('New PIN set.')
+
+ except AuthenticationBlocked as e:
+ logger.debug('PIN is blocked.', exc_info=e)
+ ctx.fail('PIN is blocked.')
+
+ except WrongPin as e:
+ logger.debug(
+ 'Failed to change PIN, %d tries left', e.tries_left, exc_info=e)
+ ctx.fail('PIN change failed - %d tries left.' % e.tries_left)
@piv.command('change-puk')
@@ -607,15 +663,21 @@
if not new_puk:
new_puk = click.prompt(
'Enter your new PUK', default='', hide_input=True,
- show_default=False, confirmation_prompt=True)
-
- (success, retries) = controller.change_puk(puk, new_puk)
+ show_default=False, confirmation_prompt=True,
+ err=True)
- if success:
+ try:
+ controller.change_puk(puk, new_puk)
click.echo('New PUK set.')
- else:
- logger.debug('Failed to change PUK, %d tries left', retries)
- ctx.fail('PUK change failed - %d tries left.' % retries)
+
+ except AuthenticationBlocked as e:
+ logger.debug('PUK is blocked.', exc_info=e)
+ ctx.fail('PUK is blocked.')
+
+ except WrongPuk as e:
+ logger.debug(
+ 'Failed to change PUK, %d tries left', e.tries_left, exc_info=e)
+ ctx.fail('PUK change failed - %d tries left.' % e.tries_left)
@piv.command('change-management-key')
@@ -651,7 +713,7 @@
"""
controller = ctx.obj['controller']
- _ensure_authenticated(
+ pin_verified = _ensure_authenticated(
ctx, controller, pin, management_key,
require_pin_and_key=protect,
mgm_key_prompt='Enter your current management key '
@@ -667,14 +729,14 @@
ctx.fail('Require touch not supported on this YubiKey.')
# If an old stored key needs to be cleared, the PIN is needed.
- if not protect and controller.has_stored_key:
+ if not pin_verified and controller.has_stored_key:
if pin:
_verify_pin(ctx, controller, pin, no_prompt=force)
- else:
- force or click.confirm(
+ 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)
+ abort=True, err=True)
if not new_management_key and not protect:
if generate:
@@ -693,7 +755,7 @@
else:
new_management_key = click.prompt(
'Enter your new management key',
- hide_input=True, confirmation_prompt=True)
+ hide_input=True, confirmation_prompt=True, err=True)
if new_management_key and type(new_management_key) is not bytes:
try:
@@ -722,17 +784,19 @@
controller = ctx.obj['controller']
if not puk:
puk = click.prompt(
- 'Enter PUK', default='', show_default=False, hide_input=True)
+ 'Enter PUK', default='', show_default=False,
+ hide_input=True, err=True)
if not new_pin:
new_pin = click.prompt(
- 'Enter a new PIN', default='', show_default=False, hide_input=True)
+ 'Enter a new PIN', default='',
+ show_default=False, hide_input=True, err=True)
controller.unblock_pin(puk, new_pin)
def _prompt_management_key(
ctx, prompt='Enter a management key [blank to use default key]'):
management_key = click.prompt(
- prompt, default='', hide_input=True, show_default=False)
+ prompt, default='', hide_input=True, show_default=False, err=True)
if management_key == '':
return DEFAULT_MANAGEMENT_KEY
try:
@@ -743,7 +807,7 @@
def _prompt_pin(ctx, prompt='Enter PIN'):
return click.prompt(
- prompt, default='', hide_input=True, show_default=False)
+ prompt, default='', hide_input=True, show_default=False, err=True)
def _ensure_authenticated(
@@ -752,17 +816,22 @@
mgm_key_prompt=None,
no_prompt=False):
+ pin_verified = False
+
if controller.has_protected_key:
if not management_key:
- _verify_pin(ctx, controller, pin, no_prompt=no_prompt)
+ pin_verified = _verify_pin(
+ ctx, controller, pin, no_prompt=no_prompt)
else:
_authenticate(ctx, controller, management_key, mgm_key_prompt,
no_prompt=no_prompt)
else:
if require_pin_and_key:
- _verify_pin(ctx, controller, pin, no_prompt=no_prompt)
+ pin_verified = _verify_pin(
+ ctx, controller, pin, no_prompt=no_prompt)
_authenticate(ctx, controller, management_key, mgm_key_prompt,
no_prompt=no_prompt)
+ return pin_verified
def _verify_pin(ctx, controller, pin, no_prompt=False):
@@ -774,7 +843,12 @@
try:
controller.verify(pin, touch_callback=prompt_for_touch)
- except APDUError:
+ return True
+ except WrongPin as e:
+ ctx.fail('PIN verification failed, {} tries left.'.format(e.tries_left))
+ except AuthenticationBlocked as e:
+ ctx.fail('PIN is blocked.')
+ except Exception:
ctx.fail('PIN verification failed.')
@@ -790,27 +864,23 @@
management_key = _prompt_management_key(ctx, mgm_key_prompt)
try:
controller.authenticate(management_key, touch_callback=prompt_for_touch)
- except (APDUError, TypeError):
+ except AuthenticationFailed:
+ ctx.fail('Incorrect management key.')
+ except Exception as e:
+ logger.error('Authentication with management key failed.', exc_info=e)
ctx.fail('Authentication with management key failed.')
-def _check_algorithm(ctx, controller, algorithm):
- # ECCP384 not supported on NEO.
- if algorithm == ALGO.ECCP384 and controller.version < (4, 0, 0):
- ctx.fail('ECCP384 is not supported by this YubiKey.')
- if algorithm == ALGO.RSA1024 and ctx.obj['dev'].is_fips:
- ctx.fail('RSA1024 is not supported by this YubiKey.')
-
-
-def _check_key_size(ctx, private_key):
- if ctx.obj['dev'].is_fips and private_key.key_size == 1024:
+def _check_key_size(ctx, controller, private_key):
+ if (private_key.key_size == 1024
+ and ALGO.RSA1024 not in controller.supported_algorithms):
ctx.fail('1024 is not a supported key size on this YubiKey.')
-def _check_pin_policy(ctx, controller, pin_policy):
+def _check_pin_policy(ctx, dev, controller, pin_policy):
if pin_policy is not None and not controller.supports_pin_policies:
ctx.fail('PIN policy is not supported by this YubiKey.')
- if ctx.obj['dev'].is_fips and pin_policy == PIN_POLICY.NEVER:
+ if dev.is_fips and pin_policy == PIN_POLICY.NEVER:
ctx.fail('PIN policy NEVER is not supported by this YubiKey.')
diff -Nru yubikey-manager-0.7.1/ykman/cli/util.py yubikey-manager-2.0.0/ykman/cli/util.py
--- yubikey-manager-0.7.1/ykman/cli/util.py 2018-07-02 02:27:29.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/cli/util.py 2018-11-08 06:33:42.000000000 -0500
@@ -31,6 +31,7 @@
import click
import sys
from ..util import parse_b32_key
+from collections import OrderedDict, MutableMapping
click_force_option = click.option('-f', '--force', is_flag=True,
help='Confirm the action without prompting.')
@@ -67,14 +68,48 @@
return wrap
-def click_skip_on_help(f):
+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.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):
- ctx = click.get_current_context()
- for arg in ctx.help_option_names:
- if arg in sys.argv:
- return
- f(*args, **kwargs)
+ click.get_current_context().obj.add_resolver(
+ str(f),
+ lambda: f(*args, **kwargs)
+ )
return inner
diff -Nru yubikey-manager-0.7.1/ykman/descriptor.py yubikey-manager-2.0.0/ykman/descriptor.py
--- yubikey-manager-0.7.1/ykman/descriptor.py 2018-07-02 02:27:28.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/descriptor.py 2018-12-11 04:02:06.000000000 -0500
@@ -174,12 +174,12 @@
key_type, dev.driver.key_type)
del dev
continue
- return dev.driver
if mode is not None and dev.driver.mode != mode:
logger.debug('Mode does not match. Want: %s, got: %s',
mode, dev.driver.mode)
del dev
continue
+ return dev.driver
# Wait a little before trying again.
logger.debug('Sleeping for %f s', sleep_time)
time.sleep(sleep_time)
diff -Nru yubikey-manager-0.7.1/ykman/device.py yubikey-manager-2.0.0/ykman/device.py
--- yubikey-manager-0.7.1/ykman/device.py 2018-07-04 06:48:30.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/device.py 2019-01-08 02:48:54.000000000 -0500
@@ -98,6 +98,8 @@
raise ValueError('Config lock key must be 16 bytes')
_set_value(values, TAG.CONFIG_LOCK, config_lock)
if usb_enabled is not None:
+ # Always add the unused CCID transport
+ usb_enabled |= TRANSPORT.CCID
_set_value(values, TAG.USB_ENABLED, usb_enabled)
if nfc_enabled is not None:
_set_value(values, TAG.NFC_ENABLED, nfc_enabled)
@@ -247,15 +249,30 @@
self._can_mode_switch = False
# Fix usb_enabled
- usb_enabled = config.usb_enabled or config.usb_supported
- if not TRANSPORT.has(self.mode.transports, TRANSPORT.OTP):
- usb_enabled &= ~APPLICATION.OTP
- if not TRANSPORT.has(self.mode.transports, TRANSPORT.FIDO):
- usb_enabled &= ~(APPLICATION.U2F | APPLICATION.FIDO2)
- if not TRANSPORT.has(self.mode.transports, TRANSPORT.CCID):
- usb_enabled &= ~(TRANSPORT.CCID | APPLICATION.OATH |
- APPLICATION.OPGP | APPLICATION.PIV)
- config._set(TAG.USB_ENABLED, usb_enabled)
+ if not config.usb_enabled:
+ usb_enabled = config.usb_supported
+ if not TRANSPORT.has(self.mode.transports, TRANSPORT.OTP):
+ usb_enabled &= ~APPLICATION.OTP
+ if not TRANSPORT.has(self.mode.transports, TRANSPORT.FIDO):
+ usb_enabled &= ~(APPLICATION.U2F | APPLICATION.FIDO2)
+ if not TRANSPORT.has(self.mode.transports, TRANSPORT.CCID):
+ usb_enabled &= ~(
+ TRANSPORT.CCID |
+ APPLICATION.OATH |
+ APPLICATION.OPGP |
+ APPLICATION.PIV)
+ config._set(TAG.USB_ENABLED, usb_enabled)
+
+ # Workaround for invalid configurations.
+ # Assume all form factors except USB_A_KEYCHAIN
+ # does not support NFC.
+ if config.form_factor in (
+ FORM_FACTOR.USB_A_NANO,
+ FORM_FACTOR.USB_C_KEYCHAIN,
+ FORM_FACTOR.USB_C_NANO,
+ ):
+ config._set(TAG.NFC_SUPPORTED, 0)
+ config._set(TAG.NFC_ENABLED, 0)
self._config = config
@@ -264,9 +281,25 @@
if not APPLICATION.has(config.usb_supported, APPLICATION.FIDO2):
logger.debug('SKY has no FIDO2, SKY 1')
self.device_name = 'FIDO U2F Security Key' # SKY 1
+ if config.nfc_supported:
+ self.device_name = 'Security Key NFC'
elif self._key_type == YUBIKEY.YK4:
- if self.version >= (5, 0, 0):
+ if (5, 1, 0) > self.version >= (5, 0, 0):
self.device_name = 'YubiKey Preview'
+ elif self.version >= (5, 1, 0):
+ logger.debug('Identified YubiKey 5')
+ self.device_name = 'YubiKey 5'
+ if (config.form_factor == FORM_FACTOR.USB_A_KEYCHAIN
+ and not config.nfc_supported):
+ self.device_name += 'A'
+ elif config.form_factor == FORM_FACTOR.USB_A_KEYCHAIN:
+ self.device_name += ' NFC'
+ elif config.form_factor == FORM_FACTOR.USB_A_NANO:
+ self.device_name += ' Nano'
+ elif config.form_factor == FORM_FACTOR.USB_C_KEYCHAIN:
+ self.device_name += 'C'
+ elif config.form_factor == FORM_FACTOR.USB_C_NANO:
+ self.device_name += 'C Nano'
elif self.is_fips:
self.device_name = 'YubiKey FIPS'
@@ -312,7 +345,11 @@
@property
def is_fips(self):
- return (4, 4, 0) <= self.version < (4, 5, 0)
+ return YubiKey.is_fips_version(self.version)
+
+ @staticmethod
+ def is_fips_version(version):
+ return (4, 4, 0) <= version < (4, 5, 0)
@mode.setter
def mode(self, mode):
diff -Nru yubikey-manager-0.7.1/ykman/driver_ccid.py yubikey-manager-2.0.0/ykman/driver_ccid.py
--- yubikey-manager-0.7.1/ykman/driver_ccid.py 2018-07-03 06:29:24.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/driver_ccid.py 2018-12-20 08:58:43.000000000 -0500
@@ -41,18 +41,45 @@
from .driver import AbstractDriver, ModeSwitchError, NotSupportedError
from .util import AID, APPLICATION, TRANSPORT, YUBIKEY, Mode
-SW_OK = 0x9000
-SW_VERIFY_FAIL_NO_RETRY = 0x63c0
-SW_WRONG_LENGTH = 0x6700
-SW_APPLICATION_NOT_FOUND = 0x6a82
-SW_NO_INPUT_DATA = 0x6285
-SW_AUTH_METHOD_BLOCKED = 0x6983
-SW_DATA_INVALID = 0x6984
-SW_CONDITIONS_NOT_SATISFIED = 0x6985
-SW_COMMAND_NOT_ALLOWED = 0x6986
GP_INS_SELECT = 0xa4
+YK_READER_NAME = 'yubico yubikey'
+
+
+ at unique
+class SW(IntEnum):
+ MORE_DATA = 0x61
+ NO_INPUT_DATA = 0x6285
+ VERIFY_FAIL_NO_RETRY = 0x63c0
+ WRONG_LENGTH = 0x6700
+ SECURITY_CONDITION_NOT_SATISFIED = 0x6982
+ AUTH_METHOD_BLOCKED = 0x6983
+ DATA_INVALID = 0x6984
+ CONDITIONS_NOT_SATISFIED = 0x6985
+ COMMAND_NOT_ALLOWED = 0x6986
+ INCORRECT_PARAMETERS = 0x6a80
+ NOT_FOUND = 0x6a82
+ NO_SPACE = 0x6a84
+ INVALID_INSTRUCTION = 0x6d00
+ COMMAND_ABORTED = 0x6f00
+ OK = 0x9000
+
+ @staticmethod
+ def is_verify_fail(sw):
+ return 0x63c0 <= sw <= 0x63cf
+
+ @classmethod
+ def tries_left(cls, sw):
+ if sw == SW.AUTH_METHOD_BLOCKED:
+ return 0
+
+ if not cls.is_verify_fail(sw):
+ raise ValueError(
+ 'Cannot read remaining tries from status word: %x' % sw)
+
+ return sw & 0xf
+
@unique
class MGR_INS(IntEnum):
@@ -119,10 +146,33 @@
transport = TRANSPORT.CCID
def __init__(self, connection, name):
- pid = _pid_from_name(name)
- super(CCIDDriver, self).__init__(pid.get_type(), Mode.from_pid(pid))
self._conn = connection
+ if name.lower().startswith(YK_READER_NAME):
+ pid = _pid_from_name(name)
+ key_type = pid.get_type()
+ mode = Mode.from_pid(pid)
+ else:
+ key_type, mode = self._probe_type_and_mode()
+ super(CCIDDriver, self).__init__(key_type, mode)
+
+ def _probe_type_and_mode(self):
+ try:
+ s = self.select(AID.OTP)
+ version = tuple(c for c in six.iterbytes(s[:3]))
+ if version < (4, 0, 0):
+ return YUBIKEY.NEO, Mode(TRANSPORT.CCID)
+ except APDUError:
+ pass
+
+ try:
+ self.select(AID.MGR)
+ return YUBIKEY.YK4, Mode(TRANSPORT.CCID)
+ except APDUError:
+ pass
+
+ raise ValueError('Couldn\'t select OTP nor MGR applet!')
+
def read_serial(self):
try:
self.select(AID.OTP)
@@ -167,7 +217,7 @@
'Missing applet: aid: %s , capability: %s', aid, code)
return capa
- def send_apdu(self, cl, ins, p1, p2, data=b'', check=SW_OK):
+ def send_apdu(self, cl, ins, p1, p2, data=b'', check=SW.OK):
header = [cl, ins, p1, p2, len(data)]
body = list(six.iterbytes(data))
try:
@@ -268,12 +318,12 @@
return System.readers()
-def open_devices(name_filter='yubico yubikey'):
+def open_devices(name_filter=YK_READER_NAME):
readers = _list_readers()
while readers:
try_again = []
for reader in readers:
- if reader.name.lower().startswith(name_filter):
+ if reader.name.lower().startswith(name_filter.lower()):
try:
conn = reader.createConnection()
conn.connect()
diff -Nru yubikey-manager-0.7.1/ykman/fido.py yubikey-manager-2.0.0/ykman/fido.py
--- yubikey-manager-0.7.1/ykman/fido.py 2018-07-06 02:37:03.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/fido.py 2018-12-20 08:58:43.000000000 -0500
@@ -32,7 +32,7 @@
from fido2.ctap1 import CTAP1, ApduError
from fido2.ctap2 import CTAP2, PinProtocolV1
from threading import Timer
-from .driver_ccid import SW_CONDITIONS_NOT_SATISFIED
+from .driver_ccid import SW
from .driver_fido import FIPS_U2F_CMD
@@ -115,7 +115,7 @@
self._pin = False
return True
except ApduError as e:
- if e.code == SW_CONDITIONS_NOT_SATISFIED:
+ if e.code == SW.CONDITIONS_NOT_SATISFIED:
time.sleep(0.5)
else:
raise e
diff -Nru yubikey-manager-0.7.1/ykman/native/pyusb.py yubikey-manager-2.0.0/ykman/native/pyusb.py
--- yubikey-manager-0.7.1/ykman/native/pyusb.py 2018-07-02 02:27:28.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/native/pyusb.py 2018-11-21 08:29:41.000000000 -0500
@@ -60,15 +60,14 @@
def _load_usb_backend():
# First try to find backend locally, if not found try the systems.
- for m in (libusb1, openusb, libusb0):
- backend = m.get_backend(find_library=_find_library_local)
+ for lib in (libusb1, openusb, libusb0):
+ backend = lib.get_backend(find_library=_find_library_local)
+ if backend is not None:
+ return backend
+ for lib in (libusb1, openusb, libusb0):
+ backend = lib.get_backend()
if backend is not None:
return backend
- else:
- for m in (libusb1, openusb, libusb0):
- backend = m.get_backend()
- if backend is not None:
- return backend
_usb_backend = None
@@ -101,8 +100,8 @@
version = lib.libusb_get_version().contents
return 'libusb {0.major}.{0.minor}.{0.micro}'.format(version)
elif isinstance(backend, openusb._OpenUSB):
- from usb.backend.openusb import _lib as lib
+ lib = openusb._lib
usb.core.find(True) # OpenUSB seems to hang if not called.
elif isinstance(backend, libusb0._LibUSB):
- from usb.backend.libusb0 import _lib as lib
+ lib = libusb0._lib
return lib._name
diff -Nru yubikey-manager-0.7.1/ykman/oath.py yubikey-manager-2.0.0/ykman/oath.py
--- yubikey-manager-0.7.1/ykman/oath.py 2018-07-03 06:29:24.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/oath.py 2018-12-20 08:58:43.000000000 -0500
@@ -39,7 +39,7 @@
from cryptography.hazmat.primitives import hmac, hashes
from cryptography.hazmat.backends import default_backend
from six.moves.urllib.parse import unquote, urlparse, parse_qs
-from .driver_ccid import APDUError, SW_OK
+from .driver_ccid import APDUError, SW
from .util import (
AID, Tlv, parse_tlvs, time_challenge, parse_b32_key,
format_code, parse_truncated, hmac_shorten_key)
@@ -101,14 +101,6 @@
TYPE = 0xf0
- at unique
-class SW(IntEnum):
- NO_SPACE = 0x6a84
- COMMAND_ABORTED = 0x6f00
- MORE_DATA = 0x61
- INVALID_INSTRUCTION = 0x6d00
-
-
class CredentialData(object):
def __init__(self, secret, issuer, name, oath_type=OATH_TYPE.TOTP,
@@ -131,19 +123,21 @@
raise ValueError('Invalid URI scheme')
params = dict((k, v[0]) for k, v in parse_qs(parsed.query).items())
- params['secret'] = parse_b32_key(params['secret'])
- params['algorithm'] = ALGO[params.get('algorithm', 'SHA1').upper()]
issuer = None
name = unquote(parsed.path)[1:] # Unquote and strip leading /
if ':' in name:
issuer, name = name.split(':', 1)
- params['issuer'] = params.get('issuer', issuer)
- params['name'] = name
- params['oath_type'] = OATH_TYPE[parsed.hostname.upper()]
- params['digits'] = int(params.get('digits', 6))
- params['period'] = int(params.get('period', 30))
- params['counter'] = int(params.get('counter', 0))
- return cls(**params)
+
+ return cls(
+ secret=parse_b32_key(params['secret']),
+ issuer=params.get('issuer', issuer),
+ name=name,
+ oath_type=OATH_TYPE[parsed.hostname.upper()],
+ algorithm=ALGO[params.get('algorithm', 'SHA1').upper()],
+ digits=int(params.get('digits', 6)),
+ period=int(params.get('period', 30)),
+ counter=int(params.get('counter', 0))
+ )
def make_key(self):
key = self.name
@@ -261,7 +255,7 @@
0, INS.SEND_REMAINING, 0, 0, b'', check=None)
resp += more
- if sw != SW_OK:
+ if sw != SW.OK:
raise APDUError(resp, sw)
return resp
diff -Nru yubikey-manager-0.7.1/ykman/opgp.py yubikey-manager-2.0.0/ykman/opgp.py
--- yubikey-manager-0.7.1/ykman/opgp.py 2018-07-02 02:27:29.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/opgp.py 2018-12-20 08:58:43.000000000 -0500
@@ -29,8 +29,7 @@
import six
from .util import AID
-from .driver_ccid import (APDUError, SW_OK, SW_NO_INPUT_DATA,
- SW_CONDITIONS_NOT_SATISFIED)
+from .driver_ccid import (APDUError, SW, GP_INS_SELECT)
from enum import IntEnum, unique
from binascii import b2a_hex
from collections import namedtuple
@@ -73,20 +72,22 @@
class OpgpController(object):
def __init__(self, driver):
- driver.select(AID.OPGP)
self._driver = driver
+ # Use send_apdu instead of driver.select()
+ # to get OpenPGP specific error handling.
+ self.send_apdu(0, GP_INS_SELECT, 0x04, 0, AID.OPGP)
self._version = self._read_version()
@property
def version(self):
return self._version
- def send_apdu(self, cl, ins, p1, p2, data=b'', check=SW_OK):
+ def send_apdu(self, cl, ins, p1, p2, data=b'', check=SW.OK):
try:
return self._driver.send_apdu(cl, ins, p1, p2, data, check)
except APDUError as e:
# If OpenPGP is in a terminated state send activate.
- if e.sw in (SW_NO_INPUT_DATA, SW_CONDITIONS_NOT_SATISFIED):
+ if e.sw in (SW.NO_INPUT_DATA, SW.CONDITIONS_NOT_SATISFIED):
self._driver.send_apdu(0, INS.ACTIVATE, 0, 0)
return self._driver.send_apdu(cl, ins, p1, p2, data, check)
raise
diff -Nru yubikey-manager-0.7.1/ykman/otp.py yubikey-manager-2.0.0/ykman/otp.py
--- yubikey-manager-0.7.1/ykman/otp.py 2018-07-03 06:29:24.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/otp.py 2018-11-29 07:27:34.000000000 -0500
@@ -264,10 +264,10 @@
finally:
ykpers.ykp_free_config(cfg)
- def configure_ndef_slot(self, slot, uri='https://my.yubico.com/neo/'):
+ def configure_ndef_slot(self, slot, prefix='https://my.yubico.com/yk/#'):
ndef = ykpers.ykp_alloc_ndef()
try:
- check(ykpers.ykp_construct_ndef_uri(ndef, uri.encode()))
+ check(ykpers.ykp_construct_ndef_uri(ndef, prefix.encode()))
check(ykpers.yk_write_ndef2(self._dev, ndef, slot))
finally:
ykpers.ykp_free_ndef(ndef)
diff -Nru yubikey-manager-0.7.1/ykman/piv.py yubikey-manager-2.0.0/ykman/piv.py
--- yubikey-manager-0.7.1/ykman/piv.py 2018-07-02 02:27:28.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/piv.py 2019-01-07 06:22:56.000000000 -0500
@@ -28,12 +28,14 @@
from __future__ import absolute_import
from enum import IntEnum, unique
-from .driver_ccid import APDUError, SW_OK, SW_APPLICATION_NOT_FOUND
+from .device import YubiKey
+from .driver_ccid import APDUError, SW
from .util import (
AID, Tlv, parse_tlvs,
+ is_cve201715361_vulnerable_firmware_version,
ensure_not_cve201715361_vulnerable_firmware_version)
-from collections import namedtuple
from cryptography import x509
+from cryptography.exceptions import InvalidSignature
from cryptography.utils import int_to_bytes, int_from_bytes
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
@@ -90,7 +92,8 @@
return cls.ECCP256
elif curve_name == 'secp384r1':
return cls.ECCP384
- raise ValueError('Unsupported key type!')
+ raise UnsupportedAlgorithm(
+ 'Unsupported key type: %s' % type(key), key=key)
@classmethod
def from_string(cls, algorithm):
@@ -102,7 +105,9 @@
return cls.ECCP256
if algorithm == 'ECCP384':
return cls.ECCP384
- raise ValueError('Unsupported algorithm!')
+ raise UnsupportedAlgorithm(
+ 'Unsupported algorithm name: %s' % algorithm,
+ algorithm_id=algorithm)
@classmethod
def is_rsa(cls, algorithm_int):
@@ -222,7 +227,7 @@
return cls.ONCE
if pin_policy == 'ALWAYS':
return cls.ALWAYS
- raise ValueError('Unsupported pin policy!')
+ raise UnknownPinPolicy(pin_policy)
@unique
@@ -242,22 +247,71 @@
return cls.ALWAYS
if touch_policy == 'CACHED':
return cls.CACHED
- raise ValueError('Unsupported touch policy!')
+ raise UnknownTouchPolicy(touch_policy)
- at unique
-class SW(IntEnum):
- NO_SPACE = 0x6a84
- COMMAND_ABORTED = 0x6f00
- MORE_DATA = 0x61
- INVALID_INSTRUCTION = 0x6d00
- NOT_FOUND = 0x6a82
- ACCESS_DENIED = 0x6982
- AUTHENTICATION_BLOCKED = 0x6983
- INCORRECT_PARAMETERS = 0x6a80
+class AuthenticationFailed(Exception):
+ def __init__(self, message, sw, applet_version):
+ super().__init__(message)
+ self.tries_left = (
+ tries_left(sw, applet_version)
+ if is_verify_fail(sw, applet_version)
+ else None)
+
+
+class AuthenticationBlocked(AuthenticationFailed):
+ def __init__(self, message, sw):
+ # Dummy applet_version since sw will always be "authentication blocked"
+ super().__init__(message, sw, ())
+
+
+class BadFormat(Exception):
+ def __init__(self, message, bad_value):
+ super().__init__(message)
+ self.bad_value = bad_value
+
+
+class KeypairMismatch(Exception):
+ def __init__(self, slot, cert):
+ super().__init__(
+ 'The certificate does not match the private key in slot %s.' % slot)
+ self.slot = slot
+ self.cert = cert
+
+
+class UnsupportedAlgorithm(Exception):
+ def __init__(self, message, algorithm_id=None, key=None, ):
+ super().__init__(message)
+ if algorithm_id is None and key is None:
+ raise ValueError(
+ 'At least one of algorithm_id and key must be given.')
+ self.algorithm_id = algorithm_id
+ self.key = key
-CodeChangeResult = namedtuple('CodeChangeResult', ['success', 'tries_left'])
+
+class UnknownPinPolicy(Exception):
+ def __init__(self, policy_name):
+ super().__init__(
+ 'Unsupported pin policy: %s' % policy_name)
+ self.policy_name = policy_name
+
+
+class UnknownTouchPolicy(Exception):
+ def __init__(self, policy_name):
+ super().__init__(
+ 'Unsupported touch policy: %s' % policy_name)
+ self.policy_name = policy_name
+
+
+class WrongPin(AuthenticationFailed):
+ def __init__(self, sw, applet_version):
+ super().__init__('Incorrect PIN', sw, applet_version)
+
+
+class WrongPuk(AuthenticationFailed):
+ def __init__(self, sw, applet_version):
+ super().__init__('Incorrect PUK', sw, applet_version)
PIN = 0x80
@@ -277,14 +331,18 @@
if isinstance(pin, six.text_type):
pin = pin.encode('utf8')
if len(pin) > 8:
- raise ValueError('PIN/PUK too large (max 8 bytes, was %d)' % len(pin))
+ raise BadFormat(
+ 'PIN/PUK too large (max 8 bytes, was %d)' % len(pin), pin)
return pin.ljust(8, b'\xff')
def _get_key_data(key):
if isinstance(key, rsa.RSAPrivateKey):
if key.public_key().public_numbers().e != 65537:
- raise ValueError('Unsupported RSA exponent!')
+ raise UnsupportedAlgorithm(
+ 'Unsupported RSA exponent: %d'
+ % key.public_key().public_numbers().e,
+ key=key)
if key.key_size == 1024:
algo = ALGO.RSA1024
@@ -293,7 +351,8 @@
algo = ALGO.RSA2048
ln = 128
else:
- raise ValueError('Unsupported RSA key size!')
+ raise UnsupportedAlgorithm(
+ 'Unsupported RSA key size: %d' % key.key_size, key=key)
priv = key.private_numbers()
data = Tlv(0x01, int_to_bytes(priv.p, ln)) + \
@@ -309,11 +368,12 @@
algo = ALGO.ECCP384
ln = 48
else:
- raise ValueError('Unsupported elliptic curve!')
+ raise UnsupportedAlgorithm(
+ 'Unsupported elliptic curve: %s', key.curve, key=key)
priv = key.private_numbers()
data = Tlv(0x06, int_to_bytes(priv.private_value, ln))
else:
- raise ValueError('Unsupported key type!')
+ raise UnsupportedAlgorithm('Unsupported key type!', key=key)
return algo, data
@@ -326,7 +386,8 @@
return ec.generate_private_key(ec.SECP256R1(), default_backend())
if algorithm == ALGO.ECCP384:
return ec.generate_private_key(ec.SECP384R1(), default_backend())
- raise ValueError('Unsupported algorithm!')
+ raise UnsupportedAlgorithm(
+ 'Unsupported algorithm: %s' % algorithm, algorithm_id=algorithm)
def _pkcs1_15_pad(algorithm, message):
@@ -364,6 +425,27 @@
return os.urandom(24)
+def is_verify_fail(sw, applet_version):
+ if applet_version < (1, 0, 4):
+ return 0x6300 <= sw <= 0x63ff
+ else:
+ return SW.is_verify_fail(sw)
+
+
+def tries_left(sw, applet_version):
+ if applet_version < (1, 0, 4):
+ if sw == SW.AUTH_METHOD_BLOCKED:
+ return 0
+
+ if not is_verify_fail(sw, applet_version):
+ raise ValueError(
+ 'Cannot read remaining tries from status word: %x' % sw)
+
+ return sw & 0xff
+ else:
+ return SW.tries_left(sw)
+
+
class PivmanData(object):
def __init__(self, raw_data=Tlv(0x80)):
@@ -458,7 +540,7 @@
def puk_blocked(self):
return self._pivman_data.puk_blocked
- def send_cmd(self, ins, p1=0, p2=0, data=b'', check=SW_OK):
+ def send_cmd(self, ins, p1=0, p2=0, data=b'', check=SW.OK):
while len(data) > 0xff:
self._driver.send_apdu(0x10, ins, p1, p2, data[:0xff])
data = data[0xff:]
@@ -484,7 +566,7 @@
self._pivman_protected_data = PivmanProtectedData(
self.get_data(OBJ.PIVMAN_PROTECTED_DATA))
except APDUError as e:
- if e.sw == SW_APPLICATION_NOT_FOUND:
+ if e.sw == SW.NOT_FOUND:
# No data there, initialise a new object.
self._pivman_protected_data = PivmanProtectedData()
else:
@@ -493,10 +575,14 @@
def verify(self, pin, touch_callback=None):
try:
self.send_cmd(INS.VERIFY, 0, PIN, _pack_pin(pin))
- except APDUError:
- raise ValueError(
- 'Pin verification failed. {} tries left.'.format(
- self.get_pin_tries()))
+ except APDUError as e:
+ if e.sw == SW.AUTH_METHOD_BLOCKED:
+ raise AuthenticationBlocked('PIN is blocked.', e.sw)
+
+ elif is_verify_fail(e.sw, self.version):
+ raise WrongPin(e.sw, self.version)
+
+ raise
if self.has_derived_key and not self._authenticated:
self.authenticate(
@@ -509,8 +595,18 @@
self.verify(pin, touch_callback)
def change_pin(self, old_pin, new_pin):
- self.send_cmd(INS.CHANGE_REFERENCE, 0, PIN,
- _pack_pin(old_pin) + _pack_pin(new_pin))
+ try:
+ self.send_cmd(INS.CHANGE_REFERENCE, 0, PIN,
+ _pack_pin(old_pin) + _pack_pin(new_pin))
+ except APDUError as e:
+ if e.sw == SW.AUTH_METHOD_BLOCKED:
+ raise AuthenticationBlocked('PIN is blocked.', e.sw)
+
+ elif is_verify_fail(e.sw, self.version):
+ raise WrongPin(e.sw, self.version)
+
+ raise
+
if self.has_derived_key:
if not self._authenticated:
self.authenticate(_derive_key(old_pin, self._pivman_data.salt))
@@ -520,22 +616,27 @@
try:
self.send_cmd(INS.CHANGE_REFERENCE, 0, PUK,
_pack_pin(old_puk) + _pack_pin(new_puk))
- return CodeChangeResult(True, None)
except APDUError as e:
- retries = self._parse_tries_left(e.sw)
- logger.debug('PUK change failed, %d tries remaining', retries,
- exc_info=e)
- return CodeChangeResult(False, retries)
+ if e.sw == SW.AUTH_METHOD_BLOCKED:
+ raise AuthenticationBlocked('PUK is blocked.', e.sw)
+
+ elif is_verify_fail(e.sw, self.version):
+ raise WrongPuk(e.sw, self.version)
+
+ raise
def unblock_pin(self, puk, new_pin):
try:
self.send_cmd(
INS.RESET_RETRY, 0, PIN, _pack_pin(puk) + _pack_pin(new_pin))
except APDUError as e:
- tries = self._parse_tries_left(e.sw)
- if tries == 0:
- raise ValueError('PUK is blocked.')
- raise ValueError('Unblock PIN failed, {} tries left.'.format(tries))
+ if e.sw == SW.AUTH_METHOD_BLOCKED:
+ raise AuthenticationBlocked('PUK is blocked.', e.sw)
+
+ elif is_verify_fail(e.sw, self.version):
+ raise WrongPuk(e.sw, self.version)
+
+ raise
def set_pin_retries(self, pin_retries, puk_retries):
self.send_cmd(INS.SET_PIN_RETRIES, pin_retries, puk_retries)
@@ -562,7 +663,12 @@
ct1 = self.send_cmd(INS.AUTHENTICATE, ALGO.TDES, SLOT.CARD_MANAGEMENT,
Tlv(TAG.DYN_AUTH, Tlv(0x80)))[4:12]
backend = default_backend()
- cipher = Cipher(algorithms.TripleDES(key), modes.ECB(), backend)
+ try:
+ cipher_key = algorithms.TripleDES(key)
+ except ValueError:
+ raise BadFormat('Management key must be exactly 24 bytes long, '
+ 'was: {}'.format(len(key)), None)
+ cipher = Cipher(cipher_key, modes.ECB(), backend)
decryptor = cipher.decryptor()
pt1 = decryptor.update(ct1) + decryptor.finalize()
ct2 = os.urandom(8)
@@ -576,6 +682,19 @@
INS.AUTHENTICATE, ALGO.TDES, SLOT.CARD_MANAGEMENT,
Tlv(TAG.DYN_AUTH, Tlv(0x80, pt1) + Tlv(0x81, ct2))
)[4:12]
+
+ except APDUError as e:
+ if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED:
+ raise AuthenticationFailed(
+ 'Incorrect management key', e.sw, self.version)
+
+ logger.error('Failed to authenticate management key.', exc_info=e)
+ raise
+
+ except Exception as e:
+ logger.error('Failed to authenticate management key.', exc_info=e)
+ raise
+
finally:
if touch_callback is not None:
touch_timer.cancel()
@@ -597,8 +716,10 @@
'store_on_device was not True')
if len(new_key) != 24:
- raise ValueError('Management key must be exactly 24 bytes long, '
- 'was: {}'.format(len(new_key)))
+ raise BadFormat(
+ 'Management key must be exactly 24 bytes long, was: {}'.format(
+ len(new_key)),
+ new_key)
if store_on_device or (not store_on_device and self.has_stored_key):
# Ensure we have access to protected data before overwriting key
@@ -638,6 +759,21 @@
self._pivman_protected_data.get_bytes())
except APDUError as e:
logger.debug("No PIN provided, can't clear key..", exc_info=e)
+ # Update CHUID and CCC if not set
+ try:
+ self.get_data(OBJ.CAPABILITY)
+ except APDUError as e:
+ if e.sw == SW.NOT_FOUND:
+ self.update_ccc()
+ else:
+ logger.debug('Failed to read CCC...', exc_info=e)
+ try:
+ self.get_data(OBJ.CHUID)
+ except APDUError as e:
+ if e.sw == SW.NOT_FOUND:
+ self.update_chuid()
+ else:
+ logger.debug('Failed to read CHUID...', exc_info=e)
def get_pin_tries(self):
"""
@@ -647,26 +783,14 @@
"""
# Verify without PIN gives number of tries left.
_, sw = self.send_cmd(INS.VERIFY, 0, PIN, check=None)
- return self._parse_tries_left(sw)
+ return tries_left(sw, self.version)
def _get_puk_tries(self):
# A failed unblock pin will return number of PUK tries left,
# but also uses one try.
_, sw = self.send_cmd(INS.RESET_RETRY, 0, PIN, _pack_pin('')*2,
check=None)
- return self._parse_tries_left(sw)
-
- def _parse_tries_left(self, sw):
- # Blocked, 0 tries left.
- if sw == SW.AUTHENTICATION_BLOCKED:
- return 0
- # YK4, NEO with PIV >= 1.0.4
- if 0x63c0 <= sw <= 0x63cf:
- return sw & 0xf
- # PIV applet < 1.04
- if 0x6300 <= sw & 0x63ff:
- return sw & 0xff
- raise ValueError('Failed reading remaining PIN/PUK tries!')
+ return tries_left(sw, self.version)
def _block_pin(self):
while self.get_pin_tries() > 0:
@@ -701,6 +825,12 @@
if ALGO.is_rsa(algorithm):
ensure_not_cve201715361_vulnerable_firmware_version(self.version)
+ if algorithm not in self.supported_algorithms:
+ raise UnsupportedAlgorithm(
+ 'Algorithm not supported on this YubiKey: {}'
+ .format(algorithm),
+ algorithm_id=algorithm)
+
data = Tlv(TAG.ALGO, six.int2byte(algorithm))
if pin_policy:
data += Tlv(TAG.PIN_POLICY, six.int2byte(pin_policy))
@@ -714,13 +844,16 @@
int_from_bytes(data[0x82], 'big'),
int_from_bytes(data[0x81], 'big')
).public_key(default_backend())
- else:
+ elif algorithm in [ALGO.ECCP256, ALGO.ECCP384]:
curve = ec.SECP256R1 if algorithm == ALGO.ECCP256 else ec.SECP384R1
return ec.EllipticCurvePublicNumbers.from_encoded_point(
curve(),
resp[5:]
).public_key(default_backend())
- raise ValueError('Invalid algorithm!')
+
+ raise UnsupportedAlgorithm(
+ 'Invalid algorithm: {}'.format(algorithm),
+ algorithm_id=algorithm)
def generate_self_signed_certificate(
self, slot, public_key, common_name, valid_from, valid_to,
@@ -752,19 +885,7 @@
exc_info=e)
raise
- # Verify that the public key used in the certificate
- # is from the same keypair as the private key.
- cert_signature = cert.signature
- cert_bytes = cert.tbs_certificate_bytes
- if isinstance(public_key, rsa.RSAPublicKey):
- public_key.verify(
- cert_signature, cert_bytes, padding.PKCS1v15(),
- cert.signature_hash_algorithm)
- elif isinstance(public_key, ec.EllipticCurvePublicKey):
- public_key.verify(
- cert_signature, cert_bytes,
- ec.ECDSA(cert.signature_hash_algorithm))
- self.import_certificate(slot, cert)
+ self.import_certificate(slot, cert, verify=False)
def generate_certificate_signing_request(self, slot, public_key, subject,
touch_callback=None):
@@ -791,10 +912,40 @@
self.send_cmd(INS.IMPORT_KEY, algorithm, slot, data)
return algorithm
- def import_certificate(self, slot, certificate):
+ def import_certificate(self, slot, certificate, verify=False):
cert_data = certificate.public_bytes(Encoding.DER)
+
+ if verify:
+ # Verify that the public key used in the certificate
+ # is from the same keypair as the private key.
+ try:
+ public_key = certificate.public_key()
+
+ test_data = b'test'
+ test_sig = self.sign(
+ slot, ALGO.from_public_key(public_key), test_data)
+
+ if isinstance(public_key, rsa.RSAPublicKey):
+ public_key.verify(
+ test_sig, test_data, padding.PKCS1v15(),
+ certificate.signature_hash_algorithm)
+ elif isinstance(public_key, ec.EllipticCurvePublicKey):
+ public_key.verify(
+ test_sig, test_data, ec.ECDSA(hashes.SHA256()))
+ else:
+ raise ValueError('Unknown key type: ' + type(public_key))
+
+ except APDUError as e:
+ if e.sw == SW.INCORRECT_PARAMETERS:
+ raise KeypairMismatch(slot, certificate)
+ raise
+
+ except InvalidSignature as e:
+ raise KeypairMismatch(slot, certificate)
+
self.put_data(OBJ.from_slot(slot), Tlv(TAG.CERTIFICATE, cert_data) +
Tlv(TAG.CERT_INFO, b'\0') + Tlv(TAG.LRC))
+ self.update_chuid()
def read_certificate(self, slot):
data = _parse_tlv_dict(self.get_data(OBJ.from_slot(slot)))
@@ -813,7 +964,9 @@
def _raw_sign_decrypt(self, slot, algorithm, payload, condition):
if not condition(len(payload.value)):
- raise ValueError('Input has invalid length!')
+ raise BadFormat(
+ 'Input has invalid length for algorithm %s' % algorithm,
+ len(payload.value))
data = Tlv(TAG.DYN_AUTH, Tlv(0x82) + payload)
resp = self.send_cmd(INS.AUTHENTICATE, algorithm, slot, data)
@@ -846,12 +999,18 @@
return certs
def update_chuid(self):
+ # Non-Federal Issuer FASC-N
+ # [9999-9999-999999-0-1-0000000000300001]
+ FASC_N=b'\xd4\xe7\x39\xda\x73\x9c\xed\x39\xce\x73\x9d\x83\x68' + \
+ b'\x58\x21\x08\x42\x10\x84\x21\xc8\x42\x10\xc3\xeb'
+ # Expires on: 2030-01-01
+ EXPIRY=b'\x32\x30\x33\x30\x30\x31\x30\x31'
+
self.put_data(
OBJ.CHUID,
- Tlv(0x30, b'\xd4\xe7\x39\xda\x73\x9c\xed\x39\xce\x73\x9d\x83\x68'
- b'\x58\x21\x08\x42\x10\x84\x21\x38\x42\x10\xc3\xf5') +
+ Tlv(0x30, FASC_N) +
Tlv(0x34, os.urandom(16)) +
- Tlv(0x35, b'\x32\x30\x33\x30\x30\x31\x30\x31') +
+ Tlv(0x35, EXPIRY) +
Tlv(0x3e) +
Tlv(TAG.LRC)
)
@@ -938,3 +1097,16 @@
TOUCH_POLICY.ALWAYS] # Cached policy was added in 4.3
else:
return [policy for policy in TOUCH_POLICY]
+
+ @property
+ def supported_algorithms(self):
+ return [
+ alg for alg in ALGO
+
+ if not alg == ALGO.TDES
+ if not (ALGO.is_rsa(alg) and
+ is_cve201715361_vulnerable_firmware_version(self.version))
+ if not (alg == ALGO.ECCP384 and self.version < (4, 0, 0))
+ if not (alg == ALGO.RSA1024 and
+ YubiKey.is_fips_version(self.version))
+ ]
diff -Nru yubikey-manager-0.7.1/ykman/scancodes/us.py yubikey-manager-2.0.0/ykman/scancodes/us.py
--- yubikey-manager-0.7.1/ykman/scancodes/us.py 2018-07-02 02:27:29.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/scancodes/us.py 2018-11-08 06:33:42.000000000 -0500
@@ -103,6 +103,7 @@
'%': 0x22 | SHIFT,
'&': 0x24 | SHIFT,
"'": 0x34,
+ '`': 0x35,
'(': 0x26 | SHIFT,
')': 0x27 | SHIFT,
'*': 0x25 | SHIFT,
diff -Nru yubikey-manager-0.7.1/ykman/settings.py yubikey-manager-2.0.0/ykman/settings.py
--- yubikey-manager-0.7.1/ykman/settings.py 2018-07-02 02:27:29.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/settings.py 2018-11-21 08:29:41.000000000 -0500
@@ -47,6 +47,12 @@
with open(self.fname, 'r') as f:
self.update(json.load(f))
+ def __eq__(self, other):
+ return other is not None and self.fname == other.fname
+
+ def __ne__(self, other):
+ return other is not None or self.fname != other.fname
+
def write(self):
conf_dir = os.path.dirname(self.fname)
if not os.path.isdir(conf_dir):
@@ -54,3 +60,5 @@
data = json.dumps(self, indent=2)
with open(self.fname, 'w') as f:
f.write(data)
+
+ __hash__ = None
diff -Nru yubikey-manager-0.7.1/ykman/util.py yubikey-manager-2.0.0/ykman/util.py
--- yubikey-manager-0.7.1/ykman/util.py 2018-07-02 02:27:28.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/util.py 2018-12-20 08:58:43.000000000 -0500
@@ -31,6 +31,7 @@
import six
import struct
import re
+import logging
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.backends import default_backend
from cryptography import x509
@@ -41,6 +42,12 @@
from .scancodes import KEYBOARD_LAYOUT
+logger = logging.getLogger(__name__)
+
+
+PEM_IDENTIFIER = b'-----BEGIN'
+
+
class BitflagEnum(IntEnum):
@classmethod
def split(cls, flags):
@@ -221,6 +228,8 @@
def from_pid(cls, pid):
return cls(PID(pid).get_transports())
+ __hash__ = None
+
class Tlv(bytes):
@@ -290,7 +299,7 @@
self._message = message
def __getattr__(self, name):
- raise ValueError(self._message)
+ raise AttributeError(self._message)
def parse_tlvs(data):
@@ -414,7 +423,7 @@
Identifies, decrypts and returns a cryptography private key object.
"""
# PEM
- if data.startswith(b'-----'):
+ if is_pem(data):
if b'ENCRYPTED' in data:
if password is None:
raise TypeError('No password provided for encrypted key.')
@@ -449,16 +458,22 @@
raise ValueError('Could not parse private key.')
-def parse_certificate(data, password):
+def parse_certificates(data, password):
"""
- Identifies, decrypts and returns a cryptography x509 certficate.
+ Identifies, decrypts and returns list of cryptography x509 certificates.
"""
+
# PEM
- if data.startswith(b'-----'):
- try:
- return x509.load_pem_x509_certificate(data, default_backend())
- except Exception:
- pass
+ if is_pem(data):
+ certs = []
+ for cert in data.split(PEM_IDENTIFIER):
+ try:
+ certs.append(
+ x509.load_pem_x509_certificate(
+ PEM_IDENTIFIER + cert, default_backend()))
+ except Exception:
+ pass
+ return certs
# PKCS12
if is_pkcs12(data):
@@ -466,19 +481,37 @@
p12 = crypto.load_pkcs12(data, password)
data = crypto.dump_certificate(
crypto.FILETYPE_PEM, p12.get_certificate())
- return x509.load_pem_x509_certificate(data, default_backend())
+ return [x509.load_pem_x509_certificate(data, default_backend())]
except crypto.Error as e:
raise ValueError(e)
# DER
try:
- return x509.load_der_x509_certificate(data, default_backend())
+ return [x509.load_der_x509_certificate(data, default_backend())]
except Exception:
pass
raise ValueError('Could not parse certificate.')
+def get_leaf_certificates(certs):
+ """
+ Extracts the leaf certificates from a list of certificates. Leaf
+ certificates are ones whose subject does not appear as issuer among the
+ others.
+ """
+ issuers = [cert.issuer.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
+ for cert in certs]
+ leafs = [cert for cert in certs
+ if (cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
+ not in issuers)]
+ return leafs
+
+
+def is_pem(data):
+ return PEM_IDENTIFIER in data if data else False
+
+
def is_pkcs12(data):
"""
Tries to identify a PKCS12 container.
diff -Nru yubikey-manager-0.7.1/ykman/VERSION yubikey-manager-2.0.0/ykman/VERSION
--- yubikey-manager-0.7.1/ykman/VERSION 2018-07-09 03:02:17.000000000 -0400
+++ yubikey-manager-2.0.0/ykman/VERSION 2019-01-08 02:55:19.000000000 -0500
@@ -1 +1 @@
-0.7.1
+2.0.0
diff -Nru yubikey-manager-0.7.1/yubikey_manager.egg-info/PKG-INFO yubikey-manager-2.0.0/yubikey_manager.egg-info/PKG-INFO
--- yubikey-manager-0.7.1/yubikey_manager.egg-info/PKG-INFO 2018-07-09 03:19:36.000000000 -0400
+++ yubikey-manager-2.0.0/yubikey_manager.egg-info/PKG-INFO 2019-01-08 03:00:36.000000000 -0500
@@ -1,6 +1,6 @@
Metadata-Version: 1.2
Name: yubikey-manager
-Version: 0.7.1
+Version: 2.0.0
Summary: Tool for managing your YubiKey configuration.
Home-page: https://github.com/Yubico/yubikey-manager
Author: Dain Nilsson
diff -Nru yubikey-manager-0.7.1/yubikey_manager.egg-info/SOURCES.txt yubikey-manager-2.0.0/yubikey_manager.egg-info/SOURCES.txt
--- yubikey-manager-0.7.1/yubikey_manager.egg-info/SOURCES.txt 2018-07-09 03:19:36.000000000 -0400
+++ yubikey-manager-2.0.0/yubikey_manager.egg-info/SOURCES.txt 2019-01-08 03:00:36.000000000 -0500
@@ -5,12 +5,32 @@
setup.cfg
setup.py
doc/development.adoc
+test/__init__.py
test/test_device.py
test/test_external_libs.py
test/test_oath.py
test/test_piv.py
test/test_scancodes.py
test/test_util.py
+test/util.py
+test/files/rsa_1024_key.pem
+test/files/rsa_2048_cert.der
+test/files/rsa_2048_cert.pem
+test/files/rsa_2048_cert_metadata.pem
+test/files/rsa_2048_key.pem
+test/files/rsa_2048_key_cert.pfx
+test/files/rsa_2048_key_cert_encrypted.pfx
+test/files/rsa_2048_key_encrypted.pem
+test/on_yubikey/__init__.py
+test/on_yubikey/test_cli_config.py
+test/on_yubikey/test_cli_misc.py
+test/on_yubikey/test_cli_oath.py
+test/on_yubikey/test_cli_openpgp.py
+test/on_yubikey/test_cli_otp.py
+test/on_yubikey/test_fips_u2f_commands.py
+test/on_yubikey/test_interfaces.py
+test/on_yubikey/test_piv.py
+test/on_yubikey/util.py
test/on_yubikey/cli_piv/__init__.py
test/on_yubikey/cli_piv/test_fips.py
test/on_yubikey/cli_piv/test_generate_cert_and_csr.py
More information about the Pkg-auth-maintainers
mailing list