Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-pynitrokey for openSUSE:Factory checked in at 2025-08-07 16:48:45 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pynitrokey (Old) and /work/SRC/openSUSE:Factory/.python-pynitrokey.new.1085 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pynitrokey" Thu Aug 7 16:48:45 2025 rev:20 rq:1298114 version:0.10.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pynitrokey/python-pynitrokey.changes 2025-07-24 18:53:42.350073580 +0200 +++ /work/SRC/openSUSE:Factory/.python-pynitrokey.new.1085/python-pynitrokey.changes 2025-08-07 16:50:05.495516122 +0200 @@ -1,0 +2,12 @@ +Wed Aug 6 05:20:00 UTC 2025 - Johannes Kastl <opensuse_buildserv...@ojkastl.de> + +- update to 0.10.0: + * Remove obsolete nkfido2 commands by @robin-nitrokey in #670 + * Remove ecdsa dependency by @robin-nitrokey in #671 + * PIV: validate PINs to be 6 to 8 numbers by @sosthene-nitrokey + in #675 + * Update Nitrokey SDK by @robin-nitrokey in #672 + * nk3: Deprecate secrets get and register aliases by + @robin-nitrokey in #673 + +------------------------------------------------------------------- Old: ---- pynitrokey-0.9.3.tar.gz New: ---- pynitrokey-0.10.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pynitrokey.spec ++++++ --- /var/tmp/diff_new_pack.d5n3If/_old 2025-08-07 16:50:06.091541109 +0200 +++ /var/tmp/diff_new_pack.d5n3If/_new 2025-08-07 16:50:06.091541109 +0200 @@ -1,7 +1,7 @@ # # spec file for package python-pynitrokey # -# Copyright (c) 2025 SUSE LLC +# Copyright (c) 2025 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-pynitrokey -Version: 0.9.3 +Version: 0.10.0 Release: 0 Summary: Python Library for Nitrokey devices License: Apache-2.0 OR MIT @@ -28,48 +28,41 @@ Source2: LICENSE-APACHE BuildRequires: %{python_module pip} BuildRequires: %{python_module poetry-core} +BuildRequires: fdupes +BuildRequires: python-rpm-macros # BuildRequires: %{python_module cffi} -BuildRequires: %{python_module click >= 8.1.6 with %python-click < 9} -BuildRequires: %{python_module click-aliases >= 1.0.5 with %python-click-aliases < 2} +BuildRequires: %{python_module click >= 8.2 with %python-click < 9} BuildRequires: %{python_module cryptography >= 43 with %python-cryptography < 46} -BuildRequires: %{python_module ecdsa} BuildRequires: %{python_module fido2 >= 2 with %python-fido2 < 3} # https://github.com/Nitrokey/pynitrokey/issues/601 BuildRequires: %{python_module hidapi >= 0.14.0.post1 with %python-hidapi < 0.14.0.post4} BuildRequires: %{python_module nethsm >= 1.4.0 with %python-nethsm < 2} -BuildRequires: %{python_module nitrokey >= 0.3.1 with %python-nitrokey < 0.4} +BuildRequires: %{python_module nitrokey >= 0.4.0 with %python-nitrokey < 0.5} BuildRequires: %{python_module nkdfu} -BuildRequires: %{python_module protobuf >= 3.17.3} BuildRequires: %{python_module pyusb} BuildRequires: %{python_module requests} BuildRequires: %{python_module semver} BuildRequires: %{python_module tlv8} BuildRequires: %{python_module tqdm} -BuildRequires: fdupes BuildRequires: intelhex -BuildRequires: python-rpm-macros # SECTION test BuildRequires: %{python_module pytest} # /SECTION Requires: intelhex Requires: python-cffi -Requires: python-click >= 8.1.6 -Requires: python-ecdsa +Requires: python-click >= 8.2 Requires: python-nkdfu -Requires: python-protobuf >= 3.17.3 Requires: python-pyusb Requires: python-requests Requires: python-semver Requires: python-tlv8 Requires: python-tqdm -Requires: (python-click-aliases >= 1.0.5 with python-click-aliases < 2) -Requires: (python-cryptography >= 41.0.4 with python-cryptography < 45) +Requires: (python-cryptography >= 43 with python-cryptography < 46) Requires: (python-fido2 >= 2 with python-fido2 < 3) Requires: (python-hidapi >= 0.14.0.post1 with python-hidapi < 0.14.0.post4) Requires: (python-nethsm >= 1.4.0 with python-nethsm < 2) -Requires: (python-nitrokey >= 0.3.1 with python-nitrokey < 0.4) -Requires: (python-spsdk >= 2.0 with python-spsdk < 2.2) +Requires: (python-nitrokey >= 0.4.0 with python-nitrokey < 0.5) Requires(post): update-alternatives Requires(postun): update-alternatives BuildArch: noarch ++++++ pynitrokey-0.9.3.tar.gz -> pynitrokey-0.10.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pynitrokey-0.9.3/PKG-INFO new/pynitrokey-0.10.0/PKG-INFO --- old/pynitrokey-0.9.3/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 +++ new/pynitrokey-0.10.0/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.3 Name: pynitrokey -Version: 0.9.3 +Version: 0.10.0 Summary: Python client for Nitrokey devices License: Apache-2.0 OR MIT Author: Nitrokey @@ -16,16 +16,14 @@ Classifier: Programming Language :: Python :: 3.13 Provides-Extra: pcsc Requires-Dist: cffi -Requires-Dist: click (>=8.1.6,<9) -Requires-Dist: click-aliases (>=1.0.5,<2) +Requires-Dist: click (>=8.2,<9) Requires-Dist: cryptography (>=43,<46) -Requires-Dist: ecdsa Requires-Dist: fido2 (>=2,<3) Requires-Dist: hidapi (>=0.14,<0.15) Requires-Dist: hidapi (>=0.14.0.post1,<0.14.0.post4) ; sys_platform == "linux" Requires-Dist: intelhex Requires-Dist: nethsm (>=1.4.0,<2) -Requires-Dist: nitrokey (>=0.3.1,<0.4) +Requires-Dist: nitrokey (>=0.4,<0.5) Requires-Dist: nkdfu Requires-Dist: pyscard (>=2.0.0,<3) ; extra == "pcsc" Requires-Dist: pyusb diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pynitrokey-0.9.3/pynitrokey/cli/__init__.py new/pynitrokey-0.10.0/pynitrokey/cli/__init__.py --- old/pynitrokey-0.9.3/pynitrokey/cli/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pynitrokey-0.10.0/pynitrokey/cli/__init__.py 1970-01-01 01:00:00.000000000 +0100 @@ -13,7 +13,6 @@ import click import pynitrokey -import pynitrokey.fido2.operations from pynitrokey.cli.exceptions import CliException from pynitrokey.cli.fido2 import fido2 from pynitrokey.cli.nethsm import nethsm @@ -61,7 +60,6 @@ pymodules = [ "pynitrokey", "cryptography", - "ecdsa", "fido2", "pyusb", ] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pynitrokey-0.9.3/pynitrokey/cli/nethsm.py new/pynitrokey-0.10.0/pynitrokey/cli/nethsm.py --- old/pynitrokey-0.9.3/pynitrokey/cli/nethsm.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pynitrokey-0.10.0/pynitrokey/cli/nethsm.py 1970-01-01 01:00:00.000000000 +0100 @@ -27,7 +27,7 @@ def __iter__(self) -> Iterator[Enum]: ... -def make_enum_type(enum_cls: EnumMeta) -> click.Choice: +def make_enum_type(enum_cls: EnumMeta) -> click.Choice[Enum]: return click.Choice([variant.value for variant in enum_cls], case_sensitive=False) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pynitrokey-0.9.3/pynitrokey/cli/nk3/__init__.py new/pynitrokey-0.10.0/pynitrokey/cli/nk3/__init__.py --- old/pynitrokey-0.9.3/pynitrokey/cli/nk3/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pynitrokey-0.10.0/pynitrokey/cli/nk3/__init__.py 1970-01-01 01:00:00.000000000 +0100 @@ -6,11 +6,12 @@ import click from nitrokey.nk3 import NK3, NK3Bootloader -from nitrokey.nk3.updates import Warning from nitrokey.trussed import Model, TrussedBase +from nitrokey.trussed.updates import Warning from pynitrokey.cli import trussed from pynitrokey.cli.exceptions import CliException +from pynitrokey.cli.trussed import print_status from pynitrokey.cli.trussed.test import TestCase from pynitrokey.helpers import local_critical, local_print @@ -126,9 +127,10 @@ from .update import update as exec_update ignore_warnings = frozenset([Warning.from_str(s) for s in ignore_warning]) - exec_update( + update_to_version, status = exec_update( ctx, image, version, ignore_pynitrokey_version, ignore_warnings, confirm ) + print_status(update_to_version, status) @nk3.command() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pynitrokey-0.9.3/pynitrokey/cli/nk3/piv.py new/pynitrokey-0.10.0/pynitrokey/cli/nk3/piv.py --- old/pynitrokey-0.9.3/pynitrokey/cli/nk3/piv.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pynitrokey-0.10.0/pynitrokey/cli/nk3/piv.py 1970-01-01 01:00:00.000000000 +0100 @@ -251,6 +251,11 @@ help="New PIN.", ) def change_pin(current_pin: str, new_pin: str) -> None: + if len(new_pin) > 8 or len(new_pin) < 6 or not new_pin.isdigit(): + local_critical( + "PIV application PIN must consist of 6 to 8 numeric characters", + support_hint=False, + ) device = PivApp() device.change_pin(current_pin, new_pin) local_print("Changed pin successfully") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pynitrokey-0.9.3/pynitrokey/cli/nk3/secrets.py new/pynitrokey-0.10.0/pynitrokey/cli/nk3/secrets.py --- old/pynitrokey-0.9.3/pynitrokey/cli/nk3/secrets.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pynitrokey-0.10.0/pynitrokey/cli/nk3/secrets.py 1970-01-01 01:00:00.000000000 +0100 @@ -7,10 +7,9 @@ import sys import typing from base64 import b32decode -from typing import Callable, List, Optional +from typing import Any, Callable, List, Optional import click -from click_aliases import ClickAliasedGroup from nitrokey.nk3.secrets_app import ( ALGORITHM_TO_KIND, STRING_TO_KIND, @@ -24,7 +23,7 @@ from pynitrokey.helpers import AskUser, b32padding, local_critical, local_print -@nk3.group(cls=ClickAliasedGroup) +@nk3.group() @click.pass_context def secrets(ctx: click.Context) -> None: """Nitrokey Secrets App. Manage OTP and Password Safe secrets on the device. @@ -175,58 +174,111 @@ local_print("Done") -@secrets.command(aliases=["register"]) +# adapted from click.decorators +_AnyCallable = Callable[..., Any] + + +def with_options( + *options: Callable[[_AnyCallable], _AnyCallable] +) -> Callable[[_AnyCallable], _AnyCallable]: + # based on https://stackoverflow.com/a/67138197 + def decorator(f: _AnyCallable) -> _AnyCallable: + for option in reversed(options): + f = option(f) + return f + + return decorator + + +add_otp_options = [ + click.argument( + "name", + type=click.STRING, + ), + click.argument( + "secret", + type=click.STRING, + ), + click.option( + "--digits-str", + "digits_str", + type=click.Choice(["6", "8"]), + help="Digits count", + default="6", + ), + click.option( + "--kind", + "kind", + type=click.Choice(choices=STRING_TO_KIND.keys(), case_sensitive=False), + help="OTP mechanism to use. Case insensitive.", + default="NOT_SET", + ), + click.option( + "--hash", + "hash", + type=click.Choice(choices=ALGORITHM_TO_KIND.keys(), case_sensitive=False), + help="Hash algorithm to use", + default="SHA1", + ), + click.option( + "--counter-start", + "counter_start", + type=click.INT, + help="Starting value for the counter (HOTP only)", + default=0, + ), + click.option( + "--touch-button", + "touch_button", + type=click.BOOL, + help="This credential requires button press before use", + is_flag=True, + ), + click.option( + "--protect-with-pin", + "pin_protection", + type=click.BOOL, + help="This credential should be additionally encrypted with a PIN, which will be required before each use", + is_flag=True, + ), +] + + +@secrets.command(deprecated="Use 'add-otp' instead.") @click.pass_obj -@click.argument( - "name", - type=click.STRING, -) -@click.argument( - "secret", - type=click.STRING, -) -@click.option( - "--digits-str", - "digits_str", - type=click.Choice(["6", "8"]), - help="Digits count", - default="6", -) -@click.option( - "--kind", - "kind", - type=click.Choice(choices=STRING_TO_KIND.keys(), case_sensitive=False), # type: ignore[arg-type] - help="OTP mechanism to use. Case insensitive.", - default="NOT_SET", -) -@click.option( - "--hash", - "hash", - type=click.Choice(choices=ALGORITHM_TO_KIND.keys(), case_sensitive=False), # type: ignore[arg-type] - help="Hash algorithm to use", - default="SHA1", -) -@click.option( - "--counter-start", - "counter_start", - type=click.INT, - help="Starting value for the counter (HOTP only)", - default=0, -) -@click.option( - "--touch-button", - "touch_button", - type=click.BOOL, - help="This credential requires button press before use", - is_flag=True, -) -@click.option( - "--protect-with-pin", - "pin_protection", - type=click.BOOL, - help="This credential should be additionally encrypted with a PIN, which will be required before each use", - is_flag=True, -) +@with_options(*add_otp_options) +def register( + ctx: Context, + name: str, + secret: str, + digits_str: str, + kind: str, + hash: str, + counter_start: int, + touch_button: bool, + pin_protection: bool, +) -> None: + """Register OTP credential. + + Write credential under the NAME. + Secret should be base32 encoded. + """ + add_otp_impl( + ctx, + name, + secret, + digits_str, + kind, + hash, + counter_start, + touch_button, + pin_protection, + ) + + +@secrets.command() +@click.pass_obj +@with_options(*add_otp_options) def add_otp( ctx: Context, name: str, @@ -243,6 +295,30 @@ Write credential under the NAME. Secret should be base32 encoded. """ + add_otp_impl( + ctx, + name, + secret, + digits_str, + kind, + hash, + counter_start, + touch_button, + pin_protection, + ) + + +def add_otp_impl( + ctx: Context, + name: str, + secret: str, + digits_str: str, + kind: str, + hash: str, + counter_start: int, + touch_button: bool, + pin_protection: bool, +) -> None: otp_kind = STRING_TO_KIND[kind.upper()] if not secret: raise click.ClickException("Please provide secret for the OTP to work") @@ -463,26 +539,44 @@ local_print("Done") -@secrets.command(aliases=["get"]) +get_otp_options = [ + click.argument( + "name", + type=click.STRING, + ), + click.option( + "--timestamp", + "timestamp", + type=click.INT, + help="The timestamp to use instead of the local time (TOTP only)", + default=0, + ), + click.option( + "--period", + "period", + type=click.INT, + help="The period to use in seconds (TOTP only)", + default=30, + ), +] + + +@secrets.command(deprecated="Use 'get-otp' instead.") @click.pass_obj -@click.argument( - "name", - type=click.STRING, -) -@click.option( - "--timestamp", - "timestamp", - type=click.INT, - help="The timestamp to use instead of the local time (TOTP only)", - default=0, -) -@click.option( - "--period", - "period", - type=click.INT, - help="The period to use in seconds (TOTP only)", - default=30, -) +@with_options(*get_otp_options) +def get( + ctx: Context, + name: str, + timestamp: int, + period: int, +) -> None: + """Generate OTP code from registered credential.""" + get_otp_impl(ctx, name, timestamp, period) + + +@secrets.command() +@click.pass_obj +@with_options(*get_otp_options) def get_otp( ctx: Context, name: str, @@ -490,6 +584,15 @@ period: int, ) -> None: """Generate OTP code from registered credential.""" + get_otp_impl(ctx, name, timestamp, period) + + +def get_otp_impl( + ctx: Context, + name: str, + timestamp: int, + period: int, +) -> None: # TODO: for TOTP get the time from a timeserver via NTP, instead of the local clock from datetime import datetime diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pynitrokey-0.9.3/pynitrokey/cli/nk3/update.py new/pynitrokey-0.10.0/pynitrokey/cli/nk3/update.py --- old/pynitrokey-0.9.3/pynitrokey/cli/nk3/update.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pynitrokey-0.10.0/pynitrokey/cli/nk3/update.py 1970-01-01 01:00:00.000000000 +0100 @@ -7,8 +7,9 @@ from typing import Any, Callable, Iterator, List, Optional from click import Abort -from nitrokey.nk3.updates import Updater, UpdateUi, Warning -from nitrokey.trussed import Version +from nitrokey.trussed import Model, TrussedBootloader, TrussedDevice, Version +from nitrokey.trussed.admin_app import Status +from nitrokey.trussed.updates import DeviceHandler, Updater, UpdateUi, Warning from pynitrokey.cli.exceptions import CliException from pynitrokey.cli.nk3 import Context @@ -142,6 +143,24 @@ self._version_printed = True +class ContextDeviceHandler(DeviceHandler): + def __init__(self, ctx: Context) -> None: + self.ctx = ctx + + def await_bootloader(self, model: Model) -> TrussedBootloader: + assert model == self.ctx.model + return self.ctx.await_bootloader() + + def await_device( + self, + model: Model, + wait_retries: Optional[int], + callback: Optional[Callable[[int, int], None]], + ) -> TrussedDevice: + assert model == self.ctx.model + return self.ctx.await_device(wait_retries, callback) + + def update( ctx: Context, image: Optional[str], @@ -149,12 +168,11 @@ ignore_pynitrokey_version: bool, ignore_warnings: Set[Warning], confirm_continue: bool, -) -> Version: +) -> tuple[Version, Status]: with ctx.connect() as device: updater = Updater( ui=UpdateCli(confirm_continue), - await_bootloader=ctx.await_bootloader, - await_device=ctx.await_device, + device_handler=ContextDeviceHandler(ctx), ignore_warnings=ignore_warnings, ) return updater.update(device, image, version, ignore_pynitrokey_version) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pynitrokey-0.9.3/pynitrokey/cli/nkfido2.py new/pynitrokey-0.10.0/pynitrokey/cli/nkfido2.py --- old/pynitrokey-0.9.3/pynitrokey/cli/nkfido2.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pynitrokey-0.10.0/pynitrokey/cli/nkfido2.py 1970-01-01 01:00:00.000000000 +0100 @@ -2,13 +2,12 @@ # Copyright Nitrokey GmbH # SPDX-License-Identifier: Apache-2.0 OR MIT -import json import os import platform import struct import sys import time -from typing import List, Optional +from typing import Optional if "linux" in platform.platform().lower(): import fcntl @@ -19,10 +18,10 @@ from fido2.hid import CtapHidDevice import pynitrokey.fido2 as nkfido2 -import pynitrokey.fido2.operations from pynitrokey.cli.monitor import monitor from pynitrokey.cli.program import program from pynitrokey.cli.update import update +from pynitrokey.exceptions import NoSoloFoundError from pynitrokey.helpers import local_critical, local_print @@ -38,110 +37,6 @@ pass -# @todo: is this working as intended? -@click.command() -@click.option("--input-seed-file") -@click.argument("output_pem_file") -def genkey(input_seed_file: Optional[str], output_pem_file: str) -> None: - """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, - ) - - -# @todo: is this working as intended ? -@click.command() -@click.argument("verifying-key") -@click.argument("app-hex") -@click.argument("output-json") -@click.option("--pages", default=128, type=int, help="Size of the MCU flash in pages") -@click.option( - "--end_page", - help="Set APPLICATION_END_PAGE. Shall be in sync with firmware settings", - default=20, - type=int, -) -def sign( - verifying_key: str, app_hex: str, output_json: str, end_page: int, pages: int -) -> None: - """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, PAGES=pages - ) - local_print(f"Saving signed firmware to: {output_json}") - with open(output_json, "wb+") as fh: - fh.write(json.dumps(msg).encode()) - - -@click.command() -@click.option("--attestation-key", help="attestation key in hex") -@click.option("--attestation-cert", help="attestation certificate file") -@click.option( - "--lock", - help="Indicate to lock device from unsigned changes permanently.", - default=False, - is_flag=True, -) -@click.argument("input_hex_files", nargs=-1) -@click.argument("output_hex_file") -@click.option( - "--end_page", - help="Set APPLICATION_END_PAGE. Should be in sync with firmware settings.", - default=20, - type=int, -) -@click.option( - "--pages", - help="Set MCU flash size in pages. Should be in sync with firmware settings.", - default=128, - type=int, -) -def mergehex( - attestation_key: Optional[bytes], - attestation_cert: Optional[bytes], - lock: bool, - input_hex_files: List[str], - output_hex_file: str, - end_page: int, - pages: int, -) -> None: - """Merges hex files, and patches in the attestation key. - - \b - If no attestation key is passed, uses default Solo Hacker one. - Note that later hex files replace data of earlier ones, if they overlap. - """ - pynitrokey.fido2.operations.mergehex( - input_hex_files, - output_hex_file, - attestation_key=attestation_key, - APPLICATION_END_PAGE=end_page, - attestation_cert=attestation_cert, - lock=lock, - PAGES=pages, - ) - - @click.command() def list() -> None: """List all 'Nitrokey FIDO2' devices""" @@ -301,13 +196,13 @@ locked = "unlocked" local_print(f"{major}.{minor}.{patch} {locked}") - except pynitrokey.exceptions.NoSoloFoundError: + except NoSoloFoundError: local_critical( "No Nitrokey found.", "If you are on Linux, are your udev rules up to date?" ) # unused ??? - except (pynitrokey.exceptions.NoSoloFoundError, ApduError): + except (NoSoloFoundError, ApduError): local_critical( "Firmware is out of date (key does not know the NITROKEY_VERSION command)." ) @@ -340,11 +235,6 @@ rng.add_command(feedkernel) util.add_command(program) - -# used for fw-signing... (does not seem to work @fixme) -util.add_command(sign) -util.add_command(genkey) -util.add_command(mergehex) util.add_command(monitor) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pynitrokey-0.9.3/pynitrokey/cli/trussed/__init__.py new/pynitrokey-0.10.0/pynitrokey/cli/trussed/__init__.py --- old/pynitrokey-0.9.3/pynitrokey/cli/trussed/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pynitrokey-0.10.0/pynitrokey/cli/trussed/__init__.py 1970-01-01 01:00:00.000000000 +0100 @@ -9,9 +9,7 @@ import click from cryptography import x509 -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey -from ecdsa import NIST256p, SigningKey +from cryptography.hazmat.primitives.asymmetric import ec from nitrokey.trussed import ( FirmwareContainer, Model, @@ -19,9 +17,11 @@ TrussedBase, TrussedBootloader, TrussedDevice, + Version, parse_firmware_image, + updates, ) -from nitrokey.trussed.admin_app import BootMode +from nitrokey.trussed.admin_app import BootMode, InitStatus, Status from nitrokey.trussed.provisioner_app import ProvisionerApp from nitrokey.updates import OverwriteError @@ -29,6 +29,7 @@ from pynitrokey.helpers import ( DownloadProgressBar, Retries, + local_critical, local_print, require_windows_admin, ) @@ -182,8 +183,9 @@ download a specific version, use the --version option. """ try: - release = ctx.model.firmware_repository.get_release_or_latest(version) - update = release.require_asset(ctx.model.firmware_pattern) + firmware_repository = updates.get_firmware_repository(ctx.model) + release = firmware_repository.get_release_or_latest(version) + update = updates.get_firmware_update(ctx.model, release) except Exception as e: if version: raise CliException(f"Failed to find firmware update {version}", e) @@ -265,21 +267,16 @@ if len(key) != 36: raise CliException(f"Invalid key length {len(key)} (expected 36)") - ecdsa_key = SigningKey.from_string(key[4:], curve=NIST256p) - pem_pubkey = serialization.load_pem_public_key( - ecdsa_key.get_verifying_key().to_pem() - ) - + ec_key = ec.derive_private_key(int(key[4:].hex(), 16), ec.SECP256R1()) + ec_pubkey = ec_key.public_key() x509_cert = x509.load_der_x509_certificate(cert) cert_pubkey = x509_cert.public_key() - if not isinstance(pem_pubkey, EllipticCurvePublicKey): - raise CliException("The FIDO2 attestation key is not an EC key") - if not isinstance(cert_pubkey, EllipticCurvePublicKey): + if not isinstance(cert_pubkey, ec.EllipticCurvePublicKey): raise CliException( "The FIDO2 attestation certificate does not contain an EC key" ) - if pem_pubkey.public_numbers() != cert_pubkey.public_numbers(): + if ec_pubkey.public_numbers() != cert_pubkey.public_numbers(): raise CliException( "The FIDO2 attestation certificate does not match the public key" ) @@ -390,6 +387,25 @@ length -= len(rng) +def print_status(version: Version, status: Status) -> None: + local_print(f"Firmware version: {version}") + if status.init_status is not None: + local_print(f"Init status: {status.init_status}") + if status.ifs_blocks is not None: + local_print(f"Free blocks (int): {status.ifs_blocks}") + if status.efs_blocks is not None: + local_print(f"Free blocks (ext): {status.efs_blocks}") + if status.variant is not None: + local_print(f"Variant: {status.variant.name}") + + # Print at the end so that other status info are written + if status.init_status is not None: + if status.init_status & InitStatus.EXT_FLASH_NEED_REFORMAT: + local_critical( + "EFS is corrupted, please contact support for information on how to solve this issue" + ) + + @click.command() @click.pass_obj def status(ctx: Context[Bootloader, Device]) -> None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pynitrokey-0.9.3/pynitrokey/fido2/operations.py new/pynitrokey-0.10.0/pynitrokey/fido2/operations.py --- old/pynitrokey-0.9.3/pynitrokey/fido2/operations.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pynitrokey-0.10.0/pynitrokey/fido2/operations.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,253 +0,0 @@ -# Copyright 2019 SoloKeys Developers -# Copyright Nitrokey GmbH -# SPDX-License-Identifier: Apache-2.0 OR MIT - -import binascii -import struct -from typing import Any, List, Optional - -import ecdsa -from intelhex import IntelHex - -from pynitrokey import helpers - - -def genkey( - output_pem_file: str, input_seed_file: Optional[str] = None -) -> ecdsa.VerifyingKey: - from ecdsa import NIST256p, SigningKey - - # TODO: it looks like this is an internal API -- do we really want to use it? - # If yes, we should add type annotations. - from ecdsa.util import randrange_from_seed__trytryagain # type: ignore[import] - - if input_seed_file is not None: - seed = input_seed_file - print("using input seed file ", seed) - rng = open(seed, "rb").read() - secexp = randrange_from_seed__trytryagain(rng, NIST256p.order) - sk = SigningKey.from_secret_exponent(secexp, curve=NIST256p) - else: - sk = SigningKey.generate(curve=NIST256p) - - sk_name = output_pem_file - print(f"Signing key for signing device firmware: {sk_name}") - with open(sk_name, "wb+") as fh: - fh.write(sk.to_pem()) - - vk = sk.get_verifying_key() - - return vk - - -hacker_attestation_cert = b"".join( - [ - b"0\x82\x02\xe90\x82\x02\x8e\xa0\x03\x02\x01\x02\x02\x01\x010" - b"\n\x06\x08*\x86H\xce=\x04\x03\x020\x81\x821\x0b0\t\x06\x03U" - b"\x04\x06\x13\x02US1\x110\x0f\x06\x03U\x04\x08\x0c\x08Maryla" - b"nd1\x140\x12\x06\x03U\x04\n\x0c\x0bSOLO HACKER1\x100\x0e\x06" - b"\x03U\x04\x0b\x0c\x07Root CA1\x150\x13\x06\x03U\x04\x03\x0c" - b"\x0csolokeys.com1!0\x1f\x06\t*\x86H\x86\xf7\r\x01\t\x01\x16" - b"\x12hello@solokeys.com0 \x17\r181211022012Z\x18\x0f20681128" - b"022012Z0\x81\x941\x0b0\t\x06\x03U\x04\x06\x13\x02US1\x110\x0f" - b"\x06\x03U\x04\x08\x0c\x08Maryland1\x140\x12\x06\x03U\x04\n\x0c" - b'\x0bSOLO HACKER1"0 \x06\x03U\x04\x0b\x0c\x19Authenticator Atte' - b"station1\x150\x13\x06\x03U\x04\x03\x0c\x0csolokeys.com1!0\x1f" - b"\x06\t*\x86H\x86\xf7\r\x01\t\x01\x16\x12hello@solokeys.com0Y0" - b"\x13\x06\x07*\x86H\xce=\x02\x01\x06\x08*\x86H\xce=\x03\x01\x07" - b"\x03B\x00\x04}x\xf6\xbe\xca@v;\xc7\\\xe3\xac\xf4'\x12\xc3\x94" - b"\x98\x137\xa6A\x0e\x92\xf6\x9a;\x15G\x8d\xb6\xce\xd9\xd3O9\x13" - b"\xed\x12{\x81\x14;\xe8\xf9L\x968\xfe\xe3\xd6\xcb\x1bS\x93\xa2t" - b"\xf7\x13\x9a\x0f\x9d^\xa6\xa3\x81\xde0\x81\xdb0\x1d\x06\x03U" - b"\x1d\x0e\x04\x16\x04\x14\x9a\xfb\xa2!\t#\xb5\xe4z*\x1dzlN\x03" - b"\x89\x92\xa3\x0e\xc20\x81\xa1\x06\x03U\x1d#\x04\x81\x990\x81" - b"\x96\xa1\x81\x88\xa4\x81\x850\x81\x821\x0b0\t\x06\x03U\x04\x06" - b"\x13\x02US1\x110\x0f\x06\x03U\x04\x08\x0c\x08Maryland1\x140\x12" - b"\x06\x03U\x04\n\x0c\x0bSOLO HACKER1\x100\x0e\x06\x03U\x04\x0b\x0c" - b"\x07Root CA1\x150\x13\x06\x03U\x04\x03\x0c\x0csolokeys.com1!0\x1f" - b"\x06\t*\x86H\x86\xf7\r\x01\t\x01\x16\x12he...@solokeys.com\x82\t" - b"\x00\xeb\xd4\x84P\x14\xab\xd1W0\t\x06\x03U\x1d\x13\x04\x020\x000" - b"\x0b\x06\x03U\x1d\x0f\x04\x04\x03\x02\x04\xf00\n\x06\x08*\x86H\xce=" - b"\x04\x03\x02\x03I\x000F\x02!\x00\xa1{*\x1dNB\xa8hmea\x1e\xf5\xfem" - b"\xc6\x99\xae| \x83\x16\xba\xd6\xe5\x0f\xd7\r~\x05\xda\xc9\x02!\x00" - b"\x92I\xf3\x0bW\xd1\x19r\xf2uZ\xa2\xe0\xb6\xbd\x0f\x078\xd0\xe5\xa2" - b"O\xa0\xf3\x87a\x82\xd8\xcdH\xfcW" - ] -) - - -def mergehex( - input_hex_files: List[str], - output_hex_file: str, - attestation_key: Optional[bytes] = None, - attestation_cert: Optional[bytes] = None, - APPLICATION_END_PAGE: int = 20, - PAGES: int = 128, - lock: bool = False, -) -> None: - """Merges hex files, and patches in the attestation key. - - If no attestation key is passed, uses default Nitrokey Hacker one. - - Note that later hex files replace data of earlier ones, if they overlap. - """ - - # XOR - if attestation_key is not None and attestation_cert is None: - raise RuntimeError("Need to provide certificate with attestation_key") - if attestation_key is None and attestation_cert is not None: - raise RuntimeError("Need to provide certificate with attestation_key") - - if attestation_key is None: - # generic / hacker attestation key - print("*** Using development attestation key") - attestation_key = ( - b"1b2626ecc8f69b0f69e34fb236d76466ba12ac16c3ab5750ba064e8b90e02448" - ) - assert len(attestation_key) == 2 * 32 - - if attestation_cert is None: - attestation_cert = hacker_attestation_cert - else: - attestation_cert = open(attestation_cert, "rb").read() - if len(attestation_cert) < 100: - raise RuntimeError("Attestation certificate is invalid") - - # TODO put definitions somewhere else - def flash_addr(num: int) -> int: - return 0x08000000 + num * 2048 - - APPLICATION_END_PAGE = PAGES - APPLICATION_END_PAGE - AUTH_WORD_ADDR = flash_addr(APPLICATION_END_PAGE) - 8 - ATTESTATION_PAGE = 15 - ATTEST_ADDR = flash_addr(PAGES - ATTESTATION_PAGE) - - print(f"PAGES: {PAGES}") - print(f"app end page: {APPLICATION_END_PAGE}") - print(f"endpage addr: {hex(flash_addr(APPLICATION_END_PAGE - 1))}") - print(f"ATTEST_PAGE page: {PAGES - ATTESTATION_PAGE}") - first = IntelHex(input_hex_files[0]) - for input_hex_file in input_hex_files[1:]: - print(f"merging {first} with {input_hex_file}") - first.merge(IntelHex(input_hex_file), overlap="replace") - - # mark start of the last application page - first[flash_addr(APPLICATION_END_PAGE - 1)] = 0x41 - first[flash_addr(APPLICATION_END_PAGE - 1) + 1] = 0x41 - - # authorize boot - first[AUTH_WORD_ADDR + 0] = 0 - first[AUTH_WORD_ADDR + 1] = 0 - first[AUTH_WORD_ADDR + 2] = 0 - first[AUTH_WORD_ADDR + 3] = 0 - - # make sure bootloader is enabled - first[AUTH_WORD_ADDR + 4] = 0xFF - first[AUTH_WORD_ADDR + 5] = 0xFF - first[AUTH_WORD_ADDR + 6] = 0xFF - first[AUTH_WORD_ADDR + 7] = 0xFF - - # patch in the attestation key - print(f"Using attestation key[:2]: {attestation_key[:4]!r}...") - key = binascii.unhexlify(attestation_key) - - for i, x in enumerate(key): - first[ATTEST_ADDR + i] = x - - offset = 32 - - # patch in device settings / i.e. lock byte in little endian 64 int. - print(f"Setting lock = {lock}") - lock_byte = 0x02 if lock else 0x00 - device_settings = struct.pack("<Q", 0xAA551E7900000000 | lock_byte) - - for i, x in enumerate(device_settings): - first[offset + ATTEST_ADDR + i] = x - - offset += 8 - - # patch in certificate size little endian 64 int. - cert_size = struct.pack("<Q", len(attestation_cert)) - - for i, x in enumerate(cert_size): - first[offset + ATTEST_ADDR + i] = x - - offset += 8 - - # patch in certificate. - for i, x in enumerate(attestation_cert): - first[offset + ATTEST_ADDR + i] = x - - first.tofile(output_hex_file, format="hex") - - -def sign_firmware( - sk_name: str, hex_file: str, APPLICATION_END_PAGE: int = 20, PAGES: int = 128 -) -> dict[str, Any]: - v1 = sign_firmware_for_version(sk_name, hex_file, 19) # noqa: F841 - v2 = sign_firmware_for_version(sk_name, hex_file, 20, PAGES=PAGES) - - # use fw from v2 since it's smaller. - fw = v2["firmware"] - - return { - "firmware": fw, - "signature": v2["signature"], - # signatures to use for different versions of bootloader - "versions": { - ">=0.7.0": {"signature": v2["signature"]}, - # "<=2.5.3": {"signature": v1["signature"]}, - ">2.5.3": {"signature": v2["signature"]}, - }, - } - - -def sign_firmware_for_version( - sk_name: str, hex_file: str, APPLICATION_END_PAGE: int, PAGES: int = 128 -) -> dict[str, Any]: - # Maybe this is not the optimal module... - - import base64 - import binascii - from hashlib import sha256 - - from ecdsa import SigningKey - from intelhex import IntelHex - - sk = SigningKey.from_pem(open(sk_name).read()) - fw = open(hex_file, "rb").read() - fw = base64.b64encode(fw) - fw = helpers.to_websafe(fw.decode()).encode() - ih = IntelHex() - ih.fromfile(hex_file, format="hex") - # start of firmware and the size of the flash region allocated for it. - # TODO put this somewhere else. - START = ih.segments()[0][0] - # keep in sync with targets/stm32l432/src/memory_layout.h - PAGE_SIZE = 2048 - END = (0x08000000 + ((PAGES - APPLICATION_END_PAGE) * PAGE_SIZE)) - 8 - - ih = IntelHex(hex_file) - # segs = ih.segments() - arr = ih.tobinarray(start=START, size=END - START) - - im_size = END - START - - print("im_size: ", im_size) - print("firmware_size: ", len(arr)) - - byts = (arr).tobytes() if hasattr(arr, "tobytes") else (arr).tostring() # type: ignore[attr-defined] - h = sha256() - h.update(byts) - sig = binascii.unhexlify(h.hexdigest()) - print("hash", binascii.hexlify(sig)) - sig = sk.sign_digest(sig) - - print("sig", binascii.hexlify(sig)) - - sig = base64.b64encode(sig) - sig = helpers.to_websafe(sig.decode()).encode() - - # msg = {'data': read()} - msg = {"firmware": fw, "signature": sig} - return msg diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pynitrokey-0.9.3/pyproject.toml new/pynitrokey-0.10.0/pyproject.toml --- old/pynitrokey-0.9.3/pyproject.toml 1970-01-01 01:00:00.000000000 +0100 +++ new/pynitrokey-0.10.0/pyproject.toml 1970-01-01 01:00:00.000000000 +0100 @@ -8,7 +8,7 @@ [project] name = "pynitrokey" -version = "0.9.3" +version = "0.10.0" description = "Python client for Nitrokey devices" license = { text = "Apache-2.0 OR MIT" } authors = [ @@ -19,9 +19,8 @@ dynamic = ["classifiers"] dependencies = [ "cffi", - "click >=8.1.6, <9", + "click >=8.2,<9", "cryptography >=43,<46", - "ecdsa", "fido2 >=2,<3", "hidapi >=0.14,<0.15", # Limit hidapi on Linux to versions using the hidraw backend, see @@ -29,12 +28,11 @@ "hidapi >=0.14.0.post1, <0.14.0.post4 ; sys_platform == 'linux'", "intelhex", "nkdfu", - "nitrokey >=0.3.1,<0.4", + "nitrokey >=0.4,<0.5", "pyusb", "requests", "tqdm", "tlv8", - "click-aliases >=1.0.5, <2", "semver", "nethsm >=1.4.0, <2", ]