Ejemplo n.º 1
0
def test(ctx: Context, pin: Optional[str]) -> None:
    """Run some tests on all connected Nitrokey 3 devices."""
    from .test import TestContext, log_devices, log_system, run_tests

    log_system()
    devices = ctx.list()

    if len(devices) == 0:
        log_devices()
        raise CliException("No connected Nitrokey 3 devices found")

    local_print(f"Found {len(devices)} Nitrokey 3 device(s):")
    for device in devices:
        local_print(f"- {device.name} at {device.path}")

    results = []
    test_ctx = TestContext(pin=pin)
    for device in devices:
        results.append(run_tests(test_ctx, device))

    n = len(devices)
    success = sum(results)
    failure = n - success
    local_print("")
    local_print(
        f"Summary: {n} device(s) tested, {success} successful, {failure} failed"
    )

    if failure > 0:
        local_print("")
        raise CliException(f"Test failed for {failure} device(s)")
Ejemplo n.º 2
0
def challenge_response(serial, host, user, prompt, credential_id, challenge,
                       udp):
    """(EXPERIMENTAL)  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 ""`.
    """

    local_print("EXPERIMENTAL: Currently disabled: challenge-response")
    return

    nkfido2.hmac_secret.simple_secret(credential_id,
                                      challenge,
                                      host=host,
                                      user_id=user,
                                      serial=serial,
                                      prompt=prompt,
                                      output=True,
                                      udp=udp)
Ejemplo n.º 3
0
def rng(ctx: Context, length: int) -> None:
    """Generate random data on the key."""
    with ctx.connect_device() as device:
        while length > 0:
            rng = device.rng()
            local_print(rng[:length].hex())
            length -= len(rng)
Ejemplo n.º 4
0
def _print_download_warning(
    release_version: Version,
    current_version: Optional[Version] = None,
) -> None:
    current_version_str = str(
        current_version) if current_version else "[unknown]"
    local_print(f"Current firmware version:  {current_version_str}")
    local_print(f"Latest firmware version:   {release_version}")

    if current_version and current_version > release_version:
        raise CliException(
            "The latest firmare release is older than the firmware on the device.",
            support_hint=False,
        )
    elif current_version and current_version == release_version:
        click.confirm(
            "You are already running the latest firmware release on the device.  Do you want "
            f"to continue and download the firmware version {release_version} anyway?",
            abort=True,
        )
    else:
        click.confirm(
            f"Do you want to download the firmware version {release_version}?",
            default=True,
            abort=True,
        )
Ejemplo n.º 5
0
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())
Ejemplo n.º 6
0
def _print_update_warning() -> None:
    local_print("")
    local_print(
        "Please do not remove the Nitrokey 3 or insert any other Nitrokey 3 devices "
        "during the update. Doing so may damage the Nitrokey 3.")
    if not click.confirm("Do you want to perform the firmware update now?"):
        logger.info("Update cancelled by user")
        raise click.Abort()
Ejemplo n.º 7
0
def progress_func(x):
    x = x * 100
    if x == 0:
        progress_func.last = 0

    if progress_func.last * 10 <= x < 100:
        progress_func.last += 1
        local_print(f"Progress: {round(x, 2)}%\r", end="", flush=True)
Ejemplo n.º 8
0
def sign(verifying_key, app_hex, output_json, end_page):
    """Signs a fw-hex file, outputs a .json file that can be used for signed update."""

    msg = pynitrokey.fido2.operations.sign_firmware(
        verifying_key, app_hex, APPLICATION_END_PAGE=end_page)
    local_print(f"Saving signed firmware to: {output_json}")
    with open(output_json, "wb+") as fh:
        fh.write(json.dumps(msg).encode())
