def challenge_response(serial, host, user, prompt, credential_id, challenge, udp, pin): """Uses `hmac-secret` to implement a challenge-response mechanism. We abuse hmac-secret, which gives us `HMAC(K, hash(challenge))`, where `K` is a secret tied to the `credential_id`. We hash the challenge first, since a 32 byte value is expected (in original usage, it's a salt). This means that we first need to setup a credential_id; this depends on the specific authenticator used. To do this, use `nitropy fido2 make-credential`. If so desired, user and relying party can be changed from the defaults. The prompt can be suppressed using `--prompt ""`. """ if not pin: pin = AskUser.hidden("Please provide pin: ") nkfido2.find().simple_secret( credential_id, challenge, host=host, user_id=user, serial=serial, prompt=prompt, output=True, udp=udp, pin=pin, )
def _enter_bootloader(serial): from pynitrokey.fido2 import find p = find(serial) local_print("please use the button on the device to confirm") p.enter_bootloader_or_die() local_print("Nitrokey rebooted. Reconnecting...") time.sleep(0.5) if find(serial) is None: local_critical(RuntimeError("Failed to reconnect!"))
def reboot(serial): """Reboot. \b This implementation actually only works for bootloader reboot """ # this implementation actually only works for bootloader # firmware doesn't have a reboot command from pynitrokey.fido2 import find find(serial).reboot()
def reset(serial, yes): """Reset key - wipes all credentials!!!""" local_print("Reset is only possible 10secs after plugging in the device.", "Please (re-)plug in your Nitrokey FIDO2 now!") if yes or AskUser.yes_no( "Warning: Your credentials will be lost!!! continue?"): local_print( "Press key to confirm -- again, your credentials will be lost!!!") try: nkfido2.find(serial).reset() except CtapError as e: local_critical( f"Reset failed ({str(e)})", "Did you confirm with a key-press 10secs after plugging in?", "Please re-try...") local_print("....aaaand they're gone")
def hexbytes(count, serial): """Output COUNT number of random bytes, hex-encoded.""" if not 0 <= count <= 255: local_critical( f"Number of bytes must be between 0 and 255, you passed {count}") local_print(nkfido2.find(serial).get_rng(count).hex())
def probe(serial, udp, hash_type, filename): """Calculate HASH""" # @todo: move to constsconf.py # all_hash_types = ("SHA256", "SHA512", "RSA2048", "Ed25519") all_hash_types = ("SHA256", "SHA512", "RSA2048") # @fixme: Ed25519 needs `nacl` dependency, which is not available currently?! if hash_type.upper() not in all_hash_types: local_critical( f"invalid [HASH_TYPE] provided: {hash_type}", f"use one of: {', '.join(all_hash_types)}", ) data = open(filename, "rb").read() # < CTAPHID_BUFFER_SIZE # https://fidoalliance.org/specs/fido-v2.0-id-20180227/ # fido-client-to-authenticator-protocol-v2.0-id-20180227.html # #usb-message-and-packet-structure # also account for padding (see data below....) # so 6kb is conservative # @todo: proper error/exception + cut in chunks? assert len(data) <= 6 * 1024 p = nkfido2.find(serial, udp=udp) serialized_command = cbor.dumps({"subcommand": hash_type, "data": data}) result = p.send_data_hid(SoloBootloader.HIDCommandProbe, serialized_command) result_hex = result.hex() local_print(result_hex) # @todo: unreachable if hash_type == "Ed25519": # @fixme: mmmh, where to get `nacl` (python-libnacl? python-pynacl?) import nacl.signing # print(f"content from hex: {bytes.fromhex(result_hex[128:]).decode()}") local_print( f"content: {result[64:]}", f"content from hex: {bytes.fromhex(result_hex[128:])}", f"signature: {result[:128]}", ) # verify_key = nacl.signing.VerifyKey(bytes.fromhex("c69995185efa20bf7a88139f5920335aa3d3e7f20464345a2c095c766dfa157a")) # @fixme: where does this 'magic-number' come from!? verify_key = nacl.signing.VerifyKey( bytes.fromhex( "c69995185efa20bf7a88139f5920335aa3d3e7f20464345a2c095c766dfa157a" )) try: verify_key.verify(result) local_print("verified!") except nacl.exceptions.BadSignatureError: local_print("failed verification!")
def make_credential(serial, host, user, udp, prompt, pin): """Generate a credential. Pass `--prompt ""` to output only the `credential_id` as hex. """ if not pin: pin = AskUser.hidden("Please provide pin: ") nkfido2.find().make_credential( host=host, user_id=user, serial=serial, output=True, prompt=prompt, udp=udp, pin=pin, )
def feedkernel(count, serial): """Feed random bytes to /dev/random.""" if os.name != "posix": local_critical("This is a Linux-specific command!") if not 0 <= count <= 255: local_critical( f"Number of bytes must be between 0 and 255, you passed {count}") p = nkfido2.find(serial) RNDADDENTROPY = 0x40085203 entropy_info_file = "/proc/sys/kernel/random/entropy_avail" print(f"entropy before: 0x{open(entropy_info_file).read().strip()}") r = p.get_rng(count) # man 4 random # RNDADDENTROPY # Add some additional entropy to the input pool, incrementing the # entropy count. This differs from writing to /dev/random or # /dev/urandom, which only adds some data but does not increment the # entropy count. The following structure is used: # struct rand_pool_info { # int entropy_count; # int buf_size; # __u32 buf[0]; # }; # Here entropy_count is the value added to (or subtracted from) the # entropy count, and buf is the buffer of size buf_size which gets # added to the entropy pool. # maximum 8, tend to be pessimistic entropy_bits_per_byte = 2 t = struct.pack(f"ii{count}s", count * entropy_bits_per_byte, count, r) try: with open("/dev/random", mode="wb") as fh: fcntl.ioctl(fh, RNDADDENTROPY, t) except PermissionError as e: local_critical( "insufficient permissions to use `fnctl.ioctl` on '/dev/random'", "please run 'nitropy' with proper permissions", e, ) local_print(f"entropy after: 0x{open(entropy_info_file).read().strip()}")
def status(serial, blink: bool): """Print device's status""" p = nkfido2.find(serial) t0 = time() while True: if time() - t0 > 5 and blink: p.wink() r = p.get_status() for b in r: local_print('{:#02d} '.format(b), end='') local_print("") sleep(0.3)
def bootloader(serial, firmware): """Program via Nitrokey bootloader interface. \b FIRMWARE argument should be either a .hex or .json file. If the bootloader is verifying, the .json is needed containing a signature for the verifying key in the bootloader. If the bootloader is nonverifying, either .hex or .json can be used. DANGER: if you try to flash a firmware with signature that doesn't match the bootloader's verifying key, you will be stuck in bootloader mode until you find a signed firmware that does match. Enter bootloader mode using `nitropy fido2 util program aux enter-bootloader` first. """ p = find(serial) try: p.use_hid() p.program_file(firmware) except CtapError as e: if e.code == CtapError.ERR.INVALID_COMMAND: local_print("Not in bootloader mode. Attempting to switch...") local_print("Please confirm with button on key!") else: local_critical(e) p.enter_bootloader_or_die() local_print("Nitrokey rebooted. Reconnecting...") time.sleep(2.0) find(serial) if p is None: local_critical("Cannot find Nitrokey device.") p.use_hid() p.program_file(firmware)
def reboot(serial, udp): """Send reboot command to key (development command)""" local_print("Reboot", "Press key to confirm!") CTAP_REBOOT = 0x53 dev = nkfido2.find(serial, udp=udp).dev try: dev.call(CTAP_REBOOT ^ 0x80, b'') except OSError: local_print("...done") except CtapError as e: local_critical(f"...failed ({str(e)})")
def simple_secret( credential_id, secret_input, host="nitrokeys.dev", user_id="they", serial=None, prompt="Touch your authenticator to generate a response...", output=True, udp=False, ): user_id = user_id.encode() from pynitrokey.fido2 import find client = find(solo_serial=serial, udp=udp).client hmac_ext = HmacSecretExtension(client.ctap2) # rp = {"id": host, "name": "Example RP"} client.host = host client.origin = f"https://{client.host}" client.user_id = user_id # user = {"id": user_id, "name": "A. User"} credential_id = binascii.a2b_hex(credential_id) allow_list = [{"type": "public-key", "id": credential_id}] challenge = secrets.token_hex(32) h = hashlib.sha256() h.update(secret_input.encode()) salt = h.digest() if prompt: print(prompt) assertions, client_data = client.get_assertion({ "rpId": host, "challenge": challenge.encode("utf8"), "allowCredentials": allow_list, "extensions": hmac_ext.get_dict(salt), }) assertion = assertions[0] # Only one cred in allowList, only one response. response = hmac_ext.results_for(assertion.auth_data)[0] if output: print(response.hex()) return response
def connect_and_flash(): # reconnect and actually flash it... # fail after 5 attempts exc = None for _ in range(5): try: client = find(serial) client.use_hid() client.program_file(fw_fn) break except Exception as e: time.sleep(0.5) exc = e # todo log exception info from each failed iteration else: local_critical("problem flashing firmware:", exc)
def make_credential( host="nitrokeys.dev", user_id="they", serial=None, prompt="Touch your authenticator to generate a credential...", output=True, udp=False, ): user_id = user_id.encode() from pynitrokey.fido2 import find client = find(solo_serial=serial, udp=udp).client rp = {"id": host, "name": "Example RP"} client.host = host client.origin = f"https://{client.host}" client.user_id = user_id user = {"id": user_id, "name": "A. User"} challenge = secrets.token_hex(32) if prompt: print(prompt) hmac_ext = HmacSecretExtension(client.ctap2) attestation_object, client_data = client.make_credential({ "rp": rp, "user": user, "challenge": challenge.encode("utf8"), "pubKeyCredParams": [{ "type": "public-key", "alg": -7 }], "extensions": hmac_ext.create_dict(), }) credential = attestation_object.auth_data.credential_data credential_id = credential.credential_id if output: print(credential_id.hex()) return credential_id
def set_pin(serial): """Set pin of current key""" new_pin = AskUser.hidden("Please enter new pin: ") confirm_pin = AskUser.hidden("Please confirm new pin: ") if new_pin != confirm_pin: local_critical("new pin does not match confirm-pin", "please try again!", support_hint=False) try: # @fixme: move this (function) into own fido2-client-class client = nkfido2.find(serial).client PIN(client.ctap2).set_pin(new_pin) local_print("done - please use new pin to verify key") except Exception as e: local_critical("failed setting new pin, maybe it's already set?", "to change an already set pin, please use:", "$ nitropy fido2 change-pin", e)
def change_pin(serial): """Change pin of current key""" old_pin = AskUser.hidden("Please enter old pin: ") new_pin = AskUser.hidden("Please enter new pin: ") confirm_pin = AskUser.hidden("Please confirm new pin: ") if new_pin != confirm_pin: local_critical("new pin does not match confirm-pin", "please try again!", support_hint=False) try: # @fixme: move this (function) into own fido2-client-class client = nkfido2.find(serial).client PIN(client.ctap2).change_pin(old_pin, new_pin) local_print("done - please use new pin to verify key") except Exception as e: local_critical("failed changing to new pin!", "did you set one already? or is it wrong?", e)
def version(serial, udp): """Version of firmware on key.""" try: res = nkfido2.find(serial, udp=udp).solo_version() major, minor, patch = res[:3] locked = "" if len(res) > 3: if res[3]: locked = "locked" else: locked = "unlocked" local_print(f"{major}.{minor}.{patch} {locked}") except pynitrokey.exceptions.NoSoloFoundError: local_critical("No Nitrokey found.", "If you are on Linux, are your udev rules up to date?") # unused ??? except (pynitrokey.exceptions.NoSoloFoundError, ApduError): local_critical( "Firmware is out of date (key does not know the NITROKEY_VERSION command)." )
def bootloader_version(serial, pubkey): """Version of bootloader.""" from pynitrokey.fido2 import find p = find(serial) if not p.is_solo_bootloader(): local_print("Not in Bootloader Mode!") return else: local_print("Detected Bootloader Mode") local_print("Version: " + ".".join(map(str, p.bootloader_version()))) from binascii import b2a_hex from hashlib import sha256 if pubkey: bpub = p.boot_pubkey() bpub = b2a_hex(bpub) local_print(f"Bootloader public key: \t\t{bpub}") s = sha256() s.update(bpub) bpubh = b2a_hex(s.digest()) local_print(f"Bootloader public key sha256: \t{bpubh}")
def update(serial, yes): """Update Nitrokey key to latest firmware version.""" # @fixme: print this and allow user to cancel (if not -y is active) #update_url = 'https://update.nitrokey.com/' #print('Please use {} to run the firmware update'.format(update_url)) #return IS_LINUX = platform.system() == "Linux" logger.debug(f"Start session {datetime.now()}") # @fixme: move to generic startup stuff logged into file exclusively! local_print("Nitrokey FIDO2 firmware update tool", f"Platform: {platform.platform()}", f"System: {platform.system()}, is_linux: {IS_LINUX}", f"Python: {platform.python_version()}", f"Saving run log to: {LOG_FN}", "", f"Starting update procedure for Nitrokey FIDO2...") from pynitrokey.fido2 import find # Determine target key client = None try: client = find(serial) except pynitrokey.exceptions.NoSoloFoundError as e: local_critical(None, "No Nitrokey key found!", e, None, "If you are on Linux, are your udev rules up to date?", "For more, see: ", " https://www.nitrokey.com/documentation/installation#os:linux", None) except pynitrokey.exceptions.NonUniqueDeviceError as e: local_critical(None, "Multiple Nitrokey keys are plugged in!", e, None, "Please unplug all but one key", None) except Exception as e: local_critical(None, "Unhandled error connecting to key", e, None) # determine asset url: we want the (signed) json file # @fixme: move to confconsts.py ... api_base_url = "https://api.github.com/repos" api_url = f"{api_base_url}/Nitrokey/nitrokey-fido2-firmware/releases/latest" try: gh_release_data = json.loads(requests.get(api_url).text) except Exception as e: local_critical("Failed downloading firmware", e) # search asset with `fn` suffix being .json and take its url assets = [(x["name"], x["browser_download_url"]) \ for x in gh_release_data["assets"]] download_url = None for fn, url in assets: if fn.endswith(".json"): download_url = url break if not download_url: local_critical("Failed to determine latest release (url)", "assets:", *map(str, assets)) # download asset url # @fixme: move to confconsts.py ... local_print(f"Downloading latest firmware: {gh_release_data['tag_name']} " f"(published at {gh_release_data['published_at']})") tmp_dir = tempfile.gettempdir() fw_fn = os.path.join(tmp_dir, "fido2_firmware.json") try: with open(fw_fn, "wb") as fd: firmware = requests.get(download_url) fd.write(firmware.content) except Exception as e: local_critical("Failed downloading firmware", e) local_print(f"Firmware saved to {fw_fn}", f"Downloaded firmware version: {gh_release_data['tag_name']}") # @fixme: whyyyyy is this here, move away... (maybe directly next to `fido2.find()`) def get_dev_details(): # @fixme: why not use `find` here... from pynitrokey.fido2 import find_all c = find_all()[0] _props = c.dev.descriptor local_print(f"Device connected:") if "serial_number" in _props: local_print(f"{_props['serial_number']}: {_props['product_string']}") else: local_print(f"{_props['path']}: {_props['product_string']}") version_raw = c.solo_version() major, minor, patch = version_raw[:3] locked = "" if len(version_raw) > 3 and version_raw[3] else "unlocked" local_print(f"Firmware version: {major}.{minor}.{patch} {locked}", None) get_dev_details() # ask for permission if not yes: local_print("This will update your Nitrokey FIDO2") if not AskUser.strict_yes_no("Do you want to continue?"): local_critical("exiting due to user input...", support_hint=False) # Ensure we are in bootloader mode if client.is_solo_bootloader(): local_print("Key already in bootloader mode, continuing...") else: try: local_print("Entering bootloader mode, please confirm with button on key!") client.enter_bootloader_or_die() time.sleep(0.5) except Exception as e: local_critical("problem switching to bootloader mode:", e) # reconnect and actually flash it... try: from pynitrokey.fido2 import find client = find(serial) client.use_hid() client.program_file(fw_fn) except Exception as e: local_critical("problem flashing firmware:", e) local_print(None, "After update check") tries = 100 for i in range(tries): try: get_dev_details() break except Exception as e: if i > tries-1: local_critical("Could not connect to device after update", e) raise time.sleep(0.5) local_print("Congratulations, your key was updated to the latest firmware.") logger.debug("Finishing session {}".format(datetime.now())) local_print("Log saved to: {}".format(LOG_FN))
def leave_bootloader(serial): """Switch from Nitrokey bootloader to Nitrokey firmware.""" from pynitrokey.fido2 import find find(serial).reboot()
def update(serial, yes, force): """Update Nitrokey key to latest firmware version.""" # @fixme: print this and allow user to cancel (if not -y is active) # update_url = 'https://update.nitrokey.com/' # print('Please use {} to run the firmware update'.format(update_url)) # return IS_LINUX = platform.system() == "Linux" logger.debug(f"Start session {datetime.now()}") # @fixme: move to generic startup stuff logged into file exclusively! local_print( "Nitrokey FIDO2 firmware update tool", f"Platform: {platform.platform()}", f"System: {platform.system()}, is_linux: {IS_LINUX}", f"Python: {platform.python_version()}", f"Saving run log to: {LOG_FN}", "", f"Starting update procedure for Nitrokey FIDO2...", ) from pynitrokey.fido2 import find # Determine target key client = None try: client = find(serial) except pynitrokey.exceptions.NoSoloFoundError as e: local_critical( None, "No Nitrokey key found!", e, None, "If you are on Linux, are your udev rules up to date?", "For more, see: ", " https://docs.nitrokey.com/fido2/linux/index.html#troubleshooting", None, ) except pynitrokey.exceptions.NonUniqueDeviceError as e: local_critical( None, "Multiple Nitrokey keys are plugged in!", e, None, "Please unplug all but one key", None, ) except Exception as e: local_critical(None, "Unhandled error connecting to key", e, None) # determine asset url: we want the (signed) json file # @fixme: move to confconsts.py ... api_base_url = "https://api.github.com/repos" api_url = f"{api_base_url}/Nitrokey/nitrokey-fido2-firmware/releases/latest" gh_release_data = None try: gh_release_data = json.loads(requests.get(api_url).text) except Exception as e: local_critical("Failed downloading firmware", e) # search asset with `fn` suffix being .json and take its url assets = [(x["name"], x["browser_download_url"]) for x in gh_release_data["assets"]] download_url = None for fn, url in assets: if fn.endswith(".json"): download_url = url break if not download_url: local_critical("Failed to determine latest release (url)", "assets:", *map(str, assets)) import os.path local_print( f"Found latest firmware: {os.path.basename(download_url)}\n" f"\t\t(published at {gh_release_data['published_at']}, under tag {gh_release_data['tag_name']})" ) ver = client.solo_version() local_print(f"\tCurrent Firmware version: {ver[0]}.{ver[1]}.{ver[2]}") # if the downloaded firmware version is the same as the current one, skip update unless force switch is provided # if f'firmware-{ver[0]}.{ver[1]}.{ver[2]}' in gh_release_data['tag_name'] and not force: if f"firmware-{ver[0]}.{ver[1]}.{ver[2]}" in download_url: if not force: local_critical( "Your firmware is up-to-date!\n" "Use --force flag to run update process anyway.", support_hint=False, ) else: local_print( "Firmware is up-to-date. Continue due to --force switch applied." ) def download_firmware(): # download asset url # @fixme: move to confconsts.py ... local_print( f"Downloading latest firmware: {gh_release_data['tag_name']} " f"(published at {gh_release_data['published_at']})") tmp_dir = tempfile.gettempdir() fw_fn = os.path.join(tmp_dir, "fido2_firmware.json") try: with open(fw_fn, "wb") as fd: firmware = requests.get(download_url) fd.write(firmware.content) except Exception as e: local_critical("Failed downloading firmware", e) local_print( f"\tFirmware saved to {fw_fn}", f"\tDownloaded firmware version: {gh_release_data['tag_name']}", ) return fw_fn fw_fn = download_firmware() # ask for permission if not yes: local_print("") local_print("This will update your Nitrokey FIDO2") if not AskUser.strict_yes_no("Do you want to continue?"): local_critical("exiting due to user input...", support_hint=False) # Ensure we are in bootloader mode if client.is_solo_bootloader(): local_print("Key already in bootloader mode, continuing...") else: try: local_print( "Entering bootloader mode, please confirm with button on key! (long 10 second press)" ) client.use_hid() client.enter_bootloader_or_die() time.sleep(0.5) except Exception as e: local_critical("problem switching to bootloader mode:", e) time.sleep(1.0) def connect_and_flash(): # reconnect and actually flash it... # fail after 5 attempts exc = None for _ in range(5): try: client = find(serial) client.use_hid() client.program_file(fw_fn) break except Exception as e: time.sleep(0.5) exc = e # todo log exception info from each failed iteration else: local_critical("problem flashing firmware:", exc) connect_and_flash() local_print(None, "After update version check...") for _ in range(100): try: client = find(serial) new_ver = client.solo_version() local_print( f"New Firmware version: {new_ver[0]}.{new_ver[1]}.{new_ver[2]}" ) break # expected until the devices comes up again except OSError: continue # unexpected... except Exception as e: local_print("unexpected error", e) break local_print( "Congratulations, your key was updated to the latest firmware.") logger.debug("Finishing session {}".format(datetime.now())) local_print("Log saved to: {}".format(LOG_FN))
def bootloader_version(serial): """Version of bootloader.""" from pynitrokey.fido2 import find p = find(serial) local_print(".".join(map(str, p.bootloader_version())))
def raw(serial): """Output raw entropy endlessly.""" p = nkfido2.find(serial) while True: r = p.get_rng(255) sys.stdout.buffer.write(r)
def verify(serial, udp): """Verify key is valid Nitrokey 'Start' or 'FIDO2' key.""" #if not pin: # pin = AskUser("PIN required: ", repeat=0, hide_input=True).ask() # Any longer and this needs to go in a submodule local_print("please press the button on your Nitrokey key") cert = None try: cert = nkfido2.find(serial, udp=udp).make_credential() except Fido2ClientError as e: cause = str(e.cause) # error 0x31 if "PIN_INVALID" in cause: local_critical( "your key has a different PIN. Please try to remember it :)", e) # error 0x34 (power cycle helps) if "PIN_AUTH_BLOCKED" in cause: local_critical( "your key's PIN auth is blocked due to too many incorrect attempts.", "please plug it out and in again, then again!", "please be careful, after too many incorrect attempts, ", " the key will fully block.", e) # error 0x32 (only reset helps) if "PIN_BLOCKED" in cause: local_critical("your key's PIN is blocked. ", "to use it again, you need to fully reset it.", "you can do this using: `nitropy fido2 reset`", e) # error 0x01 if "INVALID_COMMAND" in cause: local_critical( "error getting credential, is your key in bootloader mode?", "try: `nitropy fido2 util program aux leave-bootloader`", e) # pin required error if "PIN required" in str(e): local_critical( "your key has a PIN set - pass it using `--pin <PIN>`", e) local_critical("unexpected Fido2Client (CTAP) error", e) except Exception as e: local_critical("unexpected error", e) hashdb = { b'd7a23679007fe799aeda4388890f33334aba4097bb33fee609c8998a1ba91bd3': "Nitrokey FIDO2 1.x", b'6d586c0b00b94148df5b54f4a866acd93728d584c6f47c845ac8dade956b12cb': "Nitrokey FIDO2 2.x", b'e1f40563be291c30bc3cc381a7ef46b89ef972bdb048b716b0a888043cf9072a': "Nitrokey FIDO2 Dev 2.x ", } dev_fingerprint = cert.fingerprint(hashes.SHA256()) a_hex = binascii.b2a_hex(dev_fingerprint) if a_hex in hashdb: local_print(f"found device: {hashdb[a_hex]}") else: local_print(f"unknown fingerprint! {a_hex}")
def wink(serial, udp): """Send wink command to key (blinks LED a few times).""" nkfido2.find(serial, udp=udp).wink()