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 2024-04-04 22:26:37
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/yubikey-manager (Old)
 and      /work/SRC/openSUSE:Factory/.yubikey-manager.new.1905 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "yubikey-manager"

Thu Apr  4 22:26:37 2024 rev:24 rq:1164540 version:5.4.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/yubikey-manager/yubikey-manager.changes  
2024-03-17 22:18:35.730108338 +0100
+++ 
/work/SRC/openSUSE:Factory/.yubikey-manager.new.1905/yubikey-manager.changes    
    2024-04-04 22:28:09.079491886 +0200
@@ -1,0 +2,13 @@
+Wed Apr  3 12:02:24 UTC 2024 - pgaj...@suse.com
+
+- version update to 5.4.0
+  * Support for YubiKey Bio Multi-protocol Edition.
+  * CLI: Improve error messages for several failures.
+  * Attempt to send SIGHUP to yubikey-agent if it is blocking the connection.
+  * Bugfix: Allow "fido config" to work when no PIN is set on the YubiKey.
+  * Bugfix: MacOS - Fix race condition resulting in unneeded delay in fido 
commands over
+    USB.
+  * Bugfix: Linux - Fix error when listing OTP devices when no YubiKeys are 
attached.
+  * Bugfix: OpenPGP - Fix RSA key generation on YubiKey NEO.
+
+-------------------------------------------------------------------

Old:
----
  yubikey_manager-5.3.0.tar.gz
  yubikey_manager-5.3.0.tar.gz.sig

New:
----
  yubikey_manager-5.4.0.tar.gz
  yubikey_manager-5.4.0.tar.gz.sig

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

Other differences:
------------------
++++++ yubikey-manager.spec ++++++
--- /var/tmp/diff_new_pack.gbDKOX/_old  2024-04-04 22:28:09.703514860 +0200
+++ /var/tmp/diff_new_pack.gbDKOX/_new  2024-04-04 22:28:09.703514860 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           yubikey-manager
-Version:        5.3.0
+Version:        5.4.0
 Release:        0
 Summary:        Python 3 library and command line tool for configuring a 
YubiKey
 License:        BSD-2-Clause


++++++ yubikey_manager-5.3.0.tar.gz -> yubikey_manager-5.4.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/NEWS 
new/yubikey_manager-5.4.0/NEWS
--- old/yubikey_manager-5.3.0/NEWS      2024-01-30 10:57:19.552522400 +0100
+++ new/yubikey_manager-5.4.0/NEWS      2024-03-26 14:52:50.940620000 +0100
@@ -1,13 +1,21 @@
-* Version 5.3.0 (released 2023-01-30)
+* Version 5.4.0 (released)
+ * Support for YubiKey Bio Multi-protocol Edition.
+ * CLI: Improve error messages for several failures.
+ * Attempt to send SIGHUP to yubikey-agent if it is blocking the connection.
+ * Bugfix: Allow "fido config" to work when no PIN is set on the YubiKey.
+ * Bugfix: MacOS - Fix race condition resulting in unneeded delay in fido 
commands over
+   USB.
+ * Bugfix: Linux - Fix error when listing OTP devices when no YubiKeys are 
attached.
+ * Bugfix: OpenPGP - Fix RSA key generation on YubiKey NEO.
+
+* Version 5.3.0 (released 2024-01-31)
  ** FIDO: Add new CLI commands for PIN management and authenticator config
     (force-change, set-min-length, toggle-always-uv, enable-ep-attestation).
- ** PIV: Support new key types on supported devices (RSA 3072/4096, 
Curve25519).
- ** PIV: Support for moving and deleting keys on supported devices.
  ** PIV: Improve handling of legacy "PUK blocked" flag.
  ** PIV: Improve handling of malformed certificates.
  ** PIV: Display key information in "piv info" output on supported devices.
  ** OTP: Fix some commands incorrectly showing errors when used over NFC/CCID.
- ** Add tab-completion YubiKey serial numbers and NRC readers.
+ ** Add tab-completion for YubiKey serial numbers and NFC readers.
 
 * Version 5.2.1 (released 2023-10-10)
  ** Add support for Python 3.12.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/PKG-INFO 
new/yubikey_manager-5.4.0/PKG-INFO
--- old/yubikey_manager-5.3.0/PKG-INFO  1970-01-01 01:00:00.000000000 +0100
+++ new/yubikey_manager-5.4.0/PKG-INFO  1970-01-01 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: yubikey-manager
-Version: 5.3.0
+Version: 5.4.0
 Summary: Tool for managing your YubiKey configuration.
 Home-page: https://github.com/Yubico/yubikey-manager
 License: BSD
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/man/ykman.1 
new/yubikey_manager-5.4.0/man/ykman.1
--- old/yubikey_manager-5.3.0/man/ykman.1       2024-01-30 10:57:19.552522400 
+0100
+++ new/yubikey_manager-5.4.0/man/ykman.1       2024-03-26 14:52:50.940620000 
+0100
@@ -1,4 +1,4 @@
-.TH YKMAN "1" "January 2024" "ykman 5.3.0" "User Commands"
+.TH YKMAN "1" "March 2024" "ykman 5.4.0" "User Commands"
 .SH NAME
 ykman \- YubiKey Manager (ykman)
 .SH SYNOPSIS
@@ -44,7 +44,7 @@
 run a python script
 .TP
 config
-enable or disable applications
+configure the YubiKey, enable or disable applications
 .TP
 fido
 manage the FIDO applications
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/pyproject.toml 
new/yubikey_manager-5.4.0/pyproject.toml
--- old/yubikey_manager-5.3.0/pyproject.toml    2024-01-30 10:57:19.552522400 
+0100
+++ new/yubikey_manager-5.4.0/pyproject.toml    2024-03-26 14:52:50.940620000 
+0100
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "yubikey-manager"
-version = "5.3.0"
+version = "5.4.0"
 description = "Tool for managing your YubiKey configuration."
 authors = ["Dain Nilsson <d...@yubico.com>"]
 license = "BSD"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/yubikey_manager-5.3.0/tests/device/cli/test_config.py 
new/yubikey_manager-5.4.0/tests/device/cli/test_config.py
--- old/yubikey_manager-5.3.0/tests/device/cli/test_config.py   2023-09-17 
13:05:24.076793000 +0200
+++ new/yubikey_manager-5.4.0/tests/device/cli/test_config.py   2024-03-26 
14:52:50.940620000 +0100
@@ -14,6 +14,8 @@
 
 
 def not_sky(device, info):
+    if info.is_sky:
+        return False
     if device.transport == TRANSPORT.NFC:
         return not (
             info.serial is None
@@ -109,6 +111,7 @@
                 "OTP",
             )
 
+    @condition.capability(CAPABILITY.U2F, TRANSPORT.USB)
     def test_mode_command(self, ykman_cli, await_reboot):
         ykman_cli("config", "mode", "ccid", "-f")
         await_reboot()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/tests/device/test_piv.py 
new/yubikey_manager-5.4.0/tests/device/test_piv.py
--- old/yubikey_manager-5.3.0/tests/device/test_piv.py  2024-01-29 
09:10:25.565461600 +0100
+++ new/yubikey_manager-5.4.0/tests/device/test_piv.py  2024-03-26 
14:52:50.944620000 +0100
@@ -746,3 +746,20 @@
         session.delete_key(SLOT.AUTHENTICATION)
         with pytest.raises(ApduError):
             session.get_slot_metadata(SLOT.AUTHENTICATION)