Ejemplo n.º 9
0
def show_kdf_details(passwd):
    gnuk = None
    try:
        gnuk = get_gnuk_device(logger=logger, verbose=True)
    except ValueError as e:
        local_print("Connection error", e)
        if "No ICC present" in str(e):
            print("Cannot connect to device. Closing other open connections.")
            kill_smartcard_services()
            return
        else:
            raise
    gnuk.cmd_select_openpgp()
    # Compute passwd data
    try:
        kdf_data = gnuk.cmd_get_data(0x00, 0xF9).tobytes()
    except:
        kdf_data = b""
    if kdf_data == b"":
        print("KDF not set")
        # passwd_data = passwd.encode('UTF-8')
    else:
        (
            algo,
            subalgo,
            iters,
            salt_user,
            salt_reset,
            salt_admin,
            hash_user,
            hash_admin,
        ) = parse_kdf_data(kdf_data)
        if salt_admin:
            salt = salt_admin
        else:
            salt = salt_user
        d = {
            "algo": algo,
            "subalgo": subalgo,
            "iters": iters,
            "salt_user": binascii.b2a_hex(salt_user),
            "salt_reset": binascii.b2a_hex(salt_reset),
            "salt_admin": binascii.b2a_hex(salt_admin),
            "hash_user": binascii.b2a_hex(hash_user),
            "hash_admin": binascii.b2a_hex(hash_admin),
        }
        pprint(d, width=100)
        if passwd:
            try:
                passwd_data = kdf_calc(passwd, salt, iters)
                print(f"passwd_data: {binascii.b2a_hex(passwd_data)}")
            except ValueError as e:
                local_print("Error getting KDF", e)
        else:
            print("Provide password to calculate final hash")
Ejemplo n.º 10
0
def _reboot_to_bootloader(device: Nitrokey3Device) -> None:
    local_print(
        "Please press the touch button to reboot the device into bootloader mode ..."
    )
    try:
        device.reboot(BootMode.BOOTROM)
    except TimeoutException:
        raise CliException(
            "The reboot was not confirmed with the touch button.",
            support_hint=False,
        )
Ejemplo n.º 11
0
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!"))
Ejemplo n.º 12
0
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()}")
Ejemplo n.º 13
0
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)
Ejemplo n.º 14
0
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)})")
Ejemplo n.º 15
0
def make_credential(serial, host, user, udp, prompt):
    """(EXPERIMENTAL) Generate a credential.

    Pass `--prompt ""` to output only the `credential_id` as hex.
    """

    local_print("EXPERIMENTAL: use with care, not a fully supported function")
    nkfido2.hmac_secret.make_credential(host=host,
                                        user_id=user,
                                        serial=serial,
                                        output=True,
                                        prompt=prompt,
                                        udp=udp)
Ejemplo n.º 16
0
def set_identity(identity):
    """set given identity (one of: 0, 1, 2)"""
    if not identity.isdigit():
        local_critical("identity number must be a digit")

    identity = int(identity)
    if identity < 0 or identity > 2:
        local_print("identity must be 0, 1 or 2")

    local_print(f"Setting identity to {identity}")
    for x in range(3):
        try:
            gnuk = get_gnuk_device()
            gnuk.cmd_select_openpgp()
            try:
                gnuk.cmd_set_identity(identity)
            except USBError:
                local_print(f"reset done - now active identity: {identity}")
                break

        except ValueError as e:
            if "No ICC present" in str(e):
                local_print("Could not connect to device, trying to close scdaemon")
                result = check_output(["gpg-connect-agent",
                                       "SCD KILLSCD", "SCD BYE",
                                       "/bye"])  # gpgconf --kill all might be better?
                sleep(3)
            else:
                local_critical(e)
        except Exception as e:
            local_critical(e)
Ejemplo n.º 17
0
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!")
Ejemplo n.º 18
0
def genkey(input_seed_file, output_pem_file):
    """Generates key pair that can be used for Solo signed firmware updates.

    \b
    * Generates NIST P256 keypair.
    * Public key must be copied into correct source location in solo bootloader
    * The private key can be used for signing updates.
    * You may optionally supply a file to seed the RNG for key generating.
    """

    vk = pynitrokey.fido2.operations.genkey(output_pem_file,
                                            input_seed_file=input_seed_file)

    local_print(
        "Public key in various formats:", None, [c for c in vk.to_string()],
        None, "".join(["%02x" % c for c in vk.to_string()]), None,
        '"\\x' + "\\x".join(["%02x" % c for c in vk.to_string()]) + '"', None)
