From 1568c89bb581d8047461de6bd184a28ccca636ad Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 25 Jun 2024 14:19:50 +0200 Subject: [PATCH 1/5] Improve docstrings --- docs/conf.py | 4 ++++ yubikit/management.py | 16 ++++++++++++++++ yubikit/oath.py | 27 +++++++++++++++++---------- yubikit/piv.py | 33 +++++++++++++++++++++++++++++++-- yubikit/yubiotp.py | 11 +++++++++++ 5 files changed, 79 insertions(+), 12 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 5f88bfda..58da7a1f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -189,3 +189,7 @@ def get_version(): "python": ("https://docs.python.org/", None), "cryptography": ("https://cryptography.io/en/latest/", None), } + + +# Custom config +autodoc_member_order = "bysource" diff --git a/yubikit/management.py b/yubikit/management.py index 7c626878..28e14889 100644 --- a/yubikit/management.py +++ b/yubikit/management.py @@ -53,6 +53,7 @@ from typing import Optional, Union, Mapping import abc import struct +import warnings import logging logger = logging.getLogger(__name__) @@ -560,10 +561,19 @@ def __init__( ) def close(self) -> None: + """Close the underlying connection. + + :deprecated: call .close() on the underlying connection instead. + """ + warnings.warn( + "Deprecated: call .close() on the underlying connection instead.", + DeprecationWarning, + ) self.backend.close() @property def version(self) -> Version: + """The firmware version of the YubiKey""" return self.backend.version def read_device_info(self) -> DeviceInfo: @@ -676,6 +686,12 @@ def set_mode( logger.info("Mode configuration written") def device_reset(self) -> None: + """Global factory reset. + + This is only available for YubiKey Bio, which has a PIN that is shared between + applications. This will factory reset the global PIN as well as the associated + applications. + """ if not isinstance(self.backend, _ManagementSmartCardBackend): raise NotSupportedError("Device reset can only be performed over CCID") logger.debug("Performing device reset") diff --git a/yubikit/oath.py b/yubikit/oath.py index b40dc714..c08f40a1 100644 --- a/yubikit/oath.py +++ b/yubikit/oath.py @@ -290,22 +290,25 @@ def __init__( @property def version(self) -> Version: - """The OATH application version.""" + """The version of the OATH application.""" return self._version @property def device_id(self) -> str: - """The device ID.""" + """The device ID. + + A random static identifier that is re-generated on reset. + """ return self._device_id @property def has_key(self) -> bool: - """If True, the YubiKey has an access key.""" + """If True, the YubiKey has an access key set.""" return self._has_key @property def locked(self) -> bool: - """If True, the OATH application is password protected.""" + """If True, the OATH application is currently locked via an access key.""" return self._challenge is not None def reset(self) -> None: @@ -319,7 +322,7 @@ def reset(self) -> None: self._device_id = _get_device_id(self._salt) def derive_key(self, password: str) -> bytes: - """Derive a key from password. + """Derive an access key from a password. :param password: The derivation password. """ @@ -328,6 +331,8 @@ def derive_key(self, password: str) -> bytes: def validate(self, key: bytes) -> None: """Validate authentication with access key. + This unlocks the session for use. + :param key: The access key. """ logger.debug("Unlocking session") @@ -344,7 +349,7 @@ def validate(self, key: bytes) -> None: self._neo_unlock_workaround = False def set_key(self, key: bytes) -> None: - """Set access key for authentication. + """Set an access key for authentication. :param key: The access key. """ @@ -369,9 +374,9 @@ def set_key(self, key: bytes) -> None: self.validate(key) def unset_key(self) -> None: - """Remove access code. + """Remove the access key. - WARNING: This removes authentication. + This removes the need to authentication a session before using it. """ self.protocol.send_apdu(0, INS_SET_CODE, 0, 0, Tlv(TAG_KEY)) logger.info("Access code removed") @@ -380,7 +385,7 @@ def unset_key(self) -> None: def put_credential( self, credential_data: CredentialData, touch_required: bool = False ) -> Credential: - """Add a OATH credential. + """Add an OATH credential. :param credential_data: The credential data. :param touch_required: The touch policy. @@ -486,7 +491,9 @@ def calculate_all( ) -> Mapping[Credential, Optional[Code]]: """Calculate codes for all OATH credentials on the YubiKey. - :param timestamp: A timestamp. + This excludes credentials which require touch as well as HOTP credentials. + + :param timestamp: A timestamp used for the TOTP challenge. """ timestamp = int(timestamp or time()) challenge = _get_challenge(timestamp, DEFAULT_PERIOD) diff --git a/yubikit/piv.py b/yubikit/piv.py index c166c1ae..3bd9ce47 100755 --- a/yubikit/piv.py +++ b/yubikit/piv.py @@ -424,7 +424,7 @@ def check_key_support( This method will return None if the key (with PIN and touch policies) is supported, or it will raise a NotSupportedError if it is not. - THIS FUNCTION IS DEPRECATED! Use PivSession.check_key_support() instead. + :deprecated: Use PivSession.check_key_support() instead. """ warnings.warn( "Deprecated: use PivSession.check_key_support() instead.", @@ -524,13 +524,21 @@ def __init__( @property def version(self) -> Version: + """The version of the PIV application, + typically the same as the YubiKey firmware.""" return self._version @property def management_key_type(self) -> MANAGEMENT_KEY_TYPE: + """The algorithm of the management key currently in use.""" return self._management_key_type def reset(self) -> None: + """Factory reset the PIV application data. + + This deletes all user-data from the PIV application, and resets the default + values for PIN, PUK, and management key. + """ logger.debug("Preparing PIV reset") try: @@ -677,7 +685,7 @@ def set_management_key( logger.info("Management key set") def verify_pin(self, pin: str) -> None: - """Verify the PIN. + """Verify the user by PIN. :param pin: The PIN. """ @@ -695,6 +703,17 @@ def verify_pin(self, pin: str) -> None: def verify_uv( self, temporary_pin: bool = False, check_only: bool = False ) -> Optional[bytes]: + """Verify the user by fingerprint (YubiKey Bio only). + + Fingerprint verification will allow usage of private keys which have a PIN + policy allowing MATCH. For those using MATCH_ALWAYS, the fingerprint must be + verified just prior to using the key, or by first requesting a temporary PIN + and then later verifying the PIN just prior to key use. + + :param temporary_pin: Request a temporary PIN for later use within the session. + :param check_only: Do not verify the user, instead immediately throw an + InvalidPinException containing the number of remaining attempts. + """ logger.debug("Verifying UV") if temporary_pin and check_only: raise ValueError( @@ -724,6 +743,10 @@ def verify_uv( return response if temporary_pin else None def verify_temporary_pin(self, pin: bytes) -> None: + """Verify the user via temporary PIN. + + :param pin: A temporary PIN previously requested via verify_uv. + """ logger.debug("Verifying temporary PIN") if len(pin) != TEMPORARY_PIN_LEN: raise ValueError(f"Temporary PIN must be exactly {TEMPORARY_PIN_LEN} bytes") @@ -856,6 +879,12 @@ def get_slot_metadata(self, slot: SLOT) -> SlotMetadata: ) def get_bio_metadata(self) -> BioMetadata: + """Get YubiKey Bio metadata. + + This tells you if fingerprints are enrolled or not, how many fingerprint + verification attempts remain, and whether or not a temporary PIN is currently + active. + """ logger.debug("Getting bio metadata") try: data = Tlv.parse_dict( diff --git a/yubikit/yubiotp.py b/yubikit/yubiotp.py index 2adf6012..5dd0e667 100644 --- a/yubikit/yubiotp.py +++ b/yubikit/yubiotp.py @@ -45,6 +45,7 @@ import abc import struct +import warnings from hashlib import sha1 from threading import Event from enum import unique, IntEnum, IntFlag @@ -754,10 +755,20 @@ def init_scp03(self): raise NotSupportedError("Requires smart card connection") def close(self) -> None: + """Close the underlying connection. + + :deprecated: call .close() on the underlying connection instead. + """ + warnings.warn( + "Deprecated: call .close() on the underlying connection instead.", + DeprecationWarning, + ) self.backend.close() @property def version(self) -> Version: + """The version of the Yubico OTP application, + typically the same as the YubiKey firmware.""" return self._version def get_serial(self) -> int: From ff90ed66ff739d8a36caab1da00ba17398a062c7 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 25 Jun 2024 14:20:35 +0200 Subject: [PATCH 2/5] Add SCP to YubiOtpSession --- ykman/_cli/otp.py | 15 ++++++++++++++- ykman/_cli/util.py | 11 ++++++----- yubikit/yubiotp.py | 19 ++++++++++--------- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/ykman/_cli/otp.py b/ykman/_cli/otp.py index ff258c16..2e5b2bf1 100644 --- a/ykman/_cli/otp.py +++ b/ykman/_cli/otp.py @@ -197,11 +197,24 @@ def otp(ctx, access_code): def _get_session(ctx, types=[OtpConnection, SmartCardConnection]): dev = ctx.obj["device"] + + resolve_scp = ctx.obj.get("scp") + if resolve_scp: + if SmartCardConnection in types: + types = [SmartCardConnection] + else: + raise CliFail("SCP can only be used with SmartCardConnection") + for conn_type in types: if dev.supports_connection(conn_type): conn = dev.open_connection(conn_type) ctx.call_on_close(conn.close) - return YubiOtpSession(conn) + if resolve_scp: + scp_params = resolve_scp(conn) + else: + scp_params = None + return YubiOtpSession(conn, scp_params) + raise CliFail( "The connection type required for this command is not supported/enabled on the " "YubiKey." diff --git a/ykman/_cli/util.py b/ykman/_cli/util.py index ff99abc4..ea307713 100644 --- a/ykman/_cli/util.py +++ b/ykman/_cli/util.py @@ -367,7 +367,10 @@ def find_scp11_params( else: raise ValueError(f"No SCP key found matching KID=0x{kid:x}") try: - chain = scp.get_certificate_bundle(KeyRef(kid, kvn)) + ref = KeyRef(kid, kvn) + chain = scp.get_certificate_bundle(ref) + if not chain: + raise ValueError(f"No certificate chain stored for {ref}") if ca: logger.debug("Validating KLCC CA using supplied file") parent = parse_certificates(ca, None)[0] @@ -380,11 +383,9 @@ def find_scp11_params( logger.info("No CA supplied, skipping KLCC CA validation") pub_key = chain[-1].public_key() - return Scp11KeyParams(KeyRef(kid, kvn), pub_key) + return Scp11KeyParams(ref, pub_key) except ApduError: - raise ValueError( - f"Unable to get SCP key paramaters (KID=0x{kid:x}, KVN=ox{kvn:x})" - ) + raise ValueError(f"Unable to get SCP key paramaters ({ref})") def get_scp_params( diff --git a/yubikit/yubiotp.py b/yubikit/yubiotp.py index 5dd0e667..df7441d0 100644 --- a/yubikit/yubiotp.py +++ b/yubikit/yubiotp.py @@ -41,7 +41,7 @@ OtpProtocol, CommandRejectedError, ) -from .core.smartcard import AID, SmartCardConnection, SmartCardProtocol +from .core.smartcard import AID, SmartCardConnection, SmartCardProtocol, ScpKeyParams import abc import struct @@ -710,8 +710,14 @@ def send_and_receive(self, slot, data, expected_len, event=None, on_keepalive=No class YubiOtpSession: """A session with the YubiOTP application.""" - def __init__(self, connection: Union[OtpConnection, SmartCardConnection]): + def __init__( + self, + connection: Union[OtpConnection, SmartCardConnection], + scp_key_params: Optional[ScpKeyParams] = None, + ): if isinstance(connection, OtpConnection): + if scp_key_params: + raise ValueError("SCP can only be used with SmartCardConnection") otp_protocol = OtpProtocol(connection) self._status = otp_protocol.read_status() self._version = otp_protocol.version @@ -736,6 +742,8 @@ def __init__(self, connection: Union[OtpConnection, SmartCardConnection]): else: self._version = mgmt_version or otp_version card_protocol.configure(self._version) + if scp_key_params: + card_protocol.init_scp(scp_key_params) self.backend = _YubiOtpSmartCardBackend( card_protocol, self._version, self._status[3] ) @@ -747,13 +755,6 @@ def __init__(self, connection: Union[OtpConnection, SmartCardConnection]): f"state={self.get_config_state()}" ) - def init_scp03(self): - require_version(self.version, (5, 3, 0)) - backend = self.backend - if isinstance(backend, _YubiOtpSmartCardBackend): - return backend.protocol.init_scp03() - raise NotSupportedError("Requires smart card connection") - def close(self) -> None: """Close the underlying connection. From a977d06fe0e5ef2a1021c7ebd85a25624f274b3f Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 25 Jun 2024 16:53:14 +0200 Subject: [PATCH 3/5] Correct Python version in README --- README.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.adoc b/README.adoc index 60709a95..408f0e24 100644 --- a/README.adoc +++ b/README.adoc @@ -4,7 +4,7 @@ image:https://github.com/Yubico/yubikey-manager/actions/workflows/windows.yml/ba image:https://github.com/Yubico/yubikey-manager/actions/workflows/macOS.yml/badge.svg["MacOS build", link="https://github.com/Yubico/yubikey-manager/actions/workflows/macOS.yml"] image:https://github.com/Yubico/yubikey-manager/actions/workflows/ubuntu.yml/badge.svg["Ubuntu build", link="https://github.com/Yubico/yubikey-manager/actions/workflows/ubuntu.yml"] -Python 3.7 (or later) library and command line tool for configuring a YubiKey. +Python 3.8 (or later) library and command line tool for configuring a YubiKey. If you're looking for the graphical application, it's https://developers.yubico.com/yubikey-manager-qt/[here]. === Usage @@ -95,7 +95,7 @@ Additionally, packages are available from Homebrew and MacPorts. When running one of the `ykman otp` commands you may run into an error such as: `Failed to open device for communication: -536870174`. This indicates a problem with the permission to access the OTP (keyboard) USB interface. - + To access a YubiKey over this interface the application needs the `Input Monitoring` permission. If you are not automatically prompted to grant this permission, you may have to do so manually. Note that it is the _terminal_ you From 669944ee4e57a493c25ef001e91304a0bd3ec479 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 25 Jun 2024 11:27:13 +0200 Subject: [PATCH 4/5] Prepare 5.5.0 release --- NEWS | 15 +++++++++++++++ man/ykman.1 | 14 +++++++++++++- pyproject.toml | 2 +- ykman/__init__.py | 2 +- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/NEWS b/NEWS index bc016ee1..dfa54bfc 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,18 @@ +* Version 5.5.0 (released 2024-06-26) + * Add Secure Channel support to smartcard sessions. + * Support extended APDUs in the "apdu" command (this is now the default). + * HSMAuth: Treat management key as a PIN/password instead of a key, adding new CLI + commands. + * PIV: Deprecate explicit passing of management key type when authenticating. + * CLI: Add "config nfc --restrict" command to set "NFC restricted mode". + * CLI: Display more information about PIN complexity and FIPS status for compatible + YubiKeys. + * CLI: Improved error messages for illegal values of PIV PIN and PUK. + * CLI: Drop error messages for old 3.x commands. + * CLI: Removal of --upload for YubiCloud credentials. Export to CSV and upload via web + instead. + * CLI: Add more detailed information to the CLI output for several commands. + * Version 5.4.0 (released 2024-03-27) * Support for YubiKey Bio Multi-protocol Edition. * CLI: Improve error messages for several failures. diff --git a/man/ykman.1 b/man/ykman.1 index 3f03d217..1c540d0f 100644 --- a/man/ykman.1 +++ b/man/ykman.1 @@ -1,4 +1,4 @@ -.TH YKMAN "1" "March 2024" "ykman 5.4.0" "User Commands" +.TH YKMAN "1" "June 2024" "ykman 5.0.0" "User Commands" .SH NAME ykman \- YubiKey Manager (ykman) .SH SYNOPSIS @@ -14,6 +14,18 @@ specify which YubiKey to interact with by serial number .TP \fB\-r\fR, \fB\-\-reader\fR NAME specify a YubiKey by smart card reader name (can't be used with \-\-device or list) +.TP +\fB\-t\fR, \fB\-\-scp\-ca\fR FILENAME +specify the CA to use to verify the SCP11 card key (CA\-KLCC) +.TP +\fB\-s\fR, \fB\-\-scp\fR CRED +specify private key and certificate chain for secure messaging, can be used multiple times to provide key and certificates in multiple files (private key, certificates in leaf\-last order), OR SCP03 keys in hex separated by colon (:) K\-ENC:K\-MAC[:K\-DEK] +.TP +\fB\-p\fR, \fB\-\-scp\-password\fR PASSWORD +specify a password required to access the +.TP +\fB\-\-scp\fR \fBfile\fR, \fBif\fR \fBneeded\fR + .TP \fB\-l\fR, \fB\-\-log\-level\fR [ERROR|WARNING|INFO|DEBUG|TRAFFIC] enable logging at given verbosity level diff --git a/pyproject.toml b/pyproject.toml index 28650c69..3ae32799 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "yubikey-manager" -version = "5.4.1-dev.0" +version = "5.5.0" description = "Tool for managing your YubiKey configuration." authors = ["Dain Nilsson "] license = "BSD" diff --git a/ykman/__init__.py b/ykman/__init__.py index 71684652..1b7377d7 100644 --- a/ykman/__init__.py +++ b/ykman/__init__.py @@ -25,4 +25,4 @@ # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -__version__ = "5.4.1-dev.0" +__version__ = "5.5.0" From 43abb8ec382b189965c78bb633de9d3734248cd9 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 26 Jun 2024 13:30:33 +0200 Subject: [PATCH 5/5] Bump version. --- pyproject.toml | 2 +- ykman/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3ae32799..fc7c05db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "yubikey-manager" -version = "5.5.0" +version = "5.5.1-dev.0" description = "Tool for managing your YubiKey configuration." authors = ["Dain Nilsson "] license = "BSD" diff --git a/ykman/__init__.py b/ykman/__init__.py index 1b7377d7..4d981e66 100644 --- a/ykman/__init__.py +++ b/ykman/__init__.py @@ -25,4 +25,4 @@ # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -__version__ = "5.5.0" +__version__ = "5.5.1-dev.0"