Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package yubikey-manager for openSUSE:Factory 
checked in at 2021-05-19 17:49:15
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/yubikey-manager (Old)
 and      /work/SRC/openSUSE:Factory/.yubikey-manager.new.2988 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "yubikey-manager"

Wed May 19 17:49:15 2021 rev:16 rq:894147 version:4.0.3

Changes:
--------
--- /work/SRC/openSUSE:Factory/yubikey-manager/yubikey-manager.changes  
2021-05-10 15:41:27.241068886 +0200
+++ 
/work/SRC/openSUSE:Factory/.yubikey-manager.new.2988/yubikey-manager.changes    
    2021-05-19 17:49:27.225516110 +0200
@@ -1,0 +2,14 @@
+Tue May 18 18:39:36 UTC 2021 - Ferdinand Thiessen <[email protected]>
+
+- Update to version 4.0.3
+  * Add support for fido reset over NFC.
+  * Bugfix: The --touch argument to piv change-management-key was
+    ignored.
+  * Bugfix: Don???t prompt for password when importing PIV key/cert
+    if file is invalid.
+  * Bugfix: Fix setting touch-eject/auto-eject for YubiKey 4 and NEO.
+  * Bugfix: Detect PKCS#12 format when outer sequence uses
+    indefinite length.
+  * Dependency: Add support for Click 8.
+
+-------------------------------------------------------------------

Old:
----
  yubikey-manager-4.0.2.tar.gz
  yubikey-manager-4.0.2.tar.gz.sig

New:
----
  yubikey-manager-4.0.3.tar.gz
  yubikey-manager-4.0.3.tar.gz.sig

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ yubikey-manager.spec ++++++
--- /var/tmp/diff_new_pack.vYxNQv/_old  2021-05-19 17:49:27.773513812 +0200
+++ /var/tmp/diff_new_pack.vYxNQv/_new  2021-05-19 17:49:27.773513812 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           yubikey-manager
-Version:        4.0.2
+Version:        4.0.3
 Release:        0
 Summary:        Python 3 library and command line tool for configuring a 
YubiKey
 License:        BSD-2-Clause

++++++ yubikey-manager-4.0.2.tar.gz -> yubikey-manager-4.0.3.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/NEWS 
new/yubikey-manager-4.0.3/NEWS
--- old/yubikey-manager-4.0.2/NEWS      2021-04-12 09:23:45.807040200 +0200
+++ new/yubikey-manager-4.0.3/NEWS      2021-05-17 08:33:54.782452800 +0200
@@ -1,3 +1,11 @@
+* Version 4.0.3 (released 2021-05-17)
+ ** Add support for fido reset over NFC.
+ ** Bugfix: The --touch argument to piv change-management-key was ignored.
+ ** Bugfix: Don't prompt for password when importing PIV key/cert if file is 
invalid.
+ ** Bugfix: Fix setting touch-eject/auto-eject for YubiKey 4 and NEO.
+ ** Bugfix: Detect PKCS#12 format when outer sequence uses indefinite length.
+ ** Dependency: Add support for Click 8.
+
 * Version 4.0.2 (released 2021-04-12)
  ** Update device names.
  ** Add read_info output to the --diagnose command, and show exception types.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/PKG-INFO 
new/yubikey-manager-4.0.3/PKG-INFO
--- old/yubikey-manager-4.0.2/PKG-INFO  2021-04-12 10:03:27.030768400 +0200
+++ new/yubikey-manager-4.0.3/PKG-INFO  2021-05-17 08:34:07.618134500 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: yubikey-manager
-Version: 4.0.2
+Version: 4.0.3
 Summary: Tool for managing your YubiKey configuration.
 Home-page: https://github.com/Yubico/yubikey-manager
 License: BSD
@@ -18,7 +18,7 @@
 Classifier: Programming Language :: Python :: 3.9
 Classifier: Topic :: Security :: Cryptography
 Classifier: Topic :: Utilities
-Requires-Dist: click (>=6.0,<8.0)
+Requires-Dist: click (>=6.0,<9.0)
 Requires-Dist: cryptography (>=2.1,<4.0)
 Requires-Dist: dataclasses (>=0.8,<0.9); python_version < "3.7"
 Requires-Dist: fido2 (>=0.9,<1.0)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/README.adoc 
new/yubikey-manager-4.0.3/README.adoc
--- old/yubikey-manager-4.0.2/README.adoc       2021-04-12 09:23:08.058139000 
+0200
+++ new/yubikey-manager-4.0.3/README.adoc       2021-05-17 08:33:03.311514600 
+0200
@@ -78,7 +78,9 @@
 ==== Linux
 Packages are available for several Linux distributions by third party package
 maintainers.
-Yubico also provides packages for Ubuntu in the yubico/stable PPA:
+Yubico also provides packages for Ubuntu in the yubico/stable PPA (for amd64
+ONLY, other architectures such as arm should use the general `pip` instructions
+above instead):
 
   $ sudo apt-add-repository ppa:yubico/stable
   $ sudo apt update
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/man/ykman.1 
new/yubikey-manager-4.0.3/man/ykman.1
--- old/yubikey-manager-4.0.2/man/ykman.1       2021-04-12 09:47:51.662626700 
+0200
+++ new/yubikey-manager-4.0.3/man/ykman.1       2021-05-17 08:33:54.783446800 
+0200
@@ -1,4 +1,4 @@
-.TH YKMAN "1" "April 2021" "ykman 4.0.2" "User Commands"
+.TH YKMAN "1" "May 2021" "ykman 4.0.3" "User Commands"
 .SH NAME
 ykman \- YubiKey Manager (ykman)
 .SH SYNOPSIS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/pyproject.toml 
new/yubikey-manager-4.0.3/pyproject.toml
--- old/yubikey-manager-4.0.2/pyproject.toml    2021-04-12 09:24:28.848675000 
+0200
+++ new/yubikey-manager-4.0.3/pyproject.toml    2021-05-17 08:33:54.784445800 
+0200
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "yubikey-manager"
-version = "4.0.2"
+version = "4.0.3"
 description = "Tool for managing your YubiKey configuration."
 authors = ["Dain Nilsson <[email protected]>"]
 license = "BSD"
@@ -33,7 +33,7 @@
 pyOpenSSL = {version = ">=0.15.1", optional = true}
 pyscard = "^1.9 || ^2.0"
 fido2 = ">=0.9, <1.0"
-click = "^6.0 || ^7.0"
+click = "^6.0 || ^7.0 || ^8.0"
 pywin32 = {version = ">=223", platform = "win32"}
 
 [tool.poetry.dev-dependencies]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/setup.py 
new/yubikey-manager-4.0.3/setup.py
--- old/yubikey-manager-4.0.2/setup.py  2021-04-12 10:03:27.030369000 +0200
+++ new/yubikey-manager-4.0.3/setup.py  2021-05-17 08:34:07.617768500 +0200
@@ -14,7 +14,7 @@
 {'': ['*']}
 
 install_requires = \