+
+
+class TestPinComplexity:
+    @pytest.fixture(autouse=True)
+    def preconditions(self, info):
+        if not info.pin_complexity:
+            pytest.skip("Requires YubiKey with PIN complexity enabled")
+
+    @pytest.mark.parametrize("pin", ("111111", "22222222", "333333", 
"4444444"))
+    def test_repeated_pins(self, session, keys, pin):
+        with pytest.raises(ApduError):
+            session.change_pin(keys.pin, pin)
+
+    @pytest.mark.parametrize("pin", ("abc123", "password", "123123"))
+    def test_invalid_pins(self, session, keys, pin):
+        with pytest.raises(ApduError):
+            session.change_pin(keys.pin, pin)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/ykman/__init__.py 
new/yubikey_manager-5.4.0/ykman/__init__.py
--- old/yubikey_manager-5.3.0/ykman/__init__.py 2024-01-30 10:57:19.552522400 
+0100
+++ new/yubikey_manager-5.4.0/ykman/__init__.py 2024-03-26 14:52:50.944620000 
+0100
@@ -25,4 +25,4 @@
 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
-__version__ = "5.3.0"
+__version__ = "5.4.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/ykman/_cli/config.py 
new/yubikey_manager-5.4.0/ykman/_cli/config.py
--- old/yubikey_manager-5.3.0/ykman/_cli/config.py      2024-01-29 
09:10:25.565461600 +0100
+++ new/yubikey_manager-5.4.0/ykman/_cli/config.py      2024-03-26 
14:52:50.944620000 +0100
@@ -48,7 +48,6 @@
 )
 import os
 import re
-import sys
 import click
 import logging
 
@@ -68,7 +67,7 @@
 @click_postpone_execution
 def config(ctx):
     """
-    Enable or disable applications.
+    Configure the YubiKey, enable or disable applications.
 
     The applications may be enabled and disabled independently
     over different transports (USB and NFC). The configuration may
@@ -112,13 +111,15 @@
         )
 
 
-@config.command(hidden="--full-help" not in sys.argv)
+@config.command()
 @click.pass_context
 @click_force_option
 def reset(ctx, force):
     """
     Reset all YubiKey data.
 
+    This command is used with the YubiKey Bio Multi-protocol Edition.
+
     This action will wipe all data and restore factory settings for
     all applications on the YubiKey.
     """
@@ -127,7 +128,10 @@
     is_bio = info.form_factor in (FORM_FACTOR.USB_A_BIO, FORM_FACTOR.USB_C_BIO)
     has_piv = CAPABILITY.PIV in info.supported_capabilities.get(transport)
     if not (is_bio and has_piv):
-        raise CliFail("Full device reset is not supported on this YubiKey.")
+        raise CliFail(
+            "Full device reset is not supported on this YubiKey, "
+            "refer to reset commands for specific applications instead."
+        )
 
     force or click.confirm(
         "WARNING! This will delete all stored data and restore factory "
@@ -248,7 +252,9 @@
             f"{unsupported.display_name} not supported over {transport} on 
this "
             "YubiKey."
         )
-    new_enabled = (enabled | enable) & ~disable
+
+    # N.B. NOT (~) of IntFlag doesn't work as expected
+    new_enabled = (enabled | enable) & ~int(disable)
 
     if transport == TRANSPORT.USB:
         if sum(CAPABILITY) & new_enabled == 0:
@@ -268,11 +274,9 @@
     is_locked = info.is_locked
 
     if force and is_locked and not lock_code:
-        raise CliFail("Configuration is locked - please supply the --lock-code 
option.")
+        raise CliFail("Configuration is locked - supply the --lock-code 
option.")
     if lock_code and not is_locked:
-        raise CliFail(
-            "Configuration is not locked - please remove the --lock-code 
option."
-        )
+        raise CliFail("Configuration is not locked - remove the --lock-code 
option.")
 
     click.echo(f"{transport} configuration changes:")
     for change in changes:
@@ -635,6 +639,8 @@
                 f"Mode {mode} is not supported on this YubiKey!\n"
                 + "Use --force to attempt to set it anyway."
             )
+        elif info.is_sky and USB_INTERFACE.FIDO not in mode.interfaces:
+            raise CliFail("Security Key requires FIDO to be enabled.")
         force or click.confirm(f"Set mode of YubiKey to {mode}?", abort=True, 
err=True)
 
     try:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/ykman/_cli/fido.py 
new/yubikey_manager-5.4.0/ykman/_cli/fido.py
--- old/yubikey_manager-5.3.0/ykman/_cli/fido.py        2024-01-23 
12:00:35.030305100 +0100
+++ new/yubikey_manager-5.4.0/ykman/_cli/fido.py        2024-03-26 
14:52:50.944620000 +0100
@@ -36,6 +36,7 @@
     Config,
 )
 from fido2.pcsc import CtapPcscDevice
+from yubikit.management import CAPABILITY
 from yubikit.core.fido import FidoConnection
 from yubikit.core.smartcard import SW
 from time import sleep
@@ -63,10 +64,6 @@
 logger = logging.getLogger(__name__)
 
 
-FIPS_PIN_MIN_LENGTH = 6
-PIN_MIN_LENGTH = 4
-
-
 @click_group(connections=[FidoConnection])
 @click.pass_context
 @click_postpone_execution
@@ -172,8 +169,14 @@
     inserted, and requires a touch on the YubiKey.
     """
 
-    conn = ctx.obj["conn"]
+    info = ctx.obj["info"]
+    if CAPABILITY.FIDO2 in info.reset_blocked:
+        raise CliFail(
+            "Cannot perform FIDO reset when PIV is configured, "
+            "use 'ykman config reset' for full factory 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:
@@ -233,10 +236,10 @@
         )
         if is_fips:
             destroy_input = click_prompt(
-                "WARNING! This is a YubiKey FIPS device. This command will 
also "
-                "overwrite the U2F attestation key; this action cannot be 
undone and "
-                "this YubiKey will no longer be a FIPS compliant device.\n"
-                'To proceed, please enter the text "OVERWRITE"',
+                "WARNING! This is a YubiKey FIPS (4 Series) device. This 
command will "
+                "also overwrite the U2F attestation key; this action cannot be 
undone "
+                "and this YubiKey will no longer be a FIPS compliant device.\n"
+                'To proceed, enter the text "OVERWRITE"',
                 default="",
                 show_default=False,
             )
@@ -305,16 +308,17 @@
     "-u",
     "--u2f",
     is_flag=True,
-    help="set FIDO U2F PIN instead of FIDO2 PIN (YubiKey 4 FIPS only)",
+    help="set FIDO U2F PIN instead of FIDO2 PIN (YubiKey FIPS only)",
 )
 def change_pin(ctx, pin, new_pin, u2f):
     """
     Set or change the PIN code.
 
-    The FIDO2 PIN must be at least 4 characters long, and supports any type
-    of alphanumeric characters.
+    The FIDO2 PIN must be at least 4 characters long, and supports any type of
+    alphanumeric characters. Some YubiKeys can be configured to require a 
longer
+    PIN.
 
-    On YubiKey FIPS, a PIN can be set for FIDO U2F. That PIN must be at least
+    On YubiKey FIPS (4 Series), a PIN can be set for FIDO U2F. That PIN must 
be at least
     6 characters long.
     """
 