Ejemplo n.º 19
0
def get_firmware_file(file_name: str, type: FirmwareType):
    if file_name:
        with open(file_name, "rb") as f:
            firmware_data = f.read()
        local_print("- {}: {}".format(file_name, len(firmware_data)))
        return firmware_data

    tag = get_latest_release_data()["tag_name"]
    url = FIRMWARE_URL.get(type, None).format(tag)  # type: ignore
    firmware_data = download_file_or_exit(url)
    hash_data = hash_data_512(firmware_data)
    hash_valid = "valid" if validate_hash(url, hash_data) else "invalid"

    local_print(
        f"- {type}: {len(firmware_data)}, "
        f"hash: ...{hash_data[-8:]} {hash_valid} (from ...{url[-24:]})")
    return firmware_data
Ejemplo n.º 20
0
def fetch_update(path: str, force: bool, version: Optional[str]) -> None:
    """
    Fetches a firmware update for the Nitrokey 3 and stores it at the given path.

    If no path is given, the firmware image stored in the current working
    directory.  If the given path is a directory, the image is stored under
    that directory.  Otherwise it is written to the path.  Existing files are
    only overwritten if --force is set.

    Per default, the latest firmware release is fetched.  If you want to
    download a specific version, use the --version option.
    """
    try:
        update = get_repo().get_update_or_latest(version)
    except Exception as e:
        if version:
            raise CliException(f"Failed to find firmware update {version}", e)
        else:
            raise CliException("Failed to find latest firmware update", e)

    bar = DownloadProgressBar(desc=update.tag)

    try:
        if os.path.isdir(path):
            path = update.download_to_dir(path,
                                          overwrite=force,
                                          callback=bar.update)
        else:
            if not force and os.path.exists(path):
                raise OverwriteError(path)
            with open(path, "wb") as f:
                update.download(f, callback=bar.update)

        bar.close()

        local_print(
            f"Successfully downloaded firmware release {update.tag} to {path}")
    except OverwriteError as e:
        raise CliException(
            f"{e.path} already exists.  Use --force to overwrite the file.",
            support_hint=False,
        )
    except Exception as e:
        raise CliException(f"Failed to download firmware update {update.tag}",
                           e)
Ejemplo n.º 21
0
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)
Ejemplo n.º 22
0
    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
Ejemplo n.º 23
0
def _print_version_warning(
    metadata: FirmwareMetadata,
    current_version: Optional[Version] = None,
) -> None:
    current_version_str = str(
        current_version) if current_version else "[unknown]"
    local_print(f"Current firmware version:  {current_version_str}")
    local_print(f"Updated firmware version:  {metadata.version}")

    if current_version:
        if current_version > metadata.version:
            raise CliException(
                "The firmware image is older than the firmware on the device.",
                support_hint=False,
            )
        elif current_version == metadata.version:
            if not click.confirm(
                    "The version of the firmware image is the same as on the device.  Do you want "
                    "to continue anyway?"):
                raise click.Abort()
Ejemplo n.º 24
0
def list():
    """List all 'Nitrokey FIDO2' devices"""
    devs = nkfido2.find_all()
    local_print(":: 'Nitrokey FIDO2' keys")
    for c in devs:
        descr = c.dev.descriptor

        if hasattr(descr, "product_name"):
            name = descr.product_name
        elif c.is_solo_bootloader():
            name = "FIDO2 Bootloader device"
        else:
            name = "FIDO2 device"

        if hasattr(descr, "serial_number"):
            id_ = descr.serial_number
        else:
            id_ = descr.path

        local_print(f"{id_}: {name}")
Ejemplo n.º 25
0
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)
Ejemplo n.º 26
0
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)."
        )
Ejemplo n.º 27
0
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}")
Ejemplo n.º 28
0
def list() -> None:
    """List all Nitrokey 3 devices."""
    local_print(":: 'Nitrokey 3' keys")
    for device in list_nk3():
        with device as device:
            uuid = device.uuid()
            if uuid:
                local_print(f"{device.path}: {device.name} {device.uuid():X}")
            else:
                local_print(f"{device.path}: {device.name}")
Ejemplo n.º 29
0
    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)
Ejemplo n.º 30
0
def list():
    """List all 'Nitrokey FIDO2' devices"""
    solos = nkfido2.find_all()
    local_print(":: 'Nitrokey FIDO2' keys")
    for c in solos:
        devdata = c.dev.descriptor
        if "serial_number" in devdata:
            local_print(
                f"{devdata['serial_number']}: {devdata['product_string']}")
        else:
            local_print(f"{devdata['path']}: {devdata['product_string']}")