def _parse_mode_string(ctx, param, mode): try: mode_int = int(mode) return Mode.from_code(mode_int) except IndexError: ctx.fail(f"Invalid mode: {mode_int}") except ValueError: pass # Not a numeric mode, parse string try: if mode[0] in ["+", "-"]: info = ctx.obj["info"] usb_enabled = info.config.enabled_capabilities[TRANSPORT.USB] interfaces = USB_INTERFACE.for_capabilities(usb_enabled) for mod in re.findall(r"[+-][A-Z]+", mode.upper()): interface = _parse_interface_string(mod[1:]) if mod.startswith("+"): interfaces |= interface else: interfaces ^= interface else: interfaces = USB_INTERFACE(0) for t in re.split(r"[+]+", mode.upper()): if t: interfaces |= _parse_interface_string(t) except ValueError: ctx.fail(f"Invalid mode string: {mode}") return Mode(interfaces)
def _mode_from_usb_enabled(usb_enabled): interfaces = USB_INTERFACE(0) if CAPABILITY.OTP & usb_enabled: interfaces |= USB_INTERFACE.OTP if (CAPABILITY.U2F | CAPABILITY.FIDO2) & usb_enabled: interfaces |= USB_INTERFACE.FIDO if (CAPABILITY.OPENPGP | CAPABILITY.PIV | CAPABILITY.OATH) & usb_enabled: interfaces |= USB_INTERFACE.CCID return Mode(interfaces)
def info(ctx, check_fips): """ Show general information. Displays information about the attached YubiKey such as serial number, firmware version, capabilities, etc. """ info = ctx.obj["info"] pid = ctx.obj["pid"] if pid is None: interfaces = None key_type = None else: interfaces = pid.get_interfaces() key_type = pid.get_type() device_name = get_name(info, key_type) click.echo(f"Device type: {device_name}") if info.serial: click.echo(f"Serial number: {info.serial}") if info.version: f_version = ".".join(str(x) for x in info.version) click.echo(f"Firmware version: {f_version}") else: click.echo( "Firmware version: Uncertain, re-run with only one YubiKey connected" ) if info.form_factor: click.echo(f"Form factor: {info.form_factor!s}") if interfaces: f_interfaces = ", ".join(t.name for t in USB_INTERFACE if t in USB_INTERFACE(interfaces)) click.echo(f"Enabled USB interfaces: {f_interfaces}") if TRANSPORT.NFC in info.supported_capabilities: f_nfc = ("enabled" if info.config.enabled_capabilities.get( TRANSPORT.NFC) else "disabled") click.echo(f"NFC transport is {f_nfc}.") if info.is_locked: click.echo("Configured capabilities are protected by a lock code.") click.echo() print_app_status_table(info.supported_capabilities, info.config.enabled_capabilities) if check_fips: if is_fips_version(info.version): ctx.obj["conn"].close() _check_fips_status(pid, info) else: cli_fail( "Unable to check FIPS Approved mode - Not a YubiKey 4 FIPS")
def refresh(self): devices, state = scan_devices() n_devs = sum(devices.values()) if n_devs != 1: return failure('multiple_devices') if state != self._state: self._state = state try: connection, device, info = connect_to_device() connection.close() except: self._state = None self._dev_info = None return failure('no_device') interfaces = USB_INTERFACE(0) usb_supported = info.supported_capabilities.get(TRANSPORT.USB) if CAPABILITY.OTP & usb_supported: interfaces |= USB_INTERFACE.OTP if (CAPABILITY.U2F | CAPABILITY.FIDO2) & usb_supported: interfaces |= USB_INTERFACE.FIDO if (CAPABILITY.OPENPGP | CAPABILITY.PIV | CAPABILITY.OATH) & usb_supported: interfaces |= USB_INTERFACE.CCID self._dev_info = { 'name': get_name(info, device.pid.get_type()).replace("YubiKey BIO", "YubiKey Bio"), 'version': '.'.join(str(x) for x in info.version) if info.version else "", 'serial': info.serial or '', 'usb_enabled': [ a.name for a in CAPABILITY if a in info.config.enabled_capabilities.get(TRANSPORT.USB)], 'usb_supported': [ a.name for a in CAPABILITY if a in info.supported_capabilities.get(TRANSPORT.USB)], 'usb_interfaces_supported': [ t.name for t in USB_INTERFACE if t in interfaces], 'nfc_enabled': [ a.name for a in CAPABILITY if a in info.config.enabled_capabilities.get(TRANSPORT.NFC, [])], 'nfc_supported': [ a.name for a in CAPABILITY if a in info.supported_capabilities.get(TRANSPORT.NFC, [])], 'usb_interfaces_enabled': [i.name for i in USB_INTERFACE if i & device.pid.get_interfaces()], 'can_write_config': info.version and info.version >= (5,0,0), 'configuration_locked': info.is_locked, 'form_factor': info.form_factor } return success({'dev': self._dev_info})
def _pid_from_name(name): if YK_READER_NAME not in name.lower(): return None interfaces = USB_INTERFACE(0) for iface in USB_INTERFACE: if iface.name in name: interfaces |= iface if "U2F" in name: interfaces |= USB_INTERFACE.FIDO key_type = YUBIKEY.NEO if "NEO" in name else YUBIKEY.YK4 return key_type.get_pid(interfaces)
def get_interfaces(self) -> USB_INTERFACE: return USB_INTERFACE( sum(USB_INTERFACE[x] for x in self.name.split("_")[1:]))
def get_pid(self, interfaces: USB_INTERFACE) -> "PID": suffix = "_".join(t.name for t in USB_INTERFACE if t in USB_INTERFACE(interfaces)) return PID[self.name + "_" + suffix]
def read_info(pid: Optional[PID], conn: Connection) -> DeviceInfo: """Read out a DeviceInfo object from a YubiKey, or attempt to synthesize one.""" if pid: key_type: Optional[YUBIKEY] = pid.get_type() interfaces = pid.get_interfaces() else: # No PID for NFC connections key_type = None interfaces = USB_INTERFACE(0) if isinstance(conn, SmartCardConnection): info = _read_info_ccid(conn, key_type, interfaces) elif isinstance(conn, OtpConnection): info = _read_info_otp(conn, key_type, interfaces) elif isinstance(conn, FidoConnection): info = _read_info_ctap(conn, key_type, interfaces) else: raise TypeError("Invalid connection type") logger.debug("Read info: %s", info) # Set usb_enabled if missing (pre YubiKey 5) if (info.has_transport(TRANSPORT.USB) and TRANSPORT.USB not in info.config.enabled_capabilities): usb_enabled = info.supported_capabilities[TRANSPORT.USB] if usb_enabled == (CAPABILITY.OTP | CAPABILITY.U2F | USB_INTERFACE.CCID): # YubiKey Edge, hide unusable CCID interface usb_enabled = CAPABILITY.OTP | CAPABILITY.U2F info.supported_capabilities = {TRANSPORT.USB: usb_enabled} if USB_INTERFACE.OTP not in interfaces: usb_enabled &= ~CAPABILITY.OTP if USB_INTERFACE.FIDO not in interfaces: usb_enabled &= ~(CAPABILITY.U2F | CAPABILITY.FIDO2) if USB_INTERFACE.CCID not in interfaces: usb_enabled &= ~(USB_INTERFACE.CCID | CAPABILITY.OATH | CAPABILITY.OPENPGP | CAPABILITY.PIV) info.config.enabled_capabilities[TRANSPORT.USB] = usb_enabled # YK4-based FIPS version if is_fips_version(info.version): info.is_fips = True # Set nfc_enabled if missing (pre YubiKey 5) if (info.has_transport(TRANSPORT.NFC) and TRANSPORT.NFC not in info.config.enabled_capabilities): info.config.enabled_capabilities[ TRANSPORT.NFC] = info.supported_capabilities[TRANSPORT.NFC] # Workaround for invalid configurations. if info.version >= (4, 0, 0): if info.form_factor in ( FORM_FACTOR.USB_A_NANO, FORM_FACTOR.USB_C_NANO, FORM_FACTOR.USB_C_LIGHTNING, ) or (info.form_factor is FORM_FACTOR.USB_C_KEYCHAIN and info.version < (5, 2, 4)): # Known not to have NFC info.supported_capabilities.pop(TRANSPORT.NFC, None) info.config.enabled_capabilities.pop(TRANSPORT.NFC, None) return info
def read_info(pid: Optional[PID], conn: Connection) -> DeviceInfo: """Read out a DeviceInfo object from a YubiKey, or attempt to synthesize one.""" if pid: key_type: Optional[YUBIKEY] = pid.get_type() interfaces = pid.get_interfaces() else: # No PID for NFC connections key_type = None interfaces = USB_INTERFACE(0) if isinstance(conn, SmartCardConnection): info = _read_info_ccid(conn, key_type, interfaces) elif isinstance(conn, OtpConnection): info = _read_info_otp(conn, key_type, interfaces) elif isinstance(conn, FidoConnection): info = _read_info_ctap(conn, key_type, interfaces) else: raise TypeError("Invalid connection type") logger.debug("Read info: %s", info) # Set usb_enabled if missing (pre YubiKey 5) if (info.has_transport(TRANSPORT.USB) and TRANSPORT.USB not in info.config.enabled_capabilities): usb_enabled = info.supported_capabilities[TRANSPORT.USB] if usb_enabled == (CAPABILITY.OTP | CAPABILITY.U2F | USB_INTERFACE.CCID): # YubiKey Edge, hide unusable CCID interface usb_enabled = CAPABILITY.OTP | CAPABILITY.U2F info.supported_capabilities = {TRANSPORT.USB: usb_enabled} if USB_INTERFACE.OTP not in interfaces: usb_enabled &= ~CAPABILITY.OTP if USB_INTERFACE.FIDO not in interfaces: usb_enabled &= ~(CAPABILITY.U2F | CAPABILITY.FIDO2) if USB_INTERFACE.CCID not in interfaces: usb_enabled &= ~(USB_INTERFACE.CCID | CAPABILITY.OATH | CAPABILITY.OPENPGP | CAPABILITY.PIV) info.config.enabled_capabilities[TRANSPORT.USB] = usb_enabled # Set nfc_enabled if missing (pre YubiKey 5) if (info.has_transport(TRANSPORT.NFC) and TRANSPORT.NFC not in info.config.enabled_capabilities): info.config.enabled_capabilities[ TRANSPORT.NFC] = info.supported_capabilities[TRANSPORT.NFC] # Workaround for invalid configurations. # Assume all form factors except USB_A_KEYCHAIN and # USB_C_KEYCHAIN >= 5.2.4 does not support NFC. if not (info.version < (4, 0, 0) # No relevant programming yet or (info.form_factor is FORM_FACTOR.USB_A_KEYCHAIN) or (info.form_factor is FORM_FACTOR.USB_C_KEYCHAIN and info.version >= (5, 2, 4))): info.supported_capabilities = { TRANSPORT.USB: info.supported_capabilities[TRANSPORT.USB] } info.config.enabled_capabilities = { TRANSPORT.USB: info.config.enabled_capabilities[TRANSPORT.USB] } return info
def mode(ctx, mode, touch_eject, autoeject_timeout, chalresp_timeout, force): """ Manage connection modes (USB Interfaces). This command is generaly used with YubiKeys prior to the 5 series. Use "ykman config usb" for more granular control on YubiKey 5 and later. Get the current connection mode of the YubiKey, or set it to MODE. MODE can be a string, such as "OTP+FIDO+CCID", or a shortened form: "o+f+c". It can also be a mode number. Examples: \b Set the OTP and FIDO mode: $ ykman config mode OTP+FIDO \b Set the CCID only mode and use touch to eject the smart card: $ ykman config mode CCID --touch-eject """ info = ctx.obj["info"] mgmt = ctx.obj["controller"] usb_enabled = info.config.enabled_capabilities[TRANSPORT.USB] my_mode = Mode(USB_INTERFACE.for_capabilities(usb_enabled)) usb_supported = info.supported_capabilities[TRANSPORT.USB] interfaces_supported = USB_INTERFACE.for_capabilities(usb_supported) pid = ctx.obj["pid"] if pid: key_type = pid.get_type() else: key_type = None if autoeject_timeout: # autoeject implies touch eject touch_eject = True autoeject = autoeject_timeout if touch_eject else None if mode.interfaces != USB_INTERFACE.CCID: if touch_eject: ctx.fail( "--touch-eject can only be used when setting CCID-only mode") if not force: if mode == my_mode: cli_fail(f"Mode is already {mode}, nothing to do...", 0) elif key_type in (YUBIKEY.YKS, YUBIKEY.YKP): cli_fail("Mode switching is not supported on this YubiKey!\n" "Use --force to attempt to set it anyway.") elif mode.interfaces not in interfaces_supported: cli_fail(f"Mode {mode} is not supported on this YubiKey!\n" + "Use --force to attempt to set it anyway.") force or click.confirm( f"Set mode of YubiKey to {mode}?", abort=True, err=True) try: mgmt.set_mode(mode, chalresp_timeout, autoeject) click.echo("Mode set! You must remove and re-insert your YubiKey " "for this change to take effect.") except Exception as e: logger.debug("Failed to switch mode", exc_info=e) click.echo("Failed to switch mode on the YubiKey. Make sure your " "YubiKey does not have an access code set.")
def usb( ctx, enable, disable, list_enabled, enable_all, touch_eject, no_touch_eject, autoeject_timeout, chalresp_timeout, lock_code, force, ): """ Enable or disable applications over USB. """ _require_config(ctx) def ensure_not_all_disabled(ctx, usb_enabled): for app in CAPABILITY: if app & usb_enabled: return ctx.fail("Can not disable all applications over USB.") if not (list_enabled or enable_all or enable or disable or touch_eject or no_touch_eject or autoeject_timeout or chalresp_timeout): ctx.fail("No configuration options chosen.") info = ctx.obj["info"] usb_supported = info.supported_capabilities[TRANSPORT.USB] usb_enabled = info.config.enabled_capabilities[TRANSPORT.USB] usb_interfaces = USB_INTERFACE.for_capabilities(usb_enabled) flags = info.config.device_flags if enable_all: enable = [c for c in CAPABILITY if c in usb_supported] _ensure_not_invalid_options(ctx, enable, disable) if touch_eject and no_touch_eject: ctx.fail("Invalid options.") if not usb_supported: cli_fail("USB not supported on this YubiKey.") if list_enabled: _list_apps(ctx, usb_enabled) if touch_eject: flags |= DEVICE_FLAG.EJECT if no_touch_eject: flags &= ~DEVICE_FLAG.EJECT for app in enable: if app & usb_supported: usb_enabled |= app else: cli_fail(f"{app.name} not supported over USB on this YubiKey.") for app in disable: if app & usb_supported: usb_enabled &= ~app else: cli_fail(f"{app.name} not supported over USB on this YubiKey.") ensure_not_all_disabled(ctx, usb_enabled) reboot = usb_interfaces != USB_INTERFACE.for_capabilities(usb_enabled) f_confirm = "" if enable: f_confirm += f"Enable {', '.join(str(app) for app in enable)}.\n" if disable: f_confirm += f"Disable {', '.join(str(app) for app in disable)}.\n" if touch_eject: f_confirm += "Set touch eject.\n" elif no_touch_eject: f_confirm += "Disable touch eject.\n" if autoeject_timeout: f_confirm += f"Set autoeject timeout to {autoeject_timeout}.\n" if chalresp_timeout: f_confirm += f"Set challenge-response timeout to {chalresp_timeout}.\n" if reboot: f_confirm += "This will cause the YubiKey to reboot.\n" f_confirm += "Configure USB?" is_locked = info.is_locked if force and is_locked and not lock_code: cli_fail( "Configuration is locked - please supply the --lock-code option.") if lock_code and not is_locked: cli_fail( "Configuration is not locked - please remove the --lock-code option." ) force or click.confirm(f_confirm, abort=True, err=True) if is_locked and not lock_code: lock_code = prompt_lock_code() if lock_code: lock_code = _parse_lock_code(ctx, lock_code) app = ctx.obj["controller"] try: app.write_device_config( DeviceConfig( {TRANSPORT.USB: usb_enabled}, autoeject_timeout, chalresp_timeout, flags, ), reboot, lock_code, ) except Exception as e: logger.error("Failed to write config", exc_info=e) cli_fail("Failed to configure USB applications.")