-['click>=6.0,<8.0',
+['click>=6.0,<9.0',
  'cryptography>=2.1,<4.0',
  'fido2>=0.9,<1.0',
  'pyscard>=1.9,<3.0']
@@ -28,7 +28,7 @@
 
 setup_kwargs = {
     'name': 'yubikey-manager',
-    'version': '4.0.2',
+    'version': '4.0.3',
     'description': 'Tool for managing your YubiKey configuration.',
     'long_description': None,
     'author': 'Dain Nilsson',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/tests/device/test_piv.py 
new/yubikey-manager-4.0.3/tests/device/test_piv.py
--- old/yubikey-manager-4.0.2/tests/device/test_piv.py  2021-04-12 
09:23:08.102491000 +0200
+++ new/yubikey-manager-4.0.3/tests/device/test_piv.py  2021-05-17 
08:33:03.313509700 +0200
@@ -3,14 +3,16 @@
 import pytest
 
 from cryptography import x509
-from cryptography.hazmat.primitives import hashes
-from cryptography.hazmat.primitives.asymmetric import ec, padding
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives.asymmetric import rsa, ec, padding
 
 from yubikit.core import AID, NotSupportedError
 from yubikit.core.smartcard import ApduError
 from yubikit.management import CAPABILITY
 from yubikit.piv import (
     PivSession,
+    ALGORITHM,
     KEY_TYPE,
     PIN_POLICY,
     TOUCH_POLICY,
@@ -27,6 +29,7 @@
     pivman_set_mgm_key,
 )
 from ykman.util import parse_certificates, parse_private_key
+from ykman.device import is_fips_version
 from ..util import open_file
 from . import condition
 
@@ -95,6 +98,61 @@
     return key
 
 
+def import_key(
+    session,
+    slot=SLOT.AUTHENTICATION,
+    key_type=KEY_TYPE.ECCP256,
+    pin_policy=PIN_POLICY.DEFAULT,
+):
+
+    if key_type.algorithm == ALGORITHM.RSA:
+        private_key = rsa.generate_private_key(
+            65537, key_type.bit_len, default_backend()
+        )
+    elif key_type == KEY_TYPE.ECCP256:
+        private_key = ec.generate_private_key(ec.SECP256R1(), 
default_backend())
+    elif key_type == KEY_TYPE.ECCP384:
+        private_key = ec.generate_private_key(ec.SECP384R1(), 
default_backend())
+    session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY)
+    session.put_key(slot, private_key, pin_policy)
+    reset_state(session)
+    return private_key.public_key()
+
+
+def verify_cert_signature(cert, public_key=None):
+    if not public_key:
+        public_key = cert.public_key
+    args = [cert.signature, cert.tbs_certificate_bytes, 
cert.signature_hash_algorithm]
+    if KEY_TYPE.from_public_key(public_key).algorithm == ALGORITHM.RSA:
+        args.insert(2, padding.PKCS1v15())
+    else:
+        args[2] = ec.ECDSA(args[2])
+    public_key.verify(*args)
+
+
+class TestCertificateSignatures:
+    @pytest.mark.parametrize("key_type", list(KEY_TYPE))
+    @pytest.mark.parametrize(
+        "hash_algorithm", (hashes.SHA1, hashes.SHA256, hashes.SHA384, 
hashes.SHA512)
+    )
+    def test_generate_self_signed_certificate(self, session, key_type, 
hash_algorithm):
+        if key_type == KEY_TYPE.ECCP384 and session.version < (4, 0, 0):
+            pytest.skip("ECCP384 requires YubiKey 4 or later")
+        if key_type == KEY_TYPE.RSA1024 and is_fips_version(session.version):
+            pytest.skip("RSA1024 not available on YubiKey FIPS")
+
+        slot = SLOT.SIGNATURE
+        public_key = import_key(session, slot, key_type)
+        session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY)
+        session.verify_pin(DEFAULT_PIN)
+        cert = generate_self_signed_certificate(
+            session, slot, public_key, "CN=alice", NOW, NOW, hash_algorithm
+        )
+
+        assert cert.public_key().public_numbers() == 
public_key.public_numbers()
+        verify_cert_signature(cert, public_key)
+
+
 class TestKeyManagement:
     def test_delete_certificate_requires_authentication(self, session):
         generate_key(session, SLOT.AUTHENTICATION)
@@ -131,7 +189,8 @@
             session, SLOT.AUTHENTICATION, public_key, "CN=alice", NOW, NOW
         )
 
-    def _test_generate_self_signed_certificate(self, session, slot):
+    @pytest.mark.parametrize("slot", (SLOT.SIGNATURE, SLOT.AUTHENTICATION))
+    def test_generate_self_signed_certificate(self, session, slot):
         public_key = generate_key(session, slot)
         session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY)
         session.verify_pin(DEFAULT_PIN)
@@ -145,12 +204,6 @@
             == "alice"
         )
 
-    def test_generate_self_signed_certificate_slot_9a_works(self, session):
-        self._test_generate_self_signed_certificate(session, 
SLOT.AUTHENTICATION)
-
-    def test_generate_self_signed_certificate_slot_9c_works(self, session):
-        self._test_generate_self_signed_certificate(session, SLOT.SIGNATURE)
-
     def test_generate_key_requires_authentication(self, session):
         with pytest.raises(ApduError):
             session.generate_key(
@@ -465,3 +518,92 @@
         with pytest.raises(InvalidPinError) as ctx:
             session.change_puk(NON_DEFAULT_PUK, DEFAULT_PUK)
         assert ctx.value.attempts_remaining == puk_tries - 1
+
+
+class TestMetadata:
+    @pytest.fixture(autouse=True)
+    @condition.min_version(5, 3)
+    def preconditions(self):
+        pass
+
+    def test_pin_metadata(self, session):
+        data = session.get_pin_metadata()
+        assert data.default_value is True
+        assert data.total_attempts == 3
+        assert data.attempts_remaining == 3
+
+    def test_management_key_metadata(self, session):
+        data = session.get_management_key_metadata()
+        assert data.key_type == MANAGEMENT_KEY_TYPE.TDES
+        assert data.default_value is True
+        assert data.touch_policy is TOUCH_POLICY.NEVER
+
+        session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY)
+        session.set_management_key(
+            MANAGEMENT_KEY_TYPE.AES192, NON_DEFAULT_MANAGEMENT_KEY
+        )
+        data = session.get_management_key_metadata()
+        assert data.key_type == MANAGEMENT_KEY_TYPE.AES192
+        assert data.default_value is False
+        assert data.touch_policy is TOUCH_POLICY.NEVER
+
+        session.set_management_key(MANAGEMENT_KEY_TYPE.TDES, 
DEFAULT_MANAGEMENT_KEY)
+        data = session.get_management_key_metadata()
+        assert data.default_value is True
+
+        session.set_management_key(MANAGEMENT_KEY_TYPE.AES192, 
DEFAULT_MANAGEMENT_KEY)
+        data = session.get_management_key_metadata()
+        assert data.default_value is False
+
+    @pytest.mark.parametrize("key_type", list(KEY_TYPE))
+    def test_slot_metadata_generate(self, session, key_type):
+        slot = SLOT.SIGNATURE
+        key = generate_key(session, slot, key_type)
+        data = session.get_slot_metadata(slot)
+
+        assert data.key_type == key_type
+        assert data.pin_policy == PIN_POLICY.ALWAYS
+        assert data.touch_policy == TOUCH_POLICY.NEVER
+        assert data.generated is True
+        assert data.public_key.public_bytes(
+            encoding=serialization.Encoding.DER,
+            format=serialization.PublicFormat.SubjectPublicKeyInfo,
+        ) == key.public_bytes(
+            encoding=serialization.Encoding.DER,
+            format=serialization.PublicFormat.SubjectPublicKeyInfo,
+        )
+
+    @pytest.mark.parametrize(
+        "key",
+        [
+            rsa.generate_private_key(65537, 1024, default_backend()),
+            rsa.generate_private_key(65537, 2048, default_backend()),
+            ec.generate_private_key(ec.SECP256R1(), default_backend()),
+            ec.generate_private_key(ec.SECP384R1(), default_backend()),
+        ],
+    )
+    @pytest.mark.parametrize(
+        "slot, pin_policy",
+        [
+            (SLOT.AUTHENTICATION, PIN_POLICY.ONCE),
+            (SLOT.SIGNATURE, PIN_POLICY.ALWAYS),
+            (SLOT.KEY_MANAGEMENT, PIN_POLICY.ONCE),
+            (SLOT.CARD_AUTH, PIN_POLICY.NEVER),
+        ],
+    )
+    def test_slot_metadata_put(self, session, key, slot, pin_policy):
+        session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY)
+        session.put_key(slot, key)
+        data = session.get_slot_metadata(slot)
+
+        assert data.key_type == KEY_TYPE.from_public_key(key.public_key())
+        assert data.pin_policy == pin_policy
+        assert data.touch_policy == TOUCH_POLICY.NEVER
+        assert data.generated is False
+        assert data.public_key.public_bytes(
+            encoding=serialization.Encoding.DER,
+            format=serialization.PublicFormat.SubjectPublicKeyInfo,
+        ) == key.public_key().public_bytes(
+            encoding=serialization.Encoding.DER,
+            format=serialization.PublicFormat.SubjectPublicKeyInfo,
+        )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/tests/test_util.py 
new/yubikey-manager-4.0.3/tests/test_util.py
--- old/yubikey-manager-4.0.2/tests/test_util.py        2021-04-12 
09:23:08.116891900 +0200
+++ new/yubikey-manager-4.0.3/tests/test_util.py        2021-05-17 
08:33:03.313509700 +0200
@@ -92,8 +92,6 @@
 
     def test_is_pkcs12(self):
         with self.assertRaises(TypeError):
