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