From 2bf612edaf0447b34bac238c08b771fe5266e6c3 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Thu, 21 Dec 2023 17:52:37 +0100 Subject: [PATCH 1/3] Add new fido subcommands. --- ykman/_cli/fido.py | 99 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/ykman/_cli/fido.py b/ykman/_cli/fido.py index a283774f..5741eae4 100755 --- a/ykman/_cli/fido.py +++ b/ykman/_cli/fido.py @@ -33,6 +33,7 @@ CredentialManagement, FPBioEnrollment, CaptureError, + Config, ) from fido2.pcsc import CtapPcscDevice from yubikit.core.fido import FidoConnection @@ -124,6 +125,7 @@ def info(ctx): click.echo("PIN is set, but has been blocked.") else: click.echo("PIN is not set.") + click.echo(f"Minimum PIN length: {ctap2.info.min_pin_length}") bio_enroll = ctap2.info.options.get("bioEnroll") if bio_enroll: @@ -147,6 +149,17 @@ def info(ctx): + ("on." if always_uv else "off.") ) + remaining_creds = ctap2.info.remaining_disc_creds + if remaining_creds is not None: + click.echo(f"Credentials storage remaining: {remaining_creds}") + + ep = ctap2.info.options.get("ep") + if ep is not None: + click.echo( + "Enterprise attestation is supported, and " + + ("enabled." if ep else "disabled.") + ) + else: click.echo("PIN is not supported.") @@ -460,6 +473,57 @@ def verify(ctx, pin): click.echo("PIN verified.") +def _init_config(ctx, pin): + pin = _require_pin(ctx, pin, "Authenticator Configuration") + + ctap2 = ctx.obj.get("ctap2") + 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") + + return Config(ctap2, client_pin.protocol, token) + + +@access.command("force-change") +@click.pass_context +@click.option("-P", "--pin", help="PIN code") +def force_pin_change(ctx, pin): + """ + Force the PIN to be changed to a new value before use. + """ + config = _init_config(ctx, pin) + config.set_min_pin_length(force_change_pin=True) + + +@access.command("set-min-length") +@click.pass_context +@click.option("-P", "--pin", help="PIN code") +@click.option("-R", "--rp-id", multiple=True, help="RP ID to allow") +@click.argument("length", type=int) +def set_min_pin_length(ctx, pin, rp_id, length): + """ + Set the minimum length allowed for PIN. + + Optionally use the --rp option to specify which RPs are allowed to request this + information. + """ + config = _init_config(ctx, pin) + options = ctx.obj.get("ctap2").info.options + if not options.get("setMinPINLength"): + raise CliFail("Set minimum PIN length is not supported on this YubiKey.") + + if rp_id: + ctap2 = ctx.obj.get("ctap2") + cap = ctap2.info.max_rpids_for_min_pin + if len(rp_id) > cap: + 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) + + def _prompt_current_pin(prompt="Enter your current PIN"): return click_prompt(prompt, hide_input=True) @@ -791,3 +855,38 @@ def bio_delete(ctx, template_id, pin, force): logger.info("Fingerprint template deleted") except CtapError as e: raise CliFail(f"Failed to delete fingerprint: {e.code.name}") + + +@fido.group("config") +def config(): + """ + Manage FIDO configuration. + """ + + +@config.command("toggle-always-uv") +@click.pass_context +@click.option("-P", "--pin", help="PIN code") +def toggle_always_uv(ctx, pin): + """ + Toggles the state of Always Require User Verification. + """ + config = _init_config(ctx, pin) + options = ctx.obj.get("ctap2").info.options + if "alwaysUv" not in options: + raise CliFail("Always Require UV is not supported on this YubiKey.") + config.toggle_always_uv() + + +@config.command("enable-ep-attestation") +@click.pass_context +@click.option("-P", "--pin", help="PIN code") +def enable_ep_attestation(ctx, pin): + """ + Enables Enterprise Attestation for Authenticators pre-configured to support it. + """ + config = _init_config(ctx, pin) + options = ctx.obj.get("ctap2").info.options + if "ep" not in options: + raise CliFail("Enterprise Attestation is not supported on this YubiKey.") + config.enable_enterprise_attestation() From 6fce9524fb7069679a7e1b4f29fa57e967ac914d Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Thu, 21 Dec 2023 18:12:53 +0100 Subject: [PATCH 2/3] Check feature support first. --- ykman/_cli/fido.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/ykman/_cli/fido.py b/ykman/_cli/fido.py index 5741eae4..5eac575a 100755 --- a/ykman/_cli/fido.py +++ b/ykman/_cli/fido.py @@ -474,9 +474,11 @@ def verify(ctx, pin): def _init_config(ctx, pin): - pin = _require_pin(ctx, pin, "Authenticator Configuration") - ctap2 = ctx.obj.get("ctap2") + 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) @@ -493,6 +495,10 @@ def force_pin_change(ctx, pin): """ Force the PIN to be changed to a new value before use. """ + options = ctx.obj.get("ctap2").info.options + if not options.get("setMinPINLength"): + raise CliFail("Force change PIN is not supported on this YubiKey.") + config = _init_config(ctx, pin) config.set_min_pin_length(force_change_pin=True) @@ -501,7 +507,7 @@ def force_pin_change(ctx, pin): @click.pass_context @click.option("-P", "--pin", help="PIN code") @click.option("-R", "--rp-id", multiple=True, help="RP ID to allow") -@click.argument("length", type=int) +@click.argument("length", type=click.IntRange(4)) def set_min_pin_length(ctx, pin, rp_id, length): """ Set the minimum length allowed for PIN. @@ -509,11 +515,11 @@ def set_min_pin_length(ctx, pin, rp_id, length): Optionally use the --rp option to specify which RPs are allowed to request this information. """ - config = _init_config(ctx, pin) options = ctx.obj.get("ctap2").info.options if not options.get("setMinPINLength"): raise CliFail("Set minimum PIN length is not supported on this YubiKey.") + config = _init_config(ctx, pin) if rp_id: ctap2 = ctx.obj.get("ctap2") cap = ctap2.info.max_rpids_for_min_pin @@ -871,10 +877,11 @@ def toggle_always_uv(ctx, pin): """ Toggles the state of Always Require User Verification. """ - config = _init_config(ctx, pin) options = ctx.obj.get("ctap2").info.options if "alwaysUv" not in options: raise CliFail("Always Require UV is not supported on this YubiKey.") + + config = _init_config(ctx, pin) config.toggle_always_uv() @@ -885,8 +892,9 @@ def enable_ep_attestation(ctx, pin): """ Enables Enterprise Attestation for Authenticators pre-configured to support it. """ - config = _init_config(ctx, pin) options = ctx.obj.get("ctap2").info.options if "ep" not in options: raise CliFail("Enterprise Attestation is not supported on this YubiKey.") + + config = _init_config(ctx, pin) config.enable_enterprise_attestation() From 865d509b9ea5c18f0e905c87e8dac559f60fc62d Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 10 Jan 2024 21:15:55 +0100 Subject: [PATCH 3/3] Improve "fido info" output. --- ykman/_cli/fido.py | 46 ++++++++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/ykman/_cli/fido.py b/ykman/_cli/fido.py index 5eac575a..06801d62 100755 --- a/ykman/_cli/fido.py +++ b/ykman/_cli/fido.py @@ -46,13 +46,14 @@ click_group, prompt_timeout, is_yk4_fips, + pretty_print, ) from .util import CliFail from ..fido import is_in_fips_mode, fips_reset, fips_change_pin, fips_verify_pin from ..hid import list_ctap_devices from ..pcsc import list_devices as list_ccid from smartcard.Exceptions import NoCardException, CardConnectionException -from typing import Optional, Sequence, List +from typing import Optional, Sequence, List, Dict import io import csv as _csv @@ -102,66 +103,59 @@ def info(ctx): """ conn = ctx.obj["conn"] ctap2 = ctx.obj.get("ctap2") + info: Dict = {} + lines: List = [info] if is_yk4_fips(ctx.obj["info"]): - click.echo("FIPS Approved Mode: " + ("Yes" if is_in_fips_mode(conn) else "No")) + info["FIPS Approved Mode"] = "Yes" if is_in_fips_mode(conn) else "No" elif ctap2: client_pin = ClientPin(ctap2) # N.B. All YubiKeys with CTAP2 support PIN. if ctap2.info.options["clientPin"]: if ctap2.info.force_pin_change: - click.echo( + lines.append( "NOTE: The FIDO PIN is disabled and must be changed before it can " "be used!" ) pin_retries, power_cycle = client_pin.get_pin_retries() if pin_retries: - click.echo(f"PIN is set, with {pin_retries} attempt(s) remaining.") + info["PIN"] = f"{pin_retries} attempt(s) remaining" if power_cycle: - click.echo( + lines.append( "PIN is temporarily blocked. " "Remove and re-insert the YubiKey to unblock." ) else: - click.echo("PIN is set, but has been blocked.") + info["PIN"] = "blocked" else: - click.echo("PIN is not set.") - click.echo(f"Minimum PIN length: {ctap2.info.min_pin_length}") + info["PIN"] = "not set" + info["Minimum PIN length"] = ctap2.info.min_pin_length bio_enroll = ctap2.info.options.get("bioEnroll") if bio_enroll: uv_retries = client_pin.get_uv_retries() if uv_retries: - click.echo( - f"Fingerprints registered, with {uv_retries} attempt(s) " - "remaining." - ) + info["Fingerprints"] = f"registered, {uv_retries} attempt(s) remaining" else: - click.echo( - "Fingerprints registered, but blocked until PIN is verified." - ) + info["Fingerprints"] = "registered, blocked until PIN is verified" elif bio_enroll is False: - click.echo("No fingerprints have been registered.") + info["Fingerprints"] = "not registered" always_uv = ctap2.info.options.get("alwaysUv") if always_uv is not None: - click.echo( - "Always Require User Verification is turned " - + ("on." if always_uv else "off.") - ) + info["Always Require UV"] = "on" if always_uv else "off" remaining_creds = ctap2.info.remaining_disc_creds if remaining_creds is not None: - click.echo(f"Credentials storage remaining: {remaining_creds}") + info["Credential storage remaining"] = remaining_creds ep = ctap2.info.options.get("ep") if ep is not None: - click.echo( - "Enterprise attestation is supported, and " - + ("enabled." if ep else "disabled.") - ) + info["Enterprise Attestation"] = "enabled" if ep else "disabled" else: - click.echo("PIN is not supported.") + info["PIN"] = "not supported" + + click.echo("\n".join(pretty_print(lines))) @fido.command("reset")