-            is_pkcs12("just a string")
-        with self.assertRaises(TypeError):
             is_pkcs12(None)
 
         with open_file("rsa_2048_key.pem") as rsa_2048_key_pem:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/ykman/__init__.py 
new/yubikey-manager-4.0.3/ykman/__init__.py
--- old/yubikey-manager-4.0.2/ykman/__init__.py 2021-04-12 09:24:20.479053300 
+0200
+++ new/yubikey-manager-4.0.3/ykman/__init__.py 2021-05-17 08:33:54.786439400 
+0200
@@ -35,4 +35,4 @@
 )
 
 
-__version__ = "4.0.2"
+__version__ = "4.0.3"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/ykman/cli/__main__.py 
new/yubikey-manager-4.0.3/ykman/cli/__main__.py
--- old/yubikey-manager-4.0.2/ykman/cli/__main__.py     2021-04-12 
09:23:08.124955400 +0200
+++ new/yubikey-manager-4.0.3/ykman/cli/__main__.py     2021-05-17 
08:33:03.314506500 +0200
@@ -41,6 +41,7 @@
     list_all_devices,
     scan_devices,
     connect_to_device,
+    ConnectionNotAvailableException,
 )
 from ..util import get_windows_version
 from ..diagnostics import get_diagnostics
@@ -85,6 +86,9 @@
     while True:
         try:
             return connect_to_device(serial, connections)
+        except ConnectionNotAvailableException as e:
+            logger.error("Failed opening connection", exc_info=e)
+            raise  # No need to retry
         except Exception as e:
             logger.error("Failed opening connection", exc_info=e)
             while attempts:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/ykman/cli/config.py 
new/yubikey-manager-4.0.3/ykman/cli/config.py
--- old/yubikey-manager-4.0.2/ykman/cli/config.py       2021-04-12 
09:23:08.128411500 +0200
+++ new/yubikey-manager-4.0.3/ykman/cli/config.py       2021-05-17 
08:33:03.315503600 +0200
@@ -205,14 +205,14 @@
     "-e",
     "--enable",
     multiple=True,
-    type=EnumChoice(CAPABILITY, hidden=[CAPABILITY.HSMAUTH]),
+    type=EnumChoice(CAPABILITY),
     help="Enable applications.",
 )
 @click.option(
     "-d",
     "--disable",
     multiple=True,
-    type=EnumChoice(CAPABILITY, hidden=[CAPABILITY.HSMAUTH]),
+    type=EnumChoice(CAPABILITY),
     help="Disable applications.",
 )
 @click.option(
@@ -377,14 +377,14 @@
     "-e",
     "--enable",
     multiple=True,
-    type=EnumChoice(CAPABILITY, hidden=[CAPABILITY.HSMAUTH]),
+    type=EnumChoice(CAPABILITY),
     help="Enable applications.",
 )
 @click.option(
     "-d",
     "--disable",
     multiple=True,
-    type=EnumChoice(CAPABILITY, hidden=[CAPABILITY.HSMAUTH]),
+    type=EnumChoice(CAPABILITY),
     help="Disable applications.",
 )
 @click.option("-a", "--enable-all", is_flag=True, help="Enable all 
applications.")
@@ -607,9 +607,9 @@
     else:
         key_type = None
 
-    if autoeject_timeout:
+    if autoeject_timeout:  # autoeject implies touch eject
         touch_eject = True
-    autoeject = autoeject_timeout if touch_eject else 0
+    autoeject = autoeject_timeout if touch_eject else None
 
     if mode.interfaces != USB_INTERFACE.CCID:
         if touch_eject:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/ykman/cli/fido.py 
new/yubikey-manager-4.0.3/ykman/cli/fido.py
--- old/yubikey-manager-4.0.2/ykman/cli/fido.py 2021-04-12 09:23:08.130139400 
+0200
+++ new/yubikey-manager-4.0.3/ykman/cli/fido.py 2021-05-17 08:33:03.317499400 
+0200
@@ -34,6 +34,7 @@
     FPBioEnrollment,
     CaptureError,
 )
+from fido2.pcsc import CtapPcscDevice
 from yubikit.core.fido import FidoConnection
 from yubikit.core.smartcard import SW
 from time import sleep
@@ -48,6 +49,8 @@
 from ..fido import is_in_fips_mode, fips_reset, fips_change_pin, 
fips_verify_pin
 from ..hid import list_ctap_devices
 from ..device import is_fips_version
+from ..pcsc import list_devices as list_ccid
+from smartcard.Exceptions import NoCardException, CardConnectionException
 from typing import Optional
 
 import click
