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",
 ]

Reply via email to