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 2026-01-29 17:46:24
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-pynitrokey (Old)
 and      /work/SRC/openSUSE:Factory/.python-pynitrokey.new.1995 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-pynitrokey"

Thu Jan 29 17:46:24 2026 rev:23 rq:1329755 version:0.11.3

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-pynitrokey/python-pynitrokey.changes      
2025-11-19 14:57:37.561501724 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-pynitrokey.new.1995/python-pynitrokey.changes
    2026-01-29 17:49:12.088180335 +0100
@@ -1,0 +2,16 @@
+Mon Jan 26 06:59:38 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- update to 0.11.3:
+  * chore(deps): pin hidapi to 0.14.0.post2 on linux by @LtdSauce
+    in #708
+  * nkpk: Add admin-app commands by @robin-nitrokey in #709
+  * Update urllib3 to v2.6.0 by @robin-nitrokey in #713
+  * nk3: Only allow factory-reset-app for opcard by @robin-nitrokey
+    in #714
+  * add RSA 3072 and 4096 for PIV by @n0xena in #715
+  * nkpk: Add support for firmware updates by @robin-nitrokey in
+    #710
+  * Add test command to NetHSM cli by @mmerklinger in #718
+  * Release v0.11.3 by @mmerklinger in #719
+
+-------------------------------------------------------------------

Old:
----
  pynitrokey-0.11.2.tar.gz

New:
----
  pynitrokey-0.11.3.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-pynitrokey.spec ++++++
--- /var/tmp/diff_new_pack.s6hPbJ/_old  2026-01-29 17:49:12.948217177 +0100
+++ /var/tmp/diff_new_pack.s6hPbJ/_new  2026-01-29 17:49:12.948217177 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-pynitrokey
 #
-# Copyright (c) 2025 SUSE LLC and contributors
+# Copyright (c) 2026 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.11.2
+Version:        0.11.3
 Release:        0
 Summary:        Python Library for Nitrokey devices
 License:        Apache-2.0 OR MIT
@@ -36,10 +36,10 @@
 BuildRequires:  %{python_module cryptography >= 43 with %python-cryptography < 
47}
 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 hidapi >= 0.14.0.post2 with %python-hidapi < 
0.14.0.post3}
 BuildRequires:  %{python_module libusb1 >= 3 with %python-libusb1 < 4}
 BuildRequires:  %{python_module nethsm >= 2.0.1 with %python-nethsm < 3}
-BuildRequires:  %{python_module nitrokey >= 0.4.0 with %python-nitrokey < 0.5}
+BuildRequires:  %{python_module nitrokey >= 0.4.2 with %python-nitrokey < 0.5}
 BuildRequires:  %{python_module nkdfu >= 0.2 with %python-nkdfu < 0.3}
 BuildRequires:  %{python_module pyusb >= 1.2 with %python-pyusb < 2}
 BuildRequires:  %{python_module requests >= 2.16 with %python-requests < 3}
@@ -55,10 +55,10 @@
 Requires:       (python-click >= 8.2  with python-click < 9)
 Requires:       (python-cryptography >= 43 with python-cryptography < 47)
 Requires:       (python-fido2 >= 2 with python-fido2 < 3)
-Requires:       (python-hidapi >= 0.14.0.post1 with python-hidapi < 
0.14.0.post4)
+Requires:       (python-hidapi >= 0.14.0.post2 with python-hidapi < 
0.14.0.post3)
 Requires:       (python-libusb1 >= 3 with python-libusb1 < 4)
 Requires:       (python-nethsm >= 2.0.1 with python-nethsm < 3)
-Requires:       (python-nitrokey >= 0.4.0 with python-nitrokey < 0.5)
+Requires:       (python-nitrokey >= 0.4.2 with python-nitrokey < 0.5)
 Requires:       (python-nkdfu >= 0.2 with python-nkdfu < 0.3)
 Requires:       (python-pyusb >= 1.2 with python-pyusb < 2)
 Requires:       (python-requests >= 2.16 with python-requests < 3)

++++++ pynitrokey-0.11.2.tar.gz -> pynitrokey-0.11.3.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pynitrokey-0.11.2/PKG-INFO 
new/pynitrokey-0.11.3/PKG-INFO
--- old/pynitrokey-0.11.2/PKG-INFO      1970-01-01 01:00:00.000000000 +0100
+++ new/pynitrokey-0.11.3/PKG-INFO      1970-01-01 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: pynitrokey
-Version: 0.11.2
+Version: 0.11.3
 Summary: Python client for Nitrokey devices
 License: Apache-2.0 OR MIT
 License-File: LICENSES/Apache-2.0.txt
@@ -23,12 +23,12 @@
 Requires-Dist: click (>=8.2,<9)
 Requires-Dist: cryptography (>=43,<47)
 Requires-Dist: fido2 (>=2,<3)
+Requires-Dist: hidapi (==0.14.0.post2) ; sys_platform == "linux"
 Requires-Dist: hidapi (>=0.14,<0.15)
-Requires-Dist: hidapi (>=0.14.0.post1,<0.14.0.post4) ; sys_platform == "linux"
 Requires-Dist: intelhex (>=2.3,<3)
 Requires-Dist: libusb1 (>=3,<4)
 Requires-Dist: nethsm (>=2.0.1,<3)
-Requires-Dist: nitrokey (>=0.4,<0.5)
+Requires-Dist: nitrokey (>=0.4.2,<0.5)
 Requires-Dist: nkdfu (>=0.2,<0.3)
 Requires-Dist: pyscard (>=2,<3) ; extra == "pcsc"
 Requires-Dist: pyserial (>=3.5,<4)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pynitrokey-0.11.2/pynitrokey/cli/nethsm.py 
new/pynitrokey-0.11.3/pynitrokey/cli/nethsm.py
--- old/pynitrokey-0.11.2/pynitrokey/cli/nethsm.py      1970-01-01 
01:00:00.000000000 +0100
+++ new/pynitrokey-0.11.3/pynitrokey/cli/nethsm.py      1970-01-01 
01:00:00.000000000 +0100
@@ -1824,3 +1824,130 @@
             key_id, base64_input(data), nethsm_sdk.SignMode.from_string(mode)
         )
         print(signature.data)