@@ -322,22 +326,29 @@
 
     if is_fips and not u2f:
         raise CliFail(
-            "This is a YubiKey FIPS. To set the U2F PIN, pass the --u2f 
option."
+            "This is a YubiKey FIPS (4 Series). "
+            "To set the U2F PIN, pass the --u2f option."
         )
 
     if u2f and not is_fips:
         raise CliFail(
-            "This is not a YubiKey 4 FIPS, and therefore does not support a 
U2F PIN. "
-            "To set the FIDO2 PIN, remove the --u2f option."
+            "This is not a YubiKey FIPS (4 Series), and therefore does not 
support a "
+            "U2F PIN. To set the FIDO2 PIN, remove the --u2f option."
         )
 
     if is_fips:
         conn = ctx.obj["conn"]
+        min_len = 6
     else:
         ctap2 = ctx.obj.get("ctap2")
         if not ctap2:
             raise CliFail("PIN is not supported on this YubiKey.")
         client_pin = ClientPin(ctap2)
+        min_len = ctap2.info.min_pin_length
+
+    def _fail_if_not_valid_pin(pin=None, name="PIN"):
+        if not pin or len(pin) < min_len:
+            raise CliFail(f"{name} must be at least {min_len} characters long")
 
     def prompt_new_pin():
         return click_prompt(
@@ -347,8 +358,6 @@
         )
 
     def change_pin(pin, new_pin):
-        if pin is not None:
-            _fail_if_not_valid_pin(ctx, pin, is_fips)
         try:
             if is_fips:
                 try:
@@ -357,7 +366,7 @@
                 except ApduError as e:
                     if e.code == SW.WRONG_LENGTH:
                         pin = _prompt_current_pin()
-                        _fail_if_not_valid_pin(ctx, pin, is_fips)
+                        _fail_if_not_valid_pin(pin)
                         fips_change_pin(conn, pin, new_pin)
                     else:
                         raise
@@ -367,7 +376,7 @@
 
         except CtapError as e:
             if e.code == CtapError.ERR.PIN_POLICY_VIOLATION:
-                raise CliFail("New PIN doesn't meet policy requirements.")
+                raise CliFail("New PIN doesn't meet complexity requirements.")
             else:
                 _fail_pin_error(ctx, e, "Failed to change PIN: %s")
 
@@ -380,12 +389,12 @@
                 raise CliFail(f"Failed to change PIN: SW={e.code:04x}")
 
     def set_pin(new_pin):
-        _fail_if_not_valid_pin(ctx, new_pin, is_fips)
+        _fail_if_not_valid_pin(new_pin)
         try:
             client_pin.set_pin(new_pin)
         except CtapError as e:
             if e.code == CtapError.ERR.PIN_POLICY_VIOLATION:
-                raise CliFail("New PIN doesn't meet policy requirements.")
+                raise CliFail("New PIN doesn't meet complexity requirements.")
             else:
                 raise CliFail(f"Failed to set PIN: {e.code}")
 
@@ -399,14 +408,11 @@
 
     if not new_pin:
         new_pin = prompt_new_pin()
+    _fail_if_not_valid_pin(new_pin, "New PIN")
 
     if is_fips:
-        _fail_if_not_valid_pin(ctx, new_pin, is_fips)
         change_pin(pin, new_pin)
     else:
-        min_len = ctap2.info.min_pin_length
-        if len(new_pin) < min_len:
-            raise CliFail(f"New PIN is too short. Minimum length: {min_len}")
         if ctap2.info.options.get("clientPin"):
             change_pin(pin, new_pin)
         else:
@@ -435,7 +441,7 @@
     Verify the FIDO PIN against a YubiKey.
 
     For YubiKeys supporting FIDO2 this will reset the "retries" counter of the 
PIN.
-    For YubiKey FIPS this will unlock the session, allowing U2F registration.
+    For YubiKey FIPS (4 Series) this will unlock the session, allowing U2F 
registration.
     """
 
     ctap2 = ctx.obj.get("ctap2")
@@ -450,7 +456,6 @@
         except CtapError as e:
             raise CliFail(f"PIN verification failed: {e}")
     elif is_yk4_fips(ctx.obj["info"]):
-        _fail_if_not_valid_pin(ctx, pin, True)
         try:
             fips_verify_pin(ctx.obj["conn"], pin)
         except ApduError as e:
@@ -472,14 +477,20 @@
     if not Config.is_supported(ctap2.info):
         raise CliFail("Authenticator Configuration is not supported on this 
YubiKey.")
 
-    pin = _require_pin(ctx, pin, "Authenticator Configuration")
-    client_pin = ClientPin(ctap2)
-    try:
-        token = client_pin.get_pin_token(pin, 
ClientPin.PERMISSION.AUTHENTICATOR_CFG)
-    except CtapError as e:
-        _fail_pin_error(ctx, e, "PIN error: %s")
+    protocol = None
+    token = None
+    if ctap2.info.options.get("clientPin"):
+        pin = _require_pin(ctx, pin, "Authenticator Configuration")
+        client_pin = ClientPin(ctap2)
+        try:
+            protocol = client_pin.protocol
+            token = client_pin.get_pin_token(
+                pin, ClientPin.PERMISSION.AUTHENTICATOR_CFG
+            )
+        except CtapError as e:
+            _fail_pin_error(ctx, e, "PIN error: %s")
 
-    return Config(ctap2, client_pin.protocol, token)
+    return Config(ctap2, protocol, token)
 
 
 @access.command("force-change")
@@ -492,6 +503,8 @@
     options = ctx.obj.get("ctap2").info.options
     if not options.get("setMinPINLength"):
         raise CliFail("Force change PIN is not supported on this YubiKey.")
+    if not options.get("clientPin"):
+        raise CliFail("No PIN is set.")
 
     config = _init_config(ctx, pin)
     config.set_min_pin_length(force_change_pin=True)
@@ -509,9 +522,17 @@
     Optionally use the --rp option to specify which RPs are allowed to request 
this
     information.
     """