@@ -157,11 +160,54 @@
     inserted, and requires a touch on the YubiKey.
     """
 
-    n_keys = len(list_ctap_devices())
-    if n_keys > 1:
-        cli_fail("Only one YubiKey can be connected to perform a reset.")
+    conn = ctx.obj["conn"]
+
+    if isinstance(conn, CtapPcscDevice):  # NFC
+        readers = list_ccid(conn._name)
+        if not readers or readers[0].reader.name != conn._name:
+            logger.error(f"Multiple readers matched: {readers}")
+            cli_fail("Unable to isolate NFC reader.")
+        dev = readers[0]
+        logger.debug(f"use: {dev}")
+        is_fips = False
+
+        def prompt_re_insert():
+            click.echo(
+                "Remove and re-place your YubiKey on the NFC reader to perform 
the "
+                "reset..."
+            )
 
-    is_fips = is_fips_version(ctx.obj["info"].version)
+            removed = False
+            while True:
+                sleep(0.5)
+                try:
+                    with dev.open_connection(FidoConnection):
+                        if removed:
+                            sleep(1.0)  # Wait for the device to settle
+                            break
+                except CardConnectionException:
+                    pass  # Expected, ignore
+                except NoCardException:
+                    removed = True
+            return dev.open_connection(FidoConnection)
+
+    else:  # USB
+        n_keys = len(list_ctap_devices())
+        if n_keys > 1:
+            cli_fail("Only one YubiKey can be connected to perform a reset.")
+        is_fips = is_fips_version(ctx.obj["info"].version)
+
+        def prompt_re_insert():
+            click.echo("Remove and re-insert your YubiKey to perform the 
reset...")
+
+            removed = False
+            while True:
+                sleep(0.5)
+                keys = list_ctap_devices()
+                if not keys:
+                    removed = True
+                if removed and len(keys) == 1:
+                    return keys[0].open_connection(FidoConnection)
 
     if not force:
         if not click.confirm(
@@ -170,31 +216,7 @@
             err=True,
         ):
             ctx.abort()
-
-    def prompt_re_insert_key():
-        click.echo("Remove and re-insert your YubiKey to perform the reset...")
-
-        removed = False
-        while True:
-            sleep(0.5)
-            keys = list_ctap_devices()
-            if not keys:
-                removed = True
-            if removed and len(keys) == 1:
-                return keys[0]
-
-    def try_reset():
-        if not force:
-            dev = prompt_re_insert_key()
-            conn = dev.open_connection(FidoConnection)
-        with prompt_timeout():
-            if is_fips:
-                fips_reset(conn)
-            else:
-                Ctap2(conn).reset()
-
-    if is_fips:
-        if not force:
+        if is_fips:
             destroy_input = click_prompt(
                 "WARNING! This is a YubiKey FIPS device. This command will 
also "
                 "overwrite the U2F attestation key; this action cannot be 
undone and "
@@ -206,42 +228,39 @@
             if destroy_input != "OVERWRITE":
                 cli_fail("Reset aborted by user.")
 
-        try:
-            try_reset()
-
-        except ApduError as e:
-            logger.error("Reset failed", exc_info=e)
-            if e.code == SW.COMMAND_NOT_ALLOWED:
-                cli_fail(
-                    "Reset failed. Reset must be triggered within 5 seconds 
after the "
-                    "YubiKey is inserted."
-                )
-            else:
-                cli_fail("Reset failed.")
+        conn = prompt_re_insert()
 
-        except Exception as e:
-            logger.error("Reset failed", exc_info=e)
-            cli_fail("Reset failed.")
-
-    else:
-        try:
-            try_reset()
-        except CtapError as e:
-            logger.error(e)
-            if e.code == CtapError.ERR.ACTION_TIMEOUT:
-                cli_fail(
-                    "Reset failed. You need to touch your YubiKey to confirm 
the reset."
-                )
-            elif e.code == CtapError.ERR.NOT_ALLOWED:
-                cli_fail(
-                    "Reset failed. Reset must be triggered within 5 seconds 
after the "
-                    "YubiKey is inserted."
-                )
+    try:
+        with prompt_timeout():
+            if is_fips:
+                fips_reset(conn)
             else:
-                cli_fail(f"Reset failed: {e.code.name}")
-        except Exception as e:
-            logger.error(e)
+                Ctap2(conn).reset()
+    except CtapError as e:
+        logger.error("Reset failed", exc_info=e)
+        if e.code == CtapError.ERR.ACTION_TIMEOUT:
+            cli_fail(
+                "Reset failed. You need to touch your YubiKey to confirm the 
reset."
+            )
+        elif e.code in (CtapError.ERR.NOT_ALLOWED, 
CtapError.ERR.PIN_AUTH_BLOCKED):
+            cli_fail(
+                "Reset failed. Reset must be triggered within 5 seconds after 
the "
+                "YubiKey is inserted."
+            )
+        else:
+            cli_fail(f"Reset failed: {e.code.name}")
+    except ApduError as e:  # From fips_reset
+        logger.error("Reset failed", exc_info=e)
+        if e.code == SW.COMMAND_NOT_ALLOWED:
+            cli_fail(
+                "Reset failed. Reset must be triggered within 5 seconds after 
the "
+                "YubiKey is inserted."
+            )
+        else:
             cli_fail("Reset failed.")
+    except Exception as e:
+        logger.error(e)
+        cli_fail("Reset failed.")
 
 
 def _fail_pin_error(ctx, e, other="%s"):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/ykman/cli/info.py 
new/yubikey-manager-4.0.3/ykman/cli/info.py
--- old/yubikey-manager-4.0.2/ykman/cli/info.py 2021-04-12 09:23:08.131291400 
+0200
+++ new/yubikey-manager-4.0.3/ykman/cli/info.py 2021-05-17 08:33:03.318670700 
+0200
@@ -46,7 +46,7 @@
 
 logger = logging.getLogger(__name__)
 
-SHOWN_CAPABILITIES = set(CAPABILITY) - {CAPABILITY.HSMAUTH}
+SHOWN_CAPABILITIES = set(CAPABILITY)
 
 
 def print_app_status_table(supported_apps, enabled_apps):
@@ -142,7 +142,8 @@
 @click.option(
     "-c",
     "--check-fips",
-    help="Check if YubiKey is in FIPS Approved mode (YubiKey FIPS only).",
+    help="Check if YubiKey is in FIPS Approved mode (available on YubiKey 4 
FIPS "
+    "only).",
     is_flag=True,
 )
 @click.command()
@@ -202,4 +203,4 @@
             ctx.obj["conn"].close()
             _check_fips_status(pid, info)
         else:
-            cli_fail("Not a YubiKey FIPS")
+            cli_fail("Unable to check FIPS Approved mode - Not a YubiKey 4 
FIPS")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/ykman/cli/otp.py 
new/yubikey-manager-4.0.3/ykman/cli/otp.py
--- old/yubikey-manager-4.0.2/ykman/cli/otp.py  2021-04-12 09:23:08.134746800 
+0200
+++ new/yubikey-manager-4.0.3/ykman/cli/otp.py  2021-05-17 08:33:03.319670700 
+0200
@@ -348,8 +348,9 @@
 
     if not public_id:
         if serial_public_id:
-            serial = session.get_serial()
-            if serial is None:
+            try:
+                serial = session.get_serial()
+            except CommandError:
                 cli_fail("Serial number not set, public ID must be provided")
             public_id = modhex_encode(b"\xff\x00" + struct.pack(b">I", serial))
             click.echo(f"Using YubiKey serial as public ID: {public_id}")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/ykman/cli/piv.py 
new/yubikey-manager-4.0.3/ykman/cli/piv.py
--- old/yubikey-manager-4.0.2/ykman/cli/piv.py  2021-04-12 09:23:08.136475000 
+0200
+++ new/yubikey-manager-4.0.3/ykman/cli/piv.py  2021-05-17 08:33:03.320667300 
+0200
@@ -44,6 +44,7 @@
     get_leaf_certificates,
     parse_private_key,
     parse_certificates,
+    InvalidPasswordError,
 )
 from ..piv import (
     get_piv_info,
@@ -70,7 +71,7 @@
     prompt_timeout,
     EnumChoice,
 )
-from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives import serialization, hashes
 from cryptography.hazmat.backends import default_backend
 import click
 import datetime
@@ -118,6 +119,14 @@
         raise ValueError(val)
 
 
+@click_callback()
+def click_parse_hash(ctx, param, val):
+    try:
+        return getattr(hashes, val)
+    except AttributeError:
+        raise ValueError(val)
+
+
 click_slot_argument = click.argument("slot", callback=click_parse_piv_slot)
 click_object_argument = click.argument(
     "object_id", callback=click_parse_piv_object, metavar="OBJECT"
@@ -141,6 +150,15 @@
     default=TOUCH_POLICY.DEFAULT.name,
     help="Touch policy for slot.",
 )
+click_hash_option = click.option(
+    "-a",
+    "--hash-algorithm",
+    type=click.Choice(["SHA1", "SHA256", "SHA384", "SHA512"], 
case_sensitive=False),
+    default="SHA256",
+    show_default=True,
+    help="Hash algorithm.",
+    callback=click_parse_hash,
+)
 
 
 @ykman_group(SmartCardConnection)
@@ -575,7 +593,6 @@
     PRIVATE-KEY File containing the private key. Use '-' to use stdin.
     """
     session = ctx.obj["session"]
-    _ensure_authenticated(ctx, pin, management_key)
 
     data = private_key.read()
 
@@ -584,7 +601,8 @@
             password = password.encode()
         try:
             private_key = parse_private_key(data, password)
-        except (ValueError, TypeError):
+        except InvalidPasswordError as e:
+            logger.error("Error parsing key", exc_info=e)
             if password is None:
                 password = click_prompt(
                     "Enter password to decrypt key",
@@ -599,6 +617,7 @@
             continue
         break
 
+    _ensure_authenticated(ctx, pin, management_key)
     session.put_key(slot, private_key, pin_policy, touch_policy)
 
 
@@ -726,7 +745,6 @@
     CERTIFICATE     File containing the certificate. Use '-' to use stdin.
     """
     session = ctx.obj["session"]
-    _ensure_authenticated(ctx, pin, management_key)
 
     data = cert.read()
 
@@ -735,7 +753,8 @@
             password = password.encode()
         try:
             certs = parse_certificates(data, password)
-        except (ValueError, TypeError):
+        except InvalidPasswordError as e:
+            logger.error("Error parsing certificate", exc_info=e)
             if password is None:
                 password = click_prompt(
                     "Enter password to decrypt certificate",
@@ -758,6 +777,8 @@
     else:
         cert_to_import = certs[0]
 
+    _ensure_authenticated(ctx, pin, management_key)
+
     if verify:
 
         def do_verify():
@@ -820,8 +841,9 @@
     default=365,
     show_default=True,
 )
+@click_hash_option
 def generate_certificate(
-    ctx, management_key, pin, slot, public_key, subject, valid_days
+    ctx, management_key, pin, slot, public_key, subject, valid_days, 
hash_algorithm
 ):
     """
     Generate a self-signed X.509 certificate.
@@ -849,7 +871,7 @@
     try:
         with prompt_timeout():
             cert = generate_self_signed_certificate(
-                session, slot, public_key, subject, now, valid_to
+                session, slot, public_key, subject, now, valid_to, 
hash_algorithm
             )
             session.put_certificate(slot, cert)
             session.put_object(OBJECT_ID.CHUID, generate_chuid())
@@ -870,8 +892,9 @@
     help="Subject for the requested certificate, as an RFC 4514 string.",
     required=True,
 )
+@click_hash_option
 def generate_certificate_signing_request(
-    ctx, pin, slot, public_key, csr_output, subject
+    ctx, pin, slot, public_key, csr_output, subject, hash_algorithm
 ):
     """
     Generate a Certificate Signing Request (CSR).
@@ -896,7 +919,7 @@
 
     try:
         with prompt_timeout():
-            csr = generate_csr(session, slot, public_key, subject)
+            csr = generate_csr(session, slot, public_key, subject, 
hash_algorithm)
     except ApduError:
         cli_fail("Certificate Signing Request generation failed.")
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/ykman/cli/util.py 
new/yubikey-manager-4.0.3/ykman/cli/util.py
--- old/yubikey-manager-4.0.2/ykman/cli/util.py 2021-04-12 09:23:08.137052000 
+0200
+++ new/yubikey-manager-4.0.3/ykman/cli/util.py 2021-05-17 08:33:03.321666500 
+0200
@@ -55,7 +55,9 @@
         self.choices_enum = choices_enum
 
     def convert(self, value, param, ctx):
-        name = super(EnumChoice, self).convert(value, param, ctx).replace("-", 
"_")
+        if isinstance(value, self.choices_enum):
+            return value
+        name = super().convert(value, param, ctx).replace("-", "_")
         return self.choices_enum[name]
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/ykman/device.py 
new/yubikey-manager-4.0.3/ykman/device.py
--- old/yubikey-manager-4.0.2/ykman/device.py   2021-04-12 09:23:08.138203000 
+0200
+++ new/yubikey-manager-4.0.3/ykman/device.py   2021-05-17 08:33:03.322661000 
+0200
@@ -50,7 +50,10 @@
 )
 from yubikit.yubiotp import YubiOtpSession
 from .base import PID, YUBIKEY, YkmanDevice
-from .hid import list_otp_devices, list_ctap_devices
+from .hid import (
+    list_otp_devices as _list_otp_devices,
+    list_ctap_devices as _list_ctap_devices,
+)
 from .pcsc import list_devices as _list_ccid_devices
 from smartcard.pcsc.PCSCExceptions import EstablishContextException
 from smartcard.Exceptions import NoCardException
@@ -65,22 +68,48 @@
 logger = logging.getLogger(__name__)
 
 
-_pcsc_missing = False
+class ConnectionNotAvailableException(ValueError):
+    def __init__(self, connection_types):
+        super().__init__(
+            f"No eligiable connections are available ({connection_types})."
+        )
+        self.connection_types = connection_types
+
+
+def _warn_once(message, e_type=Exception):
+    warned: List[bool] = []
+
+    def outer(f):
+        def inner():
+            try:
+                return f()
+            except e_type:
+                if not warned:
+                    print("WARNING:", message, file=sys.stderr)
+                    warned.append(True)
+                raise
 
+        return inner
 
+    return outer
+
+
+@_warn_once(
+    "PC/SC not available. Smart card protocols will not function.",
+    EstablishContextException,
+)
 def list_ccid_devices():
-    try:
-        return _list_ccid_devices()
-    except Exception as e:
-        global _pcsc_missing
-        if not _pcsc_missing and isinstance(e, EstablishContextException):
-            _pcsc_missing = True
-            print(
-                "WARNING: PCSC not available. Smart card protocols will not 
function.",
-                file=sys.stderr,
-            )
-        logger.error("Unable to list CCID devices", exc_info=e)
-        return []
+    return _list_ccid_devices()
+
+
+@_warn_once("No CTAP HID backend available. FIDO protocols will not function.")
+def list_ctap_devices():
+    return _list_ctap_devices()
+
+
+@_warn_once("No OTP HID backend available. OTP protocols will not function.")
+def list_otp_devices():
+    return _list_otp_devices()
 
 
 def is_fips_version(version: Version) -> bool:
@@ -106,7 +135,11 @@
     fingerprints = set()
     merged: Dict[PID, int] = {}
     for list_devs in CONNECTION_LIST_MAPPING.values():
-        devs = list_devs()
+        try:
+            devs = list_devs()
+        except Exception as e:
+            logger.error("Unable to list devices for connection", exc_info=e)
+            devs = []
         merged.update(Counter(d.pid for d in devs if d.pid is not None))
         fingerprints.update({d.fingerprint for d in devs})
     if sys.platform == "win32" and not 
bool(ctypes.windll.shell32.IsUserAnAdmin()):
@@ -134,7 +167,13 @@
     devices = []
 
     for connection_type, list_devs in CONNECTION_LIST_MAPPING.items():
-        for dev in list_devs():
+        try:
+            devs = list_devs()
+        except Exception as e:
+            logger.error("Unable to list devices for connection", exc_info=e)
+            devs = []
+
+        for dev in devs:
             if dev.pid not in handled_pids and pids.get(dev.pid, True):
                 try:
                     with dev.open_connection(connection_type) as conn:
@@ -160,9 +199,19 @@
     :return: An open connection to the device, the device reference, and the 
device
         information read from the device.
     """
+    failed_connections = set()
     retry_ccid = []
     for connection_type in connection_types:
-        for dev in CONNECTION_LIST_MAPPING[connection_type]():
+        try:
+            devs = CONNECTION_LIST_MAPPING[connection_type]()
+        except Exception as e:
+            logger.error(
+                f"Error listing connection of type {connection_type}", 
exc_info=e
+            )
+            failed_connections.add(connection_type)
+            continue
+
+        for dev in devs:
             try:
                 conn = dev.open_connection(connection_type)
             except NoCardException:
@@ -175,6 +224,9 @@
             else:
                 return conn, dev, info
 
+    if set(connection_types) == failed_connections:
+        raise ConnectionNotAvailableException(connection_types)
+
     # NEO ejects the card when other interfaces are used, and returns it after 
~3s.
     for _ in range(6):
         if not retry_ccid:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/ykman/diagnostics.py 
new/yubikey-manager-4.0.3/ykman/diagnostics.py
--- old/yubikey-manager-4.0.2/ykman/diagnostics.py      2021-04-12 
09:23:08.139355700 +0200
+++ new/yubikey-manager-4.0.3/ykman/diagnostics.py      2021-05-17 
08:33:03.323658000 +0200
@@ -2,6 +2,9 @@
 from .logging_setup import log_sys_info
 from .pcsc import list_readers, list_devices as list_ccid_devices
 from .hid import list_otp_devices, list_ctap_devices