+
+
[email protected]()
[email protected](
+    "-a",
+    "--admin-passphrase",
+    default="adminadmin",
+    help="The admin passphrase to set.",
+)
[email protected](
+    "-o",
+    "--operator-passphrase",
+    default="operatoroperator",
+    help="The operator passphrase to set.",
+)
[email protected](
+    "-u",
+    "--unlock-passphrase",
+    default="unlockunlock",
+    help="The unlock passphrase to set.",
+)
[email protected](
+    "-k",
+    "--key-generations",
+    default=1000,
+    help="Number of keys to generate.",
+)
[email protected](
+    "-r",
+    "--random-length",
+    default=1024,
+    help="Number of random bytes to generate.",
+)
[email protected](
+    "--skip-factory-reset",
+    is_flag=True,
+    help="Skip the factory reset at the end of the test.",
+)
[email protected]_context
+def test(
+    ctx: Context,
+    admin_passphrase: str,
+    operator_passphrase: str,
+    unlock_passphrase: str,
+    key_generations: int,
+    random_length: int,
+    skip_factory_reset: bool,
+) -> None:
+    """Test a NetHSM by running certain operations."""
+
+    admin_username = "admin"
+    operator_username = "operator"
+
+    with connect(ctx, require_auth=False) as nethsm:
+        state = nethsm.get_state()
+        if state == State.UNPROVISIONED:
+            print("Unprovisioned NetHSM found.")
+        else:
+            raise CliException(
+                "Provisioned or operational NetHSM found.", support_hint=False
+            )
+
+        system_time = datetime.datetime.now(datetime.timezone.utc)
+        nethsm.provision(unlock_passphrase, admin_passphrase, system_time)
+        print("Provisioned the NetHSM.")
+        print(f"  Admin username: {admin_username}")
+        print(f"  Admin passphrase: {admin_passphrase}")
+        print(f"  Unlock passphrase: {unlock_passphrase}")
+
+    ctx.obj.username = admin_username
+    ctx.obj.password = admin_passphrase
+
+    with connect(ctx, require_auth=True) as nethsm:
+        nethsm.add_user(
+            operator_username,
+            nethsm_sdk.Role.OPERATOR,
+            operator_passphrase,
+            operator_username,
+            None,
+        )
+        print("Created operator user.")
+        print(f"  Operator username: {operator_username}")
+        print(f"  Operator passphrase: {operator_passphrase}")
+
+        for i in range(key_generations):
+            nethsm.generate_key(
+                nethsm_sdk.KeyType.RSA,
+                [nethsm_sdk.KeyMechanism.RSA_DECRYPTION_RAW],
+                2048,
+                f"key{i}",
+            )
+        keys = len(nethsm.list_keys())
+        if keys == key_generations:
+            print(f"Generated {key_generations} RSA2048 keys.")
+        else:
+            raise CliException(
+                f"The amount of keys requested and stored does not match.",
+                support_hint=False,
+            )
+
+    ctx.obj.username = operator_username
+    ctx.obj.password = operator_passphrase
+
+    with connect(ctx, require_auth=True) as nethsm:
+        MAX_RANDOM_LENGTH_PER_REQUEST = 1024
+        received_random_length = 0
+        remaining_random_length = random_length
+        while remaining_random_length > 0:
+            current_length = min(remaining_random_length, 
MAX_RANDOM_LENGTH_PER_REQUEST)
+            data = nethsm.get_random_data(current_length)
+            received_random_length += len(data.decode())
+            remaining_random_length -= current_length
+        if received_random_length == random_length:
+            print(f"Generated {random_length} bytes of random data.")
+        else:
+            raise CliException(
+                "The amount of random data requested and received does not 
match.",
+                support_hint=False,
+            )
+
+    if not skip_factory_reset:
+        ctx.obj.username = admin_username
+        ctx.obj.password = admin_passphrase
+
+        with connect(ctx, require_auth=True) as nethsm:
+            print("Perform factory reset.")
+            nethsm.factory_reset()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pynitrokey-0.11.2/pynitrokey/cli/nk3/__init__.py 
new/pynitrokey-0.11.3/pynitrokey/cli/nk3/__init__.py
--- old/pynitrokey-0.11.2/pynitrokey/cli/nk3/__init__.py        1970-01-01 
01:00:00.000000000 +0100
+++ new/pynitrokey-0.11.3/pynitrokey/cli/nk3/__init__.py        1970-01-01 
01:00:00.000000000 +0100
@@ -1,19 +1,14 @@
 # Copyright Nitrokey GmbH
 # SPDX-License-Identifier: Apache-2.0 OR MIT
 
-import sys
-from typing import Optional, Sequence
+from typing import Optional
 
 import click
 from nitrokey.nk3 import NK3, NK3Bootloader
-from nitrokey.trussed import Model, TrussedBase
-from nitrokey.trussed.updates import Warning
+from nitrokey.trussed import Model
 
 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 Table, local_critical, local_print
 
 
 class Context(trussed.Context[NK3Bootloader, NK3]):
@@ -34,16 +29,6 @@
             tests.test_fido2,
         ]
 
-    def open(self, path: str) -> Optional[TrussedBase]:
-        from nitrokey.nk3 import open
-
-        return open(path)
-
-    def list_all(self) -> Sequence[TrussedBase]:
-        from nitrokey.nk3 import list
-
-        return list()
-
 
 @click.group()
 @click.option("-p", "--path", "path", help="The path of the Nitrokey 3 device")
@@ -63,278 +48,6 @@
 
 
 @nk3.command()
