diff --git a/static/yubikey-manager/API_Documentation/.buildinfo b/static/yubikey-manager/API_Documentation/.buildinfo index 3203d6e1..e5d2ca0d 100644 --- a/static/yubikey-manager/API_Documentation/.buildinfo +++ b/static/yubikey-manager/API_Documentation/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 4924bcae82217482b812825346ce3423 +config: c45be3442e54ed790e6ba723d3ed956c tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/static/yubikey-manager/API_Documentation/_modules/collections.html b/static/yubikey-manager/API_Documentation/_modules/collections.html index 4eab42ab..afa3e676 100644 --- a/static/yubikey-manager/API_Documentation/_modules/collections.html +++ b/static/yubikey-manager/API_Documentation/_modules/collections.html @@ -3,7 +3,7 @@ - collections — yubikey-manager 5.4.0 documentation + collections — yubikey-manager 5.5.0 documentation @@ -13,9 +13,9 @@ - - - + + + @@ -33,7 +33,7 @@ yubikey-manager
- 5.4 + 5.5
@@ -713,7 +713,8 @@

Source code for collections

         >>> sorted(c.elements())
         ['A', 'A', 'B', 'B', 'C', 'C']
 
-        # Knuth's example for prime factors of 1836:  2**2 * 3**3 * 17**1
+        Knuth's example for prime factors of 1836:  2**2 * 3**3 * 17**1
+
         >>> import math
         >>> prime_factors = Counter({2: 2, 3: 3, 17: 1})
         >>> math.prod(prime_factors.elements())
@@ -754,7 +755,7 @@ 