+from .device import read_info, get_name
+from .piv import get_piv_info
+from .openpgp import OpenPgpController, get_openpgp_info
 
 from yubikit.core.smartcard import SmartCardConnection
 from yubikit.core.fido import FidoConnection
@@ -10,9 +13,6 @@
 from yubikit.yubiotp import YubiOtpSession
 from yubikit.piv import PivSession
 from yubikit.oath import OathSession
-from ykman.device import read_info, get_name
-from ykman.piv import get_piv_info
-from ykman.openpgp import OpenPgpController, get_openpgp_info
 from fido2.ctap import CtapError
 from fido2.ctap2 import Ctap2, ClientPin
 
@@ -86,17 +86,23 @@
         ]
 
     lines.append("Detected YubiKeys over PC/SC:")
-    for dev in list_ccid_devices():
-        lines.append(f"\t{dev!r}")
-        try:
-            with dev.open_connection(SmartCardConnection) as conn:
-                lines.extend(mgmt_info(dev.pid, conn))
-                lines.extend(piv_info(conn))
-                lines.extend(oath_info(conn))
-                lines.extend(openpgp_info(conn))
-        except Exception as e:
-            lines.append(f"\tPC/SC connection failure: {e!r}")
-        lines.append("")
+    try:
+        for dev in list_ccid_devices():
+            lines.append(f"\t{dev!r}")
+            try:
+                with dev.open_connection(SmartCardConnection) as conn:
+                    lines.extend(mgmt_info(dev.pid, conn))
+                    lines.extend(piv_info(conn))
+                    lines.extend(oath_info(conn))
+                    lines.extend(openpgp_info(conn))
+            except Exception as e:
+                lines.append(f"\tPC/SC connection failure: {e!r}")
+            lines.append("")
+    except Exception as e:
+        return [
+            f"PC/SC failure: {e!r}",
+            "",
+        ]
 
     lines.append("")
     return lines
@@ -105,20 +111,23 @@
 def otp_info():
     lines = []
     lines.append("Detected YubiKeys over HID OTP:")
-    for dev in list_otp_devices():
-        lines.append(f"\t{dev!r}")
-        try:
-            with dev.open_connection(OtpConnection) as conn:
-                lines.extend(mgmt_info(dev.pid, conn))
-                otp = YubiOtpSession(conn)
-                try:
-                    config = otp.get_config_state()
-                    lines.append(f"\tOTP: {config!r}")
-                except ValueError as e:
-                    lines.append(f"\tCouldn't read OTP state: {e!r}")
-        except Exception as e:
-            lines.append(f"\tOTP connection failure: {e!r}")
-        lines.append("")
+    try:
+        for dev in list_otp_devices():
+            lines.append(f"\t{dev!r}")
+            try:
+                with dev.open_connection(OtpConnection) as conn:
+                    lines.extend(mgmt_info(dev.pid, conn))
+                    otp = YubiOtpSession(conn)
+                    try:
+                        config = otp.get_config_state()
+                        lines.append(f"\tOTP: {config!r}")
+                    except ValueError as e:
+                        lines.append(f"\tCouldn't read OTP state: {e!r}")
+            except Exception as e:
+                lines.append(f"\tOTP connection failure: {e!r}")
+            lines.append("")
+    except Exception as e:
+        lines.append(f"\tHID OTP backend failure: {e!r}")
     lines.append("")
     return lines
 
@@ -126,35 +135,39 @@
 def fido_info():
     lines = []
     lines.append("Detected YubiKeys over HID FIDO:")