[email protected]("image", type=click.Path(exists=True, dir_okay=False), 
required=False)
[email protected](
-    "--version",
-    help="Set the firmware version to update to (default: latest stable)",
-)
[email protected](
-    "--ignore-pynitrokey-version",
-    default=False,
-    is_flag=True,
-    help="Allow updates with an outdated pynitrokey version (dangerous)",
-)
[email protected](
-    "--ignore-warning",
-    help="Ignore the warning(s) with the given ID(s) during the update 
(dangerous)",
-    type=click.Choice([w.value for w in Warning]),
-    multiple=True,
-)
[email protected](
-    "--confirm",
-    default=False,
-    is_flag=True,
-    help="Confirm all questions to allow running non-interactively",
-)
[email protected](
-    "--experimental",
-    default=False,
-    is_flag=True,
-    help="Allow to execute experimental features",
-    hidden=True,
-)
[email protected]_obj
-def update(
-    ctx: Context,
-    image: Optional[str],
-    version: Optional[str],
-    ignore_warning: list[str],
-    ignore_pynitrokey_version: bool,
-    confirm: bool,
-    experimental: bool,
-) -> None:
-    """
-    Update the firmware of the device using the given image.
-
-    This command requires that exactly one Nitrokey 3 in bootloader or 
firmware mode is connected.
-    The user is asked to confirm the operation before the update is started.  
If the --confirm
-    option is provided, this is the confirmation.  This option may be used to 
automate an update.
-    The Nitrokey 3 may not be removed during the update.  Also, additional 
Nitrokey 3 devices may
-    not be connected during the update.
-
-    If no firmware image is given, the latest firmware release is downloaded 
automatically.  If
-    the --version option is set, the given version is downloaded instead.
-
-    If the connected Nitrokey 3 device is in firmware mode, the user is 
prompted to touch the
-    device’s button to confirm rebooting to bootloader mode.
-    """
-
-    if experimental:
-        local_print(
-            "The --experimental switch is not required to run this command 
anymore and can be safely removed."
-        )
-
-    from .update import update as exec_update
-
-    ignore_warnings = frozenset([Warning.from_str(s) for s in ignore_warning])
-    update_to_version, status = exec_update(
-        ctx, image, version, ignore_pynitrokey_version, ignore_warnings, 
confirm
-    )
-    print_status(update_to_version, status)
-
-
[email protected]()
[email protected]_obj
-def list_config_fields(ctx: Context) -> None:
-    """
-    List all supported config fields.
-
-    This commands lists all config fields that can be accessed with get-config
-    and set-config as well as their type. The possible types are Bool ("true"
-    or "false") and U8 (an integer between 0 and 255).
-
-    The available config fields depend on the firmware version of the device.
-    """
-    with ctx.connect_device() as device:
-        fields = device.admin.list_available_fields()
-
-        table = Table(["config field", "type"])
-        for field in fields:
-            table.add_row(
-                [
-                    field.name,
-                    field.ty,
-                ]
-            )
-        local_print(table)
-
-
[email protected]()
[email protected]_obj
[email protected]("key")
-def get_config(ctx: Context, key: str) -> None:
-    """Query a config value."""
-    with ctx.connect_device() as device:
-        value = device.admin.get_config(key)
-        print(value)
-
-
[email protected]()
[email protected]_obj
[email protected]("key")
[email protected]("value")
[email protected](
-    "-f",
-    "--force",
-    is_flag=True,
-    default=False,
-    help="Set the config value even if it is not known to pynitrokey",
-)
[email protected](
-    "--dry-run",
-    is_flag=True,
-    default=False,
-    help="Perform all checks but don’t execute the configuration change",
-)
-def set_config(ctx: Context, key: str, value: str, force: bool, dry_run: bool) 
-> None:
-    """
-    Set a config value.
-
-    Per default, this command can only be used with configuration values that
-    are known to pynitrokey.  Changing some configuration values can have side
-    effects.  For these values, a summary of the effects of the change and a
-    confirmation prompt will be printed.
-
-    If you use the --force/-f flag, you can also set configuration values that
-    are not known to pynitrokey.  This may have unexpected side effects, for
-    example resetting an application.  It is only intended for development and
-    testing.
-
-    To see the information about a config value without actually performing the
-    change, use the --dry-run flag.
-    """
-
-    with ctx.connect_device() as device:
-        config_fields = device.admin.list_available_fields()
-
-        field_metadata = None
-        for field in config_fields:
-            if field.name == key:
-                field_metadata = field
-
-        if field_metadata is None:
-            print(
-                "Changing configuration values can have unexpected side 
effects, including data loss.",
-                file=sys.stderr,
-            )
-            print(
-                "This should only be used for development and testing.",
-                file=sys.stderr,
-            )
-            if not force:
-                raise CliException(
-                    "Unknown config values can only be set if the --force/-f 
flag is set.  Aborting.",
-                    support_hint=False,
-                )
-
-        if (
-            not force
-            and field_metadata is not None
-            and not field_metadata.ty.is_valid(value)
-        ):
-            raise CliException(
-                f"Invalid config value for {field}: expected 
{field_metadata.ty}, got `{value}`. Unknown config values can only be set if 
the --force/-f flag is set.  Aborting.",
-                support_hint=False,
-            )
-
-        if key == "opcard.use_se050_backend":
-            print(
-                "This configuration values determines whether the OpenPGP Card 
"
-                "application uses a software implementation or the secure 
element.",
-                file=sys.stderr,
-            )
-            print(
-                "Changing this configuration value will cause a factory reset 
of "
-                "the OpenPGP card application and destroy all OpenPGP keys and 
"
-                "user data currently stored on the device.",
-                file=sys.stderr,
-            )
-        elif field_metadata is not None and field_metadata.destructive:
-            print(
-                "This configuration value may delete data on your device",
-                file=sys.stderr,
-            )
-
-        if field_metadata is not None and field_metadata.destructive:
-            click.confirm("Do you want to continue anyway?", abort=True)
-
-        if dry_run:
-            print("Stopping dry run.", file=sys.stderr)
-            raise click.Abort()
-
-        if field_metadata is not None and 
field_metadata.requires_touch_confirmation:
-            print(
-                "Press the touch button to confirm the configuration change.",
-                file=sys.stderr,
-            )
-
-        device.admin.set_config(key, value)
-
-        if field_metadata is not None and field_metadata.requires_reboot:
-            print("Rebooting device to apply config change.")
-            device.reboot()
-
-        print(f"Updated configuration {key}.")
-
-
[email protected]()
[email protected]_obj
[email protected](
-    "--experimental",
-    default=False,
-    is_flag=True,
-    help="Allow to execute experimental features",
-    hidden=True,
-)
-def factory_reset(ctx: Context, experimental: bool) -> None:
-    """Factory reset all functionality of the device"""
-
-    if experimental:
-        local_print(
-            "The --experimental switch is not required to run this command 
anymore and can be safely removed."
-        )
-
-    with ctx.connect_device() as device:
-        local_print("Please touch the device to confirm the operation", 
file=sys.stderr)
-        if not device.admin.factory_reset():
-            local_critical(
-                "Factory reset is not supported by the firmware version on the 
device",
-                support_hint=False,
-            )
-
-
-# We consciously do not allow resetting the admin app
-APPLICATIONS_CHOICE = click.Choice(["fido", "opcard", "secrets", "piv", 
"webcrypt"])
-
-
[email protected]()
[email protected]_obj
[email protected]("application", type=APPLICATIONS_CHOICE, required=True)
[email protected](
-    "--experimental",
-    default=False,
-    is_flag=True,
-    help="Allow to execute experimental features",
-    hidden=True,
-)
-def factory_reset_app(ctx: Context, application: str, experimental: bool) -> 
None:
-    """Factory reset all functionality of an application"""
-
-    if experimental:
-        local_print(
-            "The --experimental switch is not required to run this command 
anymore and can be safely removed."
-        )
-
-    with ctx.connect_device() as device:
-        local_print("Please touch the device to confirm the operation", 
file=sys.stderr)
-        if not device.admin.factory_reset_app(application):
-            local_critical(
-                "Application Factory reset is not supported by the firmware 
version on the device",
-                support_hint=False,
-            )
-
-
[email protected]()
 @click.pass_obj
 def wink(ctx: Context) -> None:
     """Send wink command to the device (blinks LED a few times)."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pynitrokey-0.11.2/pynitrokey/cli/nk3/piv.py 
new/pynitrokey-0.11.3/pynitrokey/cli/nk3/piv.py
--- old/pynitrokey-0.11.2/pynitrokey/cli/nk3/piv.py     1970-01-01 
01:00:00.000000000 +0100
+++ new/pynitrokey-0.11.3/pynitrokey/cli/nk3/piv.py     1970-01-01 
01:00:00.000000000 +0100
@@ -55,7 +55,8 @@
             assert isinstance(padding, PKCS1v15)
             assert isinstance(algorithm, hashes.SHA256)
 
-            return self._device.sign_rsa2048(data, self._key_reference)
+            bits = self.key_size  # 2048 or 3072 or 4096
+            return self._device.sign_rsa(data, self._key_reference, bits)
 
         def decrypt(self, ciphertext: bytes, padding: AsymmetricPadding) -> 
bytes:
             raise NotImplementedError()
@@ -394,7 +395,9 @@
     )
     @click.option(
         "--algo",
-        type=click.Choice(["rsa2048", "nistp256"], case_sensitive=False),
+        type=click.Choice(
+            ["rsa2048", "rsa3072", "rsa4096", "nistp256"], case_sensitive=False
+        ),
         default="nistp256",
         help="Algorithm for the key.",
     )
@@ -448,12 +451,30 @@
         algo = algo.lower()
         if algo == "rsa2048":
             algo_id = b"\x07"
+        elif algo == "rsa3072":
+            algo_id = b"\x05"
+        elif algo == "rsa4096":
+            algo_id = b"\x16"
         elif algo == "nistp256":
             algo_id = b"\x11"
         else:
             local_critical("Unimplemented algorithm", support_hint=False)
 
-        body = Tlv.build([(0xAC, Tlv.build([(0x80, algo_id)]))])
+        if algo in ("rsa2048", "rsa3072", "rsa4096"):
+            key_selector = {
+                "rsa2048": b"\x10",
+                "rsa3072": b"\x18",
+                "rsa4096": b"\x20",
+            }[algo]
+        elif algo in ("nistp256",):  # TODO: add "nistp384" later
+            key_selector = {
+                "nistp256": b"\x01",
+            }[algo]
+        else:
+            local_critical("Unimplemented algorithm", support_hint=False)
+
+        body = Tlv.build([(0xAC, Tlv.build([(0x80, algo_id), (0x81, 
key_selector)]))])
+
         ins = 0x47
         p1 = 0
         p2 = key_ref
@@ -481,7 +502,7 @@
                 cryptography.hazmat.primitives.asymmetric.ec.SECP256R1(),
             )
             public_key_ecc = public_numbers_ecc.public_key()
-        elif algo == "rsa2048":
+        elif algo in ("rsa2048", "rsa3072", "rsa4096"):
             modulus_data = find_by_id(0x81, data)
             exponent_data = find_by_id(0x82, data)
             if modulus_data is None or exponent_data is None:
@@ -609,7 +630,7 @@
             certificate = certificate_builder.public_key(public_key_ecc).sign(
                 P256PivSigner(device, key_ref, public_key_ecc), hashes.SHA256()
             )
-        elif algo == "rsa2048":
+        elif algo in ("rsa2048", "rsa3072", "rsa4096"):
             if key_ref == 0x9C:
                 device.login(pin)
             csr = csr_builder.sign(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pynitrokey-0.11.2/pynitrokey/cli/nk3/update.py 
new/pynitrokey-0.11.3/pynitrokey/cli/nk3/update.py
--- old/pynitrokey-0.11.2/pynitrokey/cli/nk3/update.py  1970-01-01 
01:00:00.000000000 +0100
+++ new/pynitrokey-0.11.3/pynitrokey/cli/nk3/update.py  1970-01-01 
01:00:00.000000000 +0100
@@ -1,178 +0,0 @@
-# Copyright Nitrokey GmbH
-# SPDX-License-Identifier: Apache-2.0 OR MIT
-
-import logging
-from collections.abc import Set
-from contextlib import contextmanager
-from typing import Any, Callable, Iterator, List, Optional
-
-from click import Abort
-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
-from pynitrokey.helpers import DownloadProgressBar, ProgressBar, confirm, 
local_print
-
-logger = logging.getLogger(__name__)
-
-
-class UpdateCli(UpdateUi):
-    def __init__(self, confirm_continue: bool = False) -> None:
-        self._version_printed = False
-        self._confirm_continue = confirm_continue
-
-    def error(self, *msgs: Any) -> Exception:
-        return CliException(*msgs)
-
-    def abort(self, *msgs: Any) -> Exception:
-        return CliException(*msgs, support_hint=False)
-
-    def raise_warning(self, warning: Warning) -> Exception:
-        return self.abort(
-            f"{warning.message}\nTo ignore this warning and install the update 
at your own risk,"
-            f" set the --ignore-warning {warning.value} option."
-        )
-
-    def show_warning(self, warning: Warning) -> None:
-        logger.warning(f"Ignoring warning {warning.value}")
-        local_print(f"Warning: {warning.message}")
-        local_print(
-            f"Note: The update will continue as --ignore-warning 
{warning.value} has been set."
-        )
-
-    def abort_downgrade(self, current: Version, image: Version) -> Exception:
-        self._print_firmware_versions(current, image)
-        return self.abort(
-            "The firmware image is older than the firmware on the device."
-        )
-
-    def abort_pynitrokey_version(
-        self, current: Version, required: Version
-    ) -> Exception:
-        return self.abort(
-            f"This update requires pynitrokey version {required} (current: 
{current}). "
-            "Please update pynitrokey to install the update."
-        )
-
-    def confirm_download(self, current: Optional[Version], new: Version) -> 
None:
-        if self._confirm_continue:
-            return
-
-        confirm(
-            f"Do you want to download the firmware version {new}?",
-            default=True,
-            abort=True,
-        )
-
-    def confirm_pynitrokey_version(self, current: Version, required: Version) 
-> None:
-        local_print(
-            f"This update requires pynitrokey version {required} (current: 
{current})."
-        )
-        local_print("Using an outdated pynitrokey version is strongly 
discouraged.")
-        if not confirm(
-            "Do you want to continue with an outdated pynitrokey version at 
your own risk?"
-        ):
-            logger.info("Update cancelled by user")
-            raise Abort()
-
-    def confirm_update(self, current: Optional[Version], new: Version) -> None:
-        self._print_firmware_versions(current, new)
-        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 self._confirm_continue:
-            return
-
-        if not confirm("Do you want to perform the firmware update now?"):
-            logger.info("Update cancelled by user")
-            raise Abort()
-
-    def confirm_update_same_version(self, version: Version) -> None:
-        self._print_firmware_versions(version, version)
-        if not confirm(
-            "The version of the firmware image is the same as on the device.  
Do you want "
-            "to continue anyway?"
-        ):
-            raise Abort()
-
-    def confirm_extra_information(self, txt: List[str]) -> None:
-        if txt:
-            local_print("\n".join(txt))
-            if not confirm("Have you read these information? Do you want to 
continue?"):
-                raise Abort()
-
-    def pre_bootloader_hint(self) -> None:
-        pass
-
-    def request_bootloader_confirmation(self) -> None:
-        local_print("")
-        local_print(
-            "Please press the touch button to reboot the device into 
bootloader mode ..."
-        )
-        local_print("")
-
-    @contextmanager
-    def download_progress_bar(self, desc: str) -> Iterator[Callable[[int, 
int], None]]:
-        with DownloadProgressBar(desc) as bar:
-            yield bar.update
-
-    @contextmanager
-    def update_progress_bar(self) -> Iterator[Callable[[int, int], None]]:
-        with ProgressBar(
-            desc="Perform firmware update", unit="B", unit_scale=True
-        ) as bar:
-            yield bar.update_sum
-
-    @contextmanager
-    def finalization_progress_bar(self) -> Iterator[Callable[[int, int], 
None]]:
-        with ProgressBar(desc="Finalize upgrade", unit="%", unit_scale=False) 
as bar:
-            yield bar.update_sum
-
-    def _print_firmware_versions(
-        self, current: Optional[Version], new: Optional[Version]
-    ) -> None:
-        if not self._version_printed:
-            current_str = str(current) if current else "[unknown]"
-            local_print(f"Current firmware version:  {current_str}")
-            local_print(f"Updated firmware version:  {new}")
-            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],
-    version: Optional[str],
-    ignore_pynitrokey_version: bool,
-    ignore_warnings: Set[Warning],
-    confirm_continue: bool,
-) -> tuple[Version, Status]:
-    with ctx.connect() as device:
-        updater = Updater(
-            ui=UpdateCli(confirm_continue),
-            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.11.2/pynitrokey/cli/nkpk.py 
new/pynitrokey-0.11.3/pynitrokey/cli/nkpk.py
--- old/pynitrokey-0.11.2/pynitrokey/cli/nkpk.py        1970-01-01 
01:00:00.000000000 +0100
+++ new/pynitrokey-0.11.3/pynitrokey/cli/nkpk.py        1970-01-01 
01:00:00.000000000 +0100
@@ -1,11 +1,11 @@
 # Copyright Nitrokey GmbH
 # SPDX-License-Identifier: Apache-2.0 OR MIT
 
-from typing import Optional, Sequence
+from typing import Optional
 
 import click
 from nitrokey.nkpk import NKPK, NKPKBootloader
-from nitrokey.trussed import Model, TrussedBase
+from nitrokey.trussed import Model
 
 from pynitrokey.cli.trussed.test import TestCase
 
@@ -34,20 +34,6 @@
             tests.test_fido2,
         ]
 
-    @property
-    def device_name(self) -> str:
-        return "Nitrokey Passkey"
-
-    def open(self, path: str) -> Optional[TrussedBase]:
-        from nitrokey.nkpk import open
-
-        return open(path)
-
-    def list_all(self) -> Sequence[TrussedBase]:
-        from nitrokey.nkpk import list
-
-        return list()
-
 
 @click.group()
 @click.option("-p", "--path", "path", help="The path of the Nitrokey 3 device")
@@ -59,7 +45,7 @@
 
 
 # shared Trussed commands
-trussed.add_commands(nkpk)
+trussed.add_commands(nkpk, has_app_reset=False)
 
 
 def _list() -> None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pynitrokey-0.11.2/pynitrokey/cli/trussed/__init__.py 
new/pynitrokey-0.11.3/pynitrokey/cli/trussed/__init__.py
--- old/pynitrokey-0.11.2/pynitrokey/cli/trussed/__init__.py    1970-01-01 
01:00:00.000000000 +0100
+++ new/pynitrokey-0.11.3/pynitrokey/cli/trussed/__init__.py    1970-01-01 
01:00:00.000000000 +0100
@@ -3,6 +3,7 @@
 
 import logging
 import os.path
+import sys
 from abc import ABC, abstractmethod
 from hashlib import sha256
 from typing import BinaryIO, Callable, Generic, Optional, Sequence, TypeVar
@@ -10,6 +11,7 @@
 import click
 from cryptography import x509
 from cryptography.hazmat.primitives.asymmetric import ec
+from nitrokey import trussed
 from nitrokey.trussed import (
     FirmwareContainer,
     Model,
@@ -23,12 +25,14 @@
 )
 from nitrokey.trussed.admin_app import BootMode, InitStatus, Status
 from nitrokey.trussed.provisioner_app import ProvisionerApp
+from nitrokey.trussed.updates import Warning
 from nitrokey.updates import OverwriteError
 
 from pynitrokey.cli.exceptions import CliException
 from pynitrokey.helpers import (
     DownloadProgressBar,
     Retries,
+    Table,
     local_critical,
     local_print,
     require_windows_admin,
@@ -60,11 +64,11 @@
     @abstractmethod
     def test_cases(self) -> Sequence[TestCase]: ...
 
-    @abstractmethod
-    def open(self, path: str) -> Optional[TrussedBase]: ...
+    def open(self, path: str) -> Optional[TrussedBase]:
+        return trussed.open(path, model=self.model)
 
-    @abstractmethod
-    def list_all(self) -> Sequence[TrussedBase]: ...
+    def list_all(self) -> Sequence[TrussedBase]:
+        return trussed.list(model=self.model)
 
     def list(self) -> Sequence[TrussedBase]:
         if self.path:
@@ -77,20 +81,20 @@
             return self.list_all()
 
     def connect(self) -> TrussedBase:
-        return self._select_unique(self.model.name, self.list())
+        return self._select_unique(str(self.model), self.list())
 
     def connect_device(self) -> Device:
         devices = [
             device for device in self.list() if isinstance(device, 
self.device_type)
         ]
-        return self._select_unique(self.model.name, devices)
+        return self._select_unique(str(self.model), devices)
 
     def await_device(
         self,
         retries: Optional[int] = None,
         callback: Optional[Callable[[int, int], None]] = None,
     ) -> Device:
-        return self._await(self.model.name, self.device_type, retries, 
callback)
+        return self._await(str(self.model), self.device_type, retries, 
callback)
 
     def await_bootloader(
         self,
@@ -98,7 +102,7 @@
         callback: Optional[Callable[[int, int], None]] = None,
     ) -> Bootloader:
         return self._await(
-            f"{self.model.name} bootloader", self.bootloader_type, retries, 
callback
+            f"{self.model} bootloader", self.bootloader_type, retries, callback
         )
 
     def _select_unique(self, name: str, devices: Sequence[T]) -> T:
@@ -145,14 +149,21 @@
     require_windows_admin()
 
 
-def add_commands(group: click.Group) -> None:
+def add_commands(group: click.Group, *, has_app_reset: bool = True) -> None:
     group.add_command(fetch_update)
     group.add_command(list)
+    group.add_command(list_config_fields)
+    group.add_command(get_config)
+    group.add_command(set_config)
+    group.add_command(factory_reset)
+    if has_app_reset:
+        group.add_command(factory_reset_app)
     group.add_command(provision)
     group.add_command(reboot)
     group.add_command(rng)
     group.add_command(status)
     group.add_command(test)
+    group.add_command(update)
     group.add_command(validate_update)
     group.add_command(version)
 
@@ -223,7 +234,7 @@
 
 
 def _list(ctx: Context[Bootloader, Device]) -> None:
-    local_print(f":: '{ctx.model.name}' keys")
+    local_print(f":: '{ctx.model}' keys")
     for device in ctx.list_all():
         with device as device:
             uuid = device.uuid()
@@ -233,6 +244,186 @@
                 local_print(f"{device.path}: {device.name}")
 
 
[email protected]()
[email protected]_obj
+def list_config_fields(ctx: Context[Bootloader, Device]) -> None:
+    """
+    List all supported config fields.
+
+    This commands lists all config fields that can be accessed with get-config
+    and set-config as well as their type. The possible types are Bool ("true"
+    or "false") and U8 (an integer between 0 and 255).
+
+    The available config fields depend on the firmware version of the device.
+    """
+    with ctx.connect_device() as device:
+        fields = device.admin.list_available_fields()
+
+        table = Table(["config field", "type"])
+        for field in fields:
+            table.add_row(
+                [
+                    field.name,
+                    field.ty,
+                ]
+            )
+        local_print(table)
+
+
[email protected]()
[email protected]_obj
[email protected]("key")
+def get_config(ctx: Context[Bootloader, Device], key: str) -> None:
+    """Query a config value."""
+    with ctx.connect_device() as device:
+        value = device.admin.get_config(key)
+        print(value)
+
+
[email protected]()
[email protected]_obj
[email protected]("key")
[email protected]("value")
[email protected](
+    "-f",
+    "--force",
+    is_flag=True,
+    default=False,
+    help="Set the config value even if it is not known to pynitrokey",
+)
[email protected](
+    "--dry-run",
+    is_flag=True,
+    default=False,
+    help="Perform all checks but don’t execute the configuration change",
+)
+def set_config(
+    ctx: Context[Bootloader, Device], key: str, value: str, force: bool, 
dry_run: bool
+) -> None:
+    """
+    Set a config value.
+
+    Per default, this command can only be used with configuration values that
+    are known to pynitrokey.  Changing some configuration values can have side
+    effects.  For these values, a summary of the effects of the change and a
+    confirmation prompt will be printed.
+
+    If you use the --force/-f flag, you can also set configuration values that
+    are not known to pynitrokey.  This may have unexpected side effects, for
+    example resetting an application.  It is only intended for development and
+    testing.
+
+    To see the information about a config value without actually performing the
+    change, use the --dry-run flag.
+    """
+
+    with ctx.connect_device() as device:
+        config_fields = device.admin.list_available_fields()
+
+        field_metadata = None
+        for field in config_fields:
+            if field.name == key:
+                field_metadata = field
+
+        if field_metadata is None:
+            print(
+                "Changing configuration values can have unexpected side 
effects, including data loss.",
+                file=sys.stderr,
+            )
+            print(
+                "This should only be used for development and testing.",
+                file=sys.stderr,
+            )
+            if not force:
+                raise CliException(
+                    "Unknown config values can only be set if the --force/-f 
flag is set.  Aborting.",
+                    support_hint=False,
+                )
+
+        if (
+            not force
+            and field_metadata is not None
+            and not field_metadata.ty.is_valid(value)
+        ):
+            raise CliException(
+                f"Invalid config value for {field}: expected 
{field_metadata.ty}, got `{value}`. Unknown config values can only be set if 
the --force/-f flag is set.  Aborting.",
+                support_hint=False,
+            )
+
+        if key == "opcard.use_se050_backend":
+            print(
+                "This configuration values determines whether the OpenPGP Card 
"
+                "application uses a software implementation or the secure 
element.",
+                file=sys.stderr,
+            )
+            print(
+                "Changing this configuration value will cause a factory reset 
of "
+                "the OpenPGP card application and destroy all OpenPGP keys and 
"
+                "user data currently stored on the device.",
+                file=sys.stderr,
+            )
+        elif field_metadata is not None and field_metadata.destructive:
+            print(
+                "This configuration value may delete data on your device",
+                file=sys.stderr,
+            )
+
+        if field_metadata is not None and field_metadata.destructive:
+            click.confirm("Do you want to continue anyway?", abort=True)
+
+        if dry_run:
+            print("Stopping dry run.", file=sys.stderr)
+            raise click.Abort()
+
+        if field_metadata is not None and 
field_metadata.requires_touch_confirmation:
+            print(
+                "Press the touch button to confirm the configuration change.",
+                file=sys.stderr,
+            )
+
+        device.admin.set_config(key, value)
+
+        if field_metadata is not None and field_metadata.requires_reboot:
+            print("Rebooting device to apply config change.")
+            device.reboot()
+
+        print(f"Updated configuration {key}.")
+
+
[email protected]()
[email protected]_obj
+def factory_reset(ctx: Context[Bootloader, Device]) -> None:
+    """Factory reset all functionality of the device"""
+
+    with ctx.connect_device() as device:
+        local_print("Please touch the device to confirm the operation", 
file=sys.stderr)
+        if not device.admin.factory_reset():
+            local_critical(
+                "Factory reset is not supported by the firmware version on the 
device",
+                support_hint=False,
+            )
+
+
+# opcard is the only application that supports factory reset at the moment
+# see the reset_client_id implementations in components/apps/src/lib.rs in 
nitrokey-3-firmware
+APPLICATIONS_CHOICE = click.Choice(["opcard"])
+
+
[email protected]()
[email protected]_obj
[email protected]("application", type=APPLICATIONS_CHOICE, required=True)
+def factory_reset_app(ctx: Context[Bootloader, Device], application: str) -> 
None:
+    """Factory reset all functionality of an application"""
+
+    with ctx.connect_device() as device:
+        local_print("Please touch the device to confirm the operation", 
file=sys.stderr)
+        if not device.admin.factory_reset_app(application):
+            local_critical(
+                "Application Factory reset is not supported by the firmware 
version on the device",
+                support_hint=False,
+            )
+
+
 @click.group(hidden=True)
 def provision() -> None:
     """