-    options = ctx.obj.get("ctap2").info.options
-    if not options.get("setMinPINLength"):
+    info = ctx.obj["ctap2"].info
+    if not info.options.get("setMinPINLength"):
         raise CliFail("Set minimum PIN length is not supported on this 
YubiKey.")
+    if info.options.get("alwaysUv") and not info.options.get("clientPin"):
+        raise CliFail(
+            "Setting min PIN length requires a PIN to be set when alwaysUv is 
enabled."
+        )
+
+    min_len = info.min_pin_length
+    if length < min_len:
+        raise CliFail(f"Cannot set a minimum length that is shorter than 
{min_len}.")
 
     config = _init_config(ctx, pin)
     if rp_id:
@@ -521,6 +542,7 @@
             raise CliFail(
                 f"Authenticator supports up to {cap} RP IDs ({len(rp_id)} 
given)."
             )
+
     config.set_min_pin_length(min_pin_length=length, rp_ids=rp_id)
 
 
@@ -528,12 +550,6 @@
     return click_prompt(prompt, hide_input=True)
 
 
-def _fail_if_not_valid_pin(ctx, pin=None, is_fips=False):
-    min_length = FIPS_PIN_MIN_LENGTH if is_fips else PIN_MIN_LENGTH
-    if not pin or len(pin) < min_length:
-        ctx.fail(f"PIN must be over {min_length} characters long")
-
-
 def _gen_creds(credman):
     data = credman.get_metadata()
     if data.get(CredentialManagement.RESULT.EXISTING_CRED_COUNT) == 0:
@@ -889,6 +905,11 @@
     options = ctx.obj.get("ctap2").info.options
     if "ep" not in options:
         raise CliFail("Enterprise Attestation is not supported on this 
YubiKey.")
+    if options.get("alwaysUv") and not options.get("clientPin"):
+        raise CliFail(
+            "Enabling Enterprise Attestation requires a PIN to be set when 
alwaysUv is "
+            "enabled."
+        )
 
     config = _init_config(ctx, pin)
     config.enable_enterprise_attestation()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/ykman/_cli/hsmauth.py 
new/yubikey_manager-5.4.0/ykman/_cli/hsmauth.py
--- old/yubikey_manager-5.3.0/ykman/_cli/hsmauth.py     2023-10-30 
11:10:16.448203300 +0100
+++ new/yubikey_manager-5.4.0/ykman/_cli/hsmauth.py     2024-03-26 
14:52:50.944620000 +0100
@@ -77,6 +77,8 @@
             raise CliFail("Credential with the provided label was not found.")
         elif e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED:
             raise CliFail("The device was not touched.")
+        elif e.sw == SW.CONDITIONS_NOT_SATISFIED:
+            raise CliFail("Credential password does not meet complexity 
requirement.")
     raise CliFail(default_exception_msg)
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/ykman/_cli/info.py 
new/yubikey_manager-5.4.0/ykman/_cli/info.py
--- old/yubikey_manager-5.3.0/ykman/_cli/info.py        2023-09-17 
13:05:24.080793000 +0200
+++ new/yubikey_manager-5.4.0/ykman/_cli/info.py        2024-03-26 
14:52:50.944620000 +0100
@@ -127,7 +127,6 @@
 
 def _check_fips_status(device, info):
     fips_status = get_overall_fips_status(device, info)
-    click.echo()
 
     click.echo(f"FIPS Approved Mode: {'Yes' if all(fips_status.values()) else 
'No'}")
 
@@ -140,7 +139,7 @@
 @click.option(
     "-c",
     "--check-fips",
-    help="check if YubiKey is in FIPS Approved mode (YubiKey 4 FIPS only)",
+    help="check if YubiKey is in FIPS Approved mode (4 Series only)",
     is_flag=True,
 )
 @click_command(connections=[SmartCardConnection, OtpConnection, 
FidoConnection])
@@ -186,18 +185,23 @@
             if info.config.enabled_capabilities.get(TRANSPORT.NFC)
             else "disabled"
         )
-        click.echo(f"NFC transport is {f_nfc}.")
+        click.echo(f"NFC transport is {f_nfc}")
+    if info.pin_complexity:
+        click.echo("PIN complexity is enforced")
     if info.is_locked:
-        click.echo("Configured capabilities are protected by a lock code.")
-    click.echo()
+        click.echo("Configured capabilities are protected by a lock code")
 
+    click.echo()
     print_app_status_table(
         info.supported_capabilities, info.config.enabled_capabilities
     )
 
     if check_fips:
+        click.echo()
         if is_yk4_fips(info):
             device = ctx.obj["device"]
             _check_fips_status(device, info)
         else:
-            raise CliFail("Unable to check FIPS Approved mode - Not a YubiKey 
4 FIPS")
+            raise CliFail(
+                "Unable to check FIPS Approved mode - Not a YubiKey FIPS (4 
Series)"
+            )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/ykman/_cli/oath.py 
new/yubikey_manager-5.4.0/ykman/_cli/oath.py
--- old/yubikey_manager-5.3.0/ykman/_cli/oath.py        2023-10-30 
11:10:16.448203300 +0100
+++ new/yubikey_manager-5.4.0/ykman/_cli/oath.py        2024-03-26 
14:52:50.944620000 +0100
@@ -76,7 +76,7 @@
 
     \b
       Set a password for the OATH application:
-      $ ykman oath access change-password
+      $ ykman oath access change
     """
 
     dev = ctx.obj["device"]
@@ -356,9 +356,7 @@
 
 
 def _error_multiple_hits(ctx, hits):
-    click.echo(
-        "Error: Multiple matches, please make the query more specific.", 
err=True
-    )
+    click.echo("Error: Multiple matches, make the query more specific.", 
err=True)
     click.echo("", err=True)
     for cred in hits:
         click.echo(_string_id(cred), err=True)
@@ -618,7 +616,7 @@
 
     Generate codes from OATH accounts stored on the YubiKey.
     Provide a query string to match one or more specific accounts.
-    Accounts of type HOTP, or those that require touch, requre a single match 
to be
+    Accounts of type HOTP, or those that require touch, require a single match 
to be
     triggered.
     """
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/ykman/_cli/openpgp.py 
new/yubikey_manager-5.4.0/ykman/_cli/openpgp.py
--- old/yubikey_manager-5.3.0/ykman/_cli/openpgp.py     2024-01-23 
12:00:35.030305100 +0100
+++ new/yubikey_manager-5.4.0/ykman/_cli/openpgp.py     2024-03-26 
14:52:50.944620000 +0100
@@ -195,7 +195,12 @@
             confirmation_prompt=True,
         )
 
-    session.change_pin(pin, new_pin)
+    try:
+        session.change_pin(pin, new_pin)
+    except ApduError as e:
+        if e.sw == SW.CONDITIONS_NOT_SATISFIED:
+            raise CliFail("PIN does not meet complexity requirement.")
+        raise
 
 
 @access.command("change-reset-code")
@@ -223,7 +228,12 @@
         )
 
     session.verify_admin(admin_pin)
-    session.set_reset_code(reset_code)
+    try:
+        session.set_reset_code(reset_code)
+    except ApduError as e:
+        if e.sw == SW.CONDITIONS_NOT_SATISFIED:
+            raise CliFail("Reset Code does not meet complexity requirement.")
+        raise
 
 
 @access.command("change-admin-pin")
@@ -250,7 +260,12 @@
             confirmation_prompt=True,
         )
 
-    session.change_admin(admin_pin, new_admin_pin)
+    try:
+        session.change_admin(admin_pin, new_admin_pin)
+    except ApduError as e:
+        if e.sw == SW.CONDITIONS_NOT_SATISFIED:
+            raise CliFail("Admin PIN does not meet complexity requirement.")
+        raise
 
 
 @access.command("unblock-pin")
@@ -294,7 +309,13 @@
 
     if admin_pin:
         session.verify_admin(admin_pin)
-    session.reset_pin(new_pin, reset_code)
+
+    try:
+        session.reset_pin(new_pin, reset_code)
+    except ApduError as e:
+        if e.sw == SW.CONDITIONS_NOT_SATISFIED:
+            raise CliFail("New PIN does not meet complexity requirement.")
+        raise
 
 
 @access.command("set-signature-policy")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/ykman/_cli/otp.py 
new/yubikey_manager-5.4.0/ykman/_cli/otp.py
--- old/yubikey_manager-5.3.0/ykman/_cli/otp.py 2023-09-17 13:05:24.080793000 
+0200
+++ new/yubikey_manager-5.4.0/ykman/_cli/otp.py 2024-03-26 14:52:50.944620000 
+0100
@@ -422,7 +422,7 @@
             click.echo(f"Using YubiKey serial as public ID: {public_id}")
         elif force:
             ctx.fail(
-                "Public ID not given. Please remove the --force flag, or "
+                "Public ID not given. Remove the --force flag, or "
                 "add the --serial-public-id flag or --public-id option."
             )
         else:
@@ -441,7 +441,7 @@
             click.echo(f"Using a randomly generated private ID: 
{private_id.hex()}")
         elif force:
             ctx.fail(
-                "Private ID not given. Please remove the --force flag, or "
+                "Private ID not given. Remove the --force flag, or "
                 "add the --generate-private-id flag or --private-id option."
             )
         else:
@@ -454,7 +454,7 @@
             click.echo(f"Using a randomly generated secret key: {key.hex()}")
         elif force:
             ctx.fail(
-                "Secret key not given. Please remove the --force flag, or "
+                "Secret key not given. Remove the --force flag, or "
                 "add the --generate-key flag or --key option."
             )
         else:
@@ -619,7 +619,7 @@
     else:
         if force and not generate:
             ctx.fail(
-                "No secret key given. Please remove the --force flag, "
+                "No secret key given. Remove the --force flag, "
                 "set the KEY argument or set the --generate flag."
             )
         elif generate:
@@ -906,6 +906,8 @@
             new_access_code = parse_access_code_hex(new_access_code)
         except Exception as e:
             ctx.fail("Failed to parse access code: " + str(e))
+        if ctx.obj["info"].pin_complexity and len(set(new_access_code)) < 2:
+            raise CliFail("Access code does not meet complexity requirement.")
 
     force or click.confirm(
         f"Update the settings for slot {slot}? "
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/ykman/_cli/piv.py 
new/yubikey_manager-5.4.0/ykman/_cli/piv.py
--- old/yubikey_manager-5.3.0/ykman/_cli/piv.py 2024-01-29 09:10:25.565461600 
+0100
+++ new/yubikey_manager-5.4.0/ykman/_cli/piv.py 2024-03-26 14:52:50.944620000 
+0100
@@ -27,6 +27,7 @@
 
 from yubikit.core import NotSupportedError
 from yubikit.core.smartcard import SmartCardConnection
+from yubikit.management import CAPABILITY
 from yubikit.piv import (
     PivSession,
     InvalidPinError,
@@ -220,6 +221,13 @@
     This action will wipe all data and restore factory settings for
     the PIV application on the YubiKey.
     """
+    info = ctx.obj["info"]
+    if CAPABILITY.PIV in info.reset_blocked:
+        raise CliFail(
+            "Cannot perform PIV reset when biometrics are configured, "
+            "use 'ykman config reset' for full factory reset."
+        )
+
     force or click.confirm(
         "WARNING! This will delete all stored PIV data and restore factory "
         "settings. Proceed?",
@@ -275,6 +283,31 @@
         raise CliFail("Setting pin retries failed.")
 
 
+def _do_change_pin_puk(pin_complexity, name, current, new, fn):
+    def validate_pin_length(pin, prefix):
+        unit = "characters" if pin_complexity else "bytes"
+        pin_len = len(pin) if pin_complexity else len(pin.encode())
+        if not 6 <= pin_len <= 8:
+            raise CliFail(f"{prefix} {name} must be between 6 and 8 {unit} 
long.")
+
+    validate_pin_length(current, "Current")
+    validate_pin_length(new, "New")
+
+    try:
+        fn()
+        click.echo(f"New {name} set.")
+    except InvalidPinError as e:
+        attempts = e.attempts_remaining
+        if attempts:
+            raise CliFail(f"{name} change failed - %d tries left." % attempts)
+        else:
+            raise CliFail(f"{name} is blocked.")
+    except ApduError as e:
+        if e.sw == SW.CONDITIONS_NOT_SATISFIED:
+            raise CliFail(f"{name} does not meet complexity requirement.")
+        raise
+
+
 @access.command("change-pin")
 @click.pass_context
 @click.option("-P", "--pin", help="current PIN code")
@@ -283,11 +316,11 @@
     """
     Change the PIN code.
 
-    The PIN must be between 6 and 8 characters long, and supports any type of
+    The PIN must be between 6 and 8 bytes long, and supports any type of
     alphanumeric characters. For cross-platform compatibility, numeric PINs are
     recommended.
     """
-
+    info = ctx.obj["info"]
     session = ctx.obj["session"]
 
     if not pin:
@@ -301,21 +334,13 @@
             confirmation_prompt=True,
         )
 
-    if not _valid_pin_length(pin):
-        ctx.fail("Current PIN must be between 6 and 8 characters long.")
-
-    if not _valid_pin_length(new_pin):
-        ctx.fail("New PIN must be between 6 and 8 characters long.")
-
-    try:
-        pivman_change_pin(session, pin, new_pin)
-        click.echo("New PIN set.")
-    except InvalidPinError as e:
-        attempts = e.attempts_remaining
-        if attempts:
-            raise CliFail("PIN change failed - %d tries left." % attempts)
-        else:
-            raise CliFail("PIN is blocked.")
+    _do_change_pin_puk(
+        info.pin_complexity,
+        "PIN",
+        pin,
+        new_pin,
+        lambda: pivman_change_pin(session, pin, new_pin),
+    )
 
 
 @access.command("change-puk")
@@ -327,10 +352,12 @@
     Change the PUK code.
 
     If the PIN is lost or blocked it can be reset using a PUK.
-    The PUK must be between 6 and 8 characters long, and supports any type of
+    The PUK must be between 6 and 8 bytes long, and supports any type of
     alphanumeric characters.
     """
+    info = ctx.obj["info"]
     session = ctx.obj["session"]
+
     if not puk:
         puk = _prompt_pin("Enter the current PUK")
     if not new_puk:
@@ -342,21 +369,13 @@
             confirmation_prompt=True,
         )
 
-    if not _valid_pin_length(puk):
-        ctx.fail("Current PUK must be between 6 and 8 characters long.")
-
-    if not _valid_pin_length(new_puk):
-        ctx.fail("New PUK must be between 6 and 8 characters long.")
-
-    try:
-        session.change_puk(puk, new_puk)
-        click.echo("New PUK set.")
-    except InvalidPinError as e:
-        attempts = e.attempts_remaining
-        if attempts:
-            raise CliFail("PUK change failed - %d tries left." % attempts)
-        else:
-            raise CliFail("PUK is blocked.")
+    _do_change_pin_puk(
+        info.pin_complexity,
+        "PUK",
+        puk,
+        new_puk,
+        lambda: session.change_puk(puk, new_puk),
+    )
 
 
 @access.command("change-management-key")
@@ -461,7 +480,7 @@
                 click.echo(f"Generated management key: 
{new_management_key.hex()}")
         elif force:
             ctx.fail(
-                "New management key not given. Please remove the --force "
+                "New management key not given. Remove the --force "
                 "flag, or set the --generate flag or the "
                 "--new-management-key option."
             )
@@ -504,7 +523,11 @@
         puk = click_prompt("Enter PUK", default="", show_default=False, 
hide_input=True)
     if not new_pin:
         new_pin = click_prompt(
-            "Enter a new PIN", default="", show_default=False, hide_input=True
+            "Enter a new PIN",
+            default="",
+            show_default=False,
+            hide_input=True,
+            confirmation_prompt=True,
         )
     try:
         session.unblock_pin(puk, new_pin)
@@ -515,6 +538,10 @@
             raise CliFail("PIN unblock failed - %d tries left." % attempts)
         else:
             raise CliFail("PUK is blocked.")
+    except ApduError as e:
+        if e.sw == SW.CONDITIONS_NOT_SATISFIED:
+            raise CliFail("PIN does not meet complexity requirement.")
+        raise
 
 
 @piv.group()
@@ -686,7 +713,7 @@
     except ApduError as e:
         if e.sw == SW.REFERENCE_DATA_NOT_FOUND:
             raise CliFail(f"No key stored in slot {slot}.")
-        raise e
+        raise
 
 
 @keys.command()
@@ -804,7 +831,12 @@
     """
     session = ctx.obj["session"]
     _ensure_authenticated(ctx, pin, management_key)
-    session.delete_key(slot)
+    try:
+        session.delete_key(slot)
+    except ApduError as e:
+        if e.sw == SW.REFERENCE_DATA_NOT_FOUND:
+            raise CliFail(f"No key stored in slot {slot}.")
+        raise
 
 
 @piv.group("certificates")
@@ -892,8 +924,8 @@
                 timeout = None
         except ApduError as e:
             if e.sw == SW.REFERENCE_DATA_NOT_FOUND:
-                raise CliFail("No private key in slot {slot}")
-            raise e
+                raise CliFail(f"No private key in slot {slot}")
+            raise
         except NotSupportedError:
             timeout = 1.0
 
@@ -982,7 +1014,8 @@
             timeout = None
     except ApduError as e:
         if e.sw == SW.REFERENCE_DATA_NOT_FOUND:
-            raise CliFail("No private key in slot {slot}")
+            raise CliFail(f"No private key in slot {slot}.")
+        raise
     except NotSupportedError:
         timeout = 1.0
 
@@ -1054,7 +1087,8 @@
             timeout = None
     except ApduError as e:
         if e.sw == SW.REFERENCE_DATA_NOT_FOUND:
-            raise CliFail("No private key in slot {slot}")
+            raise CliFail(f"No private key in slot {slot}.")
+        raise
     except NotSupportedError:
         timeout = 1.0
 
@@ -1172,7 +1206,7 @@
     except ApduError as e:
         if e.sw == SW.INCORRECT_PARAMETERS:
             raise CliFail("Something went wrong, is the object id valid?")
-        raise CliFail("Error writing object")
+        raise CliFail("Error writing object.")
 
 
 @objects.command("generate")
@@ -1219,10 +1253,6 @@
     return click_prompt(prompt, default="", hide_input=True, 
show_default=False)
 
 
-def _valid_pin_length(pin):
-    return 6 <= len(pin) <= 8
-
-
 def _ensure_authenticated(
     ctx,
     pin=None,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/ykman/diagnostics.py 
new/yubikey_manager-5.4.0/ykman/diagnostics.py
--- old/yubikey_manager-5.3.0/ykman/diagnostics.py      2024-01-15 
10:53:20.944693800 +0100
+++ new/yubikey_manager-5.4.0/ykman/diagnostics.py      2024-03-26 
14:52:50.944620000 +0100
@@ -6,6 +6,7 @@
 from .openpgp import get_openpgp_info
 from .hsmauth import get_hsmauth_info
 
+from yubikit.core import Tlv
 from yubikit.core.smartcard import SmartCardConnection
 from yubikit.core.fido import FidoConnection
 from yubikit.core.otp import OtpConnection
@@ -51,9 +52,13 @@
 def mgmt_info(pid, conn):
     data: List[Any] = []
     try:
+        m = ManagementSession(conn)
+        raw_info = m.backend.read_config()
+        if Tlv.parse_dict(raw_info[1:]).get(0x10) == b"\1":
+            raw_info += m.backend.read_config(1)
         data.append(
             {
-                "Raw Info": ManagementSession(conn).backend.read_config(),
+                "Raw Info": raw_info,
             }
         )
     except Exception as e:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/ykman/fido.py 
new/yubikey_manager-5.4.0/ykman/fido.py
--- old/yubikey_manager-5.3.0/ykman/fido.py     2023-09-17 13:05:24.080793000 
+0200
+++ new/yubikey_manager-5.4.0/ykman/fido.py     2024-03-26 14:52:50.944620000 
+0100
@@ -44,7 +44,7 @@
 
 
 def is_in_fips_mode(fido_connection: FidoConnection) -> bool:
-    """Check if a YubiKey FIPS is in FIPS approved mode.
+    """Check if a YubiKey 4 FIPS is in FIPS approved mode.
 
     :param fido_connection: A FIDO connection.
     """
@@ -62,7 +62,7 @@
 def fips_change_pin(
     fido_connection: FidoConnection, old_pin: Optional[str], new_pin: str
 ):
-    """Change the PIN on a YubiKey FIPS.
+    """Change the PIN on a YubiKey 4 FIPS.
 
     If no PIN is set, pass None or an empty string as old_pin.
 
@@ -82,7 +82,7 @@
 
 
 def fips_verify_pin(fido_connection: FidoConnection, pin: str):
-    """Unlock the YubiKey FIPS U2F module for credential creation.
+    """Unlock the YubiKey 4 FIPS U2F module for credential creation.
 
     :param fido_connection: A FIDO connection.
     :param pin: The FIDO PIN.
@@ -92,11 +92,11 @@
 
 
 def fips_reset(fido_connection: FidoConnection):
-    """Reset the FIDO module of a YubiKey FIPS.
+    """Reset the FIDO module of a YubiKey 4 FIPS.
 
-    Note: This action is only permitted immediately after YubiKey FIPS 
power-up. It
-    also requires the user to touch the flashing button on the YubiKey, and 
will halt
-    until that happens, or the command times out.
+    Note: This action is only permitted immediately after YubiKey power-up. It 
also
+    requires the user to touch the flashing button on the YubiKey, and will 
halt until
+    that happens, or the command times out.
 
     :param fido_connection: A FIDO connection.
     """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/ykman/hid/linux.py 
new/yubikey_manager-5.4.0/ykman/hid/linux.py
--- old/yubikey_manager-5.3.0/ykman/hid/linux.py        2023-09-17 
13:05:24.080793000 +0200
+++ new/yubikey_manager-5.4.0/ykman/hid/linux.py        2024-03-26 
14:52:50.944620000 +0100
@@ -109,15 +109,15 @@
 
 
 def list_devices():
-    stale = set(_failed_cache)
     devices = []
     for hidraw in glob.glob("/dev/hidraw*"):
-        stale.discard(hidraw)
         try:
             with open(hidraw, "rb") as f:
                 bustype, vid, pid = get_info(f)
                 if vid == YUBICO_VID and get_usage(f) == USAGE_OTP:
                     devices.append(OtpYubiKeyDevice(hidraw, pid, 
HidrawConnection))
+            if hidraw in _failed_cache:
+                _failed_cache.remove(hidraw)
         except Exception:
             if hidraw not in _failed_cache:
                 logger.debug(
@@ -126,7 +126,4 @@
                 _failed_cache.add(hidraw)
             continue
 
-    # Remove entries from the cache that were not seen
-    _failed_cache.difference_update(hidraw)
-
     return devices
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/ykman/otp.py 
new/yubikey_manager-5.4.0/ykman/otp.py
--- old/yubikey_manager-5.3.0/ykman/otp.py      2023-09-17 13:05:24.084793000 
+0200
+++ new/yubikey_manager-5.4.0/ykman/otp.py      2024-03-26 14:52:50.944620000 
+0100
@@ -52,7 +52,7 @@
     CONNECTION_FAILED = "Failed to open HTTPS connection."
     NOT_FOUND = "Upload request not recognized by server."
     SERVICE_UNAVAILABLE = (
-        "Service temporarily unavailable, please try again later."  # noqa: 
E501
+        "Service temporarily unavailable, try again later."  # noqa: E501
     )
 
     # Defined in upload project
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/ykman/pcsc/__init__.py 
new/yubikey_manager-5.4.0/ykman/pcsc/__init__.py
--- old/yubikey_manager-5.3.0/ykman/pcsc/__init__.py    2023-09-17 
13:05:24.084793000 +0200
+++ new/yubikey_manager-5.4.0/ykman/pcsc/__init__.py    2024-03-26 
14:52:50.944620000 +0100
@@ -95,7 +95,7 @@
         try:
             return ScardSmartCardConnection(self.reader.createConnection())
         except CardConnectionException as e:
-            if kill_scdaemon():
+            if kill_scdaemon() or kill_yubikey_agent():
                 return ScardSmartCardConnection(self.reader.createConnection())
             raise e
 
@@ -152,6 +152,17 @@
     return killed
 
 
+def kill_yubikey_agent():
+    killed = False
+    return_code = subprocess.call(["pkill", "-HUP", "yubikey-agent"])  # nosec
+    if return_code == 0:
+        killed = True
+    if killed:
+        sleep(0.1)
+
+    return killed
+
+
 def list_readers():
     try:
         return System.readers()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/yubikit/management.py 
new/yubikey_manager-5.4.0/yubikit/management.py
--- old/yubikey_manager-5.3.0/yubikit/management.py     2024-01-29 
09:10:25.565461600 +0100
+++ new/yubikey_manager-5.4.0/yubikit/management.py     2024-03-26 
14:52:50.948619800 +0100
@@ -49,7 +49,7 @@
 from fido2.hid import CAPABILITY as CTAP_CAPABILITY
 
 from enum import IntEnum, IntFlag, unique
-from dataclasses import dataclass
+from dataclasses import dataclass, field
 from typing import Optional, Union, Mapping
 import abc
 import struct
@@ -97,7 +97,12 @@
         if self & (CAPABILITY.U2F | CAPABILITY.FIDO2):
             ifaces |= USB_INTERFACE.FIDO
         if self & (
-            CAPABILITY.OATH | CAPABILITY.PIV | CAPABILITY.OPENPGP | 
CAPABILITY.HSMAUTH
+            0x4  # General CCID bit
+            | 0x400  # Management over CCID bit
+            | CAPABILITY.OATH
+            | CAPABILITY.PIV
+            | CAPABILITY.OPENPGP
+            | CAPABILITY.HSMAUTH
         ):
             ifaces |= USB_INTERFACE.CCID
         return ifaces
@@ -164,16 +169,25 @@
 TAG_REBOOT = 0x0C
 TAG_NFC_SUPPORTED = 0x0D
 TAG_NFC_ENABLED = 0x0E
+TAG_IAP_DETECTION = 0x0F
+TAG_MORE_DATA = 0x10
+TAG_FREE_FORM = 0x11
+TAG_HID_INIT_DELAY = 0x12
+TAG_PART_NUMBER = 0x13
+TAG_PIN_COMPLEXITY = 0x16
+TAG_NFC_RESTRICTED = 0x17
+TAG_RESET_BLOCKED = 0x18
 
 
 @dataclass
 class DeviceConfig:
     """Management settings for YubiKey which can be configured by the user."""
 
-    enabled_capabilities: Mapping[TRANSPORT, CAPABILITY]
-    auto_eject_timeout: Optional[int]
-    challenge_response_timeout: Optional[int]
-    device_flags: Optional[DEVICE_FLAG]
+    enabled_capabilities: Mapping[TRANSPORT, CAPABILITY] = 
field(default_factory=dict)
+    auto_eject_timeout: Optional[int] = None
+    challenge_response_timeout: Optional[int] = None
+    device_flags: Optional[DEVICE_FLAG] = None
+    nfc_restricted: Optional[bool] = None
 
     def get_bytes(
         self,
@@ -200,6 +214,8 @@
             buf += Tlv(TAG_DEVICE_FLAGS, int2bytes(self.device_flags))
         if new_lock_code:
             buf += Tlv(TAG_CONFIG_LOCK, new_lock_code)
+        if self.nfc_restricted is not None:
+            buf += Tlv(TAG_NFC_RESTRICTED, b"\1" if self.nfc_restricted else 
b"\0")
         if len(buf) > 0xFF:
             raise NotSupportedError("DeviceConfiguration too large")
         return int2bytes(len(buf)) + buf
@@ -217,6 +233,8 @@
     is_locked: bool
     is_fips: bool = False
     is_sky: bool = False
+    pin_complexity: bool = False
+    reset_blocked: CAPABILITY = CAPABILITY(0)
 
     def has_transport(self, transport: TRANSPORT) -> bool:
         return transport in self.supported_capabilities
@@ -225,7 +243,12 @@
     def parse(cls, encoded: bytes, default_version: Version) -> "DeviceInfo":
         if len(encoded) - 1 != encoded[0]:
             raise BadResponseError("Invalid length")
-        data = Tlv.parse_dict(encoded[1:])
+        return cls.parse_tlvs(Tlv.parse_dict(encoded[1:]), default_version)
+
+    @classmethod
+    def parse_tlvs(
+        cls, data: Mapping[int, bytes], default_version: Version
+    ) -> "DeviceInfo":
         locked = data.get(TAG_CONFIG_LOCK) == b"\1"
         serial = bytes2int(data.get(TAG_SERIAL, b"\0")) or None
         ff_value = bytes2int(data.get(TAG_FORM_FACTOR, b"\0"))
@@ -253,9 +276,12 @@
         if TAG_NFC_SUPPORTED in data:  # YK with NFC
             supported[TRANSPORT.NFC] = 
CAPABILITY(bytes2int(data[TAG_NFC_SUPPORTED]))
             enabled[TRANSPORT.NFC] = 
CAPABILITY(bytes2int(data[TAG_NFC_ENABLED]))
+        nfc_restricted = data.get(TAG_NFC_RESTRICTED, b"\0") == b"\1"
+        pin_complexity = data.get(TAG_PIN_COMPLEXITY, b"\0") == b"\1"
+        reset_blocked = CAPABILITY(bytes2int(data.get(TAG_RESET_BLOCKED, 
b"\0")))
 
         return cls(
-            DeviceConfig(enabled, auto_eject_to, chal_resp_to, flags),
+            DeviceConfig(enabled, auto_eject_to, chal_resp_to, flags, 
nfc_restricted),
             serial,
             version,
             form_factor,
@@ -263,6 +289,8 @@
             locked,
             fips,
             sky,
+            pin_complexity,
+            reset_blocked,
         )
 
 
@@ -320,7 +348,7 @@
         ...
 
     @abc.abstractmethod
-    def read_config(self) -> bytes:
+    def read_config(self, page: int = 0) -> bytes:
         ...
 
     @abc.abstractmethod
@@ -347,8 +375,10 @@
                 return  # ProgSeq isn't updated by set mode when empty
             raise
 
-    def read_config(self):
-        response = self.protocol.send_and_receive(SLOT_YK4_CAPABILITIES)
+    def read_config(self, page: int = 0):
+        response = self.protocol.send_and_receive(
+            SLOT_YK4_CAPABILITIES, int2bytes(page)
+        )
         r_len = response[0]
         if check_crc(response[: r_len + 1 + 2]):
             return response[: r_len + 1]
@@ -397,8 +427,8 @@
         else:
             self.protocol.send_apdu(0, INS_SET_MODE, P1_DEVICE_CONFIG, 0, data)
 
-    def read_config(self):
-        return self.protocol.send_apdu(0, INS_READ_CONFIG, 0, 0)
+    def read_config(self, page: int = 0):
+        return self.protocol.send_apdu(0, INS_READ_CONFIG, page, 0)
 
     def write_config(self, config):
         self.protocol.send_apdu(0, INS_WRITE_CONFIG, 0, 0, config)
@@ -430,8 +460,8 @@
     def set_mode(self, data):
         self.ctap.call(CTAP_YUBIKEY_DEVICE_CONFIG, data)
 
-    def read_config(self):
-        return self.ctap.call(CTAP_READ_CONFIG)
+    def read_config(self, page: int = 0):
+        return self.ctap.call(CTAP_READ_CONFIG, int2bytes(page))
 
     def write_config(self, config):
         self.ctap.call(CTAP_WRITE_CONFIG, config)
@@ -464,7 +494,20 @@
     def read_device_info(self) -> DeviceInfo:
         """Get detailed information about the YubiKey."""
         require_version(self.version, (4, 1, 0))
-        return DeviceInfo.parse(self.backend.read_config(), self.version)
+        more_data = True
+        tlvs = {}
+        page = 0
+        while more_data:
+            logger.debug(f"Reading DeviceInfo page: {page}")
+            encoded = self.backend.read_config(page)
+            if len(encoded) - 1 != encoded[0]:
+                raise BadResponseError("Invalid length")
+            data = Tlv.parse_dict(encoded[1:])
+            more_data = data.pop(TAG_MORE_DATA, 0) == b"\1"
+            tlvs.update(data)
+            page += 1
+
+        return DeviceInfo.parse_tlvs(tlvs, self.version)
 
     def write_device_config(
         self,
@@ -485,7 +528,7 @@
             raise ValueError("Lock code must be 16 bytes")
         if new_lock_code is not None and len(new_lock_code) != 16:
             raise ValueError("Lock code must be 16 bytes")
-        config = config or DeviceConfig({}, None, None, None)
+        config = config or DeviceConfig()
         logger.debug(
             f"Writing device config: {config}, reboot: {reboot}, "
             f"current lock code: {cur_lock_code is not None}, "
@@ -520,9 +563,21 @@
             if USB_INTERFACE.OTP in mode.interfaces:
                 usb_enabled |= CAPABILITY.OTP
             if USB_INTERFACE.CCID in mode.interfaces:
-                usb_enabled |= CAPABILITY.OATH | CAPABILITY.PIV | 
CAPABILITY.OPENPGP
+                usb_enabled |= (
+                    CAPABILITY.OATH
+                    | CAPABILITY.PIV
+                    | CAPABILITY.OPENPGP
+                    | CAPABILITY.HSMAUTH
+                    | 0x400  # Management over CCID bit
+                )
             if USB_INTERFACE.FIDO in mode.interfaces:
                 usb_enabled |= CAPABILITY.U2F | CAPABILITY.FIDO2
+
+            # Overlay with supported capabilities
+            supported = self.read_device_info().supported_capabilities.get(
+                TRANSPORT.USB, 0
+            )
+            usb_enabled = usb_enabled & supported
             logger.debug(f"Delegating to DeviceConfig with usb_enabled: 
{usb_enabled}")
             # N.B: reboot=False, since we're using the older set_mode command
             self.write_device_config(
@@ -530,7 +585,6 @@
                     {TRANSPORT.USB: usb_enabled},
                     auto_eject_timeout,
                     chalresp_timeout,
-                    None,
                 )
             )
         else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/yubikit/openpgp.py 
new/yubikey_manager-5.4.0/yubikit/openpgp.py
--- old/yubikey_manager-5.3.0/yubikit/openpgp.py        2024-01-29 
09:10:25.569461800 +0100
+++ new/yubikey_manager-5.4.0/yubikit/openpgp.py        2024-03-26 
14:52:50.948619800 +0100
@@ -895,9 +895,11 @@
             raise ValueError("RSA keys with e != 65537 are not supported!")
         return RsaAttributes.create(
             RSA_SIZE(private_key.key_size),
-            RSA_IMPORT_FORMAT.CRT_W_MOD
-            if 0 < version[0] < 4
-            else RSA_IMPORT_FORMAT.STANDARD,
+            (
+                RSA_IMPORT_FORMAT.CRT_W_MOD
+                if 0 < version[0] < 4
+                else RSA_IMPORT_FORMAT.STANDARD
+            ),
         )
     return EcAttributes.create(key_ref, OID._from_key(private_key))
 
@@ -1521,7 +1523,12 @@
             EXTENDED_CAPABILITY_FLAGS.ALGORITHM_ATTRIBUTES_CHANGEABLE
             in self.extended_capabilities.flags
         ):
-            attributes = RsaAttributes.create(key_size)
+            import_format = (
+                RSA_IMPORT_FORMAT.CRT_W_MOD
+                if 0 < self.version[0] < 4  # Use CRT for NEO
+                else RSA_IMPORT_FORMAT.STANDARD
+            )
+            attributes = RsaAttributes.create(key_size, import_format)
             self.set_algorithm_attributes(key_ref, attributes)
         elif key_size != RSA_SIZE.RSA2048:
             raise NotSupportedError("Algorithm attributes not supported")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/yubikit/piv.py 
new/yubikey_manager-5.4.0/yubikit/piv.py
--- old/yubikey_manager-5.3.0/yubikit/piv.py    2024-01-29 09:10:25.569461800 
+0100
+++ new/yubikey_manager-5.4.0/yubikit/piv.py    2024-03-26 14:52:50.948619800 
+0100
@@ -443,9 +443,11 @@
     # FIPS
     if (4, 4, 0) <= version < (4, 5, 0):
         if key_type == KEY_TYPE.RSA1024:
-            raise NotSupportedError("RSA 1024 not supported on YubiKey FIPS")
+            raise NotSupportedError("RSA 1024 not supported on YubiKey FIPS (4 
Series)")
         if pin_policy == PIN_POLICY.NEVER:
-            raise NotSupportedError("PIN_POLICY.NEVER not allowed on YubiKey 
FIPS")
+            raise NotSupportedError(
+                "PIN_POLICY.NEVER not allowed on YubiKey FIPS (4 Series)"
+            )
 
     # New key types
     if version < (5, 7, 0) and key_type in (
@@ -1153,9 +1155,11 @@
                     TAG_DYN_AUTH,
                     Tlv(TAG_AUTH_RESPONSE)
                     + Tlv(
-                        TAG_AUTH_EXPONENTIATION
-                        if exponentiation
-                        else TAG_AUTH_CHALLENGE,
+                        (
+                            TAG_AUTH_EXPONENTIATION
+                            if exponentiation
+                            else TAG_AUTH_CHALLENGE
+                        ),
                         message,
                     ),
                 ),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yubikey_manager-5.3.0/yubikit/support.py 
new/yubikey_manager-5.4.0/yubikit/support.py
--- old/yubikey_manager-5.3.0/yubikit/support.py        2023-09-17 
13:05:24.084793000 +0200
+++ new/yubikey_manager-5.4.0/yubikit/support.py        2024-03-26 
14:52:50.948619800 +0100
@@ -403,7 +403,7 @@
                 return "YubiKey"
         elif major_version == 4:
             if info.is_fips:
-                device_name = "YubiKey FIPS"
+                device_name = "YubiKey FIPS (4 Series)"
             elif usb_supported == CAPABILITY.OTP | CAPABILITY.U2F:
                 device_name = "YubiKey Edge"
             else:

Reply via email to