[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