@@ -506,9 +697,9 @@
 
     if len(devices) == 0:
         log_devices()
-        raise CliException(f"No connected {ctx.model.name} devices found")
+        raise CliException(f"No connected {ctx.model} devices found")
 
-    local_print(f"Found {len(devices)} {ctx.model.name} device(s):")
+    local_print(f"Found {len(devices)} {ctx.model} device(s):")
     for device in devices:
         local_print(f"- {device.name} at {device.path}")
 
@@ -538,6 +729,64 @@
 
 
 @click.command()
[email protected]("image", type=click.Path(exists=True, dir_okay=False), 
required=False)
[email protected](
+    "--version",
+    help="Set the firmware version to update to (default: latest stable)",
+)
[email protected](
+    "--ignore-pynitrokey-version",
+    default=False,
+    is_flag=True,
+    help="Allow updates with an outdated pynitrokey version (dangerous)",
+)
[email protected](
+    "--ignore-warning",
+    help="Ignore the warning(s) with the given ID(s) during the update 
(dangerous)",
+    type=click.Choice([w.value for w in Warning]),
+    multiple=True,
+)
[email protected](
+    "--confirm",
+    default=False,
+    is_flag=True,
+    help="Confirm all questions to allow running non-interactively",
+)
[email protected]_obj
+def update(
+    ctx: Context[Bootloader, Device],
+    image: Optional[str],
+    version: Optional[str],
+    ignore_warning: Sequence[str],
+    ignore_pynitrokey_version: bool,
+    confirm: bool,
+) -> None:
+    """
+    Update the firmware of the device using the given image.
+
+    This command requires that exactly one Nitrokey in bootloader or firmware 
mode is connected.
+    The user is asked to confirm the operation before the update is started.  
If the --confirm
+    option is provided, this is the confirmation.  This option may be used to 
automate an update.
+    The Nitrokey may not be removed during the update.  Also, additional 
Nitrokey devices may
+    not be connected during the update.
+
+    If no firmware image is given, the latest firmware release is downloaded 
automatically.  If
+    the --version option is set, the given version is downloaded instead.
+
+    If the connected Nitrokey device is in firmware mode, the user is prompted 
to touch the
+    device’s button to confirm rebooting to bootloader mode.
+    """
+
+    from .update import update as exec_update
+
+    ignore_warnings = frozenset([Warning.from_str(s) for s in ignore_warning])
+    update_to_version, status = exec_update(
+        ctx, image, version, ignore_pynitrokey_version, ignore_warnings, 
confirm
+    )
+    print_status(update_to_version, status)
+
+
[email protected]()
 @click.argument("image", type=click.Path(exists=True, dir_okay=False))
 @click.pass_obj
 def validate_update(ctx: Context[Bootloader, Device], image: str) -> None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pynitrokey-0.11.2/pynitrokey/cli/trussed/update.py 
