Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-nitrokey for openSUSE:Factory
checked in at 2025-08-07 16:48:44
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-nitrokey (Old)
and /work/SRC/openSUSE:Factory/.python-nitrokey.new.1085 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-nitrokey"
Thu Aug 7 16:48:44 2025 rev:5 rq:1297995 version:0.4.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-nitrokey/python-nitrokey.changes
2025-07-09 17:29:56.863403012 +0200
+++
/work/SRC/openSUSE:Factory/.python-nitrokey.new.1085/python-nitrokey.changes
2025-08-07 16:49:57.539181616 +0200
@@ -1,0 +2,22 @@
+Mon Aug 4 09:21:43 UTC 2025 - Johannes Kastl
<[email protected]>
+
+- update to 0.4.0:
+ * nitrokey.trussed.admin_app.InitStatus: add support for
+ EXT_FLASH_NEED_REFORMAT
+ * Use poetry-core v2 as build backend.
+ * Bump minimum Python version to 3.10.
+ * nitrokey.trussed.Model: Remove firmware_repository and
+ firmware_pattern properties.
+ * nitrokey.nk3.updates:
+ - Move to nitrokey.trussed.updates and prepare adding NKPK
+ support.
+ - Return device status after an update.
+ - Add model argument to get_firmware_update.
+ - Add get_firmware_repository method.
+ - Replace connection callbacks in Updater with DeviceHandler
+ class.
+- add version constraints for dependencies
+- relax constraint on protobuf, see
+ https://github.com/Nitrokey/nitrokey-sdk-py/issues/84#issuecomment-3158310783
+
+-------------------------------------------------------------------
Old:
----
nitrokey-0.3.2.tar.gz
New:
----
nitrokey-0.4.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-nitrokey.spec ++++++
--- /var/tmp/diff_new_pack.OeryBv/_old 2025-08-07 16:49:59.367258511 +0200
+++ /var/tmp/diff_new_pack.OeryBv/_new 2025-08-07 16:49:59.407260193 +0200
@@ -1,7 +1,7 @@
#
# spec file for package python-nitrokey
#
-# Copyright (c) 2025 SUSE LLC
+# Copyright (c) 2025 SUSE LLC and contributors
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -18,29 +18,39 @@
%{?sle15_python_module_pythons}
Name: python-nitrokey
-Version: 0.3.2
+Version: 0.4.0
Release: 0
Summary: Nitrokey Python SDK
License: Apache-2.0
URL: https://github.com/Nitrokey/nitrokey-sdk-py
Source0:
https://files.pythonhosted.org/packages/source/n/nitrokey/nitrokey-%{version}.tar.gz
Source99: python-nitrokey.rpmlintrc
-BuildRequires: %{python_module base >= 3.9.2}
-BuildRequires: %{python_module fido2 >= 1.1.2 with %python-fido2 < 3}
+BuildRequires: %{python_module base >= 3.10}
BuildRequires: %{python_module pip}
BuildRequires: %{python_module poetry-core >= 1}
BuildRequires: %{python_module wheel}
+# Runtime dependencies
+BuildRequires: %{python_module cryptography >= 41}
+BuildRequires: %{python_module crcmod >= 1.7 with %python-crcmod < 2}
+BuildRequires: %{python_module fido2 >= 1.1.2 with %python-fido2 < 3}
+BuildRequires: %{python_module hidapi >= 0.14 with %python-hidapi < 0.15}
+BuildRequires: %{python_module protobuf >= 5.26 with %python-protobuf < 7}
+BuildRequires: %{python_module pyserial >= 3.5 with %python-pyserial < 4}
+BuildRequires: %{python_module requests >= 2 with %python-requests < 3}
+BuildRequires: %{python_module semver >= 3 with %python-semver < 4}
+BuildRequires: %{python_module tlv8 >= 0.10 with %python-tlv8 < 0.11}
+#
BuildRequires: fdupes
BuildRequires: python-rpm-macros
-Requires: python-crcmod
-Requires: python-cryptography
-Requires: python-hidapi
-Requires: python-protobuf
-Requires: python-pyserial
-Requires: python-requests
-Requires: python-semver
-Requires: python-tlv8
+Requires: python-cryptography >= 41
+Requires: (python-crcmod >= 1.7 with python-crcmod < 2)
Requires: (python-fido2 >= 1.1.2 with python-fido2 < 3)
+Requires: (python-hidapi >= 0.14 with python-hidapi < 0.15)
+Requires: (python-protobuf >= 5.26 with python-protobuf < 7)
+Requires: (python-pyserial >= 3.5 with python-pyserial < 4)
+Requires: (python-requests >= 2 with python-requests < 3)
+Requires: (python-semver >= 3 with python-semver < 4)
+Requires: (python-tlv8 >= 0.10 with python-tlv8 < 0.11)
BuildArch: noarch
%python_subpackages
++++++ nitrokey-0.3.2.tar.gz -> nitrokey-0.4.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/nitrokey-0.3.2/PKG-INFO new/nitrokey-0.4.0/PKG-INFO
--- old/nitrokey-0.3.2/PKG-INFO 1970-01-01 01:00:00.000000000 +0100
+++ new/nitrokey-0.4.0/PKG-INFO 1970-01-01 01:00:00.000000000 +0100
@@ -1,12 +1,11 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.3
Name: nitrokey
-Version: 0.3.2
+Version: 0.4.0
Summary: Nitrokey Python SDK
-Home-page: https://github.com/Nitrokey/nitrokey-sdk-py
License: Apache-2.0 or MIT
Author: Nitrokey
Author-email: [email protected]
-Requires-Python: >=3.9.2,<4.0.0
+Requires-Python: >=3.10, <4
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3
@@ -14,13 +13,13 @@
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
-Requires-Dist: crcmod (>=1.7,<2.0)
+Requires-Dist: crcmod (>=1.7,<2)
Requires-Dist: cryptography (>=41)
Requires-Dist: fido2 (>=1.1.2,<3)
Requires-Dist: hidapi (>=0.14,<0.15)
-Requires-Dist: protobuf (>=5.26,<6.0)
-Requires-Dist: pyserial (>=3.5,<4.0)
-Requires-Dist: requests (>=2,<3)
+Requires-Dist: protobuf (>=5.26,<6)
+Requires-Dist: pyserial (>=3.5,<4)
+Requires-Dist: requests (>=2.16,<3)
Requires-Dist: semver (>=3,<4)
Requires-Dist: tlv8 (>=0.10,<0.11)
Project-URL: Repository, https://github.com/Nitrokey/nitrokey-sdk-py
@@ -76,7 +75,7 @@
## Compatibility
-The Nitrokey Python SDK currently requires Python 3.9.2 or later.
+The Nitrokey Python SDK currently requires Python 3.10 or later.
Support for old Python versions may be dropped in minor releases.
## Related Projects
@@ -92,7 +91,7 @@
The following software is required for the development of the SDK:
-- Python 3.9 or newer
+- Python 3.10 or newer
- [poetry](https://python-poetry.org/)
- GNU Make
- git
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/nitrokey-0.3.2/README.md new/nitrokey-0.4.0/README.md
--- old/nitrokey-0.3.2/README.md 2025-07-08 12:41:08.000000000 +0200
+++ new/nitrokey-0.4.0/README.md 1970-01-01 01:00:00.000000000 +0100
@@ -48,7 +48,7 @@
## Compatibility
-The Nitrokey Python SDK currently requires Python 3.9.2 or later.
+The Nitrokey Python SDK currently requires Python 3.10 or later.
Support for old Python versions may be dropped in minor releases.
## Related Projects
@@ -64,7 +64,7 @@
The following software is required for the development of the SDK:
-- Python 3.9 or newer
+- Python 3.10 or newer
- [poetry](https://python-poetry.org/)
- GNU Make
- git
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/nitrokey-0.3.2/pyproject.toml
new/nitrokey-0.4.0/pyproject.toml
--- old/nitrokey-0.3.2/pyproject.toml 2025-07-08 12:41:08.000000000 +0200
+++ new/nitrokey-0.4.0/pyproject.toml 1970-01-01 01:00:00.000000000 +0100
@@ -1,37 +1,45 @@
[build-system]
-requires = ["poetry-core >=1,<3"]
+requires = ["poetry-core >=2, <3"]
build-backend = "poetry.core.masonry.api"
-[tool.poetry]
+[project]
name = "nitrokey"
-version = "0.3.2"
+version = "0.4.0"
description = "Nitrokey Python SDK"
-authors = ["Nitrokey <[email protected]>"]
+authors = [
+ { name = "Nitrokey", email = "[email protected]" },
+]
license = "Apache-2.0 or MIT"
readme = "README.md"
-repository = "https://github.com/Nitrokey/nitrokey-sdk-py"
-classifiers = [
- "Intended Audience :: Developers",
-]
packages = [
{ include = "nitrokey", from = "src" },
]
+requires-python = ">=3.10, <4"
+dynamic = ["classifiers"]
+
+dependencies = [
+ "cryptography >=41",
+ "fido2 >=1.1.2, <3",
+ "requests >=2.16, <3",
+ "semver >=3, <4",
+ "tlv8 >=0.10, <0.11",
+
+ # lpc55
+ "crcmod >=1.7, <2",
+ "hidapi >=0.14, <0.15",
+
+ # nrf52
+ "protobuf >=5.26, <6",
+ "pyserial >=3.5, <4",
+]
-[tool.poetry.dependencies]
-cryptography = ">=41"
-fido2 = ">=1.1.2, <3"
-python = "^3.9.2"
-requests = "^2"
-semver = "^3"
-tlv8 = "^0.10"
-
-# lpc55
-crcmod = "^1.7"
-hidapi = "^0.14"
-
-# nrf52
-protobuf = "^5.26"
-pyserial = "^3.5"
+[project.urls]
+repository = "https://github.com/Nitrokey/nitrokey-sdk-py"
+
+[tool.poetry]
+classifiers = [
+ "Intended Audience :: Developers",
+]
[tool.poetry.group.dev]
optional = true
@@ -45,20 +53,35 @@
rstcheck = { version = "^6", extras = ["sphinx"] }
sphinx = "^7"
types-protobuf = "^5.26"
-types-requests = "^2.32"
+types-requests = "^2.16"
typing-extensions = "^4.1"
+[tool.uv]
+dev-dependencies = [
+ # These dependencies are required for running the type checks.
+ "fake-winreg >=1.6, <2",
+ "mypy >=1.4, <2",
+ "types-protobuf >=5.26, <6",
+ "types-requests >=2.16, <3",
+
+ # These additional lower bounds are required for --resolution lowest.
+ # As these are transitive dependencies, we don’t include them in our
dependency list.
+ "cffi >=1.14.1",
+ "typing-extensions >=4.1",
+ "urllib3 >= 2",
+]
+
[tool.black]
-target-version = ["py39"]
+target-version = ["py310"]
[tool.isort]
-py_version = "39"
+py_version = "310"
profile = "black"
[tool.mypy]
mypy_path = "stubs"
show_error_codes = true
-python_version = "3.9"
+python_version = "3.10"
strict = true
[tool.rstcheck]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/nitrokey-0.3.2/src/nitrokey/nk3/_bootloader.py
new/nitrokey-0.4.0/src/nitrokey/nk3/_bootloader.py
--- old/nitrokey-0.3.2/src/nitrokey/nk3/_bootloader.py 2025-07-08
12:41:08.000000000 +0200
+++ new/nitrokey-0.4.0/src/nitrokey/nk3/_bootloader.py 1970-01-01
01:00:00.000000000 +0100
@@ -8,12 +8,17 @@
from typing import List, Optional, Sequence
from nitrokey import _VID_NITROKEY
+from nitrokey.trussed._base import Model
from nitrokey.trussed._bootloader import TrussedBootloader
from nitrokey.trussed._bootloader.lpc55 import TrussedBootloaderLpc55
from nitrokey.trussed._bootloader.nrf52 import SignatureKey,
TrussedBootloaderNrf52
class NK3Bootloader(TrussedBootloader):
+ @property
+ def model(self) -> Model:
+ return Model.NK3
+
@staticmethod
def list() -> List["NK3Bootloader"]:
devices: List[NK3Bootloader] = []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/nitrokey-0.3.2/src/nitrokey/nk3/_device.py
new/nitrokey-0.4.0/src/nitrokey/nk3/_device.py
--- old/nitrokey-0.3.2/src/nitrokey/nk3/_device.py 2025-07-08
12:41:08.000000000 +0200
+++ new/nitrokey-0.4.0/src/nitrokey/nk3/_device.py 1970-01-01
01:00:00.000000000 +0100
@@ -10,7 +10,7 @@
from fido2.hid import CtapHidDevice, list_descriptors, open_device
from nitrokey import _VID_NITROKEY
-from nitrokey.trussed import Fido2Certs, TrussedDevice, Version
+from nitrokey.trussed import Fido2Certs, Model, TrussedDevice, Version
FIDO2_CERTS = [
Fido2Certs(
@@ -37,6 +37,10 @@
super().__init__(device, FIDO2_CERTS)
@property
+ def model(self) -> Model:
+ return Model.NK3
+
+ @property
def pid(self) -> int:
from . import _PID_NK3_DEVICE
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/nitrokey-0.3.2/src/nitrokey/nk3/updates.py
new/nitrokey-0.4.0/src/nitrokey/nk3/updates.py
--- old/nitrokey-0.3.2/src/nitrokey/nk3/updates.py 2025-07-08
12:41:08.000000000 +0200
+++ new/nitrokey-0.4.0/src/nitrokey/nk3/updates.py 1970-01-01
01:00:00.000000000 +0100
@@ -1,527 +0,0 @@
-# Copyright 2022 Nitrokey Developers
-#
-# Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
-# http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
-# http://opensource.org/licenses/MIT>, at your option. This file may not be
-# copied, modified, or distributed except according to those terms.
-
-import enum
-import importlib.metadata
-import logging
-import platform
-import time
-from abc import ABC, abstractmethod
-from collections.abc import Set
-from contextlib import contextmanager
-from importlib.metadata import PackageNotFoundError
-from io import BytesIO
-from typing import TYPE_CHECKING, Any, Callable, Iterator, List, Optional,
Union
-
-from nitrokey._helpers import Retries
-from nitrokey.nk3 import NK3, NK3Bootloader
-from nitrokey.trussed import TimeoutException, TrussedBase, Version
-from nitrokey.trussed._bootloader import (
- FirmwareContainer,
- Model,
- Variant,
- validate_firmware_image,
-)
-from nitrokey.trussed._bootloader.lpc55_upload.mboot.exceptions import (
- McuBootConnectionError,
-)
-from nitrokey.trussed.admin_app import BootMode, Status
-from nitrokey.trussed.admin_app import Variant as AdminAppVariant
-from nitrokey.updates import Asset, Release
-
-if TYPE_CHECKING:
- import typing_extensions
-
-logger = logging.getLogger(__name__)
-
-
[email protected]
-class Warning(enum.Enum):
- """
- A warning that can occur during a firmware update.
-
- By default, these warnings abort the firmware update. This enum can be
used to select types
- of warnings that should be ignored and not cause the firmware update to
fail.
- """
-
- IFS_MIGRATION_V2 = "ifs-migration-v2"
- MISSING_STATUS = "missing-status"
- SDK_VERSION = "sdk-version"
- UPDATE_FROM_BOOTLOADER = "update-from-bootloader"
-
- @property
- def message(self) -> str:
- if self == Warning.IFS_MIGRATION_V2:
- return (
- "Not enough space on the internal filesystem to perform the
firmware"
- " update. See the release notes for more information:"
- "
https://github.com/Nitrokey/nitrokey-3-firmware/releases/tag/v1.8.2-test.20250312"
- )
- if self == Warning.MISSING_STATUS:
- return (
- "Could not determine the device state as the current firmware
is too old."
- " Please update to firmware version v1.3.1 first."
- )
- if self == Warning.SDK_VERSION:
- return (
- "Your Nitrokey SDK version is outdated. Please update this
program to the latest"
- " version and try again."
- )
- if self == Warning.UPDATE_FROM_BOOTLOADER:
- return (
- "The current state of the device cannot be checked as it is
already in bootloader"
- " mode. Please review the release notes at:"
- " https://github.com/Nitrokey/nitrokey-3-firmware/releases"
- )
-
- if TYPE_CHECKING:
- typing_extensions.assert_never(self)
-
- return self.value
-
- @classmethod
- def from_str(cls, s: str) -> "Warning":
- for w in cls:
- if w.value == s:
- return w
- raise ValueError(f"Unexpected update warning id: {s}")
-
-
[email protected]
-class _Migration(enum.Enum):
- # IFS migration to use journaling on the NRF52 introduced in v1.3.0
- NRF_IFS_MIGRATION = enum.auto()
- # IFS migration to filesystem layout 2 in v1.8.2 (FIDO2 RK migration)
- IFS_MIGRATION_V2 = enum.auto()
-
- @classmethod
- def get(
- cls,
- variant: Union[Variant, AdminAppVariant],
- current: Optional[Version],
- new: Version,
- ) -> frozenset["_Migration"]:
- if isinstance(variant, AdminAppVariant):
- if variant == AdminAppVariant.USBIP:
- raise ValueError("Cannot perform firmware update for USBIP
runner")
- elif variant == AdminAppVariant.LPC55:
- variant = Variant.LPC55
- elif variant == AdminAppVariant.NRF52:
- variant = Variant.NRF52
- else:
- if TYPE_CHECKING:
- typing_extensions.assert_never(variant)
-
- raise ValueError(f"Unsupported device variant: {variant}")
-
- migrations = set()
-
- if variant == Variant.NRF52:
- if (
- current is None
- or current <= Version(1, 2, 2)
- and new >= Version(1, 3, 0)
- ):
- migrations.add(cls.NRF_IFS_MIGRATION)
-
- ifs_migration_v2 = Version(1, 8, 2)
- if (
- current is not None
- and current < ifs_migration_v2
- and new >= ifs_migration_v2
- ):
- migrations.add(cls.IFS_MIGRATION_V2)
-
- return frozenset(migrations)
-
-
-def get_firmware_update(release: Release) -> Asset:
- return release.require_asset(Model.NK3.firmware_pattern)
-
-
-def _get_extra_information(migrations: Set[_Migration]) -> List[str]:
- """Return additional information for the device after update based on
update-path"""
-
- out = []
- if _Migration.NRF_IFS_MIGRATION in migrations:
- out += [
- "",
- "During this update process the internal filesystem will be
migrated!",
- "- Migration will only work, if your internal filesystem does not
contain more than 45 Resident Keys. If you have more please remove some.",
- "- After the update it might take up to 3 minutes for the first
boot.",
- "Never unplug the device while the LED is active!",
- ]
- return out
-
-
-def _get_finalization_wait_retries(migrations: Set[_Migration]) -> int:
- """Return number of retries to wait for the device after update based on
update-path"""
-
- out = 60
- if _Migration.NRF_IFS_MIGRATION in migrations:
- # max time 150secs == 300 retries
- out = 500
- return out
-
-
-class UpdateUi(ABC):
- @abstractmethod
- def error(self, *msgs: Any) -> Exception:
- pass
-
- @abstractmethod
- def show_warning(self, warning: Warning) -> None:
- pass
-
- @abstractmethod
- def raise_warning(self, warning: Warning) -> Exception:
- pass
-
- @abstractmethod
- def abort(self, *msgs: Any) -> Exception:
- pass
-
- @abstractmethod
- def abort_downgrade(self, current: Version, image: Version) -> Exception:
- pass
-
- @abstractmethod
- def abort_pynitrokey_version(
- self, current: Version, required: Version
- ) -> Exception:
- pass
-
- @abstractmethod
- def confirm_download(self, current: Optional[Version], new: Version) ->
None:
- pass
-
- @abstractmethod
- def confirm_update(self, current: Optional[Version], new: Version) -> None:
- pass
-
- @abstractmethod
- def confirm_pynitrokey_version(self, current: Version, required: Version)
-> None:
- pass
-
- @abstractmethod
- def confirm_extra_information(self, extra_info: List[str]) -> None:
- pass
-
- @abstractmethod
- def confirm_update_same_version(self, version: Version) -> None:
- pass
-
- @abstractmethod
- def pre_bootloader_hint(self) -> None:
- pass
-
- @abstractmethod
- def request_bootloader_confirmation(self) -> None:
- pass
-
- @abstractmethod
- @contextmanager
- def download_progress_bar(self, desc: str) -> Iterator[Callable[[int,
int], None]]:
- pass
-
- @abstractmethod
- @contextmanager
- def update_progress_bar(self) -> Iterator[Callable[[int, int], None]]:
- pass
-
- @abstractmethod
- @contextmanager
- def finalization_progress_bar(self) -> Iterator[Callable[[int, int],
None]]:
- pass
-
-
-class Updater:
- def __init__(
- self,
- ui: UpdateUi,
- await_bootloader: Callable[[], NK3Bootloader],
- await_device: Callable[
- [Optional[int], Optional[Callable[[int, int], None]]], NK3
- ],
- ignore_warnings: Set[Warning] = frozenset(),
- ) -> None:
- self.ui = ui
- self.await_bootloader = await_bootloader
- self.await_device = await_device
- self.ignore_warnings = ignore_warnings
-
- def _trigger_warning(self, warning: Warning) -> None:
- if warning in self.ignore_warnings:
- self.ui.show_warning(warning)
- else:
- raise self.ui.raise_warning(warning)
-
- def update(
- self,
- device: TrussedBase,
- image: Optional[str],
- update_version: Optional[str],
- ignore_pynitrokey_version: bool = False,
- ) -> Version:
- update_from_bootloader = False
- current_version = None
- status = None
- if isinstance(device, NK3Bootloader):
- update_from_bootloader = True
- self._trigger_warning(Warning.UPDATE_FROM_BOOTLOADER)
- elif isinstance(device, NK3):
- current_version = device.admin.version()
- status = device.admin.status()
- else:
- raise self.ui.error(f"Unexpected Trussed device: {device}")
-
- logger.info(f"Firmware version before update: {current_version or ''}")
- container = self._prepare_update(image, update_version,
current_version)
-
- if not update_from_bootloader:
- if status is None and container.version >
Version.from_str("1.3.1"):
- self._trigger_warning(Warning.MISSING_STATUS)
-
- self._check_minimum_version(container, ignore_pynitrokey_version)
-
- self.ui.confirm_update(current_version, container.version)
-
- migrations = None
- if status is not None and status.variant is not None:
- migrations = self._check_migrations(
- status.variant, current_version, container.version, status
- )
- elif isinstance(device, NK3Bootloader):
- migrations = self._check_migrations(
- device.variant, current_version, container.version, status
- )
-
- with self._get_bootloader(device) as bootloader:
- if bootloader.variant not in container.images:
- raise self.ui.error(
- "The firmware release does not contain an image for the "
- f"{bootloader.variant.value} hardware variant"
- )
- try:
- validate_firmware_image(
- bootloader.variant,
- container.images[bootloader.variant],
- container.version,
- Model.NK3,
- )
- except Exception as e:
- raise self.ui.error("Failed to validate firmware image", e)
-
- if migrations is None:
- migrations = self._check_migrations(
- bootloader.variant, current_version, container.version,
status
- )
-
- self._perform_update(bootloader, container)
-
- wait_retries = _get_finalization_wait_retries(migrations)
- with self.ui.finalization_progress_bar() as callback:
- with self.await_device(wait_retries, callback) as device:
- version = device.admin.version()
- if version != container.version:
- raise self.ui.error(
- f"The firmware update to {container.version} was
successful, but the "
- f"firmware is still reporting version {version}."
- )
-
- return container.version
-
- def _prepare_update(
- self,
- image: Optional[str],
- version: Optional[str],
- current_version: Optional[Version],
- ) -> FirmwareContainer:
- if image:
- try:
- container = FirmwareContainer.parse(image, Model.NK3)
- except Exception as e:
- raise self.ui.error("Failed to parse firmware container", e)
- self._validate_version(current_version, container.version)
- return container
- else:
- repository = Model.NK3.firmware_repository
- if version:
- try:
- logger.info(f"Downloading firmare version {version}")
- release = repository.get_release(version)
- except Exception as e:
- raise self.ui.error(f"Failed to get firmware release
{version}", e)
- else:
- try:
- release = repository.get_latest_release()
- logger.info(f"Latest firmware version: {release}")
- except Exception as e:
- raise self.ui.error("Failed to find latest firmware
release", e)
-
- try:
- release_version = Version.from_v_str(release.tag)
- except ValueError as e:
- raise self.ui.error("Failed to parse version from release
tag", e)
- self._validate_version(current_version, release_version)
- self.ui.confirm_download(current_version, release_version)
- return self._download_update(release)
-
- def _download_update(self, release: Release) -> FirmwareContainer:
- try:
- update = get_firmware_update(release)
- except Exception as e:
- raise self.ui.error(
- f"Failed to find firmware image for release {release}",
- e,
- )
-
- try:
- logger.info(f"Trying to download firmware update from URL:
{update.url}")
-
- with self.ui.download_progress_bar(update.tag) as callback:
- data = update.read(callback=callback)
- except Exception as e:
- raise self.ui.error(
- f"Failed to download latest firmware update {update.tag}", e
- )
-
- try:
- container = FirmwareContainer.parse(BytesIO(data), Model.NK3)
- except Exception as e:
- raise self.ui.error(
- f"Failed to parse firmware container for {update.tag}", e
- )
-
- release_version = Version.from_v_str(release.tag)
- if release_version != container.version:
- raise self.ui.error(
- f"The firmware container for {update.tag} has the version
{container.version}"
- )
-
- return container
-
- def _check_minimum_version(
- self, container: FirmwareContainer, ignore_pynitrokey_version: bool
- ) -> None:
- if container.sdk:
- try:
- sdk_version =
Version.from_str(importlib.metadata.version("nitrokey"))
- except PackageNotFoundError:
- raise self.ui.error("Failed to determine the Nitrokey SDK
version")
-
- if container.sdk > sdk_version:
- logger.warning(
- f"Minimum SDK version required for update is
{container.sdk} (current version: {sdk_version})"
- )
- self._trigger_warning(Warning.SDK_VERSION)
- elif container.pynitrokey:
- # The minimum pynitrokey version has been replaced by the minimum
SDK version, so we
- # only check it if there is no minimum SDK version set.
-
- # this is the version of pynitrokey when we moved to the SDK
- pynitrokey_version = Version.from_str("0.4.49")
- if container.pynitrokey > pynitrokey_version:
- if ignore_pynitrokey_version:
- self.ui.confirm_pynitrokey_version(
- current=pynitrokey_version,
required=container.pynitrokey
- )
- else:
- raise self.ui.abort_pynitrokey_version(
- current=pynitrokey_version,
required=container.pynitrokey
- )
-
- def _validate_version(
- self,
- current_version: Optional[Version],
- new_version: Version,
- ) -> None:
- logger.info(f"Current firmware version: {current_version}")
- logger.info(f"Updated firmware version: {new_version}")
-
- if current_version:
- if current_version.core() > new_version.core():
- raise self.ui.abort_downgrade(current_version, new_version)
- elif current_version == new_version:
- if current_version.complete and new_version.complete:
- same_version = current_version
- else:
- same_version = current_version.core()
- self.ui.confirm_update_same_version(same_version)
-
- @contextmanager
- def _get_bootloader(self, device: TrussedBase) -> Iterator[NK3Bootloader]:
- if isinstance(device, NK3):
- self.ui.request_bootloader_confirmation()
- try:
- device.admin.reboot(BootMode.BOOTROM)
- except TimeoutException:
- raise self.ui.abort(
- "The reboot was not confirmed with the touch button"
- )
-
- # needed for udev to properly handle new device
- time.sleep(1)
-
- self.ui.pre_bootloader_hint()
-
- exc = None
- for t in Retries(3):
- logger.debug(f"Trying to connect to bootloader ({t})")
- try:
- with self.await_bootloader() as bootloader:
- # noop to test communication
- bootloader.uuid
- yield bootloader
- break
- except McuBootConnectionError as e:
- logger.debug("Received connection error", exc_info=True)
- exc = e
- else:
- msgs = ["Failed to connect to Nitrokey 3 bootloader"]
- if platform.system() == "Linux":
- msgs += ["Are the Nitrokey udev rules installed and
active?"]
- raise self.ui.error(*msgs, exc)
- elif isinstance(device, NK3Bootloader):
- yield device
- else:
- raise self.ui.error(f"Unexpected Nitrokey 3 device: {device}")
-
- def _check_migrations(
- self,
- variant: Union[Variant, AdminAppVariant],
- current_version: Optional[Version],
- new_version: Version,
- status: Optional[Status],
- ) -> frozenset["_Migration"]:
- try:
- migrations = _Migration.get(
- variant=variant, current=current_version, new=new_version
- )
- except ValueError as e:
- raise self.ui.error(str(e))
-
- txt = _get_extra_information(migrations)
- self.ui.confirm_extra_information(txt)
-
- if _Migration.IFS_MIGRATION_V2 in migrations:
- if status and status.ifs_blocks is not None and status.ifs_blocks
< 5:
- self._trigger_warning(Warning.IFS_MIGRATION_V2)
-
- return migrations
-
- def _perform_update(
- self, device: NK3Bootloader, container: FirmwareContainer
- ) -> None:
- logger.debug("Starting firmware update")
- image = container.images[device.variant]
- with self.ui.update_progress_bar() as callback:
- try:
- device.update(image, callback=callback)
- except Exception as e:
- raise self.ui.error("Failed to perform firmware update", e)
- logger.debug("Firmware update finished successfully")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/nitrokey-0.3.2/src/nitrokey/nkpk.py
new/nitrokey-0.4.0/src/nitrokey/nkpk.py
--- old/nitrokey-0.3.2/src/nitrokey/nkpk.py 2025-07-08 12:41:08.000000000
+0200
+++ new/nitrokey-0.4.0/src/nitrokey/nkpk.py 1970-01-01 01:00:00.000000000
+0100
@@ -11,6 +11,7 @@
from nitrokey import _VID_NITROKEY
from nitrokey.trussed import Fido2Certs, TrussedDevice, Version
+from nitrokey.trussed._base import Model
from nitrokey.trussed._bootloader import ModelData
from nitrokey.trussed._bootloader.nrf52 import SignatureKey,
TrussedBootloaderNrf52
@@ -49,6 +50,10 @@
super().__init__(device, _FIDO2_CERTS)
@property
+ def model(self) -> Model:
+ return Model.NKPK
+
+ @property
def pid(self) -> int:
return _PID_NKPK_DEVICE
@@ -78,6 +83,10 @@
class NKPKBootloader(TrussedBootloaderNrf52):
@property
+ def model(self) -> Model:
+ return Model.NKPK
+
+ @property
def name(self) -> str:
return "Nitrokey Passkey Bootloader"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/nitrokey-0.3.2/src/nitrokey/trussed/__init__.py
new/nitrokey-0.4.0/src/nitrokey/trussed/__init__.py
--- old/nitrokey-0.3.2/src/nitrokey/trussed/__init__.py 2025-07-08
12:41:08.000000000 +0200
+++ new/nitrokey-0.4.0/src/nitrokey/trussed/__init__.py 1970-01-01
01:00:00.000000000 +0100
@@ -5,10 +5,10 @@
# http://opensource.org/licenses/MIT>, at your option. This file may not be
# copied, modified, or distributed except according to those terms.
+from ._base import Model as Model # noqa: F401
from ._base import TrussedBase as TrussedBase # noqa: F401
from ._bootloader import FirmwareContainer as FirmwareContainer # noqa: F401
from ._bootloader import FirmwareMetadata as FirmwareMetadata # noqa: F401
-from ._bootloader import Model as Model # noqa: F401
from ._bootloader import TrussedBootloader as TrussedBootloader # noqa: F401
from ._bootloader import Variant as Variant # noqa: F401
from ._bootloader import parse_firmware_image as parse_firmware_image # noqa:
F401
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/nitrokey-0.3.2/src/nitrokey/trussed/_base.py
new/nitrokey-0.4.0/src/nitrokey/trussed/_base.py
--- old/nitrokey-0.3.2/src/nitrokey/trussed/_base.py 2025-07-08
12:41:08.000000000 +0200
+++ new/nitrokey-0.4.0/src/nitrokey/trussed/_base.py 1970-01-01
01:00:00.000000000 +0100
@@ -6,6 +6,7 @@
# copied, modified, or distributed except according to those terms.
from abc import ABC, abstractmethod
+from enum import Enum
from typing import Optional, TypeVar
from nitrokey import _VID_NITROKEY
@@ -15,6 +16,21 @@
T = TypeVar("T", bound="TrussedBase")
+class Model(Enum):
+ NK3 = "Nitrokey 3"
+ NKPK = "Nitrokey Passkey"
+
+ def __str__(self) -> str:
+ return self.value
+
+ @classmethod
+ def from_str(cls, s: str) -> "Model":
+ for model in cls:
+ if model.value == s:
+ return model
+ raise ValueError(f"Unknown model {s}")
+
+
class TrussedBase(ABC):
"""
Base class for Nitrokey devices using the Trussed framework and running
@@ -35,6 +51,10 @@
)
@property
+ @abstractmethod
+ def model(self) -> Model: ...
+
+ @property
def vid(self) -> int:
return _VID_NITROKEY
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/nitrokey-0.3.2/src/nitrokey/trussed/_bootloader/__init__.py
new/nitrokey-0.4.0/src/nitrokey/trussed/_bootloader/__init__.py
--- old/nitrokey-0.3.2/src/nitrokey/trussed/_bootloader/__init__.py
2025-07-08 12:41:08.000000000 +0200
+++ new/nitrokey-0.4.0/src/nitrokey/trussed/_bootloader/__init__.py
1970-01-01 01:00:00.000000000 +0100
@@ -9,7 +9,6 @@
import hashlib
import json
import logging
-import re
import sys
from abc import abstractmethod
from dataclasses import dataclass
@@ -18,9 +17,7 @@
from typing import TYPE_CHECKING, Callable, Dict, Optional, Tuple, Union
from zipfile import ZipFile
-from nitrokey.updates import Repository
-
-from .._base import TrussedBase
+from .._base import Model, TrussedBase
from .._utils import Version
if TYPE_CHECKING:
@@ -39,39 +36,16 @@
nrf52_signature_keys: list["SignatureKey"]
-class Model(enum.Enum):
- NK3 = "Nitrokey 3"
- NKPK = "Nitrokey Passkey"
-
- def __str__(self) -> str:
- return self.value
-
- @property
- def firmware_repository(self) -> Repository:
- return Repository(owner="Nitrokey",
name=self._data.firmware_repository_name)
+def get_model_data(model: Model) -> ModelData:
+ if model == Model.NK3:
+ from nitrokey.nk3 import _NK3_DATA
+
+ return _NK3_DATA
+ if model == Model.NKPK:
+ from nitrokey.nkpk import _NKPK_DATA
- @property
- def firmware_pattern(self) -> Pattern[str]:
- return re.compile(self._data.firmware_pattern_string)
-
- @property
- def _data(self) -> "ModelData":
- if self == Model.NK3:
- from nitrokey.nk3 import _NK3_DATA
-
- return _NK3_DATA
- if self == Model.NKPK:
- from nitrokey.nkpk import _NKPK_DATA
-
- return _NKPK_DATA
- raise ValueError(f"Unknown model {self}")
-
- @classmethod
- def from_str(cls, s: str) -> "Model":
- for model in cls:
- if model.value == s:
- return model
- raise ValueError(f"Unknown model {s}")
+ return _NKPK_DATA
+ raise ValueError(f"Unknown model {model}")
class Variant(enum.Enum):
@@ -221,6 +195,8 @@
if variant == Variant.LPC55:
return parse_firmware_image_lpc55(data)
elif variant == Variant.NRF52:
- return parse_firmware_image_nrf52(data,
model._data.nrf52_signature_keys)
+ return parse_firmware_image_nrf52(
+ data, get_model_data(model).nrf52_signature_keys
+ )
else:
raise ValueError(f"Unexpected variant {variant}")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/nitrokey-0.3.2/src/nitrokey/trussed/_bootloader/nrf52_upload/dfu/dfu_transport_serial.py
new/nitrokey-0.4.0/src/nitrokey/trussed/_bootloader/nrf52_upload/dfu/dfu_transport_serial.py
---
old/nitrokey-0.3.2/src/nitrokey/trussed/_bootloader/nrf52_upload/dfu/dfu_transport_serial.py
2025-07-08 12:41:08.000000000 +0200
+++
new/nitrokey-0.4.0/src/nitrokey/trussed/_bootloader/nrf52_upload/dfu/dfu_transport_serial.py
1970-01-01 01:00:00.000000000 +0100
@@ -146,9 +146,9 @@
while finished is False:
byte = self.serial_port.read(1)
if byte:
- (byte) = struct.unpack("B", byte)[0]
+ (unpacked_byte) = struct.unpack("B", byte)[0]
(finished, current_state, decoded_data) = Slip.decode_add_byte(
- byte, decoded_data, current_state
+ unpacked_byte, decoded_data, current_state
)
else:
current_state = Slip.SLIP_STATE_CLEARING_INVALID_PACKET
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/nitrokey-0.3.2/src/nitrokey/trussed/admin_app.py
new/nitrokey-0.4.0/src/nitrokey/trussed/admin_app.py
--- old/nitrokey-0.3.2/src/nitrokey/trussed/admin_app.py 2025-07-08
12:41:08.000000000 +0200
+++ new/nitrokey-0.4.0/src/nitrokey/trussed/admin_app.py 1970-01-01
01:00:00.000000000 +0100
@@ -63,6 +63,7 @@
SE050_ERROR = 0b00010000
CONFIG_ERROR = 0b00100000
RNG_ERROR = 0b01000000
+ EXT_FLASH_NEED_REFORMAT = 0b10000000
def is_error(self) -> bool:
return self.value != 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/nitrokey-0.3.2/src/nitrokey/trussed/updates.py
new/nitrokey-0.4.0/src/nitrokey/trussed/updates.py
--- old/nitrokey-0.3.2/src/nitrokey/trussed/updates.py 1970-01-01
01:00:00.000000000 +0100
+++ new/nitrokey-0.4.0/src/nitrokey/trussed/updates.py 1970-01-01
01:00:00.000000000 +0100
@@ -0,0 +1,568 @@
+# Copyright 2022 Nitrokey Developers
+#
+# Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
+# http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
+# http://opensource.org/licenses/MIT>, at your option. This file may not be
+# copied, modified, or distributed except according to those terms.
+
+import enum
+import importlib.metadata
+import logging
+import platform
+import re
+import time
+from abc import ABC, abstractmethod
+from collections.abc import Set
+from contextlib import contextmanager
+from importlib.metadata import PackageNotFoundError
+from io import BytesIO
+from typing import TYPE_CHECKING, Any, Callable, Iterator, List, Optional,
Tuple, Union
+
+from nitrokey._helpers import Retries
+from nitrokey.trussed import TimeoutException, TrussedBase, Version
+from nitrokey.trussed._base import Model
+from nitrokey.trussed._bootloader import (
+ FirmwareContainer,
+ TrussedBootloader,
+ Variant,
+ get_model_data,
+ validate_firmware_image,
+)
+from nitrokey.trussed._bootloader.lpc55_upload.mboot.exceptions import (
+ McuBootConnectionError,
+)
+from nitrokey.trussed._device import TrussedDevice
+from nitrokey.trussed.admin_app import BootMode, Status
+from nitrokey.trussed.admin_app import Variant as AdminAppVariant
+from nitrokey.updates import Asset, Release, Repository
+
+if TYPE_CHECKING:
+ import typing_extensions
+
+logger = logging.getLogger(__name__)
+
+
[email protected]
+class Warning(enum.Enum):
+ """
+ A warning that can occur during a firmware update.
+
+ By default, these warnings abort the firmware update. This enum can be
used to select types
+ of warnings that should be ignored and not cause the firmware update to
fail.
+ """
+
+ IFS_MIGRATION_V2 = "ifs-migration-v2"
+ MISSING_STATUS = "missing-status"
+ SDK_VERSION = "sdk-version"
+ UPDATE_FROM_BOOTLOADER = "update-from-bootloader"
+
+ @property
+ def message(self) -> str:
+ if self == Warning.IFS_MIGRATION_V2:
+ return (
+ "Not enough space on the internal filesystem to perform the
firmware"
+ " update. See the release notes for more information:"
+ "
https://github.com/Nitrokey/nitrokey-3-firmware/releases/tag/v1.8.2-test.20250312"
+ )
+ if self == Warning.MISSING_STATUS:
+ return (
+ "Could not determine the device state as the current firmware
is too old."
+ " Please update to firmware version v1.3.1 first."
+ )
+ if self == Warning.SDK_VERSION:
+ return (
+ "Your Nitrokey SDK version is outdated. Please update this
program to the latest"
+ " version and try again."
+ )
+ if self == Warning.UPDATE_FROM_BOOTLOADER:
+ return (
+ "The current state of the device cannot be checked as it is
already in bootloader"
+ " mode. Please review the release notes at:"
+ " https://github.com/Nitrokey/nitrokey-3-firmware/releases"
+ )
+
+ if TYPE_CHECKING:
+ typing_extensions.assert_never(self)
+
+ return self.value
+
+ @classmethod
+ def from_str(cls, s: str) -> "Warning":
+ for w in cls:
+ if w.value == s:
+ return w
+ raise ValueError(f"Unexpected update warning id: {s}")
+
+
[email protected]
+class _Migration(enum.Enum):
+ # IFS migration to use journaling on the NRF52 introduced in v1.3.0 (NK3)
+ NRF_IFS_MIGRATION = enum.auto()
+ # IFS migration to filesystem layout 2 (FIDO2 RK migration) in v1.8.2 (NK3)
+ IFS_MIGRATION_V2 = enum.auto()
+
+ @classmethod
+ def get(
+ cls,
+ model: Model,
+ variant: Union[Variant, AdminAppVariant],
+ current: Optional[Version],
+ new: Version,
+ ) -> frozenset["_Migration"]:
+ if model != Model.NK3:
+ return frozenset()
+
+ if isinstance(variant, AdminAppVariant):
+ if variant == AdminAppVariant.USBIP:
+ raise ValueError("Cannot perform firmware update for USBIP
runner")
+ elif variant == AdminAppVariant.LPC55:
+ variant = Variant.LPC55
+ elif variant == AdminAppVariant.NRF52:
+ variant = Variant.NRF52
+ else:
+ if TYPE_CHECKING:
+ typing_extensions.assert_never(variant)
+
+ raise ValueError(f"Unsupported device variant: {variant}")
+
+ migrations = set()
+
+ if variant == Variant.NRF52:
+ if (
+ current is None
+ or current <= Version(1, 2, 2)
+ and new >= Version(1, 3, 0)
+ ):
+ migrations.add(cls.NRF_IFS_MIGRATION)
+
+ ifs_migration_v2 = Version(1, 8, 2)
+ if (
+ current is not None
+ and current < ifs_migration_v2
+ and new >= ifs_migration_v2
+ ):
+ migrations.add(cls.IFS_MIGRATION_V2)
+
+ return frozenset(migrations)
+
+
+def get_firmware_repository(model: Model) -> Repository:
+ data = get_model_data(model)
+ return Repository(owner="Nitrokey", name=data.firmware_repository_name)
+
+
+def get_firmware_update(model: Model, release: Release) -> Asset:
+ data = get_model_data(model)
+ return release.require_asset(re.compile(data.firmware_pattern_string))
+
+
+def _get_extra_information(migrations: Set[_Migration]) -> List[str]:
+ """Return additional information for the device after update based on
update-path"""
+
+ out = []
+ if _Migration.NRF_IFS_MIGRATION in migrations:
+ out += [
+ "",
+ "During this update process the internal filesystem will be
migrated!",
+ "- Migration will only work, if your internal filesystem does not
contain more than 45 Resident Keys. If you have more please remove some.",
+ "- After the update it might take up to 3 minutes for the first
boot.",
+ "Never unplug the device while the LED is active!",
+ ]
+ return out
+
+
+def _get_finalization_wait_retries(migrations: Set[_Migration]) -> int:
+ """Return number of retries to wait for the device after update based on
update-path"""
+
+ out = 60
+ if _Migration.NRF_IFS_MIGRATION in migrations:
+ # max time 150secs == 300 retries
+ out = 500
+ return out
+
+
+class UpdateUi(ABC):
+ @abstractmethod
+ def error(self, *msgs: Any) -> Exception:
+ pass
+
+ @abstractmethod
+ def show_warning(self, warning: Warning) -> None:
+ pass
+
+ @abstractmethod
+ def raise_warning(self, warning: Warning) -> Exception:
+ pass
+
+ @abstractmethod
+ def abort(self, *msgs: Any) -> Exception:
+ pass
+
+ @abstractmethod
+ def abort_downgrade(self, current: Version, image: Version) -> Exception:
+ pass
+
+ @abstractmethod
+ def abort_pynitrokey_version(
+ self, current: Version, required: Version
+ ) -> Exception:
+ pass
+
+ @abstractmethod
+ def confirm_download(self, current: Optional[Version], new: Version) ->
None:
+ pass
+
+ @abstractmethod
+ def confirm_update(self, current: Optional[Version], new: Version) -> None:
+ pass
+
+ @abstractmethod
+ def confirm_pynitrokey_version(self, current: Version, required: Version)
-> None:
+ pass
+
+ @abstractmethod
+ def confirm_extra_information(self, extra_info: List[str]) -> None:
+ pass
+
+ @abstractmethod
+ def confirm_update_same_version(self, version: Version) -> None:
+ pass
+
+ @abstractmethod
+ def pre_bootloader_hint(self) -> None:
+ pass
+
+ @abstractmethod
+ def request_bootloader_confirmation(self) -> None:
+ pass
+
+ @abstractmethod
+ @contextmanager
+ def download_progress_bar(self, desc: str) -> Iterator[Callable[[int,
int], None]]:
+ pass
+
+ @abstractmethod
+ @contextmanager
+ def update_progress_bar(self) -> Iterator[Callable[[int, int], None]]:
+ pass
+
+ @abstractmethod
+ @contextmanager
+ def finalization_progress_bar(self) -> Iterator[Callable[[int, int],
None]]:
+ pass
+
+
+class DeviceHandler(ABC):
+ @abstractmethod
+ def await_bootloader(self, model: Model) -> TrussedBootloader: ...
+
+ @abstractmethod
+ def await_device(
+ self,
+ model: Model,
+ wait_retries: Optional[int],
+ callback: Optional[Callable[[int, int], None]],
+ ) -> TrussedDevice: ...
+
+
+class Updater:
+ def __init__(
+ self,
+ ui: UpdateUi,
+ device_handler: DeviceHandler,
+ ignore_warnings: Set[Warning] = frozenset(),
+ ) -> None:
+ self.ui = ui
+ self.device_handler = device_handler
+ self.ignore_warnings = ignore_warnings
+
+ def _trigger_warning(self, warning: Warning) -> None:
+ if warning in self.ignore_warnings:
+ self.ui.show_warning(warning)
+ else:
+ raise self.ui.raise_warning(warning)
+
+ def update(
+ self,
+ device: TrussedBase,
+ image: Optional[str],
+ update_version: Optional[str],
+ ignore_pynitrokey_version: bool = False,
+ ) -> Tuple[Version, Status]:
+ model = device.model
+
+ update_from_bootloader = False
+ current_version = None
+ status = None
+ if isinstance(device, TrussedBootloader):
+ update_from_bootloader = True
+ self._trigger_warning(Warning.UPDATE_FROM_BOOTLOADER)
+ elif isinstance(device, TrussedDevice):
+ current_version = device.admin.version()
+ status = device.admin.status()
+ else:
+ raise self.ui.error(f"Unexpected Trussed device: {device}")
+
+ logger.info(f"Firmware version before update: {current_version or ''}")
+ container = self._prepare_update(model, image, update_version,
current_version)
+
+ if not update_from_bootloader:
+ if status is None:
+ if model == Model.NK3:
+ if container.version > Version.from_str("1.3.1"):
+ self._trigger_warning(Warning.MISSING_STATUS)
+ else:
+ self.ui.error(f"Missing status for {model} device")
+
+ self._check_minimum_version(container, ignore_pynitrokey_version)
+
+ self.ui.confirm_update(current_version, container.version)
+
+ migrations = None
+ if status is not None and status.variant is not None:
+ migrations = self._check_migrations(
+ model, status.variant, current_version, container.version,
status
+ )
+ elif isinstance(device, TrussedBootloader):
+ migrations = self._check_migrations(
+ model, device.variant, current_version, container.version,
status
+ )
+
+ with self._get_bootloader(device) as bootloader:
+ if bootloader.variant not in container.images:
+ raise self.ui.error(
+ "The firmware release does not contain an image for the "
+ f"{bootloader.variant.value} hardware variant"
+ )
+ try:
+ validate_firmware_image(
+ bootloader.variant,
+ container.images[bootloader.variant],
+ container.version,
+ model,
+ )
+ except Exception as e:
+ raise self.ui.error("Failed to validate firmware image", e)
+
+ if migrations is None:
+ migrations = self._check_migrations(
+ model,
+ bootloader.variant,
+ current_version,
+ container.version,
+ status,
+ )
+
+ self._perform_update(bootloader, container)
+
+ wait_retries = _get_finalization_wait_retries(migrations)
+ with self.ui.finalization_progress_bar() as callback:
+ with self.device_handler.await_device(
+ model, wait_retries, callback
+ ) as device:
+ version = device.admin.version()
+ if version != container.version:
+ raise self.ui.error(
+ f"The firmware update to {container.version} was
successful, but the "
+ f"firmware is still reporting version {version}."
+ )
+ status = device.admin.status()
+
+ return container.version, status
+
+ def _prepare_update(
+ self,
+ model: Model,
+ image: Optional[str],
+ version: Optional[str],
+ current_version: Optional[Version],
+ ) -> FirmwareContainer:
+ if image:
+ try:
+ container = FirmwareContainer.parse(image, model)
+ except Exception as e:
+ raise self.ui.error("Failed to parse firmware container", e)
+ self._validate_version(current_version, container.version)
+ return container
+ else:
+ repository = get_firmware_repository(model)
+ if version:
+ try:
+ logger.info(f"Downloading firmare version {version}")
+ release = repository.get_release(version)
+ except Exception as e:
+ raise self.ui.error(f"Failed to get firmware release
{version}", e)
+ else:
+ try:
+ release = repository.get_latest_release()
+ logger.info(f"Latest firmware version: {release}")
+ except Exception as e:
+ raise self.ui.error("Failed to find latest firmware
release", e)
+
+ try:
+ release_version = Version.from_v_str(release.tag)
+ except ValueError as e:
+ raise self.ui.error("Failed to parse version from release
tag", e)
+ self._validate_version(current_version, release_version)
+ self.ui.confirm_download(current_version, release_version)
+ return self._download_update(model, release)
+
+ def _download_update(self, model: Model, release: Release) ->
FirmwareContainer:
+ try:
+ update = get_firmware_update(model, release)
+ except Exception as e:
+ raise self.ui.error(
+ f"Failed to find firmware image for release {release}",
+ e,
+ )
+
+ try:
+ logger.info(f"Trying to download firmware update from URL:
{update.url}")
+
+ with self.ui.download_progress_bar(update.tag) as callback:
+ data = update.read(callback=callback)
+ except Exception as e:
+ raise self.ui.error(
+ f"Failed to download latest firmware update {update.tag}", e
+ )
+
+ try:
+ container = FirmwareContainer.parse(BytesIO(data), model)
+ except Exception as e:
+ raise self.ui.error(
+ f"Failed to parse firmware container for {update.tag}", e
+ )
+
+ release_version = Version.from_v_str(release.tag)
+ if release_version != container.version:
+ raise self.ui.error(
+ f"The firmware container for {update.tag} has the version
{container.version}"
+ )
+
+ return container
+
+ def _check_minimum_version(
+ self, container: FirmwareContainer, ignore_pynitrokey_version: bool
+ ) -> None:
+ if container.sdk:
+ try:
+ sdk_version =
Version.from_str(importlib.metadata.version("nitrokey"))
+ except PackageNotFoundError:
+ raise self.ui.error("Failed to determine the Nitrokey SDK
version")
+
+ if container.sdk > sdk_version:
+ logger.warning(
+ f"Minimum SDK version required for update is
{container.sdk} (current version: {sdk_version})"
+ )
+ self._trigger_warning(Warning.SDK_VERSION)
+ elif container.pynitrokey:
+ # The minimum pynitrokey version has been replaced by the minimum
SDK version, so we
+ # only check it if there is no minimum SDK version set.
+
+ # this is the version of pynitrokey when we moved to the SDK
+ pynitrokey_version = Version.from_str("0.4.49")
+ if container.pynitrokey > pynitrokey_version:
+ if ignore_pynitrokey_version:
+ self.ui.confirm_pynitrokey_version(
+ current=pynitrokey_version,
required=container.pynitrokey
+ )
+ else:
+ raise self.ui.abort_pynitrokey_version(
+ current=pynitrokey_version,
required=container.pynitrokey
+ )
+
+ def _validate_version(
+ self,
+ current_version: Optional[Version],
+ new_version: Version,
+ ) -> None:
+ logger.info(f"Current firmware version: {current_version}")
+ logger.info(f"Updated firmware version: {new_version}")
+
+ if current_version:
+ if current_version.core() > new_version.core():
+ raise self.ui.abort_downgrade(current_version, new_version)
+ elif current_version == new_version:
+ if current_version.complete and new_version.complete:
+ same_version = current_version
+ else:
+ same_version = current_version.core()
+ self.ui.confirm_update_same_version(same_version)
+
+ @contextmanager
+ def _get_bootloader(self, device: TrussedBase) ->
Iterator[TrussedBootloader]:
+ model = device.model
+ if isinstance(device, TrussedDevice):
+ self.ui.request_bootloader_confirmation()
+ try:
+ device.admin.reboot(BootMode.BOOTROM)
+ except TimeoutException:
+ raise self.ui.abort(
+ "The reboot was not confirmed with the touch button"
+ )
+
+ # needed for udev to properly handle new device
+ time.sleep(1)
+
+ self.ui.pre_bootloader_hint()
+
+ exc = None
+ for t in Retries(3):
+ logger.debug(f"Trying to connect to bootloader ({t})")
+ try:
+ with self.device_handler.await_bootloader(model) as
bootloader:
+ # noop to test communication
+ bootloader.uuid
+ yield bootloader
+ break
+ except McuBootConnectionError as e:
+ logger.debug("Received connection error", exc_info=True)
+ exc = e
+ else:
+ msgs = [f"Failed to connect to {model} bootloader"]
+ if platform.system() == "Linux":
+ msgs += ["Are the Nitrokey udev rules installed and
active?"]
+ raise self.ui.error(*msgs, exc)
+ elif isinstance(device, TrussedBootloader):
+ yield device
+ else:
+ raise self.ui.error(f"Unexpected {model} device: {device}")
+
+ def _check_migrations(
+ self,
+ model: Model,
+ variant: Union[Variant, AdminAppVariant],
+ current_version: Optional[Version],
+ new_version: Version,
+ status: Optional[Status],
+ ) -> frozenset["_Migration"]:
+ try:
+ migrations = _Migration.get(
+ model=model,
+ variant=variant,
+ current=current_version,
+ new=new_version,
+ )
+ except ValueError as e:
+ raise self.ui.error(str(e))
+
+ txt = _get_extra_information(migrations)
+ self.ui.confirm_extra_information(txt)
+
+ if _Migration.IFS_MIGRATION_V2 in migrations:
+ if status and status.ifs_blocks is not None and status.ifs_blocks
< 5:
+ self._trigger_warning(Warning.IFS_MIGRATION_V2)
+
+ return migrations
+
+ def _perform_update(
+ self, device: TrussedBootloader, container: FirmwareContainer
+ ) -> None:
+ logger.debug("Starting firmware update")
+ image = container.images[device.variant]
+ with self.ui.update_progress_bar() as callback:
+ try:
+ device.update(image, callback=callback)
+ except Exception as e:
+ raise self.ui.error("Failed to perform firmware update", e)
+ logger.debug("Firmware update finished successfully")