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-04-26 21:12:03 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pynitrokey (Old) and /work/SRC/openSUSE:Factory/.python-pynitrokey.new.11940 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pynitrokey" Sun Apr 26 21:12:03 2026 rev:25 rq:1349334 version:0.12.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pynitrokey/python-pynitrokey.changes 2026-03-17 19:06:22.657346520 +0100 +++ /work/SRC/openSUSE:Factory/.python-pynitrokey.new.11940/python-pynitrokey.changes 2026-04-26 21:14:44.677832739 +0200 @@ -1,0 +2,10 @@ +Wed Apr 1 06:26:18 UTC 2026 - Johannes Kastl <[email protected]> + +- update to 0.12.0: + * PIV: Add support for RSA 2048 private key import by + @sosthene-nitrokey in #721 + * Enable support for python 3.14 by @sosthene-nitrokey in #738 + * Add support for NetHSM v4 and release v0.12.0 by + @robin-nitrokey in #746 + +------------------------------------------------------------------- Old: ---- pynitrokey-0.11.4.tar.gz New: ---- pynitrokey-0.12.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pynitrokey.spec ++++++ --- /var/tmp/diff_new_pack.2fBs4X/_old 2026-04-26 21:14:45.357860427 +0200 +++ /var/tmp/diff_new_pack.2fBs4X/_new 2026-04-26 21:14:45.361860589 +0200 @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-pynitrokey -Version: 0.11.4 +Version: 0.12.0 Release: 0 Summary: Python Library for Nitrokey devices License: Apache-2.0 OR MIT @@ -38,7 +38,7 @@ # https://github.com/Nitrokey/pynitrokey/issues/601 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 nethsm >= 2.1 with %python-nethsm < 3} 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} @@ -57,7 +57,7 @@ Requires: (python-fido2 >= 2 with python-fido2 < 3) 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-nethsm >= 2.1 with python-nethsm < 3) 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) ++++++ pynitrokey-0.11.4.tar.gz -> pynitrokey-0.12.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pynitrokey-0.11.4/PKG-INFO new/pynitrokey-0.12.0/PKG-INFO --- old/pynitrokey-0.11.4/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 +++ new/pynitrokey-0.12.0/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: pynitrokey -Version: 0.11.4 +Version: 0.12.0 Summary: Python client for Nitrokey devices License: Apache-2.0 OR MIT License-File: LICENSES/Apache-2.0.txt @@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 Provides-Extra: pcsc Requires-Dist: cffi (>=1.15,<3) Requires-Dist: click (>=8.2,<9) @@ -27,7 +28,7 @@ Requires-Dist: hidapi (>=0.14,<0.15) Requires-Dist: intelhex (>=2.3,<3) Requires-Dist: libusb1 (>=3,<4) -Requires-Dist: nethsm (>=2.0.1,<3) +Requires-Dist: nethsm (>=2.1,<3) Requires-Dist: nitrokey (>=0.4.2,<0.5) Requires-Dist: nkdfu (>=0.2,<0.3) Requires-Dist: pyscard (>=2,<3) ; extra == "pcsc" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pynitrokey-0.11.4/pynitrokey/__init__.py new/pynitrokey-0.12.0/pynitrokey/__init__.py --- old/pynitrokey-0.11.4/pynitrokey/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pynitrokey-0.12.0/pynitrokey/__init__.py 1970-01-01 01:00:00.000000000 +0100 @@ -4,5 +4,4 @@ """Python Library for Nitrokey devices.""" - __all__ = ["client", "commands", "dfu", "enums", "exceptions", "helpers", "operations"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pynitrokey-0.11.4/pynitrokey/cli/nethsm.py new/pynitrokey-0.12.0/pynitrokey/cli/nethsm.py --- old/pynitrokey-0.11.4/pynitrokey/cli/nethsm.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pynitrokey-0.12.0/pynitrokey/cli/nethsm.py 1970-01-01 01:00:00.000000000 +0100 @@ -12,12 +12,13 @@ import sys from dataclasses import dataclass from enum import Enum +from pathlib import Path from typing import Any, Iterable, Iterator, Optional, Protocol, Sequence import click import nethsm as nethsm_sdk from click import Context -from nethsm import Authentication, Base64, NetHSM, State +from nethsm import Authentication, Base64, ClusterJoinData, NetHSM, State from nethsm.backup import EncryptedBackup from pynitrokey.cli.exceptions import CliException @@ -935,6 +936,12 @@ print(f"Key {key_id} generated on NetHSM {nethsm.host}") +def _optional_or(s: Optional[str], default: str) -> str: + if s is None: + return default + return s + + @nethsm.command() @click.option("--logging", is_flag=True, help="Query the logging configuration") @click.option("--network", is_flag=True, help="Query the network configuration") @@ -980,6 +987,12 @@ print(" IP address: ", network_config.ip_address) print(" Netmask: ", network_config.netmask) print(" Gateway: ", network_config.gateway) + ipv6 = "not configured" if network_config.ipv6 is None else "" + print(" IPv6: ", ipv6) + if network_config.ipv6 is not None: + print(" CIDR: ", network_config.ipv6.cidr) + gateway = _optional_or(network_config.ipv6.gateway, "not configured") + print(" Gateway: ", gateway) if show_all or time: time_config = nethsm.get_config_time() @@ -1164,16 +1177,30 @@ help="The new gateway", required=True, ) [email protected]("--ipv6-cidr") [email protected]("--ipv6-gateway") @click.pass_context def set_network_config( - ctx: Context, ip_address: str, netmask: str, gateway: str + ctx: Context, + ip_address: str, + netmask: str, + gateway: str, + ipv6_cidr: Optional[str], + ipv6_gateway: Optional[str], ) -> None: """Set the network configuration of a NetHSM. This command requires authentication as a user with the Administrator role.""" + ipv6 = None + if ipv6_cidr is not None or ipv6_gateway is not None: + if ipv6_cidr is None: + raise CliException( + f"--ipv6-cidr must be set if --ipv6-gateway is set", support_hint=False + ) + ipv6 = nethsm_sdk.Ipv6Config(cidr=ipv6_cidr, gateway=ipv6_gateway) with connect(ctx) as nethsm: - nethsm.set_network_config(ip_address, netmask, gateway) + nethsm.set_network_config(ip_address, netmask, gateway, ipv6=ipv6) print(f"Updated the network configuration for NetHSM {nethsm.host}") @@ -1254,7 +1281,7 @@ This command requires authentication as a user with the Administrator role.""" - (api, key_id) = get_api_or_key_id(api, key_id) + api, key_id = get_api_or_key_id(api, key_id) with connect(ctx) as nethsm: with open(filename, "rb") as f: if key_id: @@ -1283,7 +1310,7 @@ This command requires authentication as a user with the Administrator role. The certificate for a key can also be queried by a user with the Operator role.""" - (api, key_id) = get_api_or_key_id(api, key_id) + api, key_id = get_api_or_key_id(api, key_id) with connect(ctx) as nethsm: if key_id: cert = nethsm.get_key_certificate(key_id) @@ -1350,7 +1377,7 @@ This command requires authentication as a user with the Administrator role.""" - (api, key_id) = get_api_or_key_id(api, key_id) + api, key_id = get_api_or_key_id(api, key_id) with connect(ctx) as nethsm: if key_id: csr = nethsm.key_csr( @@ -1951,3 +1978,82 @@ with connect(ctx, require_auth=True) as nethsm: print("Perform factory reset.") nethsm.factory_reset() + + [email protected]() [email protected]( + "--url", + multiple=True, +) [email protected]("join_data_path", type=Path) [email protected]_context +def add_cluster_member(ctx: Context, url: tuple[str], join_data_path: Path) -> None: + with connect(ctx) as nethsm: + join_data = nethsm.add_cluster_member(list(url)) + + s = json.dumps(join_data.to_dict()) + join_data_path.write_text(s) + print("Wrote join data to {join_data_path}") + + [email protected]() [email protected]_context +def list_cluster_members(ctx: Context) -> None: + with connect(ctx) as nethsm: + cluster_members = nethsm.list_cluster_members() + + n = len(cluster_members) + print(f"{n} cluster members:") + for m in cluster_members: + print(f"- id: {m.id}") + print(f" name: {m.name}") + print(f" peer URLs: {m.urls}") + + [email protected]() [email protected]( + "--url", + multiple=True, +) [email protected]("member-id") [email protected]_context +def set_cluster_member_urls(ctx: Context, member_id: str, url: tuple[str]) -> None: + with connect(ctx) as nethsm: + nethsm.set_cluster_member_urls(member_id, list(url)) + + [email protected]() [email protected]("member-id") [email protected]_context +def remove_cluster_member(ctx: Context, member_id: str) -> None: + with connect(ctx) as nethsm: + nethsm.remove_cluster_member(member_id) + + [email protected]() [email protected]("--backup-passphrase") [email protected]("join_data_path", type=Path) [email protected]_context +def join_cluster(ctx: Context, backup_passphrase: str, join_data_path: Path) -> None: + data = json.loads(join_data_path.read_text()) + if not isinstance(data, dict): + raise ValueError("Join data must contain an object") + join_data = ClusterJoinData.from_dict(data) + with connect(ctx) as nethsm: + nethsm.join_cluster(data=join_data, backup_passphrase=backup_passphrase) + + [email protected]() [email protected]_context +def get_cluster_ca_certificate(ctx: Context) -> None: + with connect(ctx) as nethsm: + print(nethsm.get_cluster_ca_certificate()) + + [email protected]() [email protected]("filename") [email protected]_context +def set_cluster_ca_certificate(ctx: Context, filename: str) -> None: + with connect(ctx) as nethsm: + with open(filename, "rb") as f: + nethsm.set_cluster_ca_certificate(f) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pynitrokey-0.11.4/pynitrokey/cli/nk3/piv.py new/pynitrokey-0.12.0/pynitrokey/cli/nk3/piv.py --- old/pynitrokey-0.11.4/pynitrokey/cli/nk3/piv.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pynitrokey-0.12.0/pynitrokey/cli/nk3/piv.py 1970-01-01 01:00:00.000000000 +0100 @@ -13,7 +13,7 @@ from cryptography.hazmat.primitives.asymmetric import ec, rsa from cryptography.hazmat.primitives.asymmetric import utils as asym_utils from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 -from cryptography.hazmat.primitives.serialization import Encoding +from cryptography.hazmat.primitives.serialization import Encoding, pkcs12 from pynitrokey.cli.nk3 import nk3 from pynitrokey.helpers import check_experimental_flag, local_critical, local_print @@ -26,6 +26,66 @@ try: # noqa: C901 from pynitrokey.nk3.piv_app import PivApp, find_by_id + default_admin_key = "010203040506070801020304050607080102030405060708" + all_key_ids = [ + "9A", + "9C", + "9D", + "9E", + "82", + "83", + "84", + "85", + "86", + "87", + "88", + "89", + "8A", + "8B", + "8C", + "8D", + "8E", + "8F", + "90", + "91", + "92", + "93", + "94", + "95", + ] + + key_id_click_type = type = click.Choice( + [ + "9A", + "9C", + "9D", + "9E", + "82", + "83", + "84", + "85", + "86", + "87", + "88", + "89", + "8A", + "8B", + "8C", + "8D", + "8E", + "8F", + "90", + "91", + "92", + "93", + "94", + "95", + ], + case_sensitive=False, + ) + + default_key_id = "9A" + class RsaPivSigner(rsa.RSAPrivateKey): _device: PivApp _key_reference: int @@ -148,6 +208,53 @@ for row in str_data: print_row(row, widths) + def import_rsa2048( + device: PivApp, + key_ref: int, + key: rsa.RSAPrivateNumbers, + public_key: rsa.RSAPublicNumbers, + ) -> None: + + device.send_receive( + 0xFE, + 0x07, + key_ref, + Tlv.build( + [ + (0x01, key.p.to_bytes(256, "big")), + (0x02, key.q.to_bytes(256, "big")), + ( + 0x03, + public_key.e.to_bytes( + (public_key.e.bit_length() + 7) // 8, "big" + ), + ), + ] + ), + ) + + def import_certificate( + device: PivApp, + key_hex: str, + certificate: bytes, + ) -> None: + payload = Tlv.build( + [ + (0x5C, bytes(bytearray.fromhex(KEY_TO_CERT_OBJ_ID_MAP[key_hex]))), + ( + 0x53, + Tlv.build( + [ + (0x70, certificate), + (0x71, bytes([0])), + ] + ), + ), + ] + ) + + device.send_receive(0xDB, 0x3F, 0xFF, payload) + @nk3.group() @click.option( "--experimental", @@ -164,7 +271,7 @@ @click.argument( "admin-key", type=click.STRING, - default="010203040506070801020304050607080102030405060708", + default=default_admin_key, ) def admin_auth(admin_key: str) -> None: try: @@ -183,7 +290,7 @@ @click.argument( "admin-key", type=click.STRING, - default="010203040506070801020304050607080102030405060708", + default=default_admin_key, ) def init(admin_key: str) -> None: try: @@ -214,7 +321,7 @@ @click.option( "--current-admin-key", type=click.STRING, - default="010203040506070801020304050607080102030405060708", + default=default_admin_key, help="Current admin key.", ) @click.argument( @@ -352,45 +459,80 @@ except ValueError: raise click.BadParameter("Must be valid RFC4514 string.") + @piv.command(help="Import a key and a certificate from a .p12 file") + @click.option( + "--admin-key", + type=click.STRING, + default=default_admin_key, + help="Current admin key", + ) + @click.option( + "--key", + type=key_id_click_type, + default=default_key_id, + help="Key slot for operation.", + ) + @click.option( + "--path", + type=click.Path(allow_dash=True), + default="-", + help="Path to the .pem file containing the private key", + ) + def import_key( + admin_key: str, + key: str, + path: str, + ) -> None: + try: + admin_key_bytes = bytearray.fromhex(admin_key) + except ValueError: + local_critical( + "Key is expected to be an hexadecimal string", + support_hint=False, + ) + + with open(path, "rb") as key_file: + private_key, certificate, _ = pkcs12.load_key_and_certificates( + key_file.read(), password=None + ) + if ( + not isinstance(private_key, rsa.RSAPrivateKey) + or private_key.key_size != 2048 + or certificate is None + ): + local_critical( + "--path must point to a RSA 2048 private key and certificate as a p12 file", + support_hint=False, + ) + return + + key_hex = key.upper() + key_ref = int(key_hex, 16) + + piv_app = PivApp() + piv_app.authenticate_admin(admin_key_bytes) + import_rsa2048( + piv_app, + key_ref, + private_key.private_numbers(), + private_key.public_key().public_numbers(), + ) + + import_certificate(piv_app, key_hex, certificate.public_bytes(Encoding.DER)) + + return + @piv.command(help="Generate a new key and certificate signing request.") @click.option( "--admin-key", type=click.STRING, - default="010203040506070801020304050607080102030405060708", + default=default_admin_key, help="Current admin key", ) @click.option( "--key", - type=click.Choice( - [ - "9A", - "9C", - "9D", - "9E", - "82", - "83", - "84", - "85", - "86", - "87", - "88", - "89", - "8A", - "8B", - "8C", - "8D", - "8E", - "8F", - "90", - "91", - "92", - "93", - "94", - "95", - ], - case_sensitive=False, - ), - default="9A", + type=key_id_click_type, + default=default_key_id, help="Key slot for operation.", ) @click.option( @@ -647,28 +789,13 @@ with click.open_file(path, mode="wb") as file: file.write(csr.public_bytes(Encoding.DER)) - payload = Tlv.build( - [ - (0x5C, bytes(bytearray.fromhex(KEY_TO_CERT_OBJ_ID_MAP[key_hex]))), - ( - 0x53, - Tlv.build( - [ - (0x70, certificate.public_bytes(Encoding.DER)), - (0x71, bytes([0])), - ] - ), - ), - ] - ) - - device.send_receive(0xDB, 0x3F, 0xFF, payload) + import_certificate(device, key_hex, certificate.public_bytes(Encoding.DER)) @piv.command(help="Write a certificate to a key slot.") @click.argument( "admin-key", type=click.STRING, - default="010203040506070801020304050607080102030405060708", + default=default_admin_key, ) @click.option( "--format", @@ -678,36 +805,8 @@ ) @click.option( "--key", - type=click.Choice( - [ - "9A", - "9C", - "9D", - "9E", - "82", - "83", - "84", - "85", - "86", - "87", - "88", - "89", - "8A", - "8B", - "8C", - "8D", - "8E", - "8F", - "90", - "91", - "92", - "93", - "94", - "95", - ], - case_sensitive=False, - ), - default="9A", + type=key_id_click_type, + default=default_key_id, help="Key slot for operation.", ) @click.option( @@ -756,36 +855,8 @@ ) @click.option( "--key", - type=click.Choice( - [ - "9A", - "9C", - "9D", - "9E", - "82", - "83", - "84", - "85", - "86", - "87", - "88", - "89", - "8A", - "8B", - "8C", - "8D", - "8E", - "8F", - "90", - "91", - "92", - "93", - "94", - "95", - ], - case_sensitive=False, - ), - default="9A", + type=key_id_click_type, + default=default_key_id, help="Key slot for operation.", ) @click.option( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pynitrokey-0.11.4/pynitrokey/cli/pro.py new/pynitrokey-0.12.0/pynitrokey/cli/pro.py --- old/pynitrokey-0.11.4/pynitrokey/cli/pro.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pynitrokey-0.12.0/pynitrokey/cli/pro.py 1970-01-01 01:00:00.000000000 +0100 @@ -90,7 +90,7 @@ except DeviceNotFound: local_critical(f"No {nk.friendly_name} device found", support_hint=False) - (_major, minor) = nk.fw_version + _major, minor = nk.fw_version if minor < 11: local_critical( f"The connected {nk.friendly_name} does not support firmware updates", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pynitrokey-0.11.4/pynitrokey/libnk.py new/pynitrokey-0.12.0/pynitrokey/libnk.py --- old/pynitrokey-0.11.4/pynitrokey/libnk.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pynitrokey-0.12.0/pynitrokey/libnk.py 1970-01-01 01:00:00.000000000 +0100 @@ -19,6 +19,7 @@ SPDX-License-Identifier: LGPL-3.0-only """ + import os import sys from enum import IntEnum diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pynitrokey-0.11.4/pynitrokey/start/upgrade_by_passwd.py new/pynitrokey-0.12.0/pynitrokey/start/upgrade_by_passwd.py --- old/pynitrokey-0.11.4/pynitrokey/start/upgrade_by_passwd.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pynitrokey-0.12.0/pynitrokey/start/upgrade_by_passwd.py 1970-01-01 01:00:00.000000000 +0100 @@ -26,6 +26,7 @@ SPDX-License-Identifier: GPL-3.0-or-later """ + from pprint import pprint IMPORT_ERROR_HELP = """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pynitrokey-0.11.4/pyproject.toml new/pynitrokey-0.12.0/pyproject.toml --- old/pynitrokey-0.11.4/pyproject.toml 1970-01-01 01:00:00.000000000 +0100 +++ new/pynitrokey-0.12.0/pyproject.toml 1970-01-01 01:00:00.000000000 +0100 @@ -8,7 +8,7 @@ [project] name = "pynitrokey" -version = "0.11.4" +version = "0.12.0" description = "Python client for Nitrokey devices" license = { text = "Apache-2.0 OR MIT" } authors = [ @@ -29,7 +29,7 @@ "hidapi ==0.14.0.post2 ; sys_platform == 'linux'", "intelhex >=2.3, <3", "libusb1 >=3, <4", - "nethsm >=2.0.1, <3", + "nethsm >=2.1, <3", "nitrokey >=0.4.2, <0.5", "nkdfu >=0.2, <0.3", "pyusb >=1.2, <2", @@ -83,7 +83,7 @@ ] [tool.poetry.dependencies] -python = ">= 3.10, <3.14" +python = ">= 3.10, <3.15" [tool.poetry.group.dev] optional = true
