Skip to content

Commit

Permalink
Merge PR #631
Browse files Browse the repository at this point in the history
  • Loading branch information
dainnilsson committed Sep 2, 2024
2 parents 422d98a + fad423c commit b6d1bfd
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 42 deletions.
16 changes: 6 additions & 10 deletions tests/device/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from ykman.device import list_all_devices, read_info
from ykman.pcsc import list_devices
from ykman._cli.util import find_scp11_params
from yubikit.core import TRANSPORT, Version
from yubikit.core import TRANSPORT, Version, _override_version
from yubikit.core.otp import OtpConnection
from yubikit.core.fido import FidoConnection
from yubikit.core.smartcard import SmartCardConnection
Expand All @@ -24,18 +24,12 @@ def _device(pytestconfig):
else:
pytest.skip("No serial specified for device tests")

version = None
version_str = pytestconfig.getoption("use_version")
if version_str:
version = Version.from_string(version_str)

# Monkey patch all parsing of Version to use the supplied value
# N.B. There are some instances where ideally we would replace the version,
# but we don't really care
def get_version(cls, data):
return version

Version.from_bytes = classmethod(get_version)
Version.from_string = classmethod(get_version)
_override_version(version)
os.environ["_YK_OVERRIDE_VERSION"] = version_str

reader = pytestconfig.getoption("reader")
if reader:
Expand All @@ -52,6 +46,8 @@ def get_version(cls, data):
dev, info = devices[0]
if info.serial != serial:
pytest.exit("Device serial does not match: %d != %r" % (serial, info.serial))
if version:
info.version = version

return dev, info

Expand Down
27 changes: 26 additions & 1 deletion ykman/_cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

from yubikit.core import ApplicationNotAvailableError
from yubikit.core import ApplicationNotAvailableError, Version, _override_version
from yubikit.core.otp import OtpConnection
from yubikit.core.fido import FidoConnection
from yubikit.core.smartcard import SmartCardConnection
Expand Down Expand Up @@ -82,13 +82,18 @@
import time
import sys
import re
import os

import logging


logger = logging.getLogger(__name__)


# Development key builds are treated as having the following version
_OVERRIDE_VERSION = Version.from_string(os.environ.get("_YK_OVERRIDE_VERSION", "5.7.2"))


CLICK_CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"], max_content_width=999)


Expand Down Expand Up @@ -400,6 +405,26 @@ def resolve():
items = require_reader(connections, reader)
else:
items = require_device(connections, device)

if items[1].version.major == 0:
logger.info(
"Debug key detected, "
f"overriding version with {_OVERRIDE_VERSION}"
)
# Preview build, override version and get new DeviceInfo
_override_version(_OVERRIDE_VERSION)
for c in connections:
if items[0].supports_connection(c):
try:
with items[0].open_connection(c) as conn:
info = read_info(conn, items[0].pid)
items = (items[0], info)
except Exception:
logger.debug("Failed", exc_info=True)
continue
break
else:
raise CliFail("Failed to connect to YubiKey.")
setattr(resolve, "items", items)
return items

Expand Down
5 changes: 3 additions & 2 deletions ykman/_cli/oath.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
prompt_timeout,
EnumChoice,
is_yk4_fips,
check_version,
pretty_print,
get_scp_params,
)
Expand Down Expand Up @@ -569,14 +570,14 @@ def _add_cred(ctx, data, touch, force):
if len(data.secret) < 2:
raise CliFail("Secret must be at least 2 bytes.")

if touch and version < (4, 2, 6):
if touch and not check_version(version, (4, 2, 6)):
raise CliFail("Require touch is not supported on this YubiKey.")

if data.counter and data.oath_type != OATH_TYPE.HOTP:
raise CliFail("Counter only supported for HOTP accounts.")

if data.hash_algorithm == HASH_ALGORITHM.SHA512 and (
version < (4, 3, 1) or is_yk4_fips(ctx.obj["info"])
not check_version(version, (4, 3, 1)) or is_yk4_fips(ctx.obj["info"])
):
raise CliFail("Algorithm SHA512 not supported on this YubiKey.")

Expand Down
10 changes: 9 additions & 1 deletion ykman/_cli/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
# POSSIBILITY OF SUCH DAMAGE.

from ..util import parse_certificates
from yubikit.core import TRANSPORT
from yubikit.core import TRANSPORT, Version, require_version, NotSupportedError
from yubikit.core.smartcard import SmartCardConnection, ApduError
from yubikit.core.smartcard.scp import ScpKid, KeyRef, ScpKeyParams, Scp11KeyParams
from yubikit.management import DeviceInfo, CAPABILITY
Expand Down Expand Up @@ -328,6 +328,14 @@ def pretty_print(value, level: int = 0) -> Sequence[str]:
return lines


def check_version(version: Version, req: Tuple[int, int, int]) -> bool:
try:
require_version(version, req)
return True
except NotSupportedError:
return False


def is_yk4_fips(info: DeviceInfo) -> bool:
return info.version[0] == 4 and info.is_fips

Expand Down
26 changes: 24 additions & 2 deletions yubikit/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
)
import re
import abc
import logging


logger = logging.getLogger(__name__)


_VERSION_STRING_PATTERN = re.compile(r"\b(?P<major>\d+).(?P<minor>\d).(?P<patch>\d)\b")
Expand Down Expand Up @@ -234,12 +238,30 @@ def __init__(self, attempts_remaining: int, message: Optional[str] = None):
self.attempts_remaining = attempts_remaining


class _OverrideVersion:
def __init__(self):
self._version: Optional[Version] = None

def __call__(self, value):
logger.info("Overriding version check for development devices with {version}")
self._version = value


# Set this to override a version with major version == 0 in version checks
_override_version = _OverrideVersion()


def require_version(
my_version: Version, min_version: Tuple[int, int, int], message=None
):
"""Ensure a version is at least min_version."""
# Skip version checks for major == 0, used for development builds.
if my_version < min_version and my_version[0] != 0:
# Allow overriding version checks for development devices
v = my_version[0] == 0 and _override_version._version
if v:
logger.debug("Overriding version check with {v}")
my_version = v

if my_version < min_version:
if not message:
message = "This action requires YubiKey %d.%d.%d or later" % min_version
raise NotSupportedError(message)
Expand Down
4 changes: 3 additions & 1 deletion yubikit/hsmauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,9 @@ def get_challenge(

data: bytes = Tlv(TAG_LABEL, _parse_label(label))

if credential_password is not None and self.version >= (5, 7, 1):
if credential_password is not None and (
self.version >= (5, 7, 1) or self.version[0] == 0
):
data += Tlv(
TAG_CREDENTIAL_PASSWORD, _parse_credential_password(credential_password)
)
Expand Down
10 changes: 8 additions & 2 deletions yubikit/openpgp.py
Original file line number Diff line number Diff line change
Expand Up @@ -1023,8 +1023,14 @@ def __init__(

def _read_version(self) -> Version:
logger.debug("Getting version number")
bcd = self.protocol.send_apdu(0, INS.GET_VERSION, 0, 0)
return Version(*(_bcd(x) for x in bcd))
try:
bcd = self.protocol.send_apdu(0, INS.GET_VERSION, 0, 0)
return Version(*(_bcd(x) for x in bcd))
except ApduError as e:
# Pre 1.0.2 versions don't support reading the version
if e.sw == SW.CONDITIONS_NOT_SATISFIED:
return Version(1, 0, 0)
raise

@property
def aid(self) -> OpenPgpAid:
Expand Down
34 changes: 13 additions & 21 deletions yubikit/piv.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
# POSSIBILITY OF SUCH DAMAGE.

from .core import (
require_version as _require_version,
require_version,
int2bytes,
bytes2int,
Version,
Expand Down Expand Up @@ -94,13 +94,6 @@ class ALGORITHM(str, Enum):
RSA = "rsa"


# Don't treat pre 1.0 versions as "developer builds".
def require_version(my_version: Version, *args, **kwargs):
if my_version <= (0, 1, 4): # Last pre 1.0 release of ykneo-piv
my_version = Version(1, 0, 0)
_require_version(my_version, *args, **kwargs)


@unique
class KEY_TYPE(IntEnum):
RSA1024 = 0x06
Expand Down Expand Up @@ -599,17 +592,16 @@ def _do_check_key_support(
generate: bool = True,
fips_restrictions: bool = False,
) -> None:
if version[0] == 0 and version > (0, 1, 3):
return # Development build, skip version checks

if version < (4, 0, 0):
if key_type == KEY_TYPE.ECCP384:
raise NotSupportedError("ECCP384 requires YubiKey 4 or later")
if touch_policy != TOUCH_POLICY.DEFAULT or pin_policy != PIN_POLICY.DEFAULT:
raise NotSupportedError("PIN/Touch policy requires YubiKey 4 or later")

if version < (4, 3, 0) and touch_policy == TOUCH_POLICY.CACHED:
raise NotSupportedError("Cached touch policy requires YubiKey 4.3 or later")
if key_type == KEY_TYPE.ECCP384:
require_version(version, (4, 0, 0), "ECCP384 requires YubiKey 4 or later")
if touch_policy != TOUCH_POLICY.DEFAULT or pin_policy != PIN_POLICY.DEFAULT:
require_version(
version, (4, 0, 0), "PIN/Touch policy requires YubiKey 4 or later"
)
if touch_policy == TOUCH_POLICY.CACHED:
require_version(
version, (4, 3, 0), "Cached touch policy requires YubiKey 4.3 or later"
)

# ROCA
if (4, 2, 0) <= version < (4, 3, 5):
Expand All @@ -624,13 +616,13 @@ def _do_check_key_support(
raise NotSupportedError("PIN_POLICY.NEVER not allowed on YubiKey FIPS")

# New key types
if version < (5, 7, 0) and key_type in (
if key_type in (
KEY_TYPE.RSA3072,
KEY_TYPE.RSA4096,
KEY_TYPE.ED25519,
KEY_TYPE.X25519,
):
raise NotSupportedError(f"{key_type} requires YubiKey 5.7 or later")
require_version(version, (5, 7, 0), f"{key_type} requires YubiKey 5.7 or later")


def _parse_device_public_key(key_type, encoded):
Expand Down
6 changes: 4 additions & 2 deletions yubikit/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,10 @@ def _read_info_ccid(conn, key_type, interfaces):
try:
return mgmt.read_device_info()
except NotSupportedError:
# Workaround to "de-select" the Management Applet needed for NEO
conn.send_and_receive(b"\xa4\x04\x00\x08")
if version.major == 3:
# Workaround to "de-select" the Management Applet needed for NEO
logger.debug("Send NEO de-select workaround...")
conn.send_and_receive(b"\xa4\x04\x00\x08")
except ApplicationNotAvailableError:
logger.debug("Couldn't select Management application, use fallback")

Expand Down

0 comments on commit b6d1bfd

Please sign in to comment.