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: