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