From c9cc56837bf9bb2c147b908ecd8c8c021fa91c1b Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 19 Jun 2024 13:23:45 +0200 Subject: [PATCH] Improve handling of PIN/PUK/RESET for Bio MPE --- ykman/_cli/config.py | 27 ++++++++++++++++++--------- ykman/_cli/piv.py | 17 ++++++++++++++++- ykman/piv.py | 32 ++++++++++++++++---------------- 3 files changed, 50 insertions(+), 26 deletions(-) diff --git a/ykman/_cli/config.py b/ykman/_cli/config.py index 322fe892..d31527c4 100644 --- a/ykman/_cli/config.py +++ b/ykman/_cli/config.py @@ -35,7 +35,6 @@ CAPABILITY, USB_INTERFACE, DEVICE_FLAG, - FORM_FACTOR, Mode, ) from .util import ( @@ -123,15 +122,17 @@ def reset(ctx, force): This action will wipe all data and restore factory settings for all applications on the YubiKey. """ - transport = ctx.obj["device"].transport info = ctx.obj["info"] - 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, " - "refer to reset commands for specific applications instead." - ) + # reset_blocked is a sure indicator of the command + if not info.reset_blocked: + # No reset blocked, we can still check for Bio MPE + transport = ctx.obj["device"].transport + has_piv = CAPABILITY.PIV in info.supported_capabilities.get(transport) + if not (info._is_bio and has_piv): + 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 " @@ -250,6 +251,14 @@ def _configure_applications( force, ): info = ctx.obj["info"] + + # If any app reset is blocked, we will not be able to toggle applications + if info.reset_blocked: + raise CliFail( + "This YubiKey must be in a newly reset state before applications can be " + "toggled." + ) + supported = info.supported_capabilities.get(transport) enabled = info.config.enabled_capabilities.get(transport) diff --git a/ykman/_cli/piv.py b/ykman/_cli/piv.py index f7ea8e8b..8b47ee81 100644 --- a/ykman/_cli/piv.py +++ b/ykman/_cli/piv.py @@ -287,6 +287,12 @@ def set_pin_retries(ctx, management_key, pin, pin_retries, puk_retries, force): "Retry attempts must be set before PIN/PUK have been changed." ) + try: # Can't change retries on Bio MPE + session.get_bio_metadata() + raise CliFail("PIN/PUK retries cannot be changed on this YubiKey.") + except NotSupportedError: + pass + _ensure_authenticated( ctx, pin, management_key, require_pin_and_key=True, no_prompt=force ) @@ -304,7 +310,7 @@ def set_pin_retries(ctx, management_key, pin, pin_retries, puk_retries, force): click.echo("\tPIN:\t123456") click.echo("\tPUK:\t12345678") except Exception: - raise CliFail("Setting pin retries failed.") + raise CliFail("Setting PIN retries failed.") def _do_change_pin_puk(pin_complexity, name, current, new, fn): @@ -347,6 +353,9 @@ def change_pin(ctx, pin, new_pin): info = ctx.obj["info"] session = ctx.obj["session"] + if not session.get_pin_attempts(): + raise CliFail("PIN is blocked.") + if not pin: pin = _prompt_pin("Enter the current PIN") if not new_pin: @@ -382,6 +391,12 @@ def change_puk(ctx, puk, new_puk): info = ctx.obj["info"] session = ctx.obj["session"] + try: + if not session.get_puk_metadata().attempts_remaining: + raise CliFail("PUK is blocked.") + except NotSupportedError: + pass + if not puk: puk = _prompt_pin("Enter the current PUK") if not new_puk: diff --git a/ykman/piv.py b/ykman/piv.py index a0df6dcb..632dad7e 100644 --- a/ykman/piv.py +++ b/ykman/piv.py @@ -526,22 +526,8 @@ def get_piv_info(session: PivSession): tries = session.get_pin_attempts() tries_str = "15 or more" if tries == 15 else str(tries) info["PIN tries remaining"] = tries_str - try: - puk_data = session.get_puk_metadata() - if puk_data.attempts_remaining == 0: - lines.append("PUK is blocked") - elif puk_data.default_value: - lines.append("WARNING: Using default PUK!") - tries_str = "%d/%d" % ( - puk_data.attempts_remaining, - puk_data.total_attempts, - ) - info["PUK tries remaining"] = tries_str - except NotSupportedError: - if pivman.puk_blocked: - lines.append("PUK is blocked") - try: + try: # Bio metadata bio = session.get_bio_metadata() if bio.configured: info[ @@ -550,7 +536,21 @@ def get_piv_info(session: PivSession): else: info["Biometrics"] = "Not configured" except NotSupportedError: - pass + try: # PUK metadata (on non-bio) + puk_data = session.get_puk_metadata() + if puk_data.attempts_remaining == 0: + lines.append("PUK is blocked") + elif puk_data.default_value: + lines.append("WARNING: Using default PUK!") + tries_str = "%d/%d" % ( + puk_data.attempts_remaining, + puk_data.total_attempts, + ) + info["PUK tries remaining"] = tries_str + except NotSupportedError: + # YK < 5.3 + if pivman.puk_blocked: + lines.append("PUK is blocked") try: metadata = session.get_management_key_metadata()