Skip to content

Commit

Permalink
Improve handling of PIN/PUK/RESET for Bio MPE
Browse files Browse the repository at this point in the history
  • Loading branch information
dainnilsson committed Jun 19, 2024
1 parent 1d265f5 commit b00294d
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 40 deletions.
27 changes: 18 additions & 9 deletions ykman/_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
CAPABILITY,
USB_INTERFACE,
DEVICE_FLAG,
FORM_FACTOR,
Mode,
)
from .util import (
Expand Down Expand Up @@ -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 "
Expand Down Expand Up @@ -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)

Expand Down
70 changes: 55 additions & 15 deletions ykman/_cli/piv.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,12 +249,22 @@ def reset(ctx, force):
)

click.echo("Resetting PIV data...")
ctx.obj["session"].reset()
session = ctx.obj["session"]
session.reset()

try:
has_puk = session.get_puk_metadata().attempts_remaining > 0
except NotSupportedError:
has_puk = True

click.echo("Reset complete. All PIV data has been cleared from the YubiKey.")
click.echo("Your YubiKey now has the default PIN, PUK and Management Key:")
click.echo("\tPIN:\t123456")
click.echo("\tPUK:\t12345678")
if has_puk:
click.echo("Your YubiKey now has the default PIN, PUK and Management Key:")
click.echo("\tPIN:\t123456")
click.echo("\tPUK:\t12345678")
else:
click.echo("Your YubiKey now has the default PIN and Management Key:")
click.echo("\tPIN:\t123456")
click.echo("\tManagement Key:\t010203040506070801020304050607080102030405060708")


Expand Down Expand Up @@ -287,6 +297,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
)
Expand All @@ -304,18 +320,24 @@ 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):
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.")
def _validate_pin_length(pin, name, pin_complexity, min_len):
unit = "characters" if pin_complexity else "bytes"
pin_len = len(pin) if pin_complexity else len(pin.encode())
if not min_len <= pin_len <= 8:
if min_len == 8:
raise CliFail(f"{name} must be exactly 8 {unit} long.")
else:
raise CliFail(f"{name} must be between {min_len} and 8 {unit} long.")


validate_pin_length(current, "Current")
validate_pin_length(new, "New")
def _do_change_pin_puk(info, name, current, new, fn):
pin_complexity = info.pin_complexity
min_len = 8 if CAPABILITY.PIV in info.fips_capable else 6
_validate_pin_length(current, f"Current {name}", pin_complexity, 6)
_validate_pin_length(new, f"New {name}", pin_complexity, min_len)

try:
fn()
Expand Down Expand Up @@ -347,6 +369,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:
Expand All @@ -359,7 +384,7 @@ def change_pin(ctx, pin, new_pin):
)

_do_change_pin_puk(
info.pin_complexity,
info,
"PIN",
pin,
new_pin,
Expand All @@ -382,6 +407,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:
Expand All @@ -394,7 +425,7 @@ def change_puk(ctx, puk, new_puk):
)

_do_change_pin_puk(
info.pin_complexity,
info,
"PUK",
puk,
new_puk,
Expand Down Expand Up @@ -564,6 +595,15 @@ def unblock_pin(ctx, puk, new_pin):
hide_input=True,
confirmation_prompt=True,
)

info = ctx.obj["info"]
_validate_pin_length(
new_pin,
"New PIN",
info.pin_complexity,
8 if CAPABILITY.PIV in info.fips_capable else 6,
)

try:
session.unblock_pin(puk, new_pin)
click.echo("New PIN set.")
Expand Down
32 changes: 16 additions & 16 deletions ykman/piv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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[
Expand All @@ -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()
Expand Down

0 comments on commit b00294d

Please sign in to comment.