Source code for collections

 
         '''
         # The regular dict.update() operation makes no sense here because the
-        # replace behavior results in the some of original untouched counts
+        # replace behavior results in some of the original untouched counts
         # being mixed-in with all of the other counts for a mismash that
         # doesn't have a straight-forward interpretation in most counting
         # contexts.  Instead, we implement straight-addition.  Both the inputs
diff --git a/static/yubikey-manager/API_Documentation/_modules/index.html b/static/yubikey-manager/API_Documentation/_modules/index.html
index d1c127bc..64b1fb86 100644
--- a/static/yubikey-manager/API_Documentation/_modules/index.html
+++ b/static/yubikey-manager/API_Documentation/_modules/index.html
@@ -3,7 +3,7 @@
 
   
   
-  Overview: module code — yubikey-manager 5.4.0 documentation
+  Overview: module code — yubikey-manager 5.5.0 documentation
       
       
     
@@ -13,9 +13,9 @@
   
         
         
-        
-        
-        
+        
+        
+        
     
     
      
@@ -33,7 +33,7 @@
             yubikey-manager
           
               
- 5.4 + 5.5
@@ -72,24 +72,33 @@

All modules for which code is available

- diff --git a/static/yubikey-manager/API_Documentation/_modules/ykman/base.html b/static/yubikey-manager/API_Documentation/_modules/ykman/base.html index efaa973c..83a7c986 100644 --- a/static/yubikey-manager/API_Documentation/_modules/ykman/base.html +++ b/static/yubikey-manager/API_Documentation/_modules/ykman/base.html @@ -3,7 +3,7 @@ - ykman.base — yubikey-manager 5.4.0 documentation + ykman.base — yubikey-manager 5.5.0 documentation @@ -13,9 +13,9 @@ - - - + + + @@ -33,7 +33,7 @@ yubikey-manager
- 5.4 + 5.5
@@ -104,7 +104,9 @@

Source code for ykman.base

 from typing import Optional, Hashable
 
 
-
[docs]class YkmanDevice(YubiKeyDevice): +
+[docs] +class YkmanDevice(YubiKeyDevice): """YubiKey device reference, with optional PID""" def __init__(self, transport: TRANSPORT, fingerprint: Hashable, pid: Optional[PID]): @@ -122,6 +124,7 @@

Source code for ykman.base

             self.pid or 0,
             self.fingerprint,
         )
+
diff --git a/static/yubikey-manager/API_Documentation/_modules/ykman/device.html b/static/yubikey-manager/API_Documentation/_modules/ykman/device.html index 63dfefe4..c7e75ba0 100644 --- a/static/yubikey-manager/API_Documentation/_modules/ykman/device.html +++ b/static/yubikey-manager/API_Documentation/_modules/ykman/device.html @@ -3,7 +3,7 @@ - ykman.device — yubikey-manager 5.4.0 documentation + ykman.device — yubikey-manager 5.5.0 documentation @@ -13,9 +13,9 @@ - - - + + + @@ -33,7 +33,7 @@ yubikey-manager
- 5.4 + 5.5
@@ -155,7 +155,9 @@

Source code for ykman.device

     return outer
 
 
-
[docs]@_warn_once( +
+[docs] +@_warn_once( "PC/SC not available. Smart card (CCID) protocols will not function.", EstablishContextException, ) @@ -164,18 +166,25 @@

Source code for ykman.device

     return _list_ccid_devices()
-
[docs]@_warn_once("No CTAP HID backend available. FIDO protocols will not function.") + +
+[docs] +@_warn_once("No CTAP HID backend available. FIDO protocols will not function.") def list_ctap_devices(): """List CTAP devices.""" return _list_ctap_devices()
-
[docs]@_warn_once("No OTP HID backend available. OTP protocols will not function.") + +
+[docs] +@_warn_once("No OTP HID backend available. OTP protocols will not function.") def list_otp_devices(): """List OTP devices.""" return _list_otp_devices()
+ _CONNECTION_LIST_MAPPING = { SmartCardConnection: list_ccid_devices, OtpConnection: list_otp_devices, @@ -183,7 +192,9 @@

Source code for ykman.device

 }
 
 
-
[docs]def scan_devices() -> Tuple[Mapping[PID, int], int]: +
+[docs] +def scan_devices() -> Tuple[Mapping[PID, int], int]: """Scan USB for attached YubiKeys, without opening any connections. :return: A dict mapping PID to device count, and a state object which can be used to @@ -214,6 +225,7 @@

Source code for ykman.device

     return merged, hash(tuple(fingerprints))
+ class _PidGroup: def __init__(self, pid): self._pid = pid @@ -345,7 +357,9 @@

Source code for ykman.device

         return self._group.connect(self._key, connection_type)
 
 
-
[docs]def list_all_devices( +
+[docs] +def list_all_devices( connection_types: Iterable[Type[Connection]] = _CONNECTION_LIST_MAPPING.keys(), ) -> List[Tuple[YkmanDevice, DeviceInfo]]: """Connect to all attached YubiKeys and read device info from them. @@ -372,6 +386,7 @@

Source code for ykman.device

     for group in groups.values():
         devices.extend(group.get_devices())
     return devices
+
diff --git a/static/yubikey-manager/API_Documentation/_modules/ykman/diagnostics.html b/static/yubikey-manager/API_Documentation/_modules/ykman/diagnostics.html new file mode 100644 index 00000000..bc5c003b --- /dev/null +++ b/static/yubikey-manager/API_Documentation/_modules/ykman/diagnostics.html @@ -0,0 +1,371 @@ + + + + + + ykman.diagnostics — yubikey-manager 5.5.0 documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for ykman.diagnostics

+from . import __version__ as ykman_version
+from .util import get_windows_version
+from .pcsc import list_readers, list_devices as list_ccid_devices
+from .hid import list_otp_devices, list_ctap_devices
+from .piv import get_piv_info
+from .openpgp import get_openpgp_info
+from .hsmauth import get_hsmauth_info
+
+from yubikit.core import Tlv
+from yubikit.core.smartcard import SmartCardConnection
+from yubikit.core.fido import FidoConnection
+from yubikit.core.otp import OtpConnection
+from yubikit.management import ManagementSession
+from yubikit.yubiotp import YubiOtpSession
+from yubikit.piv import PivSession
+from yubikit.oath import OathSession
+from yubikit.openpgp import OpenPgpSession
+from yubikit.hsmauth import HsmAuthSession
+from yubikit.support import read_info, get_name
+from fido2.ctap import CtapError
+from fido2.ctap2 import Ctap2, ClientPin
+
+from dataclasses import asdict
+from datetime import datetime
+from typing import List, Dict, Any
+import platform
+import ctypes
+import sys
+import os
+
+
+
+[docs] +def sys_info(): + info: Dict[str, Any] = { + "ykman": ykman_version, + "Python": sys.version, + "Platform": sys.platform, + "Arch": platform.machine(), + "System date": datetime.today().strftime("%Y-%m-%d"), + } + if sys.platform == "win32": + info.update( + { + "Running as admin": bool(ctypes.windll.shell32.IsUserAnAdmin()), + "Windows version": get_windows_version(), + } + ) + else: + info["Running as admin"] = os.getuid() == 0 + return info
+ + + +
+[docs] +def mgmt_info(pid, conn): + data: List[Any] = [] + try: + m = ManagementSession(conn) + raw_info = m.backend.read_config() + if Tlv.parse_dict(raw_info[1:]).get(0x10) == b"\1": + raw_info += m.backend.read_config(1) + data.append( + { + "Raw Info": raw_info, + } + ) + except Exception as e: + data.append(f"Failed to read device info via Management: {e!r}") + + try: + info = read_info(conn, pid) + data.append( + { + "DeviceInfo": asdict(info), + "Name": get_name(info, pid.yubikey_type), + } + ) + except Exception as e: + data.append(f"Failed to read device info: {e!r}") + + return data
+ + + +
+[docs] +def piv_info(conn): + try: + piv = PivSession(conn) + return get_piv_info(piv) + except Exception as e: + return f"PIV not accessible {e!r}"
+ + + +
+[docs] +def openpgp_info(conn): + try: + openpgp = OpenPgpSession(conn) + return get_openpgp_info(openpgp) + except Exception as e: + return f"OpenPGP not accessible {e!r}"
+ + + +
+[docs] +def oath_info(conn): + try: + oath = OathSession(conn) + return { + "Oath version": ".".join("%d" % d for d in oath.version), + "Password protected": oath.locked, + } + except Exception as e: + return f"OATH not accessible {e!r}"
+ + + +
+[docs] +def hsmauth_info(conn): + try: + hsmauth = HsmAuthSession(conn) + return get_hsmauth_info(hsmauth) + except Exception as e: + return f"YubiHSM Auth not accessible {e!r}"
+ + + +
+[docs] +def ccid_info(): + try: + readers = {} + for reader in list_readers(): + try: + c = reader.createConnection() + c.connect() + c.disconnect() + result = "Success" + except Exception as e: + result = f"<{e.__class__.__name__}>" + readers[reader.name] = result + + yubikeys: Dict[str, Any] = {} + for dev in list_ccid_devices(): + try: + with dev.open_connection(SmartCardConnection) as conn: + yubikeys[f"{dev!r}"] = { + "Management": mgmt_info(dev.pid, conn), + "PIV": piv_info(conn), + "OATH": oath_info(conn), + "OpenPGP": openpgp_info(conn), + "YubiHSM Auth": hsmauth_info(conn), + } + except Exception as e: + yubikeys[f"{dev!r}"] = f"PC/SC connection failure: {e!r}" + + return { + "Detected PC/SC readers": readers, + "Detected YubiKeys over PC/SC": yubikeys, + } + except Exception as e: + return f"PC/SC failure: {e!r}"
+ + + +
+[docs] +def otp_info(): + try: + yubikeys: Dict[str, Any] = {} + for dev in list_otp_devices(): + try: + dev_info = [] + with dev.open_connection(OtpConnection) as conn: + dev_info.append( + { + "Management": mgmt_info(dev.pid, conn), + } + ) + otp = YubiOtpSession(conn) + try: + config = otp.get_config_state() + dev_info.append({"OTP": [f"{config}"]}) + except ValueError as e: + dev_info.append({"OTP": f"Couldn't read OTP state: {e!r}"}) + yubikeys[f"{dev!r}"] = dev_info + except Exception as e: + yubikeys[f"{dev!r}"] = f"OTP connection failure: {e!r}" + + return { + "Detected YubiKeys over HID OTP": yubikeys, + } + except Exception as e: + return f"HID OTP backend failure: {e!r}"
+ + + +
+[docs] +def fido_info(): + try: + yubikeys: Dict[str, Any] = {} + for dev in list_ctap_devices(): + try: + dev_info: List[Any] = [] + with dev.open_connection(FidoConnection) as conn: + dev_info.append( + { + "CTAP device version": "%d.%d.%d" % conn.device_version, + "CTAPHID protocol version": conn.version, + "Capabilities": conn.capabilities, + "Management": mgmt_info(dev.pid, conn), + } + ) + try: + ctap2 = Ctap2(conn) + ctap_data: Dict[str, Any] = {"Ctap2Info": asdict(ctap2.info)} + if ctap2.info.options.get("clientPin"): + client_pin = ClientPin(ctap2) + ctap_data["PIN retries"] = client_pin.get_pin_retries() + + bio_enroll = ctap2.info.options.get("bioEnroll") + if bio_enroll: + ctap_data[ + "Fingerprint retries" + ] = client_pin.get_uv_retries() + elif bio_enroll is False: + ctap_data["Fingerprints"] = "Not configured" + else: + ctap_data["PIN"] = "Not configured" + dev_info.append(ctap_data) + except (ValueError, CtapError) as e: + dev_info.append(f"Couldn't get CTAP2 info: {e!r}") + yubikeys[f"{dev!r}"] = dev_info + except Exception as e: + yubikeys[f"{dev!r}"] = f"FIDO connection failure: {e!r}" + return { + "Detected YubiKeys over HID FIDO": yubikeys, + } + + except Exception as e: + return f"HID FIDO backend failure: {e!r}"
+ + + +
+[docs] +def get_diagnostics(): + """Runs diagnostics. + + The result of this can be printed using pretty_print. + """ + return [ + sys_info(), + ccid_info(), + otp_info(), + fido_info(), + "End of diagnostics", + ]
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/static/yubikey-manager/API_Documentation/_modules/ykman/fido.html b/static/yubikey-manager/API_Documentation/_modules/ykman/fido.html index 28136552..696a5461 100644 --- a/static/yubikey-manager/API_Documentation/_modules/ykman/fido.html +++ b/static/yubikey-manager/API_Documentation/_modules/ykman/fido.html @@ -3,7 +3,7 @@ - ykman.fido — yubikey-manager 5.4.0 documentation + ykman.fido — yubikey-manager 5.5.0 documentation @@ -13,9 +13,9 @@ - - - + + + @@ -33,7 +33,7 @@ yubikey-manager
- 5.4 + 5.5
@@ -118,7 +118,9 @@

Source code for ykman.fido

 INS_FIPS_VERIFY_FIPS_MODE = U2F_VENDOR_FIRST + 6
 
 
-
[docs]def is_in_fips_mode(fido_connection: FidoConnection) -> bool: +
+[docs] +def is_in_fips_mode(fido_connection: FidoConnection) -> bool: """Check if a YubiKey 4 FIPS is in FIPS approved mode. :param fido_connection: A FIDO connection. @@ -134,7 +136,10 @@

Source code for ykman.fido

         raise
-
[docs]def fips_change_pin( + +
+[docs] +def fips_change_pin( fido_connection: FidoConnection, old_pin: Optional[str], new_pin: str ): """Change the PIN on a YubiKey 4 FIPS. @@ -156,7 +161,10 @@

Source code for ykman.fido

     ctap.send_apdu(ins=INS_FIPS_SET_PIN, data=data)
-
[docs]def fips_verify_pin(fido_connection: FidoConnection, pin: str): + +
+[docs] +def fips_verify_pin(fido_connection: FidoConnection, pin: str): """Unlock the YubiKey 4 FIPS U2F module for credential creation. :param fido_connection: A FIDO connection. @@ -166,7 +174,10 @@

Source code for ykman.fido

     ctap.send_apdu(ins=INS_FIPS_VERIFY_PIN, data=pin.encode())
-
[docs]def fips_reset(fido_connection: FidoConnection): + +
+[docs] +def fips_reset(fido_connection: FidoConnection): """Reset the FIDO module of a YubiKey 4 FIPS. Note: This action is only permitted immediately after YubiKey power-up. It also @@ -185,6 +196,7 @@

Source code for ykman.fido

                 time.sleep(0.5)
             else:
                 raise e
+
diff --git a/static/yubikey-manager/API_Documentation/_modules/ykman/hsmauth.html b/static/yubikey-manager/API_Documentation/_modules/ykman/hsmauth.html index 878d9282..f30bcc66 100644 --- a/static/yubikey-manager/API_Documentation/_modules/ykman/hsmauth.html +++ b/static/yubikey-manager/API_Documentation/_modules/ykman/hsmauth.html @@ -3,7 +3,7 @@ - ykman.hsmauth — yubikey-manager 5.4.0 documentation + ykman.hsmauth — yubikey-manager 5.5.0 documentation @@ -13,9 +13,9 @@ - - - + + + @@ -33,7 +33,7 @@ yubikey-manager
- 5.4 + 5.5
@@ -105,7 +105,9 @@

Source code for ykman.hsmauth

 import os
 
 
-
[docs]def get_hsmauth_info(session: HsmAuthSession): +
+[docs] +def get_hsmauth_info(session: HsmAuthSession): """Get information about the YubiHSM Auth application.""" retries = session.get_management_key_retries() info = { @@ -116,9 +118,13 @@

Source code for ykman.hsmauth

     return info
-
[docs]def generate_random_management_key() -> bytes: + +
+[docs] +def generate_random_management_key() -> bytes: """Generate a new random management key.""" return os.urandom(16)
+
diff --git a/static/yubikey-manager/API_Documentation/_modules/ykman/logging.html b/static/yubikey-manager/API_Documentation/_modules/ykman/logging.html new file mode 100644 index 00000000..f20e644f --- /dev/null +++ b/static/yubikey-manager/API_Documentation/_modules/ykman/logging.html @@ -0,0 +1,200 @@ + + + + + + ykman.logging — yubikey-manager 5.5.0 documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for ykman.logging

+# Copyright (c) 2022 Yubico AB
+# All rights reserved.
+#
+#   Redistribution and use in source and binary forms, with or
+#   without modification, are permitted provided that the following
+#   conditions are met:
+#
+#    1. Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#    2. Redistributions in binary form must reproduce the above
+#       copyright notice, this list of conditions and the following
+#       disclaimer in the documentation and/or other materials provided
+#       with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from yubikit.logging import LOG_LEVEL
+import logging
+
+
+logging.addLevelName(LOG_LEVEL.TRAFFIC, LOG_LEVEL.TRAFFIC.name)
+logger = logging.getLogger(__name__)
+
+
+def _print_box(*lines):
+    w = max([len(ln) for ln in lines])
+    bar = "#" * (w + 4)
+    box = ["", bar]
+    for ln in [""] + list(lines) + [""]:
+        box.append(f"# {ln.ljust(w)} #")
+    box.append(bar)
+    return "\n".join(box)
+
+
+TRAFFIC_WARNING = (
+    "WARNING: All data sent to/from the YubiKey will be logged!",
+    "This data may contain sensitive values, such as secret keys, PINs or passwords!",
+)
+
+DEBUG_WARNING = (
+    "WARNING: Sensitive data may be logged!",
+    "Some personally identifying information may be logged, such as usernames!",
+)
+
+
+
+[docs] +def set_log_level(level: LOG_LEVEL): + logging.getLogger().setLevel(level) + + logger.info(f"Logging at level: {level.name}") + if level <= LOG_LEVEL.TRAFFIC: + logger.warning(_print_box(*TRAFFIC_WARNING)) + elif level <= LOG_LEVEL.DEBUG: + logger.warning(_print_box(*DEBUG_WARNING))
+ + + +
+[docs] +def init_logging(log_level: LOG_LEVEL, log_file=None, replace=False): + formatter = logging.Formatter( + "%(levelname)s %(asctime)s.%(msecs)d [%(name)s.%(funcName)s:%(lineno)d] " + "%(message)s", + "%H:%M:%S", + "%", + ) + if log_file: + handler: logging.Handler = logging.FileHandler(log_file) + else: + handler = logging.StreamHandler() + handler.setFormatter(formatter) + + root = logging.getLogger() + if replace: + for h in root.handlers[:]: + root.removeHandler(h) + + root.addHandler(handler) + set_log_level(log_level) + + if log_file: + logger.warning(f"Logging to file: {log_file}")
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/static/yubikey-manager/API_Documentation/_modules/ykman/logging_setup.html b/static/yubikey-manager/API_Documentation/_modules/ykman/logging_setup.html new file mode 100644 index 00000000..b09ee3d6 --- /dev/null +++ b/static/yubikey-manager/API_Documentation/_modules/ykman/logging_setup.html @@ -0,0 +1,177 @@ + + + + + + ykman.logging_setup — yubikey-manager 5.5.0 documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for ykman.logging_setup

+# Copyright (c) 2015 Yubico AB
+# All rights reserved.
+#
+#   Redistribution and use in source and binary forms, with or
+#   without modification, are permitted provided that the following
+#   conditions are met:
+#
+#    1. Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#    2. Redistributions in binary form must reproduce the above
+#       copyright notice, this list of conditions and the following
+#       disclaimer in the documentation and/or other materials provided
+#       with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from ykman import __version__ as ykman_version
+from ykman.util import get_windows_version
+from ykman.logging import init_logging
+from yubikit.logging import LOG_LEVEL
+from datetime import datetime
+import platform
+import logging
+import warnings
+import ctypes
+import sys
+import os
+
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +def log_sys_info(log): + log(f"ykman: {ykman_version}") + log(f"Python: {sys.version}") + log(f"Platform: {sys.platform}") + log(f"Arch: {platform.machine()}") + if sys.platform == "win32": + log(f"Windows version: {get_windows_version()}") + is_admin = bool(ctypes.windll.shell32.IsUserAnAdmin()) + else: + is_admin = os.getuid() == 0 + log(f"Running as admin: {is_admin}") + log("System date: %s", datetime.today().strftime("%Y-%m-%d"))
+ + + +
+[docs] +def setup(log_level_name, log_file=None): + warnings.warn( + "logging_setup.setup is deprecated, use logging.init_loging instead", + DeprecationWarning, + ) + + log_level = LOG_LEVEL[log_level_name.upper()] + init_logging(log_level, log_file=log_file, replace=log_file is None) + log_sys_info(logger.debug)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/static/yubikey-manager/API_Documentation/_modules/ykman/oath.html b/static/yubikey-manager/API_Documentation/_modules/ykman/oath.html index 673f340c..8d9e3e09 100644 --- a/static/yubikey-manager/API_Documentation/_modules/ykman/oath.html +++ b/static/yubikey-manager/API_Documentation/_modules/ykman/oath.html @@ -3,7 +3,7 @@ - ykman.oath — yubikey-manager 5.4.0 documentation + ykman.oath — yubikey-manager 5.5.0 documentation @@ -13,9 +13,9 @@ - - - + + + @@ -33,7 +33,7 @@ yubikey-manager
- 5.4 + 5.5
@@ -115,17 +115,25 @@

Source code for ykman.oath

 STEAM_CHAR_TABLE = "23456789BCDFGHJKMNPQRTVWXY"
 
 
-
[docs]def is_hidden(credential: Credential) -> bool: +
+[docs] +def is_hidden(credential: Credential) -> bool: """Check if OATH credential is hidden.""" return credential.issuer == "_hidden"
-
[docs]def is_steam(credential: Credential) -> bool: + +
+[docs] +def is_steam(credential: Credential) -> bool: """Check if OATH credential is steam.""" return credential.oath_type == OATH_TYPE.TOTP and credential.issuer == "Steam"
-
[docs]def calculate_steam( + +
+[docs] +def calculate_steam( app: OathSession, credential: Credential, timestamp: Optional[int] = None ) -> str: """Calculate steam codes.""" @@ -140,12 +148,18 @@

Source code for ykman.oath

     return "".join(chars)
-
[docs]def is_in_fips_mode(app: OathSession) -> bool: + +
+[docs] +def is_in_fips_mode(app: OathSession) -> bool: """Check if OATH application is in FIPS mode.""" return app.locked
-
[docs]def delete_broken_credential(app: OathSession) -> bool: + +
+[docs] +def delete_broken_credential(app: OathSession) -> bool: """Checks for credential in a broken state and deletes it.""" logger.debug("Probing for broken credentials") creds = app.list_credentials() @@ -174,6 +188,7 @@

Source code for ykman.oath

 
     logger.warning(f"Requires a single broken credential, found {len(broken)}")
     return False
+
diff --git a/static/yubikey-manager/API_Documentation/_modules/ykman/openpgp.html b/static/yubikey-manager/API_Documentation/_modules/ykman/openpgp.html index 43d7a199..b2f17a64 100644 --- a/static/yubikey-manager/API_Documentation/_modules/ykman/openpgp.html +++ b/static/yubikey-manager/API_Documentation/_modules/ykman/openpgp.html @@ -3,7 +3,7 @@ - ykman.openpgp — yubikey-manager 5.4.0 documentation + ykman.openpgp — yubikey-manager 5.5.0 documentation @@ -13,9 +13,9 @@ - - - + + + @@ -33,7 +33,7 @@ yubikey-manager
- 5.4 + 5.5
@@ -103,7 +103,9 @@

Source code for ykman.openpgp

 from yubikit.openpgp import OpenPgpSession, KEY_REF, KdfNone
 
 
-
[docs]def get_openpgp_info(session: OpenPgpSession): +
+[docs] +def get_openpgp_info(session: OpenPgpSession): """Get human readable information about the OpenPGP configuration. :param session: The OpenPGP session. @@ -133,6 +135,7 @@

Source code for ykman.openpgp

         info["Touch policies"] = touch
 
     return info
+
diff --git a/static/yubikey-manager/API_Documentation/_modules/ykman/otp.html b/static/yubikey-manager/API_Documentation/_modules/ykman/otp.html new file mode 100644 index 00000000..619e6f97 --- /dev/null +++ b/static/yubikey-manager/API_Documentation/_modules/ykman/otp.html @@ -0,0 +1,250 @@ + + + + + + ykman.otp — yubikey-manager 5.5.0 documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for ykman.otp

+# Copyright (c) 2018 Yubico AB
+# All rights reserved.
+#
+#   Redistribution and use in source and binary forms, with or
+#   without modification, are permitted provided that the following
+#   conditions are met:
+#
+#    1. Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#    2. Redistributions in binary form must reproduce the above
+#       copyright notice, this list of conditions and the following
+#       disclaimer in the documentation and/or other materials provided
+#       with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from .scancodes import KEYBOARD_LAYOUT
+from yubikit.core.otp import modhex_encode
+from yubikit.yubiotp import YubiOtpSession
+from yubikit.oath import parse_b32_key
+from datetime import datetime
+from typing import Iterable, Optional
+
+import struct
+import random
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +def is_in_fips_mode(session: YubiOtpSession) -> bool: + """Check if the OTP application of a FIPS YubiKey is in FIPS approved mode. + + :param session: The YubiOTP session. + """ + return session.backend.send_and_receive(0x14, b"", 1) == b"\1" # type: ignore
+ + + +DEFAULT_PW_CHAR_BLOCKLIST = ["\t", "\n", " "] + + +
+[docs] +def generate_static_pw( + length: int, + keyboard_layout: KEYBOARD_LAYOUT = KEYBOARD_LAYOUT.MODHEX, + blocklist: Iterable[str] = DEFAULT_PW_CHAR_BLOCKLIST, +) -> str: + """Generate a random password. + + :param length: The length of the password. + :param keyboard_layout: The keyboard layout. + :param blocklist: The list of characters to block. + """ + chars = [k for k in keyboard_layout.value.keys() if k not in blocklist] + sr = random.SystemRandom() + return "".join([sr.choice(chars) for _ in range(length)])
+ + + +
+[docs] +def parse_oath_key(val: str) -> bytes: + """Parse a secret key encoded as either Hex or Base32. + + :param val: The secret key. + """ + try: + return bytes.fromhex(val) + except ValueError: + return parse_b32_key(val)
+ + + +
+[docs] +def format_oath_code(response: bytes, digits: int = 6) -> str: + """Format an OATH code from a hash response. + + :param response: The response. + :param digits: The number of digits in the OATH code. + """ + offs = response[-1] & 0xF + code = struct.unpack_from(">I", response[offs:])[0] & 0x7FFFFFFF + return ("%%0%dd" % digits) % (code % 10**digits)
+ + + +
+[docs] +def time_challenge(timestamp: int, period: int = 30) -> bytes: + """Format a HMAC-SHA1 challenge based on an OATH timestamp and period. + + :param timestamp: The timestamp. + :param period: The period. + """ + return struct.pack(">q", int(timestamp // period))
+ + + +
+[docs] +def format_csv( + serial: int, + public_id: bytes, + private_id: bytes, + key: bytes, + access_code: Optional[bytes] = None, + timestamp: Optional[datetime] = None, +) -> str: + """Produce a CSV line in the "Yubico" format. + + :param serial: The serial number. + :param public_id: The public ID. + :param private_id: The private ID. + :param key: The secret key. + :param access_code: The access code. + """ + ts = timestamp or datetime.now() + return ",".join( + [ + str(serial), + modhex_encode(public_id), + private_id.hex(), + key.hex(), + access_code.hex() if access_code else "", + ts.isoformat(timespec="seconds"), + "", # Add trailing comma + ] + )
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/static/yubikey-manager/API_Documentation/_modules/ykman/piv.html b/static/yubikey-manager/API_Documentation/_modules/ykman/piv.html index 54ec5c58..93272b02 100644 --- a/static/yubikey-manager/API_Documentation/_modules/ykman/piv.html +++ b/static/yubikey-manager/API_Documentation/_modules/ykman/piv.html @@ -3,7 +3,7 @@ - ykman.piv — yubikey-manager 5.4.0 documentation + ykman.piv — yubikey-manager 5.5.0 documentation @@ -13,9 +13,9 @@ - - - + + + @@ -33,7 +33,7 @@ yubikey-manager
- 5.4 + 5.5
@@ -113,6 +113,7 @@

Source code for ykman.piv

     TAG_LRC,
     SlotMetadata,
 )
+from .util import display_serial
 
 from cryptography import x509
 from cryptography.exceptions import InvalidSignature
@@ -192,7 +193,9 @@ 

Source code for ykman.piv

 _DOTTED_STRING_RE = re.compile(r"\d(\.\d+)+")
 
 
-
[docs]def parse_rfc4514_string(value: str) -> x509.Name: +
+[docs] +def parse_rfc4514_string(value: str) -> x509.Name: """Parse an RFC 4514 string into a x509.Name. See: https://tools.ietf.org/html/rfc4514.html @@ -219,6 +222,7 @@

Source code for ykman.piv

     return x509.Name(attributes)
+ def _dummy_key(key_type): if key_type.algorithm == ALGORITHM.RSA: return rsa.generate_private_key(65537, key_type.bit_len, default_backend()) @@ -233,7 +237,9 @@

Source code for ykman.piv

     raise ValueError("Invalid algorithm")
 
 
-
[docs]def derive_management_key(pin: str, salt: bytes) -> bytes: +
+[docs] +def derive_management_key(pin: str, salt: bytes) -> bytes: """Derive a management key from the users PIN and a salt. NOTE: This method of derivation is deprecated! Protect the management key using @@ -246,7 +252,10 @@

Source code for ykman.piv

     return kdf.derive(pin.encode("utf-8"))
-
[docs]def generate_random_management_key(algorithm: MANAGEMENT_KEY_TYPE) -> bytes: + +
+[docs] +def generate_random_management_key(algorithm: MANAGEMENT_KEY_TYPE) -> bytes: """Generate a new random management key. :param algorithm: The algorithm for the management key. @@ -254,7 +263,10 @@

Source code for ykman.piv

     return os.urandom(algorithm.key_len)
-
[docs]class PivmanData: + +
+[docs] +class PivmanData: def __init__(self, raw_data: bytes = Tlv(0x80)): data = Tlv.parse_dict(Tlv(raw_data).value) self._flags = struct.unpack(">B", data[0x81])[0] if 0x81 in data else None @@ -298,7 +310,9 @@

Source code for ykman.piv

     def has_stored_key(self) -> bool:
         return self.mgm_key_protected
 
-
[docs] def get_bytes(self) -> bytes: +
+[docs] + def get_bytes(self) -> bytes: data = b"" if self._flags: data += Tlv(0x81, struct.pack(">B", self._flags)) @@ -306,22 +320,32 @@

Source code for ykman.piv

             data += Tlv(0x82, self.salt)
         if self.pin_timestamp is not None:
             data += Tlv(0x83, struct.pack(">I", self.pin_timestamp))
-        return Tlv(0x80, data)
+ return Tlv(0x80, data) if data else b""
+
-
[docs]class PivmanProtectedData: + +
+[docs] +class PivmanProtectedData: def __init__(self, raw_data: bytes = Tlv(0x88)): data = Tlv.parse_dict(Tlv(raw_data).value) self.key = data.get(0x89) -
[docs] def get_bytes(self) -> bytes: +
+[docs] + def get_bytes(self) -> bytes: data = b"" if self.key is not None: data += Tlv(0x89, self.key) - return Tlv(0x88, data)
+ return Tlv(0x88, data) if data else b""
+
+ -
[docs]def get_pivman_data(session: PivSession) -> PivmanData: +
+[docs] +def get_pivman_data(session: PivSession) -> PivmanData: """Read out the Pivman data from a YubiKey. :param session: The PIV session. @@ -337,7 +361,10 @@

Source code for ykman.piv

         raise
-
[docs]def get_pivman_protected_data(session: PivSession) -> PivmanProtectedData: + +
+[docs] +def get_pivman_protected_data(session: PivSession) -> PivmanProtectedData: """Read out the Pivman protected data from a YubiKey. This function requires PIN verification prior to being called. @@ -355,7 +382,10 @@

Source code for ykman.piv

         raise
-
[docs]def pivman_set_mgm_key( + +
+[docs] +def pivman_set_mgm_key( session: PivSession, new_key: bytes, algorithm: MANAGEMENT_KEY_TYPE, @@ -371,6 +401,7 @@

Source code for ykman.piv

     :param store_on_device: If set, the management key is stored on device.
     """
     pivman = get_pivman_data(session)
+    pivman_old_bytes = pivman.get_bytes()
     pivman_prot = None
 
     if store_on_device or (not store_on_device and pivman.has_stored_key):
@@ -393,8 +424,10 @@ 

Source code for ykman.piv

     # Set flag for stored or not stored key.
     pivman.mgm_key_protected = store_on_device
 
-    # Update readable pivman data
-    session.put_object(OBJECT_ID_PIVMAN_DATA, pivman.get_bytes())
+    # Update readable pivman data, if changed
+    pivman_bytes = pivman.get_bytes()
+    if pivman_old_bytes != pivman_bytes:
+        session.put_object(OBJECT_ID_PIVMAN_DATA, pivman_bytes)
 
     if pivman_prot is not None:
         if store_on_device:
@@ -416,7 +449,10 @@ 

Source code for ykman.piv

                 logger.debug("No PIN provided, can't clear key...", exc_info=True)
-
[docs]def pivman_change_pin(session: PivSession, old_pin: str, new_pin: str) -> None: + +
+[docs] +def pivman_change_pin(session: PivSession, old_pin: str, new_pin: str) -> None: """Change the PIN, while keeping PivmanData in sync. :param session: The PIV session. @@ -429,7 +465,6 @@

Source code for ykman.piv

     if pivman.has_derived_key:
         logger.debug("Has derived management key, update for new PIN")
         session.authenticate(
-            MANAGEMENT_KEY_TYPE.TDES,
             derive_management_key(old_pin, cast(bytes, pivman.salt)),
         )
         session.verify_pin(new_pin)
@@ -440,7 +475,10 @@ 

Source code for ykman.piv

         session.put_object(OBJECT_ID_PIVMAN_DATA, pivman.get_bytes())
-
[docs]def pivman_set_pin_attempts( + +
+[docs] +def pivman_set_pin_attempts( session: PivSession, pin_attempts: int, puk_attempts: int ) -> None: """Set the number of PIN and PUK retry attempts, while keeping PivmanData in sync. @@ -456,7 +494,10 @@

Source code for ykman.piv

         session.put_object(OBJECT_ID_PIVMAN_DATA, pivman.get_bytes())
-
[docs]def list_certificates(session: PivSession) -> Mapping[SLOT, Optional[x509.Certificate]]: + +
+[docs] +def list_certificates(session: PivSession) -> Mapping[SLOT, Optional[x509.Certificate]]: """Read out and parse stored certificates. Only certificates which are successfully parsed are returned. @@ -475,6 +516,7 @@

Source code for ykman.piv

     return certs
+ def _list_keys(session: PivSession) -> Mapping[SLOT, SlotMetadata]: keys = {} for slot in set(SLOT) - {SLOT.ATTESTATION}: @@ -486,7 +528,9 @@

Source code for ykman.piv

     return keys
 
 
-
[docs]def check_key( +
+[docs] +def check_key( session: PivSession, slot: SLOT, public_key: Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey], @@ -538,7 +582,10 @@

Source code for ykman.piv

         return False
-
[docs]def generate_chuid() -> bytes: + +
+[docs] +def generate_chuid() -> bytes: """Generate a CHUID (Cardholder Unique Identifier).""" # Non-Federal Issuer FASC-N # [9999-9999-999999-0-1-0000000000300001] @@ -558,7 +605,10 @@

Source code for ykman.piv

     )
-
[docs]def generate_ccc() -> bytes: + +
+[docs] +def generate_ccc() -> bytes: """Generate a CCC (Card Capability Container).""" return ( Tlv(0xF0, b"\xa0\x00\x00\x01\x16\xff\x02" + os.urandom(14)) @@ -577,7 +627,10 @@

Source code for ykman.piv

     )
-
[docs]def get_piv_info(session: PivSession): + +
+[docs] +def get_piv_info(session: PivSession): """Get human readable information about the PIV configuration. :param session: The PIV session. @@ -598,22 +651,8 @@

Source code for ykman.piv

         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[
@@ -622,7 +661,21 @@ 

Source code for ykman.piv

         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()
@@ -670,18 +723,8 @@ 

Source code for ykman.piv

         cert = certs.get(slot, None)
         if cert:
             try:
-                # Try to read out full DN, fallback to only CN.
-                # Support for DN was added in crytography 2.5
                 subject_dn = cert.subject.rfc4514_string()
                 issuer_dn = cert.issuer.rfc4514_string()
-                print_dn = True
-            except AttributeError:
-                print_dn = False
-                logger.debug("Failed to read DN, falling back to only CNs")
-                cn = cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
-                subject_cn = cn[0].value if cn else "None"
-                cn = cert.issuer.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
-                issuer_cn = cn[0].value if cn else "None"
             except ValueError as e:
                 # Malformed certificates may throw ValueError
                 logger.debug("Failed parsing certificate", exc_info=True)
@@ -713,13 +756,9 @@ 

Source code for ykman.piv

 
             # Print out everything
             cert_data["Public key type"] = key_algo
-            if print_dn:
-                cert_data["Subject DN"] = subject_dn
-                cert_data["Issuer DN"] = issuer_dn
-            else:
-                cert_data["Subject CN"] = subject_cn
-                cert_data["Issuer CN"] = issuer_cn
-            cert_data["Serial"] = serial
+            cert_data["Subject DN"] = subject_dn
+            cert_data["Issuer DN"] = issuer_dn
+            cert_data["Serial"] = display_serial(serial)
             cert_data["Fingerprint"] = fingerprint
             if not_before:
                 cert_data["Not before"] = not_before.isoformat()
@@ -731,6 +770,7 @@ 

Source code for ykman.piv

     return lines
+ _AllowedHashTypes = Union[ hashes.SHA224, hashes.SHA256, @@ -749,7 +789,9 @@

Source code for ykman.piv

     return hash_algorithm()
 
 
-
[docs]def sign_certificate_builder( +
+[docs] +def sign_certificate_builder( session: PivSession, slot: SLOT, key_type: KEY_TYPE, @@ -785,7 +827,10 @@

Source code for ykman.piv

     return x509.load_der_x509_certificate(der, default_backend())
-
[docs]def sign_csr_builder( + +
+[docs] +def sign_csr_builder( session: PivSession, slot: SLOT, public_key: Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey], @@ -834,7 +879,10 @@

Source code for ykman.piv

     return x509.load_der_x509_csr(der, default_backend())
-
[docs]def generate_self_signed_certificate( + +
+[docs] +def generate_self_signed_certificate( session: PivSession, slot: SLOT, public_key: Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey], @@ -870,7 +918,10 @@

Source code for ykman.piv

     return sign_certificate_builder(session, slot, key_type, builder, hash_algorithm)
-
[docs]def generate_csr( + +
+[docs] +def generate_csr( session: PivSession, slot: SLOT, public_key: Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey], @@ -891,6 +942,7 @@

Source code for ykman.piv

     )
 
     return sign_csr_builder(session, slot, public_key, builder, hash_algorithm)
+
diff --git a/static/yubikey-manager/API_Documentation/_modules/ykman/scripting.html b/static/yubikey-manager/API_Documentation/_modules/ykman/scripting.html index f61290d1..b5ecca95 100644 --- a/static/yubikey-manager/API_Documentation/_modules/ykman/scripting.html +++ b/static/yubikey-manager/API_Documentation/_modules/ykman/scripting.html @@ -3,7 +3,7 @@ - ykman.scripting — yubikey-manager 5.4.0 documentation + ykman.scripting — yubikey-manager 5.5.0 documentation @@ -13,9 +13,9 @@ - - - + + + @@ -33,7 +33,7 @@ yubikey-manager
- 5.4 + 5.5
@@ -138,7 +138,9 @@

Source code for ykman.scripting

 """
 
 
-
[docs]class ScriptingDevice: +
+[docs] +class ScriptingDevice: """Scripting-friendly proxy for YkmanDevice. This wrapper adds some helpful utility methods useful for scripting. @@ -164,23 +166,35 @@

Source code for ykman.scripting

     def name(self) -> str:
         return self._name
 
-
[docs] def otp(self) -> OtpConnection: +
+[docs] + def otp(self) -> OtpConnection: """Establish a OTP connection.""" return self.open_connection(OtpConnection)
-
[docs] def smart_card(self) -> SmartCardConnection: + +
+[docs] + def smart_card(self) -> SmartCardConnection: """Establish a Smart Card connection.""" return self.open_connection(SmartCardConnection)
-
[docs] def fido(self) -> FidoConnection: + +
+[docs] + def fido(self) -> FidoConnection: """Establish a FIDO connection.""" - return self.open_connection(FidoConnection)
+ return self.open_connection(FidoConnection)
+
+ YkmanDevice.register(ScriptingDevice) -
[docs]def single(*, prompt=True) -> ScriptingDevice: +
+[docs] +def single(*, prompt=True) -> ScriptingDevice: """Connect to a YubiKey. :param prompt: When set, you will be prompted to @@ -200,7 +214,10 @@

Source code for ykman.scripting

     raise ValueError("Failed to get single YubiKey")
-
[docs]def multi( + +
+[docs] +def multi( *, ignore_duplicates: bool = True, allow_initial: bool = False, prompt: bool = True ) -> Generator[ScriptingDevice, None, None]: """Connect to multiple YubiKeys. @@ -243,6 +260,7 @@

Source code for ykman.scripting

                 return  # Stop waiting
+ def _get_reader(reader) -> YkmanDevice: readers = [d for d in list_ccid(reader) if d.transport == TRANSPORT.NFC] if not readers: @@ -253,7 +271,9 @@

Source code for ykman.scripting

     return readers[0]
 
 
-
[docs]def single_nfc(reader="", *, prompt=True) -> ScriptingDevice: +
+[docs] +def single_nfc(reader="", *, prompt=True) -> ScriptingDevice: """Connect to a YubiKey over NFC. :param reader: The name of the NFC reader. @@ -273,7 +293,10 @@

Source code for ykman.scripting

             sleep(1.0)
-
[docs]def multi_nfc( + +
+[docs] +def multi_nfc( reader="", *, ignore_duplicates=True, allow_initial=False, prompt=True ) -> Generator[ScriptingDevice, None, None]: """Connect to multiple YubiKeys over NFC. @@ -327,6 +350,7 @@

Source code for ykman.scripting

             sleep(1.0)  # No change, sleep for 1 second.
         except KeyboardInterrupt:
             return  # Stop waiting
+
diff --git a/static/yubikey-manager/API_Documentation/_modules/ykman/settings.html b/static/yubikey-manager/API_Documentation/_modules/ykman/settings.html new file mode 100644 index 00000000..7c99bf0e --- /dev/null +++ b/static/yubikey-manager/API_Documentation/_modules/ykman/settings.html @@ -0,0 +1,246 @@ + + + + + + ykman.settings — yubikey-manager 5.5.0 documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for ykman.settings

+# Copyright (c) 2017 Yubico AB
+# All rights reserved.
+#
+#   Redistribution and use in source and binary forms, with or
+#   without modification, are permitted provided that the following
+#   conditions are met:
+#
+#    1. Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#    2. Redistributions in binary form must reproduce the above
+#       copyright notice, this list of conditions and the following
+#       disclaimer in the documentation and/or other materials provided
+#       with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import json
+import keyring
+from pathlib import Path
+from cryptography.fernet import Fernet, InvalidToken
+
+
+XDG_DATA_HOME = os.environ.get("XDG_DATA_HOME", "~/.local/share") + "/ykman"
+XDG_CONFIG_HOME = os.environ.get("XDG_CONFIG_HOME", "~/.config") + "/ykman"
+
+KEYRING_SERVICE = os.environ.get("YKMAN_KEYRING_SERVICE", "ykman")
+KEYRING_KEY = os.environ.get("YKMAN_KEYRING_KEY", "wrap_key")
+
+
+
+[docs] +class Settings(dict): + _config_dir = XDG_CONFIG_HOME + + def __init__(self, name): + self.fname = Path(self._config_dir).expanduser().resolve() / (name + ".json") + if self.fname.is_file(): + with self.fname.open("r") as fd: + self.update(json.load(fd)) + + def __eq__(self, other): + return other is not None and self.fname == other.fname + + def __ne__(self, other): + return other is None or self.fname != other.fname + +
+[docs] + def write(self): + conf_dir = self.fname.parent + if not conf_dir.is_dir(): + conf_dir.mkdir(0o700, parents=True) + with self.fname.open("w") as fd: + json.dump(self, fd, indent=2)
+ + + __hash__ = None
+ + + +
+[docs] +class Configuration(Settings): + _config_dir = XDG_CONFIG_HOME
+ + + +
+[docs] +class KeystoreError(Exception): + """Error accessing the OS keystore"""
+ + + +
+[docs] +class UnwrapValueError(Exception): + """Error unwrapping a particular secret value"""
+ + + +
+[docs] +class AppData(Settings): + _config_dir = XDG_DATA_HOME + + def __init__(self, name, keyring_service=KEYRING_SERVICE, keyring_key=KEYRING_KEY): + super().__init__(name) + self._service = keyring_service + self._username = keyring_key + + @property + def keyring_unlocked(self) -> bool: + return hasattr(self, "_fernet") + +
+[docs] + def ensure_unlocked(self): + if not self.keyring_unlocked: + try: + wrap_key = keyring.get_password(self._service, self._username) + except keyring.errors.KeyringError: + raise KeystoreError("Keyring locked or unavailable") + + if wrap_key is None: + key = Fernet.generate_key() + keyring.set_password(self._service, self._username, key.decode()) + self._fernet = Fernet(key) + else: + self._fernet = Fernet(wrap_key)
+ + +
+[docs] + def get_secret(self, key: str): + self.ensure_unlocked() + try: + return json.loads(self._fernet.decrypt(self[key].encode())) + except InvalidToken: + raise UnwrapValueError("Undecryptable value")
+ + +
+[docs] + def put_secret(self, key: str, value) -> None: + self.ensure_unlocked() + self[key] = self._fernet.encrypt(json.dumps(value).encode()).decode()
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/static/yubikey-manager/API_Documentation/_modules/ykman/util.html b/static/yubikey-manager/API_Documentation/_modules/ykman/util.html new file mode 100644 index 00000000..b5ff04ab --- /dev/null +++ b/static/yubikey-manager/API_Documentation/_modules/ykman/util.html @@ -0,0 +1,326 @@ + + + + + + ykman.util — yubikey-manager 5.5.0 documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for ykman.util

+# Copyright (c) 2015 Yubico AB
+# All rights reserved.
+#
+#   Redistribution and use in source and binary forms, with or
+#   without modification, are permitted provided that the following
+#   conditions are met:
+#
+#    1. Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#    2. Redistributions in binary form must reproduce the above
+#       copyright notice, this list of conditions and the following
+#       disclaimer in the documentation and/or other materials provided
+#       with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from yubikit.core import Tlv, int2bytes
+from cryptography.hazmat.primitives.serialization import pkcs12
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.backends import default_backend
+from cryptography import x509
+from typing import Tuple
+import ctypes
+
+import logging
+
+
+logger = logging.getLogger(__name__)
+
+
+PEM_IDENTIFIER = b"-----BEGIN"
+
+
+
+[docs] +class InvalidPasswordError(Exception): + """Raised when parsing key/certificate and the password might be wrong/missing."""
+ + + +def _parse_pkcs12(data, password): + try: + key, cert, cas = pkcs12.load_key_and_certificates( + data, password, default_backend() + ) + if cert: + cas.insert(0, cert) + return key, cas + except ValueError as e: # cryptography raises ValueError on wrong password + raise InvalidPasswordError(e) + + +
+[docs] +def parse_private_key(data, password): + """Identify, decrypt and return a cryptography private key object. + + :param data: The private key in bytes. + :param password: The password to decrypt the private key + (if it is encrypted). + """ + # PEM + if is_pem(data): + encrypted = b"ENCRYPTED" in data + if encrypted and password is None: + raise InvalidPasswordError("No password provided for encrypted key.") + try: + return serialization.load_pem_private_key( + data, password, backend=default_backend() + ) + except ValueError as e: + # Cryptography raises ValueError if decryption fails. + if encrypted: + raise InvalidPasswordError(e) + logger.debug("Failed to parse PEM private key ", exc_info=True) + except Exception: + logger.debug("Failed to parse PEM private key ", exc_info=True) + + # PKCS12 + if is_pkcs12(data): + return _parse_pkcs12(data, password)[0] + + # DER + try: + return serialization.load_der_private_key( + data, password, backend=default_backend() + ) + except Exception: + logger.debug("Failed to parse private key as DER", exc_info=True) + + # All parsing failed + raise ValueError("Could not parse private key.")
+ + + +
+[docs] +def parse_certificates(data, password): + """Identify, decrypt and return a list of cryptography x509 certificates. + + :param data: The certificate(s) in bytes. + :param password: The password to decrypt the certificate(s). + """ + logger.debug("Attempting to parse certificate using PEM, PKCS12 and DER") + + # PEM + if is_pem(data): + certs = [] + for cert in data.split(PEM_IDENTIFIER): + if cert: + try: + certs.append( + x509.load_pem_x509_certificate( + PEM_IDENTIFIER + cert, default_backend() + ) + ) + except Exception: + logger.debug("Failed to parse PEM certificate", exc_info=True) + # Could be valid PEM but not certificates. + if not certs: + raise ValueError("PEM file does not contain any certificate(s)") + return certs + + # PKCS12 + if is_pkcs12(data): + return _parse_pkcs12(data, password)[1] + + # DER + try: + return [x509.load_der_x509_certificate(data, default_backend())] + except Exception: + logger.debug("Failed to parse certificate as DER", exc_info=True) + + raise ValueError("Could not parse certificate.")
+ + + +
+[docs] +def get_leaf_certificates(certs): + """Extract the leaf certificates from a list of certificates. + + Leaf certificates are ones whose subject does not appear as + issuer among the others. + + :param certs: The list of cryptography x509 certificate objects. + """ + issuers = [cert.issuer for cert in certs] + leafs = [cert for cert in certs if cert.subject not in issuers] + return leafs
+ + + +
+[docs] +def is_pem(data): + return data and PEM_IDENTIFIER in data
+ + + +
+[docs] +def is_pkcs12(data): + """ + Tries to identify a PKCS12 container. + The PFX PDU version is assumed to be v3. + See: https://tools.ietf.org/html/rfc7292. + """ + try: + header = Tlv.parse_from(Tlv.unpack(0x30, data))[0] + return header.tag == 0x02 and header.value == b"\x03" + except ValueError: + logger.debug("Unable to parse TLV", exc_info=True) + return False
+ + + +
+[docs] +def display_serial(serial: int) -> str: + """Displays an x509 certificate serial number in a readable format.""" + if serial >= 0x10000000000000000: + return ":".join(f"{b:02x}" for b in int2bytes(serial, 20)) + return f"{serial} ({hex(serial)})"
+ + + +
+[docs] +class OSVERSIONINFOW(ctypes.Structure): + _fields_ = [ + ("dwOSVersionInfoSize", ctypes.c_ulong), + ("dwMajorVersion", ctypes.c_ulong), + ("dwMinorVersion", ctypes.c_ulong), + ("dwBuildNumber", ctypes.c_ulong), + ("dwPlatformId", ctypes.c_ulong), + ("szCSDVersion", ctypes.c_wchar * 128), + ]
+ + + +
+[docs] +def get_windows_version() -> Tuple[int, int, int]: + """Get the true Windows version, since sys.getwindowsversion lies.""" + osvi = OSVERSIONINFOW() + osvi.dwOSVersionInfoSize = ctypes.sizeof(osvi) + ctypes.windll.Ntdll.RtlGetVersion(ctypes.byref(osvi)) # type: ignore + return osvi.dwMajorVersion, osvi.dwMinorVersion, osvi.dwBuildNumber
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/static/yubikey-manager/API_Documentation/_modules/yubikit/core.html b/static/yubikey-manager/API_Documentation/_modules/yubikit/core.html index a1d29510..86245200 100644 --- a/static/yubikey-manager/API_Documentation/_modules/yubikit/core.html +++ b/static/yubikey-manager/API_Documentation/_modules/yubikit/core.html @@ -3,7 +3,7 @@ - yubikit.core — yubikey-manager 5.4.0 documentation + yubikit.core — yubikey-manager 5.5.0 documentation @@ -13,9 +13,9 @@ - - - + + + @@ -33,7 +33,7 @@ yubikey-manager
- 5.4 + 5.5
@@ -121,7 +121,9 @@

Source code for yubikit.core

 _VERSION_STRING_PATTERN = re.compile(r"\b(?P<major>\d+).(?P<minor>\d).(?P<patch>\d)\b")
 
 
-
[docs]class Version(NamedTuple): +
+[docs] +class Version(NamedTuple): """3-digit version tuple.""" major: int @@ -131,21 +133,33 @@

Source code for yubikit.core

     def __str__(self):
         return "%d.%d.%d" % self
 
-
[docs] @classmethod + def __bool__(self): + return any(self) + +
+[docs] + @classmethod def from_bytes(cls, data: bytes) -> "Version": return cls(*data)
-
[docs] @classmethod + +
+[docs] + @classmethod def from_string(cls, data: str) -> "Version": m = _VERSION_STRING_PATTERN.search(data) if m: return cls( int(m.group("major")), int(m.group("minor")), int(m.group("patch")) ) - raise ValueError("No version found in string")
+ raise ValueError("No version found in string")
+
+ -
[docs]@unique +
+[docs] +@unique class TRANSPORT(str, Enum): """YubiKey physical connection transports.""" @@ -156,7 +170,10 @@

Source code for yubikit.core

         return super().__str__().upper()
-
[docs]@unique + +
+[docs] +@unique class USB_INTERFACE(IntFlag): """YubiKey USB interface identifiers.""" @@ -165,7 +182,10 @@

Source code for yubikit.core

     CCID = 0x04
-
[docs]@unique + +
+[docs] +@unique class YUBIKEY(Enum): """YubiKey hardware platforms.""" @@ -176,14 +196,20 @@

Source code for yubikit.core

     YK4 = "YubiKey"  # This includes YubiKey 5
-
[docs]class Connection(abc.ABC): + +
+[docs] +class Connection(abc.ABC): """A connection to a YubiKey""" usb_interface: ClassVar[USB_INTERFACE] = USB_INTERFACE(0) -
[docs] def close(self) -> None: +
+[docs] + def close(self) -> None: """Close the device, releasing any held resources."""
+ def __enter__(self): return self @@ -191,7 +217,10 @@

Source code for yubikit.core

         self.close()
-
[docs]@unique + +
+[docs] +@unique class PID(IntEnum): """USB Product ID values for YubiKey devices.""" @@ -221,19 +250,28 @@

Source code for yubikit.core

     def usb_interfaces(self) -> USB_INTERFACE:
         return USB_INTERFACE(sum(USB_INTERFACE[x] for x in self.name.split("_")[1:]))
 
-
[docs] @classmethod +
+[docs] + @classmethod def of(cls, key_type: YUBIKEY, interfaces: USB_INTERFACE) -> "PID": suffix = "_".join(t.name or str(t) for t in USB_INTERFACE if t in interfaces) return cls[key_type.name + "_" + suffix]
-
[docs] def supports_connection(self, connection_type: Type[Connection]) -> bool: - return connection_type.usb_interface in self.usb_interfaces
+ +
+[docs] + def supports_connection(self, connection_type: Type[Connection]) -> bool: + return connection_type.usb_interface in self.usb_interfaces
+
+ T_Connection = TypeVar("T_Connection", bound=Connection) -
[docs]class YubiKeyDevice(abc.ABC): +
+[docs] +class YubiKeyDevice(abc.ABC): """YubiKey device reference""" def __init__(self, transport: TRANSPORT, fingerprint: Hashable): @@ -245,17 +283,23 @@

Source code for yubikit.core

         """Get the transport used to communicate with this YubiKey"""
         return self._transport
 
-
[docs] def supports_connection(self, connection_type: Type[Connection]) -> bool: +
+[docs] + def supports_connection(self, connection_type: Type[Connection]) -> bool: """Check if a YubiKeyDevice supports a specific Connection type""" return False
+ # mypy will not accept abstract types in Type[T_Connection] -
[docs] def open_connection( +
+[docs] + def open_connection( self, connection_type: Union[Type[T_Connection], Callable[..., T_Connection]] ) -> T_Connection: """Opens a connection to the YubiKey""" raise ValueError("Unsupported Connection type")
+ @property def fingerprint(self) -> Hashable: """Used to identify that device references from different enumerations represent @@ -273,27 +317,45 @@

Source code for yubikit.core

         return f"{type(self).__name__}(fingerprint={self.fingerprint!r})"
-
[docs]class CommandError(Exception): + +
+[docs] +class CommandError(Exception): """An error response from a YubiKey"""
-
[docs]class BadResponseError(CommandError): + +
+[docs] +class BadResponseError(CommandError): """Invalid response data from the YubiKey"""
-
[docs]class TimeoutError(CommandError): + +
+[docs] +class TimeoutError(CommandError): """An operation timed out waiting for something"""
-
[docs]class ApplicationNotAvailableError(CommandError): + +
+[docs] +class ApplicationNotAvailableError(CommandError): """The application is either disabled or not supported on this YubiKey"""
-
[docs]class NotSupportedError(ValueError): + +
+[docs] +class NotSupportedError(ValueError): """Attempting an action that is not supported on this YubiKey"""
-
[docs]class InvalidPinError(CommandError, ValueError): + +
+[docs] +class InvalidPinError(CommandError, ValueError): """An incorrect PIN/PUK was used, with the number of attempts now remaining. WARNING: This exception currently inherits from ValueError for @@ -306,7 +368,10 @@

Source code for yubikit.core

         self.attempts_remaining = attempts_remaining
-
[docs]def require_version( + +
+[docs] +def require_version( my_version: Version, min_version: Tuple[int, int, int], message=None ): """Ensure a version is at least min_version.""" @@ -317,7 +382,10 @@

Source code for yubikit.core

         raise NotSupportedError(message)
-
[docs]def int2bytes(value: int, min_len: int = 0) -> bytes: + +
+[docs] +def int2bytes(value: int, min_len: int = 0) -> bytes: buf = [] while value > 0xFF: buf.append(value & 0xFF) @@ -326,10 +394,14 @@

Source code for yubikit.core

     return bytes(reversed(buf)).rjust(min_len, b"\0")
-
[docs]def bytes2int(data: bytes) -> int: + +
+[docs] +def bytes2int(data: bytes) -> int: return int.from_bytes(data, "big")
+ def _tlv_parse(data, offset=0): try: tag = data[offset] @@ -364,7 +436,9 @@

Source code for yubikit.core

 T_Tlv = TypeVar("T_Tlv", bound="Tlv")
 
 
-
[docs]class Tlv(bytes): +
+[docs] +class Tlv(bytes): @property def tag(self) -> int: return self._tag @@ -411,12 +485,17 @@

Source code for yubikit.core

     def __repr__(self):
         return f"Tlv(tag=0x{self.tag:02x}, value={self.value.hex()})"
 
-
[docs] @classmethod +
+[docs] + @classmethod def parse_from(cls: Type[T_Tlv], data: bytes) -> Tuple[T_Tlv, bytes]: tag, offs, ln, end = _tlv_parse(data) return cls(data[:end]), data[end:]
-
[docs] @classmethod + +
+[docs] + @classmethod def parse_list(cls: Type[T_Tlv], data: bytes) -> List[T_Tlv]: res = [] while data: @@ -424,16 +503,24 @@

Source code for yubikit.core

             res.append(tlv)
         return res
-
[docs] @classmethod + +
+[docs] + @classmethod def parse_dict(cls: Type[T_Tlv], data: bytes) -> Dict[int, bytes]: return dict((tlv.tag, tlv.value) for tlv in cls.parse_list(data))
-
[docs] @classmethod + +
+[docs] + @classmethod def unpack(cls: Type[T_Tlv], tag: int, data: bytes) -> bytes: tlv = cls(data) if tlv.tag != tag: raise ValueError(f"Wrong tag, got 0x{tlv.tag:02x} expected 0x{tag:02x}") - return tlv.value
+ return tlv.value
+
+
diff --git a/static/yubikey-manager/API_Documentation/_modules/yubikit/core/otp.html b/static/yubikey-manager/API_Documentation/_modules/yubikit/core/otp.html index e4129254..bfa19dc8 100644 --- a/static/yubikey-manager/API_Documentation/_modules/yubikit/core/otp.html +++ b/static/yubikey-manager/API_Documentation/_modules/yubikit/core/otp.html @@ -3,7 +3,7 @@ - yubikit.core.otp — yubikey-manager 5.4.0 documentation + yubikit.core.otp — yubikey-manager 5.5.0 documentation @@ -13,9 +13,9 @@ - - - + + + @@ -33,7 +33,7 @@ yubikey-manager
- 5.4 + 5.5
@@ -117,26 +117,40 @@

Source code for yubikit.core.otp

 MODHEX_ALPHABET = "cbdefghijklnrtuv"
 
 
-
[docs]class CommandRejectedError(CommandError): +
+[docs] +class CommandRejectedError(CommandError): """The issues command was rejected by the YubiKey"""
-
[docs]class OtpConnection(Connection, metaclass=abc.ABCMeta): + +
+[docs] +class OtpConnection(Connection, metaclass=abc.ABCMeta): usb_interface = USB_INTERFACE.OTP -
[docs] @abc.abstractmethod +
+[docs] + @abc.abstractmethod def receive(self) -> bytes: """Reads an 8 byte feature report"""
-
[docs] @abc.abstractmethod + +
+[docs] + @abc.abstractmethod def send(self, data: bytes) -> None: - """Writes an 8 byte feature report"""
+ """Writes an 8 byte feature report"""
+
+ CRC_OK_RESIDUAL = 0xF0B8 -
[docs]def calculate_crc(data: bytes) -> int: +
+[docs] +def calculate_crc(data: bytes) -> int: crc = 0xFFFF for index in range(len(data)): crc ^= data[index] @@ -148,16 +162,25 @@

Source code for yubikit.core.otp

     return crc & 0xFFFF
-
[docs]def check_crc(data: bytes) -> bool: + +
+[docs] +def check_crc(data: bytes) -> bool: return calculate_crc(data) == CRC_OK_RESIDUAL
-
[docs]def modhex_encode(data: bytes) -> str: + +
+[docs] +def modhex_encode(data: bytes) -> str: """Encode a bytes-like object using Modhex (modified hexadecimal) encoding.""" return "".join(MODHEX_ALPHABET[b >> 4] + MODHEX_ALPHABET[b & 0xF] for b in data)
-
[docs]def modhex_decode(string: str) -> bytes: + +
+[docs] +def modhex_decode(string: str) -> bytes: """Decode the Modhex (modified hexadecimal) string.""" if len(string) % 2: raise ValueError("Length must be a multiple of 2") @@ -168,6 +191,7 @@

Source code for yubikit.core.otp

     )
+ FEATURE_RPT_SIZE = 8 FEATURE_RPT_DATA_SIZE = FEATURE_RPT_SIZE - 1 @@ -198,7 +222,9 @@

Source code for yubikit.core.otp

     return payload + struct.pack("<BH", slot, calculate_crc(payload)) + b"\0\0\0"
 
 
-
[docs]class OtpProtocol: +
+[docs] +class OtpProtocol: """An implementation of the OTP protocol.""" def __init__(self, otp_connection: OtpConnection): @@ -212,10 +238,15 @@

Source code for yubikit.core.otp

             except CommandRejectedError:
                 pass  # This is expected
 
-
[docs] def close(self) -> None: +
+[docs] + def close(self) -> None: self.connection.close()
-
[docs] def send_and_receive( + +
+[docs] + def send_and_receive( self, slot: int, data: Optional[bytes] = None, @@ -248,6 +279,7 @@

Source code for yubikit.core.otp

         logger.log(LOG_LEVEL.TRAFFIC, "RECV: %s", response.hex())
         return response
+ def _receive(self): report = self.connection.receive() if len(report) != FEATURE_RPT_SIZE: @@ -257,7 +289,9 @@

Source code for yubikit.core.otp

             )
         return report
 
-
[docs] def read_status(self) -> bytes: +
+[docs] + def read_status(self) -> bytes: """Receive status bytes from YubiKey. :return: Status bytes (first 3 bytes are the firmware version). @@ -265,6 +299,7 @@

Source code for yubikit.core.otp

         """
         return self._receive()[1:-1]
+ def _await_ready_to_write(self): """Sleep for up to ~1s waiting for the WRITE flag to be unset""" for _ in range(20): @@ -341,6 +376,7 @@

Source code for yubikit.core.otp

     def _reset_state(self):
         """Reset the state of YubiKey from reading"""
         self.connection.send(b"\xff".rjust(FEATURE_RPT_SIZE, b"\0"))
+
diff --git a/static/yubikey-manager/API_Documentation/_modules/yubikit/core/smartcard.html b/static/yubikey-manager/API_Documentation/_modules/yubikit/core/smartcard.html index 240185c6..c4b7def5 100644 --- a/static/yubikey-manager/API_Documentation/_modules/yubikit/core/smartcard.html +++ b/static/yubikey-manager/API_Documentation/_modules/yubikit/core/smartcard.html @@ -3,7 +3,7 @@ - yubikit.core.smartcard — yubikey-manager 5.4.0 documentation + yubikit.core.smartcard — yubikey-manager 5.5.0 documentation @@ -13,9 +13,9 @@ - - - + + + @@ -33,7 +33,7 @@ yubikey-manager
- 5.4 + 5.5
@@ -101,25 +101,54 @@

Source code for yubikit.core.smartcard

 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
-from . import (
+from .. import (
     Version,
     TRANSPORT,
     USB_INTERFACE,
     Connection,
-    CommandError,
+    NotSupportedError,
     ApplicationNotAvailableError,
+    CommandError,
+    BadResponseError,
 )
-from time import time
+from .scp import (
+    ScpState,
+    ScpKeyParams,
+    Scp03KeyParams,
+    Scp11KeyParams,
+    INS_EXTERNAL_AUTHENTICATE,
+)
+from yubikit.logging import LOG_LEVEL
 from enum import Enum, IntEnum, unique
+from time import time
 from typing import Tuple
 import abc
 import struct
 import logging
+import warnings
+
+
+__all__ = ["ApduError", "ApduFormat", "SW", "AID"]
 
 logger = logging.getLogger(__name__)
 
 
-
[docs]class ApduError(CommandError): +class SmartCardConnection(Connection, metaclass=abc.ABCMeta): + usb_interface = USB_INTERFACE.CCID + + @property + @abc.abstractmethod + def transport(self) -> TRANSPORT: + """Get the transport type of the connection (USB or NFC)""" + + @abc.abstractmethod + def send_and_receive(self, apdu: bytes) -> Tuple[bytes, int]: + """Sends a command APDU and returns the response""" + + +
+[docs] +class ApduError(CommandError): """Thrown when an APDU response has the wrong SW code""" def __init__(self, data: bytes, sw: int): @@ -127,10 +156,17 @@

Source code for yubikit.core.smartcard

         self.sw = sw
 
     def __str__(self):
-        return f"APDU error: SW=0x{self.sw:04x}"
+ try: + name = SW(self.sw).name + return f"APDU error: SW=0x{self.sw:04x} ({name})" + except ValueError: + return f"APDU error: SW=0x{self.sw:04x}"
+ -
[docs]@unique +
+[docs] +@unique class ApduFormat(str, Enum): """APDU encoding format""" @@ -138,7 +174,10 @@

Source code for yubikit.core.smartcard

     EXTENDED = "extended"
-
[docs]@unique + +
+[docs] +@unique class AID(bytes, Enum): """YubiKey Application smart card AID values.""" @@ -148,10 +187,14 @@

Source code for yubikit.core.smartcard

     OATH = bytes.fromhex("a0000005272101")
     PIV = bytes.fromhex("a000000308")
     FIDO = bytes.fromhex("a0000006472f0001")
-    HSMAUTH = bytes.fromhex("a000000527210701")
+ HSMAUTH = bytes.fromhex("a000000527210701") + SECURE_DOMAIN = bytes.fromhex("a000000151000000")
+ -
[docs]@unique +
+[docs] +@unique class SW(IntEnum): NO_INPUT_DATA = 0x6285 VERIFY_FAIL_NO_RETRY = 0x63C0 @@ -170,48 +213,189 @@

Source code for yubikit.core.smartcard

     APPLET_SELECT_FAILED = 0x6999
     WRONG_PARAMETERS_P1P2 = 0x6B00
     INVALID_INSTRUCTION = 0x6D00
+    CLASS_NOT_SUPPORTED = 0x6E00
     COMMAND_ABORTED = 0x6F00
     OK = 0x9000
-
[docs]class SmartCardConnection(Connection, metaclass=abc.ABCMeta): - usb_interface = USB_INTERFACE.CCID - - @property - @abc.abstractmethod - def transport(self) -> TRANSPORT: - """Get the transport type of the connection (USB or NFC)""" - -
[docs] @abc.abstractmethod - def send_and_receive(self, apdu: bytes) -> Tuple[bytes, int]: - """Sends a command APDU and returns the response"""
- INS_SELECT = 0xA4 P1_SELECT = 0x04 P2_SELECT = 0x00 INS_SEND_REMAINING = 0xC0 -SW1_HAS_MORE_DATA = 0x61 + + +class ApduProcessor(abc.ABC): + @abc.abstractmethod + def send_apdu( + self, + cla: int, + ins: int, + p1: int, + p2: int, + data: bytes, + le: int, + ) -> Tuple[bytes, int]: + ... + + +class ApduFormatProcessor(ApduProcessor): + def __init__(self, connection: SmartCardConnection): + self.connection = connection + + def send_apdu(self, cla, ins, p1, p2, data, le): + apdu = self.format_apdu(cla, ins, p1, p2, data, le) + return self.connection.send_and_receive(apdu) + + @abc.abstractmethod + def format_apdu( + self, cla: int, ins: int, p1: int, p2: int, data: bytes, le: int + ) -> bytes: + ... + SHORT_APDU_MAX_CHUNK = 0xFF -def _encode_short_apdu(cla, ins, p1, p2, data, le=0): - buf = struct.pack(">BBBBB", cla, ins, p1, p2, len(data)) + data - if le: - buf += struct.pack(">B", le) - return buf +class ShortApduProcessor(ApduFormatProcessor): + def format_apdu(self, cla, ins, p1, p2, data, le): + buf = struct.pack(">BBBBB", cla, ins, p1, p2, len(data)) + data + if le: + buf += struct.pack(">B", le) + return buf + + def send_apdu(self, cla, ins, p1, p2, data, le): + while len(data) > SHORT_APDU_MAX_CHUNK: + chunk, data = ( + data[:SHORT_APDU_MAX_CHUNK], + data[SHORT_APDU_MAX_CHUNK:], + ) + apdu = self.format_apdu(0x10 | cla, ins, p1, p2, chunk, le) + response, sw = self.connection.send_and_receive(apdu) + if sw != SW.OK: + return response, sw + return super().send_apdu(cla, ins, p1, p2, data, le) + + +class ExtendedApduProcessor(ApduFormatProcessor): + def __init__(self, connection, max_apdu_size): + super().__init__(connection) + self._max_apdu_size = max_apdu_size + + def format_apdu(self, cla, ins, p1, p2, data, le): + buf = struct.pack(">BBBBBH", cla, ins, p1, p2, 0, len(data)) + data + if le: + buf += struct.pack(">H", le) + if len(buf) > self._max_apdu_size: + raise NotSupportedError("APDU length exceeds YubiKey capability") + return buf + + +SW1_HAS_MORE_DATA = 0x61 + + +class ChainedResponseProcessor(ApduProcessor): + def __init__( + self, + connection: SmartCardConnection, + extended_apdus: bool, + max_apdu_size: int, + ins_send_remaining: int = INS_SEND_REMAINING, + ): + self.connection = connection + self.processor = ( + ExtendedApduProcessor(connection, max_apdu_size) + if extended_apdus + else ShortApduProcessor(connection) + ) + self._get_data = self.processor.format_apdu(0, ins_send_remaining, 0, 0, b"", 0) + + def send_apdu(self, cla, ins, p1, p2, data, le): + response, sw = self.processor.send_apdu(cla, ins, p1, p2, data, le) + + # Read chained response + buf = b"" + while sw >> 8 == SW1_HAS_MORE_DATA: + buf += response + response, sw = self.connection.send_and_receive(self._get_data) + + buf += response + return buf, sw + + +class TouchWorkaroundProcessor(ChainedResponseProcessor): + def __init__( + self, + connection: SmartCardConnection, + ins_send_remaining: int = INS_SEND_REMAINING, + ): + super().__init__( + connection, True, _MaxApduSize.YK4, ins_send_remaining=ins_send_remaining + ) + self._last_long_resp = 0.0 + + def send_apdu(self, cla, ins, p1, p2, data, le): + if self._last_long_resp > 0 and time() - self._last_long_resp < 2: + logger.debug("Sending dummy APDU as touch workaround") + # Dummy APDU, returns error + super().send_apdu(0, 0, 0, 0, b"", 0) + self._last_long_resp = 0 + + resp, sw = super().send_apdu(cla, ins, p1, p2, data, le) + + if len(resp) > 54: + self._last_long_resp = time() + else: + self._last_long_resp = 0 + + return resp, sw + + +class ScpProcessor(ChainedResponseProcessor): + def __init__( + self, + connection: SmartCardConnection, + scp_state: ScpState, + max_apdu_size: int, + ins_send_remaining: int = INS_SEND_REMAINING, + ): + super().__init__( + connection, True, max_apdu_size, ins_send_remaining=ins_send_remaining + ) + self._state = scp_state + + def send_apdu(self, cla, ins, p1, p2, data, le, encrypt: bool = True): + cla |= 0x04 + + if encrypt: + logger.log(LOG_LEVEL.TRAFFIC, "Plaintext data: %s", data.hex()) + data = self._state.encrypt(data) + + # Calculate and add MAC to data + apdu = self.processor.format_apdu(cla, ins, p1, p2, data + b"\0" * 8, le) + mac = self._state.mac(apdu[:-8]) + data = data + mac + + resp, sw = super().send_apdu(cla, ins, p1, p2, data, le) + + # Un-MAC and decrypt, if needed + if resp: + resp = self._state.unmac(resp, sw) + if resp: + resp = self._state.decrypt(resp) + logger.log(LOG_LEVEL.TRAFFIC, "Plaintext resp: %s", resp.hex()) + return resp, sw -def _encode_extended_apdu(cla, ins, p1, p2, data, le=0): - buf = struct.pack(">BBBBBH", cla, ins, p1, p2, 0, len(data)) + data - if le: - buf += struct.pack(">H", le) - return buf +class _MaxApduSize(IntEnum): + NEO = 1390 + YK4 = 2038 + YK4_3 = 3062 -
[docs]class SmartCardProtocol: + +class SmartCardProtocol: """An implementation of the Smart Card protocol.""" def __init__( @@ -219,26 +403,108 @@

Source code for yubikit.core.smartcard

         smartcard_connection: SmartCardConnection,
         ins_send_remaining: int = INS_SEND_REMAINING,
     ):
-        self.apdu_format = ApduFormat.SHORT
         self.connection = smartcard_connection
+        self._max_apdu_size = _MaxApduSize.NEO
+        self._apdu_format = ApduFormat.SHORT
         self._ins_send_remaining = ins_send_remaining
-        self._touch_workaround = False
-        self._last_long_resp = 0.0
+        self._reset_processor()
+
+    def _reset_processor(self) -> None:
+        self._processor = ChainedResponseProcessor(
+            self.connection,
+            self._apdu_format == ApduFormat.EXTENDED,
+            self._max_apdu_size,
+            self._ins_send_remaining,
+        )
+
+    @property
+    def apdu_format(self) -> ApduFormat:
+        warnings.warn(
+            "Deprecated: do not read apdu_format.",
+            DeprecationWarning,
+        )
 
-
[docs] def close(self) -> None: - self.connection.close()
+ return self._apdu_format -
[docs] def enable_touch_workaround(self, version: Version) -> None: - self._touch_workaround = self.connection.transport == TRANSPORT.USB and ( - (4, 2, 0) <= version <= (4, 2, 6) + @apdu_format.setter + def apdu_format(self, value) -> None: + if value == self._apdu_format: + return + if value != ApduFormat.EXTENDED: + raise ValueError(f"Cannot change to {value}") + self._apdu_format = value + self._reset_processor() + + def close(self) -> None: + self.connection.close() + + def enable_touch_workaround(self, version: Version) -> None: + warnings.warn( + "Deprecated: use SmartCardProtocol.configure(version) instead.", + DeprecationWarning, ) - logger.debug(f"Touch workaround enabled={self._touch_workaround}")
+ self._do_enable_touch_workaround(version) + + def _do_enable_touch_workaround(self, version: Version) -> bool: + if self.connection.transport == TRANSPORT.USB and ( + (4, 2, 0) <= version <= (4, 2, 6) + ): + self._max_apdu_size = _MaxApduSize.YK4 + self._processor = TouchWorkaroundProcessor( + self.connection, self._ins_send_remaining + ) + logger.debug("Touch workaround enabled") + return True + return False + + def configure(self, version: Version) -> None: + """Configure the connection optimally for the given YubiKey version.""" + if isinstance(self._processor, ScpProcessor): + return + if self._do_enable_touch_workaround(version): + return + + if version[0] > 3: + self._apdu_format = ApduFormat.EXTENDED + self._max_apdu_size = ( + _MaxApduSize.YK4_3 if version >= (4, 3) else _MaxApduSize.YK4 + ) + self._reset_processor() + + def send_apdu( + self, + cla: int, + ins: int, + p1: int, + p2: int, + data: bytes = b"", + le: int = 0, + ) -> bytes: + """Send APDU message. + + :param cla: The instruction class. + :param ins: The instruction code. + :param p1: The instruction parameter. + :param p2: The instruction parameter. + :param data: The command data in bytes. + :param le: The maximum number of bytes in the data + field of the response. + """ + resp, sw = self._processor.send_apdu(cla, ins, p1, p2, data, le) + + if sw != SW.OK: + raise ApduError(resp, sw) + + return resp -
[docs] def select(self, aid: bytes) -> bytes: + def select(self, aid: bytes) -> bytes: """Perform a SELECT instruction. :param aid: The YubiKey application AID value. """ + logger.debug(f"Selecting AID: {aid.hex()}") + self._reset_processor() + try: return self.send_apdu(0, INS_SELECT, P1_SELECT, P2_SELECT, aid) except ApduError as e: @@ -249,68 +515,50 @@

Source code for yubikit.core.smartcard

                 SW.WRONG_PARAMETERS_P1P2,
             ):
                 raise ApplicationNotAvailableError()
-            raise
- -
[docs] def send_apdu( - self, cla: int, ins: int, p1: int, p2: int, data: bytes = b"", le: int = 0 - ) -> bytes: - """Send APDU message. - - :param cla: The instruction class. - :param ins: The instruction code. - :param p1: The instruction parameter. - :param p2: The instruction parameter. - :param data: The command data in bytes. - :param le: The maximum number of bytes in the data - field of the response. - """ - if ( - self._touch_workaround - and self._last_long_resp > 0 - and time() - self._last_long_resp < 2 - ): - logger.debug("Sending dummy APDU as touch workaround") - self.connection.send_and_receive( - _encode_short_apdu(0, 0, 0, 0, b"") - ) # Dummy APDU, returns error - self._last_long_resp = 0 + raise - if self.apdu_format is ApduFormat.SHORT: - while len(data) > SHORT_APDU_MAX_CHUNK: - chunk, data = data[:SHORT_APDU_MAX_CHUNK], data[SHORT_APDU_MAX_CHUNK:] - response, sw = self.connection.send_and_receive( - _encode_short_apdu(0x10 | cla, ins, p1, p2, chunk, le) + def init_scp(self, key_params: ScpKeyParams) -> None: + try: + if isinstance(key_params, Scp03KeyParams): + self._scp03_init(key_params) + elif isinstance(key_params, Scp11KeyParams): + self._scp11_init(key_params) + else: + raise ValueError("Unsupported ScpKeyParams") + self._apdu_format = ApduFormat.EXTENDED + self._max_apdu_size = _MaxApduSize.YK4_3 + logger.info("SCP initialized") + except ApduError as e: + if e.sw == SW.CLASS_NOT_SUPPORTED: + raise NotSupportedError( + "This YubiKey does not support secure messaging" ) - if sw != SW.OK: - raise ApduError(response, sw) - response, sw = self.connection.send_and_receive( - _encode_short_apdu(cla, ins, p1, p2, data, le) - ) - get_data = _encode_short_apdu(0, self._ins_send_remaining, 0, 0, b"") - elif self.apdu_format is ApduFormat.EXTENDED: - response, sw = self.connection.send_and_receive( - _encode_extended_apdu(cla, ins, p1, p2, data, le) - ) - get_data = _encode_extended_apdu(0, self._ins_send_remaining, 0, 0, b"") - else: - raise TypeError("Invalid ApduFormat set") - - # Read chained response - buf = b"" - while sw >> 8 == SW1_HAS_MORE_DATA: - buf += response - response, sw = self.connection.send_and_receive(get_data) - - if sw != SW.OK: - raise ApduError(response, sw) - buf += response + if e.sw == SW.REFERENCE_DATA_NOT_FOUND: + raise ValueError("Incorrect SCP parameters") + raise + except BadResponseError: + raise ValueError("Incorrect SCP parameters") + + def _scp03_init(self, key_params: Scp03KeyParams) -> None: + logger.debug("Initializing SCP03") + scp, host_cryptogram = ScpState.scp03_init(self.send_apdu, key_params) + processor = ScpProcessor( + self.connection, scp, _MaxApduSize.YK4_3, self._ins_send_remaining + ) - if self._touch_workaround and len(buf) > 54: - self._last_long_resp = time() - else: - self._last_long_resp = 0 + # Send EXTERNAL AUTHENTICATE + # P1 = C-DECRYPTION, R-ENCRYPTION, C-MAC, and R-MAC + processor.send_apdu( + 0x84, INS_EXTERNAL_AUTHENTICATE, 0x33, 0, host_cryptogram, 0, encrypt=False + ) + self._processor = processor - return buf
+ def _scp11_init(self, key_params: Scp11KeyParams) -> None: + logger.debug("Initializing SCP11") + scp = ScpState.scp11_init(self.send_apdu, key_params) + self._processor = ScpProcessor( + self.connection, scp, _MaxApduSize.YK4_3, self._ins_send_remaining + )
diff --git a/static/yubikey-manager/API_Documentation/_modules/yubikit/core/smartcard/scp.html b/static/yubikey-manager/API_Documentation/_modules/yubikit/core/smartcard/scp.html new file mode 100644 index 00000000..e8517aba --- /dev/null +++ b/static/yubikey-manager/API_Documentation/_modules/yubikit/core/smartcard/scp.html @@ -0,0 +1,526 @@ + + + + + + yubikit.core.smartcard.scp — yubikey-manager 5.5.0 documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for yubikit.core.smartcard.scp

+# Copyright (c) 2023 Yubico AB
+# All rights reserved.
+#
+#   Redistribution and use in source and binary forms, with or
+#   without modification, are permitted provided that the following
+#   conditions are met:
+#
+#    1. Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#    2. Redistributions in binary form must reproduce the above
+#       copyright notice, this list of conditions and the following
+#       disclaimer in the documentation and/or other materials provided
+#       with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from .. import Tlv, BadResponseError
+from cryptography import x509
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import cmac, hashes, serialization, constant_time
+from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.primitives.kdf.x963kdf import X963KDF
+from dataclasses import dataclass, field
+from enum import IntEnum, unique
+from typing import NamedTuple, Tuple, Optional, Callable, Sequence, Union
+
+import os
+import abc
+import struct
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+INS_INITIALIZE_UPDATE = 0x50
+INS_EXTERNAL_AUTHENTICATE = 0x82
+INS_INTERNAL_AUTHENTICATE = 0x88
+INS_PERFORM_SECURITY_OPERATION = 0x2A
+
+_DEFAULT_KEY = b"\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f"
+
+_KEY_ENC = 0x04
+_KEY_MAC = 0x06
+_KEY_RMAC = 0x07
+_CARD_CRYPTOGRAM = 0x00
+_HOST_CRYPTOGRAM = 0x01
+
+
+def _derive(key: bytes, t: int, context: bytes, L: int = 0x80) -> bytes:
+    # this only supports aes128
+    if L != 0x80 and L != 0x40:
+        raise ValueError("L must be 0x40 or 0x80")
+
+    i = b"\0" * 11 + struct.pack("!BBHB", t, 0, L, 1) + context
+
+    c = cmac.CMAC(algorithms.AES(key), backend=default_backend())
+    c.update(i)
+    return c.finalize()[: L // 8]
+
+
+def _calculate_mac(key: bytes, chain: bytes, message: bytes) -> Tuple[bytes, bytes]:
+    c = cmac.CMAC(algorithms.AES(key), backend=default_backend())
+    c.update(chain)
+    c.update(message)
+    chain = c.finalize()
+    return chain, chain[:8]
+
+
+def _init_cipher(key: bytes, counter: int, response=False) -> Cipher:
+    encryptor = Cipher(
+        algorithms.AES(key), modes.ECB(), backend=default_backend()  # nosec ECB
+    ).encryptor()
+    iv_data = (b"\x80" if response else b"\x00") + int.to_bytes(counter, 15, "big")
+    iv = encryptor.update(iv_data) + encryptor.finalize()
+    return Cipher(
+        algorithms.AES(key),
+        modes.CBC(iv),
+        backend=default_backend(),
+    )
+
+
+
+[docs] +class SessionKeys(NamedTuple): + """SCP Session Keys.""" + + key_senc: bytes + key_smac: bytes + key_srmac: bytes + key_dek: Optional[bytes] = None
+ + + +
+[docs] +class StaticKeys(NamedTuple): + """SCP03 Static Keys.""" + + key_enc: bytes + key_mac: bytes + key_dek: Optional[bytes] = None + +
+[docs] + @classmethod + def default(cls) -> "StaticKeys": + return cls(_DEFAULT_KEY, _DEFAULT_KEY, _DEFAULT_KEY)
+ + +
+[docs] + def derive(self, context: bytes) -> SessionKeys: + return SessionKeys( + _derive(self.key_enc, _KEY_ENC, context), + _derive(self.key_mac, _KEY_MAC, context), + _derive(self.key_mac, _KEY_RMAC, context), + self.key_dek, + )
+
+ + + +
+[docs] +@unique +class ScpKid(IntEnum): + SCP03 = 0x1 + SCP11a = 0x11 + SCP11b = 0x13 + SCP11c = 0x15
+ + + +
+[docs] +class KeyRef(bytes): + @property + def kid(self) -> int: + return self[0] + + @property + def kvn(self) -> int: + return self[1] + + def __new__(cls, kid_or_data: Union[int, bytes], kvn: Optional[int] = None): + """This allows creation by passing either binary data, or kid and kvn.""" + if isinstance(kid_or_data, int): # kid and kvn + if kvn is None: + raise ValueError("Missing kvn") + data = bytes([kid_or_data, kvn]) + else: # Binary id and version + if kvn is not None: + raise ValueError("kvn can only be provided if kid_or_data is a kid") + data = kid_or_data + + # mypy thinks this is wrong + return super(KeyRef, cls).__new__(cls, data) # type: ignore + + def __init__(self, kid_or_data: Union[int, bytes], kvn: Optional[int] = None): + if len(self) != 2: + raise ValueError("Incorrect length") + + def __repr__(self): + return f"KeyRef(kid=0x{self.kid:02x}, kvn=0x{self.kvn:02x})" + + def __str__(self): + return repr(self)
+ + + +
+[docs] +@dataclass(frozen=True) +class ScpKeyParams(abc.ABC): + ref: KeyRef
+ + + +
+[docs] +@dataclass(frozen=True) +class Scp03KeyParams(ScpKeyParams): + ref: KeyRef = KeyRef(ScpKid.SCP03, 0) + keys: StaticKeys = StaticKeys.default()
+ + + +
+[docs] +@dataclass(frozen=True) +class Scp11KeyParams(ScpKeyParams): + pk_sd_ecka: ec.EllipticCurvePublicKey + # For SCP11 a/c we need an OCE key, with its trust chain + oce_ref: Optional[KeyRef] = None + sk_oce_ecka: Optional[ec.EllipticCurvePrivateKey] = None + # Certificate chain for sk_oce_ecka, leaf-last order + certificates: Sequence[x509.Certificate] = field(default_factory=list)
+ + + +SendApdu = Callable[[int, int, int, int, bytes], bytes] + + +
+[docs] +class ScpState: + def __init__( + self, + session_keys: SessionKeys, + mac_chain: bytes = b"\0" * 16, + enc_counter: int = 1, + ): + self._keys = session_keys + self._mac_chain = mac_chain + self._enc_counter = enc_counter + +
+[docs] + def encrypt(self, data: bytes) -> bytes: + # Pad the data + msg = data + padlen = 15 - len(msg) % 16 + msg += b"\x80" + msg = msg.ljust(len(msg) + padlen, b"\0") + + # Encrypt + cipher = _init_cipher(self._keys.key_senc, self._enc_counter) + encryptor = cipher.encryptor() + encrypted = encryptor.update(msg) + encryptor.finalize() + self._enc_counter += 1 + return encrypted
+ + +
+[docs] + def mac(self, data: bytes) -> bytes: + next_mac_chain, mac = _calculate_mac(self._keys.key_smac, self._mac_chain, data) + self._mac_chain = next_mac_chain + return mac
+ + +
+[docs] + def unmac(self, data: bytes, sw: int) -> bytes: + msg, mac = data[:-8], data[-8:] + rmac = _calculate_mac( + self._keys.key_srmac, self._mac_chain, msg + struct.pack("!H", sw) + )[1] + if not constant_time.bytes_eq(mac, rmac): + raise BadResponseError("Wrong MAC") + return msg
+ + +
+[docs] + def decrypt(self, encrypted: bytes) -> bytes: + # Decrypt + cipher = _init_cipher(self._keys.key_senc, self._enc_counter - 1, True) + decryptor = cipher.decryptor() + decrypted = decryptor.update(encrypted) + decryptor.finalize() + + # Unpad + unpadded = decrypted.rstrip(b"\x00") + if unpadded[-1] != 0x80: + raise BadResponseError("Wrong padding") + unpadded = unpadded[:-1] + + return unpadded
+ + +
+[docs] + @classmethod + def scp03_init( + cls, + send_apdu: SendApdu, + key_params: Scp03KeyParams, + *, + host_challenge: Optional[bytes] = None, + ) -> Tuple["ScpState", bytes]: + logger.debug("Initializing SCP03 handshake") + host_challenge = host_challenge or os.urandom(8) + resp = send_apdu( + 0x80, INS_INITIALIZE_UPDATE, key_params.ref.kvn, 0x00, host_challenge + ) + + diversification_data = resp[:10] # noqa: unused + key_info = resp[10:13] # noqa: unused + card_challenge = resp[13:21] + card_cryptogram = resp[21:29] + + context = host_challenge + card_challenge + session_keys = key_params.keys.derive(context) + + gen_card_crypto = _derive( + session_keys.key_smac, _CARD_CRYPTOGRAM, context, 0x40 + ) + if not constant_time.bytes_eq(gen_card_crypto, card_cryptogram): + # This means wrong keys + raise BadResponseError("Wrong SCP03 key set") + + host_cryptogram = _derive( + session_keys.key_smac, _HOST_CRYPTOGRAM, context, 0x40 + ) + + return cls(session_keys), host_cryptogram
+ + +
+[docs] + @classmethod + def scp11_init( + cls, + send_apdu: SendApdu, + key_params: Scp11KeyParams, + ) -> "ScpState": + kid = ScpKid(key_params.ref.kid) + logger.debug(f"Initializing {kid.name} handshake") + + # GPC v2.3 Amendment F (SCP11) v1.4 §7.1.1 + if kid == ScpKid.SCP11a: + params = 0b01 + elif kid == ScpKid.SCP11b: + params = 0b00 + elif kid == ScpKid.SCP11c: + params = 0b11 + else: + raise ValueError("Invalid SCP KID") + + if kid in (ScpKid.SCP11a, ScpKid.SCP11c): + # GPC v2.3 Amendment F (SCP11) v1.4 §7.5 + assert key_params.sk_oce_ecka # nosec + n = len(key_params.certificates) - 1 + assert n >= 0 # nosec + oce_ref = key_params.oce_ref or KeyRef(0, 0) + logger.debug("Sending certificate chain") + for i, cert in enumerate(key_params.certificates): + p2 = oce_ref.kid | (0x80 if i < n else 0) + data = cert.public_bytes(serialization.Encoding.DER) + logger.debug(f"Sending cert: {cert.subject}") + send_apdu(0x80, INS_PERFORM_SECURITY_OPERATION, oce_ref.kvn, p2, data) + + key_usage = bytes( + [0x3C] + ) # AUTHENTICATED | C_MAC | C_DECRYPTION | R_MAC | R_ENCRYPTION + key_type = bytes([0x88]) # AES + key_len = bytes([16]) # 128-bit + + # Host ephemeral key + esk_oce_ecka = ec.generate_private_key(key_params.pk_sd_ecka.curve) + epk_oce_ecka = esk_oce_ecka.public_key() + + # GPC v2.3 Amendment F (SCP11) v1.4 §7.6.2.3 + data = Tlv( + 0xA6, + Tlv(0x90, bytes([0x11, params])) + + Tlv(0x95, key_usage) + + Tlv(0x80, key_type) + + Tlv(0x81, key_len), + ) + Tlv( + 0x5F49, + epk_oce_ecka.public_bytes( + serialization.Encoding.X962, + serialization.PublicFormat.UncompressedPoint, + ), + ) + + # Static host key (SCP11a/c), or ephemeral key again (SCP11b) + sk_oce_ecka = key_params.sk_oce_ecka or esk_oce_ecka + + logger.debug("Performing key agreement") + ins = ( + INS_INTERNAL_AUTHENTICATE + if key_params.ref.kid == ScpKid.SCP11b + else INS_EXTERNAL_AUTHENTICATE + ) + resp = send_apdu(0x80, ins, key_params.ref.kvn, key_params.ref.kid, data) + + epk_sd_ecka_tlv, resp = Tlv.parse_from(resp) + epk_sd_ecka = Tlv.unpack(0x5F49, epk_sd_ecka_tlv) + receipt = Tlv.unpack(0x86, resp) + + # GPC v2.3 Amendment F (SCP11) v1.3 §3.1.2 Key Derivation + key_agreement_data = data + epk_sd_ecka_tlv + sharedinfo = key_usage + key_type + key_len + keys = X963KDF(hashes.SHA256(), 5 * key_len[0], sharedinfo).derive( + esk_oce_ecka.exchange( + ec.ECDH(), + ec.EllipticCurvePublicKey.from_encoded_point( + sk_oce_ecka.curve, epk_sd_ecka + ), + ) + + sk_oce_ecka.exchange(ec.ECDH(), key_params.pk_sd_ecka) + ) + + # 5 keys were derived, one for verification of receipt + ln = key_len[0] + keys = [keys[i : i + ln] for i in range(0, ln * 5, ln)] + c = cmac.CMAC(algorithms.AES(keys.pop(0))) + c.update(key_agreement_data) + c.verify(receipt) + # The 4 remaining keys are session keys + session_keys = SessionKeys(*keys) + + return cls(session_keys, receipt)
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/static/yubikey-manager/API_Documentation/_modules/yubikit/hsmauth.html b/static/yubikey-manager/API_Documentation/_modules/yubikit/hsmauth.html index 87e5514a..30b49c47 100644 --- a/static/yubikey-manager/API_Documentation/_modules/yubikit/hsmauth.html +++ b/static/yubikey-manager/API_Documentation/_modules/yubikit/hsmauth.html @@ -3,7 +3,7 @@ - yubikit.hsmauth — yubikey-manager 5.4.0 documentation + yubikit.hsmauth — yubikey-manager 5.5.0 documentation @@ -13,9 +13,9 @@ - - - + + + @@ -33,7 +33,7 @@ yubikey-manager
- 5.4 + 5.5
@@ -108,7 +108,14 @@

Source code for yubikit.hsmauth

     Tlv,
     InvalidPinError,
 )
-from .core.smartcard import AID, SmartCardConnection, SmartCardProtocol, ApduError, SW
+from .core.smartcard import (
+    AID,
+    SmartCardConnection,
+    SmartCardProtocol,
+    ApduError,
+    SW,
+    ScpKeyParams,
+)
 
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives import hashes
@@ -167,7 +174,9 @@ 

Source code for yubikit.hsmauth

 INITIAL_RETRY_COUNTER = 8
 
 
-
[docs]@unique +
+[docs] +@unique class ALGORITHM(IntEnum): """Algorithms for YubiHSM Auth credentials.""" @@ -187,6 +196,7 @@

Source code for yubikit.hsmauth

             return 64
+ def _parse_credential_password(credential_password: Union[bytes, str]) -> bytes: if isinstance(credential_password, str): pw = credential_password.encode().ljust(CREDENTIAL_PASSWORD_LEN, b"\0") @@ -243,7 +253,9 @@

Source code for yubikit.hsmauth

     return None
 
 
-
[docs]@total_ordering +
+[docs] +@total_ordering @dataclass(order=False, frozen=True) class Credential: """A YubiHSM Auth credential object.""" @@ -265,14 +277,19 @@

Source code for yubikit.hsmauth

         return hash(self.label)
-
[docs]class SessionKeys(NamedTuple): + +
+[docs] +class SessionKeys(NamedTuple): """YubiHSM Session Keys.""" key_senc: bytes key_smac: bytes key_srmac: bytes -
[docs] @classmethod +
+[docs] + @classmethod def parse(cls, response: bytes) -> "SessionKeys": key_senc = response[:16] key_smac = response[16:32] @@ -282,27 +299,44 @@

Source code for yubikit.hsmauth

             key_senc=key_senc,
             key_smac=key_smac,
             key_srmac=key_srmac,
-        )
+ )
+
+ -
[docs]class HsmAuthSession: +
+[docs] +class HsmAuthSession: """A session with the YubiHSM Auth application.""" - def __init__(self, connection: SmartCardConnection) -> None: + def __init__( + self, + connection: SmartCardConnection, + scp_key_params: Optional[ScpKeyParams] = None, + ) -> None: self.protocol = SmartCardProtocol(connection) self._version = _parse_select(self.protocol.select(AID.HSMAUTH)) + if scp_key_params: + self.protocol.init_scp(scp_key_params) + self.protocol.configure(self._version) + @property def version(self) -> Version: """The YubiHSM Auth application version.""" return self._version -
[docs] def reset(self) -> None: +
+[docs] + def reset(self) -> None: """Perform a factory reset on the YubiHSM Auth application.""" self.protocol.send_apdu(0, INS_RESET, 0xDE, 0xAD) logger.info("YubiHSM Auth application data reset performed")
-
[docs] def list_credentials(self) -> List[Credential]: + +
+[docs] + def list_credentials(self) -> List[Credential]: """List YubiHSM Auth credentials on YubiKey""" creds = [] @@ -317,6 +351,7 @@

Source code for yubikit.hsmauth

             creds.append(Credential(label, algorithm, counter, touch_required))
         return creds
+ def _put_credential( self, management_key: bytes, @@ -369,7 +404,9 @@

Source code for yubikit.hsmauth

 
         return Credential(label, algorithm, INITIAL_RETRY_COUNTER, touch_required)
 
-
[docs] def put_credential_symmetric( +
+[docs] + def put_credential_symmetric( self, management_key: bytes, label: str, @@ -404,7 +441,10 @@

Source code for yubikit.hsmauth

             touch_required,
         )
-
[docs] def put_credential_derived( + +
+[docs] + def put_credential_derived( self, management_key: bytes, label: str, @@ -428,7 +468,10 @@

Source code for yubikit.hsmauth

             management_key, label, key_enc, key_mac, credential_password, touch_required
         )
-
[docs] def put_credential_asymmetric( + +
+[docs] + def put_credential_asymmetric( self, management_key: bytes, label: str, @@ -463,7 +506,10 @@

Source code for yubikit.hsmauth

             touch_required,
         )
-
[docs] def generate_credential_asymmetric( + +
+[docs] + def generate_credential_asymmetric( self, management_key: bytes, label: str, @@ -492,7 +538,10 @@

Source code for yubikit.hsmauth

             touch_required,
         )
-
[docs] def get_public_key(self, label: str) -> ec.EllipticCurvePublicKey: + +
+[docs] + def get_public_key(self, label: str) -> ec.EllipticCurvePublicKey: """Get the public key for an asymmetric credential. This will return the long-term public key "PK-OCE" for an @@ -506,7 +555,10 @@

Source code for yubikit.hsmauth

 
         return ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256R1(), res)
-
[docs] def delete_credential(self, management_key: bytes, label: str) -> None: + +
+[docs] + def delete_credential(self, management_key: bytes, label: str) -> None: """Delete a YubiHSM Auth credential. :param management_key: The management key. @@ -534,7 +586,10 @@

Source code for yubikit.hsmauth

                 message=f"Invalid management key, {retries} attempts remaining",
             )
-
[docs] def put_management_key( + +
+[docs] + def put_management_key( self, management_key: bytes, new_management_key: bytes, @@ -569,12 +624,16 @@

Source code for yubikit.hsmauth

                 message=f"Invalid management key, {retries} attempts remaining",
             )
-
[docs] def get_management_key_retries(self) -> int: + +
+[docs] + def get_management_key_retries(self) -> int: """Get retries remaining for Management key""" res = self.protocol.send_apdu(0, INS_GET_MANAGEMENT_KEY_RETRIES, 0, 0) return bytes2int(res)
+ def _calculate_session_keys( self, label: str, @@ -609,7 +668,9 @@

Source code for yubikit.hsmauth

 
         return res
 
-
[docs] def calculate_session_keys_symmetric( +
+[docs] + def calculate_session_keys_symmetric( self, label: str, context: bytes, @@ -634,7 +695,10 @@

Source code for yubikit.hsmauth

             )
         )
-
[docs] def calculate_session_keys_asymmetric( + +
+[docs] + def calculate_session_keys_asymmetric( self, label: str, context: bytes, @@ -674,17 +738,33 @@

Source code for yubikit.hsmauth

             )
         )
-
[docs] def get_challenge(self, label: str) -> bytes: + +
+[docs] + def get_challenge( + self, label: str, credential_password: Union[bytes, str, None] = None + ) -> bytes: """Get the Host Challenge. - For symmetric credentials this is Host Challenge, a random - 8 byte value. For asymmetric credentials this is EPK-OCE. + For symmetric credentials this is Host Challenge, a random 8 byte value. + For asymmetric credentials this is EPK-OCE. :param label: The label of the credential. + :param credential_password: The password used to protect access to the + credential, needed for asymmetric credentials. """ require_version(self.version, (5, 6, 0)) - data = Tlv(TAG_LABEL, _parse_label(label)) - return self.protocol.send_apdu(0, INS_GET_CHALLENGE, 0, 0, data)
+ + data: bytes = Tlv(TAG_LABEL, _parse_label(label)) + + if credential_password is not None and self.version >= (5, 7, 1): + data += Tlv( + TAG_CREDENTIAL_PASSWORD, _parse_credential_password(credential_password) + ) + + return self.protocol.send_apdu(0, INS_GET_CHALLENGE, 0, 0, data)
+
+
diff --git a/static/yubikey-manager/API_Documentation/_modules/yubikit/logging.html b/static/yubikey-manager/API_Documentation/_modules/yubikit/logging.html index 4490f324..dfed99f5 100644 --- a/static/yubikey-manager/API_Documentation/_modules/yubikit/logging.html +++ b/static/yubikey-manager/API_Documentation/_modules/yubikit/logging.html @@ -3,7 +3,7 @@ - yubikit.logging — yubikey-manager 5.4.0 documentation + yubikit.logging — yubikey-manager 5.5.0 documentation @@ -13,9 +13,9 @@ - - - + + + @@ -33,7 +33,7 @@ yubikey-manager
- 5.4 + 5.5
@@ -104,7 +104,9 @@

Source code for yubikit.logging

 import logging
 
 
-
[docs]@unique +
+[docs] +@unique class LOG_LEVEL(IntEnum): ERROR = logging.ERROR WARNING = logging.WARNING @@ -112,6 +114,7 @@

Source code for yubikit.logging

     DEBUG = logging.DEBUG
     TRAFFIC = 5  # Used for logging YubiKey traffic
     NOTSET = logging.NOTSET
+
diff --git a/static/yubikey-manager/API_Documentation/_modules/yubikit/management.html b/static/yubikey-manager/API_Documentation/_modules/yubikit/management.html index 665cf835..ecc056dc 100644 --- a/static/yubikey-manager/API_Documentation/_modules/yubikit/management.html +++ b/static/yubikey-manager/API_Documentation/_modules/yubikit/management.html @@ -3,7 +3,7 @@ - yubikit.management — yubikey-manager 5.4.0 documentation + yubikit.management — yubikey-manager 5.5.0 documentation @@ -13,9 +13,9 @@ - - - + + + @@ -33,7 +33,7 @@ yubikey-manager
- 5.4 + 5.5
@@ -120,7 +120,7 @@

Source code for yubikit.management

     CommandRejectedError,
 )
 from .core.fido import FidoConnection
-from .core.smartcard import AID, SmartCardConnection, SmartCardProtocol
+from .core.smartcard import AID, SmartCardConnection, SmartCardProtocol, ScpKeyParams
 from fido2.hid import CAPABILITY as CTAP_CAPABILITY
 
 from enum import IntEnum, IntFlag, unique
@@ -128,12 +128,15 @@ 

Source code for yubikit.management

 from typing import Optional, Union, Mapping
 import abc
 import struct
+import warnings
 import logging
 
 logger = logging.getLogger(__name__)
 
 
-
[docs]@unique +
+[docs] +@unique class CAPABILITY(IntFlag): """YubiKey Application identifiers.""" @@ -149,8 +152,45 @@

Source code for yubikit.management

         name = "|".join(c.name or str(c) for c in CAPABILITY if c in self)
         return f"{name}: {hex(self)}"
 
+    @classmethod
+    def _from_fips(cls, fips: int) -> "CAPABILITY":
+        c = CAPABILITY(0)
+        if fips & (1 << 0):
+            c |= CAPABILITY.FIDO2
+        if fips & (1 << 1):
+            c |= CAPABILITY.PIV
+        if fips & (1 << 2):
+            c |= CAPABILITY.OPENPGP
+        if fips & (1 << 3):
+            c |= CAPABILITY.OATH
+        if fips & (1 << 4):
+            c |= CAPABILITY.HSMAUTH
+        return c
+
+    @classmethod
+    def _from_aid(cls, aid: AID) -> "CAPABILITY":
+        # TODO: match on prefix?
+        try:
+            return getattr(CAPABILITY, aid.name)
+        except AttributeError:
+            pass
+        if aid == AID.FIDO:
+            return CAPABILITY.FIDO2
+        raise ValueError("Unhandled AID")
+
     @property
     def display_name(self) -> str:
+        if self == 0:
+            return "None"
+        if f"{self:b}".count("1") > 1:
+            i = 1
+            names = []
+            while i < self:
+                if i & self:
+                    names.append(CAPABILITY(i).display_name)
+                i <<= 1
+            return ", ".join(names)
+
         if self == CAPABILITY.OTP:
             return "Yubico OTP"
         elif self == CAPABILITY.U2F:
@@ -159,10 +199,7 @@ 

Source code for yubikit.management

             return "OpenPGP"
         elif self == CAPABILITY.HSMAUTH:
             return "YubiHSM Auth"
-        # mypy bug?
-        return self.name or ", ".join(
-            c.display_name for c in CAPABILITY if c in self  # type: ignore
-        )
+        return self.name or f"Unknown(0x{self:x})"
 
     @property
     def usb_interfaces(self) -> USB_INTERFACE:
@@ -183,7 +220,10 @@ 

Source code for yubikit.management

         return ifaces
-
[docs]@unique + +
+[docs] +@unique class FORM_FACTOR(IntEnum): """YubiKey device form factors.""" @@ -214,15 +254,21 @@

Source code for yubikit.management

         else:
             return "Unknown"
 
-
[docs] @classmethod +
+[docs] + @classmethod def from_code(cls, code: int) -> "FORM_FACTOR": if code and not isinstance(code, int): raise ValueError(f"Invalid form factor code: {code}") code &= 0xF - return cls(code) if code in cls.__members__.values() else cls.UNKNOWN
+ return cls(code) if code in cls.__members__.values() else cls.UNKNOWN
+
+ -
[docs]@unique +
+[docs] +@unique class DEVICE_FLAG(IntFlag): """Configuration flags.""" @@ -230,6 +276,7 @@

Source code for yubikit.management

     EJECT = 0x80
+ TAG_USB_SUPPORTED = 0x01 TAG_SERIAL = 0x02 TAG_USB_ENABLED = 0x03 @@ -249,12 +296,18 @@

Source code for yubikit.management

 TAG_FREE_FORM = 0x11
 TAG_HID_INIT_DELAY = 0x12
 TAG_PART_NUMBER = 0x13
+TAG_FIPS_CAPABLE = 0x14
+TAG_FIPS_APPROVED = 0x15
 TAG_PIN_COMPLEXITY = 0x16
 TAG_NFC_RESTRICTED = 0x17
 TAG_RESET_BLOCKED = 0x18
+TAG_FPS_VERSION = 0x20
+TAG_STM_VERSION = 0x21
 
 
-
[docs]@dataclass +
+[docs] +@dataclass class DeviceConfig: """Management settings for YubiKey which can be configured by the user.""" @@ -264,7 +317,9 @@

Source code for yubikit.management

     device_flags: Optional[DEVICE_FLAG] = None
     nfc_restricted: Optional[bool] = None
 
-
[docs] def get_bytes( +
+[docs] + def get_bytes( self, reboot: bool, cur_lock_code: Optional[bytes] = None, @@ -289,14 +344,18 @@

Source code for yubikit.management

             buf += Tlv(TAG_DEVICE_FLAGS, int2bytes(self.device_flags))
         if new_lock_code:
             buf += Tlv(TAG_CONFIG_LOCK, new_lock_code)
-        if self.nfc_restricted is not None:
-            buf += Tlv(TAG_NFC_RESTRICTED, b"\1" if self.nfc_restricted else b"\0")
+        if self.nfc_restricted:
+            buf += Tlv(TAG_NFC_RESTRICTED, b"\1")
         if len(buf) > 0xFF:
             raise NotSupportedError("DeviceConfiguration too large")
-        return int2bytes(len(buf)) + buf
+ return int2bytes(len(buf)) + buf
+
-
[docs]@dataclass + +
+[docs] +@dataclass class DeviceInfo: """Information about a YubiKey readable using the ManagementSession.""" @@ -308,19 +367,36 @@

Source code for yubikit.management

     is_locked: bool
     is_fips: bool = False
     is_sky: bool = False
+    part_number: Optional[str] = None
+    fips_capable: CAPABILITY = CAPABILITY(0)
+    fips_approved: CAPABILITY = CAPABILITY(0)
     pin_complexity: bool = False
     reset_blocked: CAPABILITY = CAPABILITY(0)
+    fps_version: Optional[Version] = None
+    stm_version: Optional[Version] = None
+
+    @property
+    def _is_bio(self) -> bool:
+        return self.form_factor in (FORM_FACTOR.USB_A_BIO, FORM_FACTOR.USB_C_BIO)
 
-
[docs] def has_transport(self, transport: TRANSPORT) -> bool: +
+[docs] + def has_transport(self, transport: TRANSPORT) -> bool: return transport in self.supported_capabilities
-
[docs] @classmethod + +
+[docs] + @classmethod def parse(cls, encoded: bytes, default_version: Version) -> "DeviceInfo": if len(encoded) - 1 != encoded[0]: raise BadResponseError("Invalid length") return cls.parse_tlvs(Tlv.parse_dict(encoded[1:]), default_version)
-
[docs] @classmethod + +
+[docs] + @classmethod def parse_tlvs( cls, data: Mapping[int, bytes], default_version: Version ) -> "DeviceInfo": @@ -352,8 +428,20 @@

Source code for yubikit.management

             supported[TRANSPORT.NFC] = CAPABILITY(bytes2int(data[TAG_NFC_SUPPORTED]))
             enabled[TRANSPORT.NFC] = CAPABILITY(bytes2int(data[TAG_NFC_ENABLED]))
         nfc_restricted = data.get(TAG_NFC_RESTRICTED, b"\0") == b"\1"
+        try:
+            part_number = data.get(TAG_PART_NUMBER, b"").decode() or None
+        except UnicodeDecodeError:
+            part_number = None
+        fips_capable = CAPABILITY._from_fips(
+            bytes2int(data.get(TAG_FIPS_CAPABLE, b"\0"))
+        )
+        fips_approved = CAPABILITY._from_fips(
+            bytes2int(data.get(TAG_FIPS_APPROVED, b"\0"))
+        )
         pin_complexity = data.get(TAG_PIN_COMPLEXITY, b"\0") == b"\1"
         reset_blocked = CAPABILITY(bytes2int(data.get(TAG_RESET_BLOCKED, b"\0")))
+        fps_version = Version.from_bytes(data.get(TAG_FPS_VERSION, b"\0\0\0"))
+        stm_version = Version.from_bytes(data.get(TAG_STM_VERSION, b"\0\0\0"))
 
         return cls(
             DeviceConfig(enabled, auto_eject_to, chal_resp_to, flags, nfc_restricted),
@@ -364,9 +452,16 @@ 

Source code for yubikit.management

             locked,
             fips,
             sky,
+            part_number,
+            fips_capable,
+            fips_approved,
             pin_complexity,
             reset_blocked,
-        )
+ fps_version or None, + stm_version or None, + )
+
+ _MODES = [ @@ -380,7 +475,9 @@

Source code for yubikit.management

 ]
 
 
-
[docs]@dataclass(init=False, repr=False) +
+[docs] +@dataclass(init=False, repr=False) class Mode: """YubiKey USB Mode configuration for use with YubiKey NEO and 4.""" @@ -397,13 +494,17 @@

Source code for yubikit.management

     def __repr__(self):
         return "+".join(t.name or str(t) for t in USB_INTERFACE if t in self.interfaces)
 
-
[docs] @classmethod +
+[docs] + @classmethod def from_code(cls, code: int) -> "Mode": # Mode is determined from the lowest 3 bits try: return cls(_MODES[code & 0b00000111]) except IndexError: - raise ValueError("Invalid mode code")
+ raise ValueError("Invalid mode code")
+
+ SLOT_DEVICE_CONFIG = 0x11 @@ -471,19 +572,23 @@

Source code for yubikit.management

 
 
 class _ManagementSmartCardBackend(_Backend):
-    def __init__(self, smartcard_connection):
+    def __init__(self, smartcard_connection, scp_key_params):
         self.protocol = SmartCardProtocol(smartcard_connection)
         try:
             select_bytes = self.protocol.select(AID.MANAGEMENT)
-            if select_bytes[-2:] == b"\x90\x00":
+
+            if scp_key_params:
+                self.protocol.init_scp(scp_key_params)
+            elif select_bytes[-2:] == b"\x90\x00":
                 # YubiKey Edge incorrectly appends SW twice.
                 select_bytes = select_bytes[:-2]
+
             select_str = select_bytes.decode()
             self.version = Version.from_string(select_str)
             # For YubiKey NEO, we use the OTP application for further commands
             if self.version[0] == 3:
                 # Workaround to "de-select" on NEO, otherwise it gets stuck.
-                self.protocol.connection.send_and_receive(b"\xa4\x04\x00\x08")
+                smartcard_connection.send_and_receive(b"\xa4\x04\x00\x08")
                 self.protocol.select(AID.OTP)
         except ApplicationNotAvailableError:
             if smartcard_connection.transport == TRANSPORT.NFC:
@@ -492,6 +597,7 @@ 

Source code for yubikit.management

                 self.version = Version.from_bytes(status[:3])
             else:
                 raise
+        self.protocol.configure(self.version)
 
     def close(self):
         self.protocol.close()
@@ -542,15 +648,23 @@ 

Source code for yubikit.management

         self.ctap.call(CTAP_WRITE_CONFIG, config)
 
 
-
[docs]class ManagementSession: +
+[docs] +class ManagementSession: def __init__( - self, connection: Union[OtpConnection, SmartCardConnection, FidoConnection] + self, + connection: Union[OtpConnection, SmartCardConnection, FidoConnection], + scp_key_params: Optional[ScpKeyParams] = None, ): if isinstance(connection, OtpConnection): + if scp_key_params: + raise ValueError("SCP can only be used with SmartCardConnection") self.backend: _Backend = _ManagementOtpBackend(connection) elif isinstance(connection, SmartCardConnection): - self.backend = _ManagementSmartCardBackend(connection) + self.backend = _ManagementSmartCardBackend(connection, scp_key_params) elif isinstance(connection, FidoConnection): + if scp_key_params: + raise ValueError("SCP can only be used with SmartCardConnection") self.backend = _ManagementCtapBackend(connection) else: raise TypeError("Unsupported connection type") @@ -559,14 +673,28 @@

Source code for yubikit.management

             f"connection={type(connection).__name__}, version={self.version}"
         )
 
-
[docs] def close(self) -> None: +
+[docs] + 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 -
[docs] def read_device_info(self) -> DeviceInfo: +
+[docs] + def read_device_info(self) -> DeviceInfo: """Get detailed information about the YubiKey.""" require_version(self.version, (4, 1, 0)) more_data = True @@ -584,7 +712,10 @@

Source code for yubikit.management

 
         return DeviceInfo.parse_tlvs(tlvs, self.version)
-
[docs] def write_device_config( + +
+[docs] + def write_device_config( self, config: Optional[DeviceConfig] = None, reboot: bool = False, @@ -614,7 +745,10 @@

Source code for yubikit.management

         )
         logger.info("Device config written")
-
[docs] def set_mode( + +
+[docs] + def set_mode( self, mode: Mode, chalresp_timeout: int = 0, @@ -675,12 +809,23 @@

Source code for yubikit.management

             )
             logger.info("Mode configuration written")
-
[docs] def device_reset(self) -> None: + +
+[docs] + 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") self.backend.device_reset() - logger.info("Device reset performed")
+ logger.info("Device reset performed")
+
+
diff --git a/static/yubikey-manager/API_Documentation/_modules/yubikit/oath.html b/static/yubikey-manager/API_Documentation/_modules/yubikit/oath.html index 38bd8bfc..d8fa02b0 100644 --- a/static/yubikey-manager/API_Documentation/_modules/yubikit/oath.html +++ b/static/yubikey-manager/API_Documentation/_modules/yubikit/oath.html @@ -3,7 +3,7 @@ - yubikit.oath — yubikey-manager 5.4.0 documentation + yubikit.oath — yubikey-manager 5.5.0 documentation @@ -13,9 +13,9 @@ - - - + + + @@ -33,7 +33,7 @@ yubikey-manager
- 5.4 + 5.5
@@ -80,8 +80,9 @@

Source code for yubikit.oath

     Version,
     Tlv,
     BadResponseError,
+    NotSupportedError,
 )
-from .core.smartcard import AID, SmartCardConnection, SmartCardProtocol
+from .core.smartcard import AID, SmartCardConnection, SmartCardProtocol, ScpKeyParams
 
 from urllib.parse import unquote, urlparse, parse_qs
 from functools import total_ordering
@@ -138,23 +139,31 @@ 

Source code for yubikit.oath

 HMAC_MINIMUM_KEY_SIZE = 14
 
 
-
[docs]@unique +
+[docs] +@unique class HASH_ALGORITHM(IntEnum): SHA1 = 0x01 SHA256 = 0x02 SHA512 = 0x03
-
[docs]@unique + +
+[docs] +@unique class OATH_TYPE(IntEnum): HOTP = 0x10 TOTP = 0x20
+ PROP_REQUIRE_TOUCH = 0x02 -
[docs]def parse_b32_key(key: str): +
+[docs] +def parse_b32_key(key: str): """Parse Base32 encoded key. :param key: The Base32 encoded key. @@ -164,6 +173,7 @@

Source code for yubikit.oath

     return b32decode(key)
+ def _parse_select(response): data = Tlv.parse_dict(response) return ( @@ -173,7 +183,9 @@

Source code for yubikit.oath

     )
 
 
-
[docs]@dataclass +
+[docs] +@dataclass class CredentialData: """An object holding OATH credential data.""" @@ -186,7 +198,9 @@

Source code for yubikit.oath

     counter: int = DEFAULT_IMF
     issuer: Optional[str] = None
 
-
[docs] @classmethod +
+[docs] + @classmethod def parse_uri(cls, uri: str) -> "CredentialData": """Parse OATH credential data from URI. @@ -217,11 +231,18 @@

Source code for yubikit.oath

             issuer=params.get("issuer", issuer),
         )
-
[docs] def get_id(self) -> bytes: - return _format_cred_id(self.issuer, self.name, self.oath_type, self.period)
+
+[docs] + def get_id(self) -> bytes: + return _format_cred_id(self.issuer, self.name, self.oath_type, self.period)
+
-
[docs]@dataclass + + +
+[docs] +@dataclass class Code: """An OATH code object.""" @@ -230,7 +251,10 @@

Source code for yubikit.oath

     valid_to: int
-
[docs]@total_ordering + +
+[docs] +@total_ordering @dataclass(order=False, frozen=True) class Credential: """An OATH credential object.""" @@ -259,6 +283,7 @@

Source code for yubikit.oath

         return hash((self.device_id, self.id))
+ def _format_cred_id(issuer, name, oath_type, period=DEFAULT_PERIOD): cred_id = "" if oath_type == OATH_TYPE.TOTP and period != DEFAULT_PERIOD: @@ -334,18 +359,31 @@

Source code for yubikit.oath

     )
 
 
-
[docs]class OathSession: +
+[docs] +class OathSession: """A session with the OATH application.""" - def __init__(self, connection: SmartCardConnection): + def __init__( + self, + connection: SmartCardConnection, + scp_key_params: Optional[ScpKeyParams] = None, + ): self.protocol = SmartCardProtocol(connection, INS_SEND_REMAINING) self._version, self._salt, self._challenge = _parse_select( self.protocol.select(AID.OATH) ) + + if scp_key_params: + if (5, 0, 0) <= self._version < (5, 6, 3): + raise NotSupportedError("SCP for OATH requires YubiKey 5.6.3 or later") + self.protocol.init_scp(scp_key_params) + self._scp_params = scp_key_params + self._has_key = self._challenge is not None self._device_id = _get_device_id(self._salt) - self.protocol.enable_touch_workaround(self._version) - self._neo_unlock_workaround = self.version < (3, 0, 0) + self.protocol.configure(self.version) + self._neo_unlock_workaround = not scp_key_params and self.version < (3, 0, 0) logger.debug( f"OATH session initialized (version={self.version}, " f"has_key={self._has_key})" @@ -353,42 +391,57 @@

Source code for yubikit.oath

 
     @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
 
-
[docs] def reset(self) -> None: +
+[docs] + def reset(self) -> None: """Perform a factory reset on the OATH application.""" self.protocol.send_apdu(0, INS_RESET, 0xDE, 0xAD) _, self._salt, self._challenge = _parse_select(self.protocol.select(AID.OATH)) + if self._scp_params: + self.protocol.init_scp(self._scp_params) logger.info("OATH application data reset performed") self._has_key = False self._device_id = _get_device_id(self._salt)
-
[docs] def derive_key(self, password: str) -> bytes: - """Derive a key from password. + +
+[docs] + def derive_key(self, password: str) -> bytes: + """Derive an access key from a password. :param password: The derivation password. """ return _derive_key(self._salt, password)
-
[docs] def validate(self, key: bytes) -> None: + +
+[docs] + 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") @@ -404,8 +457,11 @@

Source code for yubikit.oath

         self._challenge = None
         self._neo_unlock_workaround = False
-
[docs] def set_key(self, key: bytes) -> None: - """Set access key for authentication. + +
+[docs] + def set_key(self, key: bytes) -> None: + """Set an access key for authentication. :param key: The access key. """ @@ -429,19 +485,25 @@

Source code for yubikit.oath

             self._challenge = _parse_select(self.protocol.select(AID.OATH))[2]
             self.validate(key)
-
[docs] def unset_key(self) -> None: - """Remove access code. - WARNING: This removes authentication. +
+[docs] + def unset_key(self) -> None: + """Remove the access key. + + 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") self._has_key = False
-
[docs] def put_credential( + +
+[docs] + 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. @@ -479,7 +541,10 @@

Source code for yubikit.oath

             touch_required,
         )
-
[docs] def rename_credential( + +
+[docs] + def rename_credential( self, credential_id: bytes, name: str, issuer: Optional[str] = None ) -> bytes: """Rename a OATH credential. @@ -488,6 +553,7 @@

Source code for yubikit.oath

         :param name: The new name of the credential.
         :param issuer: The credential issuer.
         """
+        logger.debug(f"Renaming credential '{credential_id!r}' to '{issuer}:{name}'")
         require_version(self.version, (5, 3, 1))
         _, _, period = _parse_cred_id(credential_id, OATH_TYPE.TOTP)
         new_id = _format_cred_id(issuer, name, OATH_TYPE.TOTP, period)
@@ -497,8 +563,12 @@ 

Source code for yubikit.oath

         logger.info("Credential renamed")
         return new_id
-
[docs] def list_credentials(self) -> List[Credential]: + +
+[docs] + def list_credentials(self) -> List[Credential]: """List OATH credentials.""" + logger.debug("Listing OATH credentials...") creds = [] for tlv in Tlv.parse_list(self.protocol.send_apdu(0, INS_LIST, 0, 0)): data = Tlv.unpack(TAG_NAME_LIST, tlv) @@ -512,12 +582,16 @@

Source code for yubikit.oath

             )
         return creds
-
[docs] def calculate(self, credential_id: bytes, challenge: bytes) -> bytes: + +
+[docs] + def calculate(self, credential_id: bytes, challenge: bytes) -> bytes: """Perform a calculate for an OATH credential. :param credential_id: The id of the credential. :param challenge: The challenge. """ + logger.debug(f"Calculating response for credential: {credential_id!r}") resp = Tlv.unpack( TAG_RESPONSE, self.protocol.send_apdu( @@ -530,20 +604,29 @@

Source code for yubikit.oath

         )
         return resp[1:]
-
[docs] def delete_credential(self, credential_id: bytes) -> None: + +
+[docs] + def delete_credential(self, credential_id: bytes) -> None: """Delete an OATH credential. :param credential_id: The id of the credential. """ + logger.debug(f"Deleting crededential: {credential_id!r}") self.protocol.send_apdu(0, INS_DELETE, 0, 0, Tlv(TAG_NAME, credential_id)) logger.info("Credential deleted")
-
[docs] def calculate_all( + +
+[docs] + def calculate_all( self, timestamp: Optional[int] = None ) -> 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) @@ -579,7 +662,10 @@

Source code for yubikit.oath

 
         return entries
-
[docs] def calculate_code( + +
+[docs] + def calculate_code( self, credential: Credential, timestamp: Optional[int] = None ) -> Code: """Calculate code for an OATH credential. @@ -611,7 +697,9 @@

Source code for yubikit.oath

                 Tlv(TAG_NAME, credential.id) + Tlv(TAG_CHALLENGE, challenge),
             ),
         )
-        return _format_code(credential, timestamp, response)
+ return _format_code(credential, timestamp, response)
+
+
diff --git a/static/yubikey-manager/API_Documentation/_modules/yubikit/openpgp.html b/static/yubikey-manager/API_Documentation/_modules/yubikit/openpgp.html index 4b375e9a..6927f5ef 100644 --- a/static/yubikey-manager/API_Documentation/_modules/yubikit/openpgp.html +++ b/static/yubikey-manager/API_Documentation/_modules/yubikit/openpgp.html @@ -3,7 +3,7 @@ - yubikit.openpgp — yubikey-manager 5.4.0 documentation + yubikit.openpgp — yubikey-manager 5.5.0 documentation @@ -13,9 +13,9 @@ - - - + + + @@ -33,7 +33,7 @@ yubikey-manager
- 5.4 + 5.5
@@ -112,10 +112,10 @@

Source code for yubikit.openpgp

 from .core.smartcard import (
     SmartCardConnection,
     SmartCardProtocol,
-    ApduFormat,
     ApduError,
     AID,
     SW,
+    ScpKeyParams,
 )
 
 from cryptography import x509
@@ -157,7 +157,9 @@ 

Source code for yubikit.openpgp

 DEFAULT_ADMIN_PIN = "12345678"
 
 
-
[docs]@unique +
+[docs] +@unique class UIF(IntEnum): # noqa: N801 OFF = 0x00 ON = 0x01 @@ -165,10 +167,13 @@

Source code for yubikit.openpgp

     CACHED = 0x03
     CACHED_FIXED = 0x04
 
-
[docs] @classmethod +
+[docs] + @classmethod def parse(cls, encoded: bytes): return cls(encoded[0])
+ def __bytes__(self) -> bytes: return struct.pack(">BB", self, GENERAL_FEATURE_MANAGEMENT.BUTTON) @@ -188,7 +193,10 @@

Source code for yubikit.openpgp

         return self.name[0] + self.name[1:].lower()
-
[docs]@unique + +
+[docs] +@unique class PIN_POLICY(IntEnum): # noqa: N801 ALWAYS = 0x00 ONCE = 0x01 @@ -197,7 +205,10 @@

Source code for yubikit.openpgp

         return self.name[0] + self.name[1:].lower()
-
[docs]@unique + +
+[docs] +@unique class INS(IntEnum): # noqa: N801 VERIFY = 0x20 CHANGE_PIN = 0x24 @@ -217,6 +228,7 @@

Source code for yubikit.openpgp

     GET_ATTESTATION = 0xFB
+ _INVALID_PIN = b"\0" * 8 @@ -230,14 +242,19 @@

Source code for yubikit.openpgp

 TAG_PUBLIC_KEY = 0x7F49
 
 
-
[docs]@unique +
+[docs] +@unique class PW(IntEnum): USER = 0x81 RESET = 0x82 ADMIN = 0x83
-
[docs]@unique + +
+[docs] +@unique class DO(IntEnum): PRIVATE_USE_1 = 0x0101 PRIVATE_USE_2 = 0x0102 @@ -283,11 +300,14 @@

Source code for yubikit.openpgp

     ATT_CERTIFICATE = 0xFC
+ def _bcd(value: int) -> int: return 10 * (value >> 4) + (value & 0xF) -
[docs]class OpenPgpAid(bytes): +
+[docs] +class OpenPgpAid(bytes): """OpenPGP Application Identifier (AID) The OpenPGP AID is a string of bytes identifying the OpenPGP application. @@ -322,7 +342,10 @@

Source code for yubikit.openpgp

             return -struct.unpack(">I", self[10:14])[0]
-
[docs]@unique + +
+[docs] +@unique class EXTENDED_CAPABILITY_FLAGS(IntFlag): KDF = 1 << 0 PSO_DEC_ENC_AES = 1 << 1 @@ -334,37 +357,52 @@

Source code for yubikit.openpgp

     SECURE_MESSAGING = 1 << 7
-
[docs]@dataclass + +
+[docs] +@dataclass class CardholderRelatedData: name: bytes language: bytes sex: int -
[docs] @classmethod +
+[docs] + @classmethod def parse(cls, encoded) -> "CardholderRelatedData": data = Tlv.parse_dict(Tlv.unpack(DO.CARDHOLDER_RELATED_DATA, encoded)) return cls( data[DO.NAME], data[DO.LANGUAGE], data[DO.SEX][0], - )
+ )
+
-
[docs]@dataclass + +
+[docs] +@dataclass class ExtendedLengthInfo: request_max_bytes: int response_max_bytes: int -
[docs] @classmethod +
+[docs] + @classmethod def parse(cls, encoded) -> "ExtendedLengthInfo": data = Tlv.parse_list(encoded) return cls( bytes2int(Tlv.unpack(0x02, data[0])), bytes2int(Tlv.unpack(0x02, data[1])), - )
+ )
+
+ -
[docs]@unique +
+[docs] +@unique class GENERAL_FEATURE_MANAGEMENT(IntFlag): TOUCHSCREEN = 1 << 0 MICROPHONE = 1 << 1 @@ -376,7 +414,10 @@

Source code for yubikit.openpgp

     DISPLAY = 1 << 7
-
[docs]@dataclass + +
+[docs] +@dataclass class ExtendedCapabilities: flags: EXTENDED_CAPABILITY_FLAGS sm_algorithm: int @@ -386,7 +427,9 @@

Source code for yubikit.openpgp

     pin_block_2_format: bool
     mse_command: bool
 
-
[docs] @classmethod +
+[docs] + @classmethod def parse(cls, encoded: bytes) -> "ExtendedCapabilities": return cls( EXTENDED_CAPABILITY_FLAGS(encoded[0]), @@ -396,10 +439,14 @@

Source code for yubikit.openpgp

             bytes2int(encoded[6:8]),
             encoded[8] == 1,
             encoded[9] == 1,
-        )
+ )
+
-
[docs]@dataclass + +
+[docs] +@dataclass class PwStatus: pin_policy_user: PIN_POLICY max_len_user: int @@ -409,13 +456,21 @@

Source code for yubikit.openpgp

     attempts_reset: int
     attempts_admin: int
 
-
[docs] def get_max_len(self, pw: PW) -> int: +
+[docs] + def get_max_len(self, pw: PW) -> int: return getattr(self, f"max_len_{pw.name.lower()}")
-
[docs] def get_attempts(self, pw: PW) -> int: + +
+[docs] + def get_attempts(self, pw: PW) -> int: return getattr(self, f"attempts_{pw.name.lower()}")
-
[docs] @classmethod + +
+[docs] + @classmethod def parse(cls, encoded: bytes) -> "PwStatus": try: policy = PIN_POLICY(encoded[0]) @@ -429,10 +484,14 @@

Source code for yubikit.openpgp

             encoded[4],
             encoded[5],
             encoded[6],
-        )
+ )
+
-
[docs]@unique + +
+[docs] +@unique class CRT(bytes, Enum): """Control Reference Template values.""" @@ -442,7 +501,10 @@

Source code for yubikit.openpgp

     ATT = Tlv(0xB6, Tlv(0x84, b"\x81"))
-
[docs]@unique + +
+[docs] +@unique class KEY_REF(IntEnum): # noqa: N801 SIG = 0x01 DEC = 0x02 @@ -470,13 +532,17 @@

Source code for yubikit.openpgp

         return getattr(CRT, self.name)
-
[docs]@unique + +
+[docs] +@unique class KEY_STATUS(IntEnum): NONE = 0 GENERATED = 1 IMPORTED = 2
+ KeyInformation = Mapping[KEY_REF, KEY_STATUS] Fingerprints = Mapping[KEY_REF, bytes] GenerationTimes = Mapping[KEY_REF, int] @@ -498,14 +564,18 @@

Source code for yubikit.openpgp

 
 
 # mypy doesn't handle abstract dataclasses well
-
[docs]@dataclass # type: ignore[misc] +
+[docs] +@dataclass # type: ignore[misc] class AlgorithmAttributes(abc.ABC): """OpenPGP key algorithm attributes.""" _supported_ids: ClassVar[Sequence[int]] algorithm_id: int -
[docs] @classmethod +
+[docs] + @classmethod def parse(cls, encoded: bytes) -> "AlgorithmAttributes": algorithm_id = encoded[0] for sub_cls in cls.__subclasses__(): @@ -513,6 +583,7 @@

Source code for yubikit.openpgp

                 return sub_cls._parse_data(algorithm_id, encoded[1:])
         raise ValueError("Unsupported algorithm ID")
+ @abc.abstractmethod def __bytes__(self) -> bytes: raise NotImplementedError() @@ -523,14 +594,20 @@

Source code for yubikit.openpgp

         raise NotImplementedError()
-
[docs]@unique + +
+[docs] +@unique class RSA_SIZE(IntEnum): RSA2048 = 2048 RSA3072 = 3072 RSA4096 = 4096
-
[docs]@unique + +
+[docs] +@unique class RSA_IMPORT_FORMAT(IntEnum): STANDARD = 0 STANDARD_W_MOD = 1 @@ -538,7 +615,10 @@

Source code for yubikit.openpgp

     CRT_W_MOD = 3
-
[docs]@dataclass + +
+[docs] +@dataclass class RsaAttributes(AlgorithmAttributes): _supported_ids = [0x01] @@ -546,7 +626,9 @@

Source code for yubikit.openpgp

     e_len: int
     import_format: RSA_IMPORT_FORMAT
 
-
[docs] @classmethod +
+[docs] + @classmethod def create( cls, n_len: RSA_SIZE, @@ -554,6 +636,7 @@

Source code for yubikit.openpgp

     ) -> "RsaAttributes":
         return cls(0x01, n_len, 17, import_format)
+ @classmethod def _parse_data(cls, alg, encoded) -> "RsaAttributes": n, e, f = struct.unpack(">HHB", encoded) @@ -565,7 +648,10 @@

Source code for yubikit.openpgp

         )
-
[docs]class CurveOid(bytes): + +
+[docs] +class CurveOid(bytes): def _get_name(self) -> str: for oid in OID: if self.startswith(oid): @@ -580,7 +666,10 @@

Source code for yubikit.openpgp

         return f"{name}({self.hex()})"
-
[docs]class OID(CurveOid, Enum): + +
+[docs] +class OID(CurveOid, Enum): SECP256R1 = CurveOid(b"\x2a\x86\x48\xce\x3d\x03\x01\x07") SECP256K1 = CurveOid(b"\x2b\x81\x04\x00\x0a") SECP384R1 = CurveOid(b"\x2b\x81\x04\x00\x22") @@ -613,20 +702,28 @@

Source code for yubikit.openpgp

         return str(self.value)
-
[docs]@unique + +
+[docs] +@unique class EC_IMPORT_FORMAT(IntEnum): STANDARD = 0 STANDARD_W_PUBKEY = 0xFF
-
[docs]@dataclass + +
+[docs] +@dataclass class EcAttributes(AlgorithmAttributes): _supported_ids = [0x12, 0x13, 0x16] oid: CurveOid import_format: EC_IMPORT_FORMAT -
[docs] @classmethod +
+[docs] + @classmethod def create(cls, key_ref: KEY_REF, oid: CurveOid) -> "EcAttributes": if oid == OID.Ed25519: alg = 0x16 # EdDSA @@ -636,6 +733,7 @@

Source code for yubikit.openpgp

             alg = 0x13  # ECDSA
         return cls(alg, oid, EC_IMPORT_FORMAT.STANDARD)
+ @classmethod def _parse_data(cls, alg, encoded) -> "EcAttributes": if encoded[-1] == 0xFF: @@ -654,6 +752,7 @@

Source code for yubikit.openpgp

         return buf
+ def _parse_key_information(encoded: bytes) -> KeyInformation: return { KEY_REF(encoded[i]): KEY_STATUS(encoded[i + 1]) @@ -676,7 +775,9 @@

Source code for yubikit.openpgp

     }
 
 
-
[docs]@dataclass +
+[docs] +@dataclass class DiscretionaryDataObjects: extended_capabilities: ExtendedCapabilities attributes_sig: AlgorithmAttributes @@ -693,7 +794,9 @@

Source code for yubikit.openpgp

     uif_aut: Optional[UIF]
     uif_att: Optional[UIF]
 
-
[docs] @classmethod +
+[docs] + @classmethod def parse(cls, encoded: bytes) -> "DiscretionaryDataObjects": data = Tlv.parse_dict(encoded) return cls( @@ -717,11 +820,18 @@

Source code for yubikit.openpgp

             (UIF.parse(data[DO.UIF_ATT]) if DO.UIF_ATT in data else None),
         )
-
[docs] def get_algorithm_attributes(self, key_ref: KEY_REF) -> AlgorithmAttributes: - return getattr(self, f"attributes_{key_ref.name.lower()}")
+ +
+[docs] + def get_algorithm_attributes(self, key_ref: KEY_REF) -> AlgorithmAttributes: + return getattr(self, f"attributes_{key_ref.name.lower()}")
+
+ -
[docs]@dataclass +
+[docs] +@dataclass class ApplicationRelatedData: """OpenPGP related data.""" @@ -731,7 +841,9 @@

Source code for yubikit.openpgp

     general_feature_management: Optional[GENERAL_FEATURE_MANAGEMENT]
     discretionary: DiscretionaryDataObjects
 
-
[docs] @classmethod +
+[docs] + @classmethod def parse(cls, encoded: bytes) -> "ApplicationRelatedData": outer = Tlv.unpack(DO.APPLICATION_RELATED_DATA, encoded) data = Tlv.parse_dict(outer) @@ -752,34 +864,49 @@

Source code for yubikit.openpgp

             ),
             # Older keys have data in outer dict
             DiscretionaryDataObjects.parse(data[TAG_DISCRETIONARY] or outer),
-        )
+ )
+
+ -
[docs]@dataclass +
+[docs] +@dataclass class SecuritySupportTemplate: signature_counter: int -
[docs] @classmethod +
+[docs] + @classmethod def parse(cls, encoded: bytes) -> "SecuritySupportTemplate": data = Tlv.parse_dict(Tlv.unpack(DO.SECURITY_SUPPORT_TEMPLATE, encoded)) - return cls(bytes2int(data[TAG_SIGNATURE_COUNTER]))
+ return cls(bytes2int(data[TAG_SIGNATURE_COUNTER]))
+
+ # mypy doesn't handle abstract dataclasses well -
[docs]@dataclass # type: ignore[misc] +
+[docs] +@dataclass # type: ignore[misc] class Kdf(abc.ABC): algorithm: ClassVar[int] -
[docs] @abc.abstractmethod +
+[docs] + @abc.abstractmethod def process(self, pw: PW, pin: str) -> bytes: """Run the KDF on the input PIN."""
+ @classmethod @abc.abstractmethod def _parse_data(cls, data: Mapping[int, bytes]) -> "Kdf": raise NotImplementedError() -
[docs] @classmethod +
+[docs] + @classmethod def parse(cls, encoded: bytes) -> "Kdf": data = Tlv.parse_dict(encoded) try: @@ -791,12 +918,16 @@

Source code for yubikit.openpgp

             pass  # Fall though to KdfNone
         return KdfNone()
+ @abc.abstractmethod def __bytes__(self) -> bytes: raise NotImplementedError()
-
[docs]@dataclass + +
+[docs] +@dataclass class KdfNone(Kdf): algorithm = 0 @@ -804,24 +935,36 @@

Source code for yubikit.openpgp

     def _parse_data(cls, data) -> "KdfNone":
         return cls()
 
-
[docs] def process(self, pw, pin): +
+[docs] + def process(self, pw, pin): return pin.encode()
+ def __bytes__(self): return Tlv(0x81, struct.pack(">B", self.algorithm))
-
[docs]@unique + +
+[docs] +@unique class HASH_ALGORITHM(IntEnum): SHA256 = 0x08 SHA512 = 0x0A -
[docs] def create_digest(self): +
+[docs] + def create_digest(self): algorithm = getattr(hashes, self.name) - return hashes.Hash(algorithm(), default_backend())
+ return hashes.Hash(algorithm(), default_backend())
+
-
[docs]@dataclass + +
+[docs] +@dataclass class KdfIterSaltedS2k(Kdf): algorithm = 3 @@ -845,7 +988,9 @@

Source code for yubikit.openpgp

         digest.update(data[:trailing_bytes])
         return digest.finalize()
 
-
[docs] @classmethod +
+[docs] + @classmethod def create( cls, hash_algorithm: HASH_ALGORITHM = HASH_ALGORITHM.SHA256, @@ -867,6 +1012,7 @@

Source code for yubikit.openpgp

             ),
         )
+ @classmethod def _parse_data(cls, data) -> "KdfIterSaltedS2k": return cls( @@ -879,14 +1025,20 @@

Source code for yubikit.openpgp

             data.get(0x88),
         )
 
-
[docs] def get_salt(self, pw: PW) -> bytes: +
+[docs] + def get_salt(self, pw: PW) -> bytes: return getattr(self, f"salt_{pw.name.lower()}")
-
[docs] def process(self, pw, pin): + +
+[docs] + def process(self, pw, pin): salt = self.get_salt(pw) or self.salt_user data = salt + pin.encode() return self._do_process(self.hash_algorithm, self.iteration_count, data)
+ def __bytes__(self): return ( Tlv(0x81, struct.pack(">B", self.algorithm)) @@ -900,8 +1052,11 @@

Source code for yubikit.openpgp

         )
+ # mypy doesn't handle abstract dataclasses well -
[docs]@dataclass # type: ignore[misc] +
+[docs] +@dataclass # type: ignore[misc] class PrivateKeyTemplate(abc.ABC): crt: CRT @@ -918,7 +1073,10 @@

Source code for yubikit.openpgp

         )
-
[docs]@dataclass + +
+[docs] +@dataclass class RsaKeyTemplate(PrivateKeyTemplate): e: bytes p: bytes @@ -932,7 +1090,10 @@

Source code for yubikit.openpgp

         )
-
[docs]@dataclass + +
+[docs] +@dataclass class RsaCrtKeyTemplate(RsaKeyTemplate): iqmp: bytes dmp1: bytes @@ -949,7 +1110,10 @@

Source code for yubikit.openpgp

         )
-
[docs]@dataclass + +
+[docs] +@dataclass class EcKeyTemplate(PrivateKeyTemplate): private_key: bytes public_key: Optional[bytes] @@ -962,6 +1126,7 @@

Source code for yubikit.openpgp

         return tlvs
+ def _get_key_attributes( private_key: PrivateKey, key_ref: KEY_REF, version: Version ) -> AlgorithmAttributes: @@ -1064,10 +1229,16 @@

Source code for yubikit.openpgp

             raise ValueError(f"Unsupported hash algorithm for RSA: {hash_algorithm}")
 
 
-
[docs]class OpenPgpSession: +
+[docs] +class OpenPgpSession: """A session with the OpenPGP application.""" - def __init__(self, connection: SmartCardConnection): + def __init__( + self, + connection: SmartCardConnection, + scp_key_params: Optional[ScpKeyParams] = None, + ): self.protocol = SmartCardProtocol(connection) try: self.protocol.select(AID.OPENPGP) @@ -1079,11 +1250,13 @@

Source code for yubikit.openpgp

                 self.protocol.select(AID.OPENPGP)
             else:
                 raise
+
+        if scp_key_params:
+            self.protocol.init_scp(scp_key_params)
+
         self._version = self._read_version()
 
-        self.protocol.enable_touch_workaround(self.version)
-        if not 0 < self.version[0] < 4:
-            self.protocol.apdu_format = ApduFormat.EXTENDED
+        self.protocol.configure(self.version)
 
         # Note: This value is cached!
         # Do not rely on contained information that can change!
@@ -1113,7 +1286,9 @@ 

Source code for yubikit.openpgp

         """Get the Extended Capabilities from the YubiKey."""
         return self._app_data.discretionary.extended_capabilities
 
-
[docs] def get_challenge(self, length: int) -> bytes: +
+[docs] + def get_challenge(self, length: int) -> bytes: """Get random data from the YubiKey. :param length: Length of the returned data. @@ -1127,7 +1302,10 @@

Source code for yubikit.openpgp

         logger.debug(f"Getting {length} random bytes")
         return self.protocol.send_apdu(0, INS.GET_CHALLENGE, 0, 0, le=length)
-
[docs] def get_data(self, do: DO) -> bytes: + +
+[docs] + def get_data(self, do: DO) -> bytes: """Get a Data Object from the YubiKey. :param do: The Data Object to get. @@ -1135,7 +1313,10 @@

Source code for yubikit.openpgp

         logger.debug(f"Reading Data Object {do.name} ({do:X})")
         return self.protocol.send_apdu(0, INS.GET_DATA, do >> 8, do & 0xFF)
-
[docs] def put_data(self, do: DO, data: Union[bytes, SupportsBytes]) -> None: + +
+[docs] + def put_data(self, do: DO, data: Union[bytes, SupportsBytes]) -> None: """Write a Data Object to the YubiKey. :param do: The Data Object to write to. @@ -1144,20 +1325,32 @@

Source code for yubikit.openpgp

         self.protocol.send_apdu(0, INS.PUT_DATA, do >> 8, do & 0xFF, bytes(data))
         logger.info(f"Wrote Data Object {do.name} ({do:X})")
-
[docs] def get_pin_status(self) -> PwStatus: + +
+[docs] + def get_pin_status(self) -> PwStatus: """Get the current status of PINS.""" return PwStatus.parse(self.get_data(DO.PW_STATUS_BYTES))
-
[docs] def get_signature_counter(self) -> int: + +
+[docs] + def get_signature_counter(self) -> int: """Get the number of times the signature key has been used.""" s = SecuritySupportTemplate.parse(self.get_data(DO.SECURITY_SUPPORT_TEMPLATE)) return s.signature_counter
-