-    for dev in list_ctap_devices():
-        lines.append(f"\t{dev!r}")
-        try:
-            with dev.open_connection(FidoConnection) as conn:
-                lines.append("CTAP device version: %d.%d.%d" % 
conn.device_version)
-                lines.append(f"CTAPHID protocol version: {conn.version}")
-                lines.append("Capabilities: %d" % conn.capabilities)
-                lines.extend(mgmt_info(dev.pid, conn))
-                try:
-                    ctap2 = Ctap2(conn)
-                    lines.append(f"\tCtap2Info: {ctap2.info.data!r}")
-                    if ctap2.info.options.get("clientPin"):
-                        client_pin = ClientPin(ctap2)
-                        lines.append(f"PIN retries: 
{client_pin.get_pin_retries()}")
-                        bio_enroll = ctap2.info.options.get("bioEnroll")
-                        if bio_enroll:
-                            lines.append(
-                                f"Fingerprint retries: 
{client_pin.get_uv_retries()}"
-                            )
-                        elif bio_enroll is False:
-                            lines.append("Fingerprints: Not configured")
-                    else:
-                        lines.append("PIN: Not configured")
-
-                except (ValueError, CtapError) as e:
-                    lines.append(f"\tCouldn't get info: {e!r}")
-        except Exception as e:
-            lines.append(f"\tFIDO connection failure: {e!r}")
-        lines.append("")
+    try:
+        for dev in list_ctap_devices():
+            lines.append(f"\t{dev!r}")
+            try:
+                with dev.open_connection(FidoConnection) as conn:
+                    lines.append("CTAP device version: %d.%d.%d" % 
conn.device_version)
+                    lines.append(f"CTAPHID protocol version: {conn.version}")
+                    lines.append("Capabilities: %d" % conn.capabilities)
+                    lines.extend(mgmt_info(dev.pid, conn))
+                    try:
+                        ctap2 = Ctap2(conn)
+                        lines.append(f"\tCtap2Info: {ctap2.info.data!r}")
+                        if ctap2.info.options.get("clientPin"):
+                            client_pin = ClientPin(ctap2)
+                            lines.append(f"PIN retries: 
{client_pin.get_pin_retries()}")
+                            bio_enroll = ctap2.info.options.get("bioEnroll")
+                            if bio_enroll:
+                                lines.append(
+                                    "Fingerprint retries: "
+                                    f"{client_pin.get_uv_retries()}"
+                                )
+                            elif bio_enroll is False:
+                                lines.append("Fingerprints: Not configured")
+                        else:
+                            lines.append("PIN: Not configured")
+
+                    except (ValueError, CtapError) as e:
+                        lines.append(f"\tCouldn't get info: {e!r}")
+            except Exception as e:
+                lines.append(f"\tFIDO connection failure: {e!r}")
+            lines.append("")
+    except Exception as e:
+        lines.append(f"\tHID FIDO backend failure: {e!r}")
     return lines
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/ykman/hid/__init__.py 
new/yubikey-manager-4.0.3/ykman/hid/__init__.py
--- old/yubikey-manager-4.0.2/ykman/hid/__init__.py     2021-04-12 
09:23:08.142234800 +0200
+++ new/yubikey-manager-4.0.3/ykman/hid/__init__.py     2021-05-17 
08:33:03.324655300 +0200
@@ -28,7 +28,6 @@
 from ..base import YkmanDevice, PID
 from .base import OtpYubiKeyDevice
 from yubikit.core import TRANSPORT
-from fido2.hid import list_descriptors, open_connection, CtapHidDevice
 from typing import List, Callable
 import sys
 import logging
@@ -43,36 +42,59 @@
 elif sys.platform.startswith("darwin"):
     from . import macos as backend
 else:
-    raise Exception("Unsupported platform")
 
+    class backend:
+        @staticmethod
+        def list_devices():
+            raise NotImplementedError(
+                "OTP HID support is not implemented on this platform"
+            )
 
-list_otp_devices: Callable[[], List[OtpYubiKeyDevice]] = backend.list_devices
 
+list_otp_devices: Callable[[], List[OtpYubiKeyDevice]] = backend.list_devices
 
-class CtapYubiKeyDevice(YkmanDevice):
-    """YubiKey FIDO USB HID device"""
 
-    def __init__(self, descriptor):
-        super(CtapYubiKeyDevice, self).__init__(
-            TRANSPORT.USB, descriptor.path, PID(descriptor.pid)
-        )
-        self.descriptor = descriptor
+try:
+    from fido2.hid import list_descriptors, open_connection, CtapHidDevice
 
-    def supports_connection(self, connection_type):
-        return issubclass(CtapHidDevice, connection_type)
+    class CtapYubiKeyDevice(YkmanDevice):
+        """YubiKey FIDO USB HID device"""
 
-    def open_connection(self, connection_type):
-        if self.supports_connection(connection_type):
-            return CtapHidDevice(self.descriptor, 
open_connection(self.descriptor))
-        return super(CtapYubiKeyDevice, self).open_connection(connection_type)
-
-
-def list_ctap_devices() -> List[CtapYubiKeyDevice]:
-    devs = []
-    for desc in list_descriptors():
-        if desc.vid == 0x1050:
-            try:
-                devs.append(CtapYubiKeyDevice(desc))
-            except ValueError:
-                logger.debug(f"Unsupported Yubico device with PID: 
{desc.pid:02x}")
-    return devs
+        def __init__(self, descriptor):
+            super(CtapYubiKeyDevice, self).__init__(
+                TRANSPORT.USB, descriptor.path, PID(descriptor.pid)
+            )
+            self.descriptor = descriptor
+
+        def supports_connection(self, connection_type):
+            return issubclass(CtapHidDevice, connection_type)
+
+        def open_connection(self, connection_type):
+            if self.supports_connection(connection_type):
+                return CtapHidDevice(self.descriptor, 
open_connection(self.descriptor))
+            return super(CtapYubiKeyDevice, 
self).open_connection(connection_type)
+
+    def list_ctap_devices() -> List[CtapYubiKeyDevice]:
+        devs = []
+        for desc in list_descriptors():
+            if desc.vid == 0x1050:
+                try:
+                    devs.append(CtapYubiKeyDevice(desc))
+                except ValueError:
+                    logger.debug(f"Unsupported Yubico device with PID: 
{desc.pid:02x}")
+        return devs
+
+
+except Exception:
+    # CTAP not supported on this platform
+
+    class CtapYubiKeyDevice(YkmanDevice):  # type: ignore
+        def __init__(self, *args, **kwargs):
+            raise NotImplementedError(
+                "CTAP HID support is not implemented on this platform"
+            )
+
+    def list_ctap_devices() -> List[CtapYubiKeyDevice]:
+        raise NotImplementedError(
+            "CTAP HID support is not implemented on this platform"
+        )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/ykman/logging_setup.py 
new/yubikey-manager-4.0.3/ykman/logging_setup.py
--- old/yubikey-manager-4.0.2/ykman/logging_setup.py    2021-04-12 
09:23:08.147425200 +0200
+++ new/yubikey-manager-4.0.3/ykman/logging_setup.py    2021-05-17 
08:33:03.325653000 +0200
@@ -27,6 +27,7 @@
 
 from ykman import __version__ as ykman_version
 from ykman.util import get_windows_version
+import platform
 import logging
 import ctypes
 import sys
@@ -46,6 +47,7 @@
 def log_sys_info(log):
     log(f"Python: {sys.version}")
     log(f"Platform: {sys.platform}")
+    log(f"Arch: {platform.machine()}")
     if sys.platform == "win32":
         log(f"Windows version: {get_windows_version()}")
         is_admin = bool(ctypes.windll.shell32.IsUserAnAdmin())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/ykman/piv.py 
new/yubikey-manager-4.0.3/ykman/piv.py
--- old/yubikey-manager-4.0.2/ykman/piv.py      2021-04-12 09:23:08.152604000 
+0200
+++ new/yubikey-manager-4.0.3/ykman/piv.py      2021-05-17 08:33:03.326650400 
+0200
@@ -52,7 +52,7 @@
 import struct
 import os
 
-from typing import Union, Mapping, Optional, List, cast
+from typing import Union, Mapping, Optional, List, Type, cast
 
 
 logger = logging.getLogger(__name__)
@@ -273,7 +273,7 @@
                 raise
 
     # Set the new management key
-    session.set_management_key(algorithm, new_key)
+    session.set_management_key(algorithm, new_key, touch)
 
     if pivman.has_derived_key:
         # Clear salt for old derived keys.
@@ -529,16 +529,17 @@
     slot: SLOT,
     key_type: KEY_TYPE,
     builder: x509.CertificateBuilder,
+    hash_algorithm: Type[hashes.HashAlgorithm] = hashes.SHA256,
 ) -> x509.Certificate:
     """Sign a Certificate."""
     dummy_key = _dummy_key(key_type)
-    cert = builder.sign(dummy_key, hashes.SHA256(), default_backend())
+    cert = builder.sign(dummy_key, hash_algorithm(), default_backend())
 
     sig = session.sign(
         slot,
         key_type,
         cert.tbs_certificate_bytes,
-        hashes.SHA256(),
+        hash_algorithm(),
         padding.PKCS1v15(),  # Only used for RSA
     )
 
@@ -556,11 +557,12 @@
     slot: SLOT,
     public_key: Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey],
     builder: x509.CertificateSigningRequestBuilder,
+    hash_algorithm: Type[hashes.HashAlgorithm] = hashes.SHA256,
 ) -> x509.CertificateSigningRequest:
     """Sign a CSR."""
     key_type = KEY_TYPE.from_public_key(public_key)
     dummy_key = _dummy_key(key_type)
-    csr = builder.sign(dummy_key, hashes.SHA256(), default_backend())
+    csr = builder.sign(dummy_key, hash_algorithm(), default_backend())
     seq = Tlv.parse_list(Tlv.unpack(0x30, csr.public_bytes(Encoding.DER)))
 
     # Replace public key
@@ -577,7 +579,7 @@
         slot,
         key_type,
         seq[0],
-        hashes.SHA256(),
+        hash_algorithm(),
         padding.PKCS1v15(),  # Only used for RSA
     )
 
@@ -596,6 +598,7 @@
     subject_str: str,
     valid_from: datetime,
     valid_to: datetime,
+    hash_algorithm: Type[hashes.HashAlgorithm] = hashes.SHA256,
 ) -> x509.Certificate:
     """Generate a self-signed certificate using a private key in a slot."""
     key_type = KEY_TYPE.from_public_key(public_key)
@@ -612,7 +615,9 @@
     )
 
     try:
-        return sign_certificate_builder(session, slot, key_type, builder)
+        return sign_certificate_builder(
+            session, slot, key_type, builder, hash_algorithm
+        )
     except ApduError as e:
         logger.error("Failed to generate certificate for slot %s", slot, 
exc_info=e)
         raise
@@ -623,6 +628,7 @@
     slot: SLOT,
     public_key: Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey],
     subject_str: str,
+    hash_algorithm: Type[hashes.HashAlgorithm] = hashes.SHA256,
 ) -> x509.CertificateSigningRequest:
     """Generate a CSR using a private key in a slot."""
     builder = x509.CertificateSigningRequestBuilder().subject_name(
@@ -630,7 +636,7 @@
     )
 
     try:
-        return sign_csr_builder(session, slot, public_key, builder)
+        return sign_csr_builder(session, slot, public_key, builder, 
hash_algorithm)
     except ApduError as e:
         logger.error(
             "Failed to generate Certificate Signing Request for slot %s",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/ykman/util.py 
new/yubikey-manager-4.0.3/ykman/util.py
--- old/yubikey-manager-4.0.2/ykman/util.py     2021-04-12 09:23:08.162971000 
+0200
+++ new/yubikey-manager-4.0.3/ykman/util.py     2021-05-17 08:33:03.326650400 
+0200
@@ -42,9 +42,18 @@
 PEM_IDENTIFIER = b"-----BEGIN"
 
 
+class InvalidPasswordError(Exception):
+    """Raised when parsing key/certificate and the password might be 
wrong/missing."""
+
+
 def _parse_pkcs12_cryptography(pkcs12, data, password):
-    key, cert, cas = pkcs12.load_key_and_certificates(data, password, 
default_backend())
-    return key, [cert] + cas
+    try:
+        key, cert, cas = pkcs12.load_key_and_certificates(
+            data, password, default_backend()
+        )
+        return key, [cert] + cas
+    except ValueError as e:  # cryptography raises ValueError on wrong password
+        raise InvalidPasswordError(e)
 
 
 def _parse_pkcs12_pyopenssl(crypto, data, password):
@@ -68,7 +77,7 @@
         ]
         return key, certs
     except crypto.Error as e:
-        raise ValueError(e)
+        raise InvalidPasswordError(e)
 
 
 def _parse_pkcs12_unsupported(data, password):
@@ -96,14 +105,14 @@
     if is_pem(data):
         if b"ENCRYPTED" in data:
             if password is None:
-                raise TypeError("No password provided for encrypted key.")
+                raise InvalidPasswordError("No password provided for encrypted 
key.")
         try:
             return serialization.load_pem_private_key(
                 data, password, backend=default_backend()
             )
-        except ValueError:
+        except ValueError as e:
             # Cryptography raises ValueError if decryption fails.
-            raise
+            raise InvalidPasswordError(e)
         except Exception as e:
             logger.debug("Failed to parse PEM private key ", exc_info=e)
 
@@ -142,8 +151,9 @@
                 except Exception as e:
                     logger.debug("Failed to parse PEM certificate", exc_info=e)
         # Could be valid PEM but not certificates.
-        if len(certs) > 0:
-            return certs
+        if not certs:
+            raise ValueError("PEM file does not contain any certificate(s)")
+        return certs
 
     # PKCS12
     if is_pkcs12(data):
@@ -178,7 +188,7 @@
 
 
 def is_pem(data):
-    return PEM_IDENTIFIER in data if data else False
+    return data and PEM_IDENTIFIER in data
 
 
 def is_pkcs12(data):
@@ -188,10 +198,11 @@
     See: https://tools.ietf.org/html/rfc7292.
     """
     try:
-        header = Tlv.parse_list(Tlv.unpack(0x30, data))[0]
+        header = Tlv.parse_from(Tlv.unpack(0x30, data))[0]
         return header.tag == 0x02 and header.value == b"\x03"
-    except ValueError:
-        return False
+    except ValueError as e:
+        logger.debug("Unable to parse TLV", exc_info=e)
+    return False
 
 
 class OSVERSIONINFOW(ctypes.Structure):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/yubikit/core/__init__.py 
new/yubikey-manager-4.0.3/yubikit/core/__init__.py
--- old/yubikey-manager-4.0.2/yubikit/core/__init__.py  2021-04-12 
09:23:08.165852000 +0200
+++ new/yubikey-manager-4.0.3/yubikit/core/__init__.py  2021-05-17 
08:33:03.328645000 +0200
@@ -183,27 +183,36 @@
     return int.from_bytes(data, "big")
 
 
-def _tlv_parse(data):
+def _tlv_parse(data, offset=0):
     try:
-        tag, rest = data[0], data[1:]
+        tag = data[offset]
+        offset += 1
         if tag & 0x1F == 0x1F:  # Long form
-            tag, rest = tag << 8 | rest[0], rest[1:]
+            tag = tag << 8 | data[offset]
+            offset += 1
             while tag & 0x80 == 0x80:  # Additional bytes
-                tag, rest = tag << 8 | rest[0], rest[1:]
+                tag = tag << 8 | data[offset]
+                offset += 1
 
-        ln, rest = rest[0], rest[1:]
-        if ln == 0x80:
-            raise ValueError("Indefinite length not supported")
-        if ln > 0x80:
-            n_bytes = ln - 0x80
-            ln, rest = bytes2int(rest[:n_bytes]), rest[n_bytes:]
+        ln = data[offset]
+        offset += 1
+        if ln == 0x80:  # Indefinite length
+            end = offset
+            while data[end] or data[end + 1]:  # Run until 0x0000
+                end = _tlv_parse(data, end)[3]  # Skip over TLV
+            ln = end - offset
+            end += 2  # End after 0x0000
+        else:
+            if ln > 0x80:  # Length spans multiple bytes
+                n_bytes = ln - 0x80
+                ln = bytes2int(data[offset : offset + n_bytes])
+                offset += n_bytes
+            end = offset + ln
 
-        value, rest = rest[:ln], rest[ln:]
+        return tag, offset, ln, end
     except IndexError:
         raise ValueError("Invalid encoding of tag/length")
 
-    return tag, ln, value, rest
-
 
 T_Tlv = TypeVar("T_Tlv", bound="Tlv")
 
@@ -215,11 +224,11 @@
 
     @property
     def length(self) -> int:
-        return len(self) - self._value_offset
+        return self._value_ln
 
     @property
     def value(self) -> bytes:
-        return self[self._value_offset :]
+        return self[self._value_offset : self._value_offset + self._value_ln]
 
     def __new__(cls, tag_or_data: Union[int, bytes], value: Optional[bytes] = 
None):
         """This allows creation by passing either binary data, or tag and 
value."""
@@ -248,18 +257,17 @@
         return super(Tlv, cls).__new__(cls, data)  # type: ignore
 
     def __init__(self, tag_or_data: Union[int, bytes], value: Optional[bytes] 
= None):
-        self._tag, ln, value, rest = _tlv_parse(self)
-        if rest:
+        self._tag, self._value_offset, self._value_ln, end = _tlv_parse(self)
+        if len(self) != end:
             raise ValueError("Incorrect TLV length")
-        self._value_offset = len(self) - ln
 
     def __repr__(self):
         return f"Tlv(tag=0x{self.tag:02x}, value={self.value.hex()})"
 
     @classmethod
     def parse_from(cls: Type[T_Tlv], data: bytes) -> Tuple[T_Tlv, bytes]:
-        tag, ln, value, rest = _tlv_parse(data)
-        return cls(data[: len(data) - len(rest)]), rest
+        tag, offs, ln, end = _tlv_parse(data)
+        return cls(data[:end]), data[end:]
 
     @classmethod
     def parse_list(cls: Type[T_Tlv], data: bytes) -> List[T_Tlv]:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey-manager-4.0.2/yubikit/management.py 
new/yubikey-manager-4.0.3/yubikit/management.py
--- old/yubikey-manager-4.0.2/yubikit/management.py     2021-04-12 
09:23:08.170460200 +0200
+++ new/yubikey-manager-4.0.3/yubikit/management.py     2021-05-17 
08:33:03.328645000 +0200
@@ -441,7 +441,10 @@
         )
 
     def set_mode(
-        self, mode: Mode, chalresp_timeout: int = 0, auto_eject_timeout: int = 0
+        self,
+        mode: Mode,
+        chalresp_timeout: int = 0,
+        auto_eject_timeout: Optional[int] = None,
     ) -> None:
         if self.version >= (5, 0, 0):
             # Translate into DeviceConfig
@@ -461,6 +464,13 @@
                 )
             )
         else:
+            code = mode.code
+            if auto_eject_timeout is not None:
+                if mode.interfaces == USB_INTERFACE.CCID:
+                    code |= DEVICE_FLAG.EJECT
+                else:
+                    raise ValueError("Touch-eject only applicable for mode: 
CCID")
             self.backend.set_mode(
-                struct.pack(">BBH", mode.code, chalresp_timeout, 
auto_eject_timeout)
+                # N.B. This is little endian!
+                struct.pack("<BBH", code, chalresp_timeout, auto_eject_timeout 
or 0)
             )

Reply via email to