new/pynitrokey-0.11.3/pynitrokey/cli/trussed/update.py
--- old/pynitrokey-0.11.2/pynitrokey/cli/trussed/update.py      1970-01-01 
01:00:00.000000000 +0100
+++ new/pynitrokey-0.11.3/pynitrokey/cli/trussed/update.py      1970-01-01 
01:00:00.000000000 +0100
@@ -0,0 +1,178 @@
+# Copyright Nitrokey GmbH
+# SPDX-License-Identifier: Apache-2.0 OR MIT
+
+import logging
+from collections.abc import Set
+from contextlib import contextmanager
+from typing import Any, Callable, Iterator, List, Optional
+
+from click import Abort
+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.trussed import Bootloader, Context, Device
+from pynitrokey.helpers import DownloadProgressBar, ProgressBar, confirm, 
local_print
+
+logger = logging.getLogger(__name__)
+
+
+class UpdateCli(UpdateUi):
+    def __init__(self, confirm_continue: bool = False) -> None:
+        self._version_printed = False
+        self._confirm_continue = confirm_continue
+
+    def error(self, *msgs: Any) -> Exception:
+        return CliException(*msgs)
+
+    def abort(self, *msgs: Any) -> Exception:
+        return CliException(*msgs, support_hint=False)
+
+    def raise_warning(self, warning: Warning) -> Exception:
+        return self.abort(
+            f"{warning.message}\nTo ignore this warning and install the update 
at your own risk,"
+            f" set the --ignore-warning {warning.value} option."
+        )
+
+    def show_warning(self, warning: Warning) -> None:
+        logger.warning(f"Ignoring warning {warning.value}")
+        local_print(f"Warning: {warning.message}")
+        local_print(
+            f"Note: The update will continue as --ignore-warning 
{warning.value} has been set."
+        )
+
+    def abort_downgrade(self, current: Version, image: Version) -> Exception:
+        self._print_firmware_versions(current, image)
+        return self.abort(
+            "The firmware image is older than the firmware on the device."
+        )
+
+    def abort_pynitrokey_version(
+        self, current: Version, required: Version
+    ) -> Exception:
+        return self.abort(
+            f"This update requires pynitrokey version {required} (current: 
{current}). "
+            "Please update pynitrokey to install the update."
+        )
+
+    def confirm_download(self, current: Optional[Version], new: Version) -> 
None:
+        if self._confirm_continue:
+            return
+
+        confirm(
+            f"Do you want to download the firmware version {new}?",
+            default=True,
+            abort=True,
+        )
+
+    def confirm_pynitrokey_version(self, current: Version, required: Version) 
-> None:
+        local_print(
+            f"This update requires pynitrokey version {required} (current: 
{current})."
+        )
+        local_print("Using an outdated pynitrokey version is strongly 
discouraged.")
+        if not confirm(
+            "Do you want to continue with an outdated pynitrokey version at 
your own risk?"
+        ):
+            logger.info("Update cancelled by user")
+            raise Abort()
+
+    def confirm_update(self, current: Optional[Version], new: Version) -> None:
+        self._print_firmware_versions(current, new)
+        local_print("")
+        local_print(
+            "Please do not remove the device or insert any other Nitrokey 
devices "
+            "during the update. Doing so may damage the device."
+        )
+
+        if self._confirm_continue:
+            return
+
+        if not confirm("Do you want to perform the firmware update now?"):
+            logger.info("Update cancelled by user")
+            raise Abort()
+
+    def confirm_update_same_version(self, version: Version) -> None:
+        self._print_firmware_versions(version, version)
+        if not confirm(
+            "The version of the firmware image is the same as on the device.  
Do you want "
+            "to continue anyway?"
+        ):
+            raise Abort()
+
+    def confirm_extra_information(self, txt: List[str]) -> None:
+        if txt:
+            local_print("\n".join(txt))
+            if not confirm("Have you read these information? Do you want to 
continue?"):
+                raise Abort()
+
+    def pre_bootloader_hint(self) -> None:
+        pass
+
+    def request_bootloader_confirmation(self) -> None:
+        local_print("")
+        local_print(
+            "Please press the touch button to reboot the device into 
bootloader mode ..."
+        )
+        local_print("")
+
+    @contextmanager
+    def download_progress_bar(self, desc: str) -> Iterator[Callable[[int, 
int], None]]:
+        with DownloadProgressBar(desc) as bar:
+            yield bar.update
+
+    @contextmanager
+    def update_progress_bar(self) -> Iterator[Callable[[int, int], None]]:
+        with ProgressBar(
+            desc="Perform firmware update", unit="B", unit_scale=True
+        ) as bar:
+            yield bar.update_sum
+
+    @contextmanager
+    def finalization_progress_bar(self) -> Iterator[Callable[[int, int], 
None]]:
+        with ProgressBar(desc="Finalize upgrade", unit="%", unit_scale=False) 
as bar:
+            yield bar.update_sum
+
+    def _print_firmware_versions(
+        self, current: Optional[Version], new: Optional[Version]
+    ) -> None:
+        if not self._version_printed:
+            current_str = str(current) if current else "[unknown]"
+            local_print(f"Current firmware version:  {current_str}")
+            local_print(f"Updated firmware version:  {new}")
+            self._version_printed = True
+
+
+class ContextDeviceHandler(DeviceHandler):
+    def __init__(self, ctx: Context[Bootloader, Device]) -> 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[Bootloader, Device],
+    image: Optional[str],
+    version: Optional[str],
+    ignore_pynitrokey_version: bool,
+    ignore_warnings: Set[Warning],
+    confirm_continue: bool,
+) -> tuple[Version, Status]:
+    with ctx.connect() as device:
+        updater = Updater(
+            ui=UpdateCli(confirm_continue),
+            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.11.2/pynitrokey/nk3/piv_app.py 
new/pynitrokey-0.11.3/pynitrokey/nk3/piv_app.py
--- old/pynitrokey-0.11.2/pynitrokey/nk3/piv_app.py     1970-01-01 
01:00:00.000000000 +0100
+++ new/pynitrokey-0.11.3/pynitrokey/nk3/piv_app.py     1970-01-01 
01:00:00.000000000 +0100
@@ -27,16 +27,21 @@
 
 
 # size is in bytes
-def prepare_for_pkcs1v15_sign_2048(data: bytes) -> bytes:
+def prepare_for_pkcs1v15_sign(data: bytes, key_size_bytes: int) -> bytes:
     digest = hashes.Hash(hashes.SHA256())
     digest.update(data)
     hashed = digest.finalize()
 
+    # ASN.1 DigestInfo prefix for SHA-256
     prefix = bytearray.fromhex("3031300d060960864801650304020105000420")
-    padding_len = 256 - 32 - 19 - 3
+
+    # PKCS#1 v1.5 block:
+    #  0x00 0x01 FF FF ... FF 0x00 prefix hash
+    padding_len = key_size_bytes - 3 - len(prefix) - len(hashed)
     padding = b"\x00\x01" + (b"\xff" * padding_len) + b"\x00"
+
     total = padding + prefix + hashed
-    assert len(total) == 256
+    assert len(total) == key_size_bytes
     return total
 
 
@@ -302,9 +307,12 @@
         payload = digest.finalize()
         return self.raw_sign(payload, key, 0x11)
 
-    def sign_rsa2048(self, data: bytes, key: int) -> bytes:
-        payload = prepare_for_pkcs1v15_sign_2048(data)
-        return self.raw_sign(payload, key, 0x07)
+    def sign_rsa(self, data: bytes, key: int, bits: int) -> bytes:
+        key_size_bytes = bits // 8
+        payload = prepare_for_pkcs1v15_sign(data, key_size_bytes)
+        algo_map = {2048: 0x07, 3072: 0x05, 4096: 0x16}
+        algo_id = algo_map[bits]
+        return self.raw_sign(payload, key, algo_id)
 
     def raw_sign(self, payload: bytes, key: int, algo: int) -> bytes:
         body = Tlv.build([(0x7C, Tlv.build([(0x81, payload), (0x82, b"")]))])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pynitrokey-0.11.2/pyproject.toml 
new/pynitrokey-0.11.3/pyproject.toml
--- old/pynitrokey-0.11.2/pyproject.toml        1970-01-01 01:00:00.000000000 
+0100
+++ new/pynitrokey-0.11.3/pyproject.toml        1970-01-01 01:00:00.000000000 
+0100
@@ -8,7 +8,7 @@
 
 [project]
 name = "pynitrokey"
-version = "0.11.2"
+version = "0.11.3"
 description = "Python client for Nitrokey devices"
 license = { text = "Apache-2.0 OR MIT" }
 authors = [
@@ -23,13 +23,14 @@
   "cryptography >=43, <47",
   "fido2 >=2, <3",
   "hidapi >=0.14, <0.15",
-  # Limit hidapi on Linux to versions using the hidraw backend, see
-  # https://github.com/Nitrokey/pynitrokey/issues/601
-  "hidapi >=0.14.0.post1, <0.14.0.post4 ; sys_platform == 'linux'",
+  # Limit hidapi on Linux to version using the hidraw backend, see
+  # https://github.com/Nitrokey/pynitrokey/issues/601 and
+  # https://github.com/Nitrokey/pynitrokey/issues/707
+  "hidapi ==0.14.0.post2 ; sys_platform == 'linux'",
   "intelhex >=2.3, <3",
   "libusb1 >=3, <4",
   "nethsm >=2.0.1, <3",
-  "nitrokey >=0.4, <0.5",
+  "nitrokey >=0.4.2, <0.5",
   "nkdfu >=0.2, <0.3",
   "pyusb >=1.2, <2",
   "pyserial >=3.5, <4",

Reply via email to