From f599d960b1e63ffef2a79b1ea2ca7473d38c48d3 Mon Sep 17 00:00:00 2001 From: Kushal Das Date: Sun, 21 Dec 2025 07:18:00 +0100 Subject: [PATCH] feat: adds typing Now we have typing information added to the project and also enabled checking in CI. --- .github/workflows/quality.yml | 8 +- MANIFEST.in | 3 +- pkcs11/__init__.py | 14 +- pkcs11/_pkcs11.pyi | 38 +++ pkcs11/attributes.py | 112 +++++---- pkcs11/constants.py | 11 +- pkcs11/defaults.py | 32 +-- pkcs11/exceptions.py | 2 + pkcs11/mechanisms.py | 24 +- pkcs11/py.typed | 0 pkcs11/types.py | 441 +++++++++++++++++++++++----------- pkcs11/util/__init__.py | 11 +- pkcs11/util/dh.py | 15 +- pkcs11/util/dsa.py | 19 +- pkcs11/util/ec.py | 22 +- pkcs11/util/rsa.py | 38 +-- pkcs11/util/x509.py | 15 +- pyproject.toml | 4 + tests/test_rsa.py | 90 ++++++- uv.lock | 35 ++- 20 files changed, 681 insertions(+), 253 deletions(-) create mode 100644 pkcs11/_pkcs11.pyi create mode 100644 pkcs11/py.typed diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 0587b43..92e83cc 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -30,4 +30,10 @@ jobs: run: uv run ruff format --diff . - name: ruff check - run: uv run ruff check --diff . \ No newline at end of file + run: uv run ruff check --diff . + + - name: Install project dependencies for type checking + run: uv pip install asn1crypto + + - name: ty type check + run: uv run ty check pkcs11/ \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index c426022..8ccfb70 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ graft extern/ -include pkcs11/*.pxd \ No newline at end of file +include pkcs11/*.pxd +include pkcs11/py.typed \ No newline at end of file diff --git a/pkcs11/__init__.py b/pkcs11/__init__.py index 7cd7899..ba595d1 100644 --- a/pkcs11/__init__.py +++ b/pkcs11/__init__.py @@ -2,16 +2,24 @@ :mod:`pkcs11` defines a high-level, "Pythonic" interface to PKCS#11. """ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + from pkcs11.constants import * # noqa: F403 from pkcs11.exceptions import * # noqa: F403 from pkcs11.mechanisms import * # noqa: F403 from pkcs11.types import * # noqa: F403 from pkcs11.util import dh, dsa, ec, rsa, x509 # noqa: F401 -_loaded = {} +if TYPE_CHECKING: + from pkcs11._pkcs11 import lib as _lib_type + + +_loaded: dict[str, Any] = {} -def lib(so): +def lib(so: str) -> _lib_type: """ Wrap the main library call coming from Cython with a preemptive dynamic loading. @@ -34,7 +42,7 @@ def lib(so): return _lib -def unload(so): +def unload(so: str) -> None: global _loaded try: loaded_lib = _loaded[so] diff --git a/pkcs11/_pkcs11.pyi b/pkcs11/_pkcs11.pyi new file mode 100644 index 0000000..2d1ccad --- /dev/null +++ b/pkcs11/_pkcs11.pyi @@ -0,0 +1,38 @@ +"""Type stubs for the _pkcs11 Cython extension module.""" + +from __future__ import annotations + +from typing import Any, Iterator + +from pkcs11.types import Slot, Token + +class lib: + """Main entry point for PKCS#11 library.""" + + so: str + manufacturer_id: str + library_description: str + initialized: bool + + def __init__(self, so: str) -> None: ... + @property + def library_version(self) -> tuple[int, int]: ... + @property + def cryptoki_version(self) -> tuple[int, int]: ... + def initialize(self) -> None: ... + def finalize(self) -> None: ... + def reinitialize(self) -> None: ... + def unload(self) -> None: ... + def get_slots(self, token_present: bool = False) -> list[Slot]: ... + def get_tokens( + self, + token_label: str | None = None, + token_serial: bytes | None = None, + token_flags: Any | None = None, + slot_flags: Any | None = None, + mechanisms: Any | None = None, + ) -> Iterator[Token]: ... + def get_token(self, **kwargs: Any) -> Token: ... + def wait_for_slot_event(self, blocking: bool = True) -> Slot: ... + def __str__(self) -> str: ... + def __repr__(self) -> str: ... diff --git a/pkcs11/attributes.py b/pkcs11/attributes.py index c838f3a..eacd419 100644 --- a/pkcs11/attributes.py +++ b/pkcs11/attributes.py @@ -1,36 +1,46 @@ +from __future__ import annotations + from datetime import datetime +from enum import IntEnum from struct import Struct +from typing import Any, Callable, Final -from pkcs11.constants import ( - Attribute, - CertificateType, - MechanismFlag, - ObjectClass, -) +from pkcs11.constants import Attribute, CertificateType, MechanismFlag, ObjectClass from pkcs11.mechanisms import KeyType, Mechanism +# Type aliases for pack/unpack function pairs +PackFunc = Callable[[Any], bytes] +UnpackFunc = Callable[[bytes], Any] +Handler = tuple[PackFunc, UnpackFunc] + # (Pack Function, Unpack Function) functions -handle_bool = (Struct("?").pack, lambda v: False if len(v) == 0 else Struct("?").unpack(v)[0]) -handle_ulong = (Struct("L").pack, lambda v: Struct("L").unpack(v)[0]) -handle_str = (lambda s: s.encode("utf-8"), lambda b: b.decode("utf-8")) -handle_date = ( +_bool_struct = Struct("?") +_ulong_struct = Struct("L") + +handle_bool: Handler = ( + _bool_struct.pack, + lambda v: False if len(v) == 0 else _bool_struct.unpack(v)[0], +) +handle_ulong: Handler = (_ulong_struct.pack, lambda v: _ulong_struct.unpack(v)[0]) +handle_str: Handler = (lambda s: s.encode("utf-8"), lambda b: b.decode("utf-8")) +handle_date: Handler = ( lambda s: s.strftime("%Y%m%d").encode("ascii"), lambda s: datetime.strptime(s.decode("ascii"), "%Y%m%d").date(), ) -handle_bytes = (bytes, bytes) +handle_bytes: Handler = (bytes, bytes) # The PKCS#11 biginteger type is an array of bytes in network byte order. # If you have an int type, wrap it in biginteger() -handle_biginteger = handle_bytes +handle_biginteger: Handler = handle_bytes -def _enum(type_): +def _enum(type_: type[IntEnum]) -> Handler: """Factory to pack/unpack ints into IntEnums.""" pack, unpack = handle_ulong return (lambda v: pack(int(v)), lambda v: type_(unpack(v))) -ATTRIBUTE_TYPES = { +ATTRIBUTE_TYPES: dict[Attribute, Handler] = { Attribute.ALWAYS_AUTHENTICATE: handle_bool, Attribute.ALWAYS_SENSITIVE: handle_bool, Attribute.APPLICATION: handle_str, @@ -96,7 +106,7 @@ def _enum(type_): Map of attributes to (serialize, deserialize) functions. """ -ALL_CAPABILITIES = ( +ALL_CAPABILITIES: Final[tuple[Attribute, ...]] = ( Attribute.ENCRYPT, Attribute.DECRYPT, Attribute.WRAP, @@ -107,7 +117,12 @@ def _enum(type_): ) -def _apply_common(template, id_, label, store): +def _apply_common( + template: dict[Attribute, Any], + id_: bytes | None, + label: str | None, + store: bool, +) -> None: if id_: template[Attribute.ID] = id_ if label: @@ -115,12 +130,16 @@ def _apply_common(template, id_, label, store): template[Attribute.TOKEN] = bool(store) -def _apply_capabilities(template, possible_capas, capabilities): +def _apply_capabilities( + template: dict[Attribute, Any], + possible_capas: tuple[Attribute, ...], + capabilities: MechanismFlag | int, +) -> None: for attr in possible_capas: template[attr] = _capa_attr_to_mechanism_flag[attr] & capabilities -_capa_attr_to_mechanism_flag = { +_capa_attr_to_mechanism_flag: Final[dict[Attribute, MechanismFlag]] = { Attribute.ENCRYPT: MechanismFlag.ENCRYPT, Attribute.DECRYPT: MechanismFlag.DECRYPT, Attribute.WRAP: MechanismFlag.WRAP, @@ -136,7 +155,12 @@ class AttributeMapper: Class mapping PKCS#11 attributes to and from Python values. """ - def __init__(self): + attribute_types: dict[Attribute, Handler] + default_secret_key_template: dict[Attribute, Any] + default_public_key_template: dict[Attribute, Any] + default_private_key_template: dict[Attribute, Any] + + def __init__(self) -> None: self.attribute_types = dict(ATTRIBUTE_TYPES) self.default_secret_key_template = { Attribute.CLASS: ObjectClass.SECRET_KEY, @@ -158,21 +182,21 @@ def __init__(self): Attribute.SENSITIVE: True, } - def register_handler(self, key, pack, unpack): + def register_handler(self, key: Attribute, pack: PackFunc, unpack: UnpackFunc) -> None: self.attribute_types[key] = (pack, unpack) - def _handler(self, key): + def _handler(self, key: Attribute) -> Handler: try: return self.attribute_types[key] except KeyError as e: raise NotImplementedError(f"Can't handle attribute type {hex(key)}.") from e - def pack_attribute(self, key, value): + def pack_attribute(self, key: Attribute, value: Any) -> bytes: """Pack a Attribute value into a bytes array.""" pack, _ = self._handler(key) return pack(value) - def unpack_attributes(self, key, value): + def unpack_attributes(self, key: Attribute, value: bytes) -> Any: """Unpack a Attribute bytes array into a Python value.""" _, unpack = self._handler(key) return unpack(value) @@ -180,11 +204,11 @@ def unpack_attributes(self, key, value): def public_key_template( self, *, - capabilities, - id_, - label, - store, - ): + capabilities: MechanismFlag | int, + id_: bytes | None, + label: str | None, + store: bool, + ) -> dict[Attribute, Any]: template = self.default_public_key_template _apply_capabilities( template, (Attribute.ENCRYPT, Attribute.WRAP, Attribute.VERIFY), capabilities @@ -195,11 +219,11 @@ def public_key_template( def private_key_template( self, *, - capabilities, - id_, - label, - store, - ): + capabilities: MechanismFlag | int, + id_: bytes | None, + label: str | None, + store: bool, + ) -> dict[Attribute, Any]: template = self.default_private_key_template _apply_capabilities( template, @@ -212,11 +236,11 @@ def private_key_template( def secret_key_template( self, *, - capabilities, - id_, - label, - store, - ): + capabilities: MechanismFlag | int, + id_: bytes | None, + label: str | None, + store: bool, + ) -> dict[Attribute, Any]: return self.generic_key_template( self.default_secret_key_template, capabilities=capabilities, @@ -227,13 +251,13 @@ def secret_key_template( def generic_key_template( self, - base_template, + base_template: dict[Attribute, Any], *, - capabilities, - id_, - label, - store, - ): + capabilities: MechanismFlag | int, + id_: bytes | None, + label: str | None, + store: bool, + ) -> dict[Attribute, Any]: template = dict(base_template) _apply_capabilities(template, ALL_CAPABILITIES, capabilities) _apply_common(template, id_, label, store) diff --git a/pkcs11/constants.py b/pkcs11/constants.py index 6532d25..e06fc9e 100644 --- a/pkcs11/constants.py +++ b/pkcs11/constants.py @@ -5,9 +5,12 @@ use these classes. """ +from __future__ import annotations + from enum import IntEnum, IntFlag, unique +from typing import Final -DEFAULT = object() +DEFAULT: Final[object] = object() """Sentinel value used in templates. Not all pkcs11 attribute sets are accepted by HSMs. @@ -55,11 +58,11 @@ class ObjectClass(IntEnum): _VENDOR_DEFINED = 0x80000000 - def __repr__(self): + def __repr__(self) -> str: return "" % self.name -_ARRAY_ATTRIBUTE = 0x40000000 +_ARRAY_ATTRIBUTE: Final[int] = 0x40000000 """Attribute consists of an array of values.""" @@ -343,7 +346,7 @@ class Attribute(IntEnum): _VENDOR_DEFINED = 0x80000000 - def __repr__(self): + def __repr__(self) -> str: return "" % self.name diff --git a/pkcs11/defaults.py b/pkcs11/defaults.py index 5d8a403..8a69c8f 100644 --- a/pkcs11/defaults.py +++ b/pkcs11/defaults.py @@ -5,12 +5,14 @@ assumed. """ -from pkcs11.constants import ( - MechanismFlag, -) +from __future__ import annotations + +from typing import Final + +from pkcs11.constants import MechanismFlag from pkcs11.mechanisms import MGF, KeyType, Mechanism -DEFAULT_GENERATE_MECHANISMS = { +DEFAULT_GENERATE_MECHANISMS: Final[dict[KeyType, Mechanism]] = { KeyType.AES: Mechanism.AES_KEY_GEN, KeyType.DES2: Mechanism.DES2_KEY_GEN, KeyType.DES3: Mechanism.DES3_KEY_GEN, @@ -26,11 +28,11 @@ Default mechanisms for generating keys. """ -_ENCRYPTION = MechanismFlag.ENCRYPT | MechanismFlag.DECRYPT -_SIGNING = MechanismFlag.SIGN | MechanismFlag.VERIFY -_WRAPPING = MechanismFlag.WRAP | MechanismFlag.UNWRAP +_ENCRYPTION: Final[MechanismFlag] = MechanismFlag.ENCRYPT | MechanismFlag.DECRYPT +_SIGNING: Final[MechanismFlag] = MechanismFlag.SIGN | MechanismFlag.VERIFY +_WRAPPING: Final[MechanismFlag] = MechanismFlag.WRAP | MechanismFlag.UNWRAP -DEFAULT_KEY_CAPABILITIES = { +DEFAULT_KEY_CAPABILITIES: Final[dict[KeyType, MechanismFlag | int]] = { KeyType.AES: _ENCRYPTION | _SIGNING | _WRAPPING, KeyType.DES2: _ENCRYPTION | _SIGNING | _WRAPPING, KeyType.DES3: _ENCRYPTION | _SIGNING | _WRAPPING, @@ -45,7 +47,7 @@ Default capabilities for generating keys. """ -DEFAULT_ENCRYPT_MECHANISMS = { +DEFAULT_ENCRYPT_MECHANISMS: Final[dict[KeyType, Mechanism]] = { KeyType.AES: Mechanism.AES_CBC_PAD, KeyType.DES2: Mechanism.DES3_CBC_PAD, KeyType.DES3: Mechanism.DES3_CBC_PAD, @@ -55,7 +57,7 @@ Default mechanisms for encrypt/decrypt. """ -DEFAULT_SIGN_MECHANISMS = { +DEFAULT_SIGN_MECHANISMS: Final[dict[KeyType, Mechanism]] = { KeyType.AES: Mechanism.AES_MAC, KeyType.DES2: Mechanism.DES3_MAC, KeyType.DES3: Mechanism.DES3_MAC, @@ -68,7 +70,7 @@ Default mechanisms for sign/verify. """ -DEFAULT_WRAP_MECHANISMS = { +DEFAULT_WRAP_MECHANISMS: Final[dict[KeyType, Mechanism]] = { KeyType.AES: Mechanism.AES_KEY_WRAP, KeyType.DES2: Mechanism.DES3_ECB, KeyType.DES3: Mechanism.DES3_ECB, @@ -78,7 +80,7 @@ Default mechanism for wrap/unwrap. """ -DEFAULT_DERIVE_MECHANISMS = { +DEFAULT_DERIVE_MECHANISMS: Final[dict[KeyType, Mechanism]] = { KeyType.DH: Mechanism.DH_PKCS_DERIVE, KeyType.EC: Mechanism.ECDH1_DERIVE, KeyType.X9_42_DH: Mechanism.X9_42_DH_DERIVE, @@ -87,7 +89,7 @@ Default mechanisms for key derivation """ -DEFAULT_PARAM_GENERATE_MECHANISMS = { +DEFAULT_PARAM_GENERATE_MECHANISMS: Final[dict[KeyType, Mechanism]] = { KeyType.DH: Mechanism.DH_PKCS_PARAMETER_GEN, KeyType.DSA: Mechanism.DSA_PARAMETER_GEN, KeyType.X9_42_DH: Mechanism.X9_42_DH_PARAMETER_GEN, @@ -97,7 +99,9 @@ """ -DEFAULT_MECHANISM_PARAMS = { +DEFAULT_MECHANISM_PARAMS: Final[ + dict[Mechanism, tuple[Mechanism, MGF, None] | tuple[Mechanism, MGF, int]] +] = { Mechanism.RSA_PKCS_OAEP: (Mechanism.SHA_1, MGF.SHA1, None), Mechanism.RSA_PKCS_PSS: (Mechanism.SHA_1, MGF.SHA1, 20), } diff --git a/pkcs11/exceptions.py b/pkcs11/exceptions.py index 0227460..824d970 100644 --- a/pkcs11/exceptions.py +++ b/pkcs11/exceptions.py @@ -3,6 +3,8 @@ :class:`PKCS11Error`. """ +from __future__ import annotations + class PKCS11Error(RuntimeError): """ diff --git a/pkcs11/mechanisms.py b/pkcs11/mechanisms.py index d9ee9e4..a1bd19a 100644 --- a/pkcs11/mechanisms.py +++ b/pkcs11/mechanisms.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from enum import IntEnum from pkcs11.exceptions import ArgumentsBad @@ -107,7 +109,7 @@ class KeyType(IntEnum): _VENDOR_DEFINED = 0x80000000 - def __repr__(self): + def __repr__(self) -> str: return "" % self.name @@ -751,7 +753,7 @@ class Mechanism(IntEnum): _VENDOR_DEFINED = 0x80000000 - def __repr__(self): + def __repr__(self) -> str: return "" % self.name @@ -775,7 +777,7 @@ class KDF(IntEnum): SHA3_384_KDF = 0x0000000C SHA3_512_KDF = 0x0000000D - def __repr__(self): + def __repr__(self) -> str: return "" % self.name @@ -794,12 +796,18 @@ class MGF(IntEnum): SHA3_384 = 0x00000008 SHA3_512 = 0x00000009 - def __repr__(self): + def __repr__(self) -> str: return "" % self.name class GCMParams: - def __init__(self, nonce, aad=None, tag_bits=128): + """Parameters for AES-GCM mode.""" + + nonce: bytes + aad: bytes | None + tag_bits: int + + def __init__(self, nonce: bytes, aad: bytes | None = None, tag_bits: int = 128) -> None: if len(nonce) > 12: raise ArgumentsBad("IV must be less than 12 bytes") self.nonce = nonce @@ -808,7 +816,11 @@ def __init__(self, nonce, aad=None, tag_bits=128): class CTRParams: - def __init__(self, nonce): + """Parameters for AES-CTR mode.""" + + nonce: bytes + + def __init__(self, nonce: bytes) -> None: if len(nonce) >= 16: raise ArgumentsBad( f"{nonce.hex()} is too long to serve as a CTR nonce, must be 15 bytes or less " diff --git a/pkcs11/py.typed b/pkcs11/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/pkcs11/types.py b/pkcs11/types.py index 35527d0..64dea7f 100644 --- a/pkcs11/types.py +++ b/pkcs11/types.py @@ -4,14 +4,21 @@ This module provides stubs that are overrideen in pkcs11._pkcs11. """ +from __future__ import annotations + from binascii import hexlify from functools import cached_property +from typing import TYPE_CHECKING, Any, Final, Iterator from pkcs11 import CancelStrategy from pkcs11.constants import ( Attribute, + CertificateType, MechanismFlag, ObjectClass, + SlotFlag, + TokenFlag, + UserType, ) from pkcs11.exceptions import ( ArgumentsBad, @@ -23,24 +30,27 @@ ) from pkcs11.mechanisms import KeyType, Mechanism -PROTECTED_AUTH = object() +if TYPE_CHECKING: + from pkcs11.attributes import AttributeMapper + +PROTECTED_AUTH: Final[object] = object() """Indicate the pin should be supplied via an external mechanism (e.g. pin pad)""" class IdentifiedBy: __slots__ = () - def _identity(self): + def _identity(self) -> Any: raise NotImplementedError() - def __eq__(self, other): + def __eq__(self, other: object) -> bool: return isinstance(other, IdentifiedBy) and self._identity() == other._identity() - def __hash__(self): + def __hash__(self) -> int: return hash(self._identity()) -def _CK_UTF8CHAR_to_str(data): +def _CK_UTF8CHAR_to_str(data: bytes) -> str: """ Convert CK_UTF8CHAR to string. @@ -50,12 +60,12 @@ def _CK_UTF8CHAR_to_str(data): return data.rstrip(b"\0").decode("utf-8", errors="replace").rstrip() -def _CK_VERSION_to_tuple(data): +def _CK_VERSION_to_tuple(data: dict[str, int]) -> tuple[int, int]: """Convert CK_VERSION to tuple.""" return (data["major"], data["minor"]) -def _CK_MECHANISM_TYPE_to_enum(mechanism): +def _CK_MECHANISM_TYPE_to_enum(mechanism: int) -> Mechanism | int: """Convert CK_MECHANISM_TYPE to enum or be okay.""" try: return Mechanism(mechanism) @@ -70,7 +80,21 @@ class MechanismInfo: See :meth:`pkcs11.Slot.get_mechanism_info`. """ - def __init__(self, slot, mechanism, ulMinKeySize=None, ulMaxKeySize=None, flags=None, **kwargs): + slot: Slot + mechanism: Mechanism | int + min_key_length: int | None + max_key_length: int | None + flags: MechanismFlag + + def __init__( + self, + slot: Slot, + mechanism: Mechanism | int, + ulMinKeySize: int | None = None, + ulMaxKeySize: int | None = None, + flags: int | None = None, + **kwargs: Any, + ) -> None: self.slot = slot """:class:`pkcs11.Slot` this information is for.""" self.mechanism = mechanism @@ -79,10 +103,10 @@ def __init__(self, slot, mechanism, ulMinKeySize=None, ulMaxKeySize=None, flags= """Minimum key length in bits (:class:`int`).""" self.max_key_length = ulMaxKeySize """Maximum key length in bits (:class:`int`).""" - self.flags = MechanismFlag(flags) + self.flags = MechanismFlag(flags) if flags is not None else MechanismFlag(0) """Mechanism capabilities (:class:`pkcs11.constants.MechanismFlag`).""" - def __str__(self): + def __str__(self) -> str: return "\n".join( ( "Supported key lengths: [%s, %s]" % (self.min_key_length, self.max_key_length), @@ -90,7 +114,7 @@ def __str__(self): ) ) - def __repr__(self): + def __repr__(self) -> str: return "<{klass} (mechanism={mechanism}, flags={flags})>".format( klass=type(self).__name__, mechanism=str(self.mechanism), flags=str(self.flags) ) @@ -108,41 +132,41 @@ class Slot(IdentifiedBy): __slots__ = () @property - def flags(self): + def flags(self) -> SlotFlag: """Capabilities of this slot (:class:`SlotFlag`).""" raise NotImplementedError() @property - def hardware_version(self): + def hardware_version(self) -> tuple[int, int]: """Hardware version (:class:`tuple`).""" raise NotImplementedError() @property - def firmware_version(self): + def firmware_version(self) -> tuple[int, int]: """Firmware version (:class:`tuple`).""" raise NotImplementedError() @property - def cryptoki_version(self): + def cryptoki_version(self) -> tuple[int, int]: """PKCS#11 API version (:class: `tuple`)""" raise NotImplementedError() @property - def slot_id(self): + def slot_id(self) -> int: """Slot identifier (opaque).""" raise NotImplementedError() @property - def slot_description(self): + def slot_description(self) -> str: """Slot name (:class:`str`).""" raise NotImplementedError() @property - def manufacturer_id(self): + def manufacturer_id(self) -> str: """Slot/device manufacturer's name (:class:`str`).""" raise NotImplementedError() - def get_token(self): + def get_token(self) -> Token: """ Returns the token loaded into this slot. @@ -150,7 +174,7 @@ def get_token(self): """ raise NotImplementedError() - def get_mechanisms(self): + def get_mechanisms(self) -> set[Mechanism]: """ Returns the mechanisms supported by this device. @@ -158,7 +182,7 @@ def get_mechanisms(self): """ raise NotImplementedError() - def get_mechanism_info(self, mechanism): + def get_mechanism_info(self, mechanism: Mechanism) -> MechanismInfo: """ Returns information about the mechanism. @@ -179,54 +203,54 @@ class Token(IdentifiedBy): __slots__ = () @property - def flags(self): + def flags(self) -> TokenFlag: """Capabilities of this token (:class:`TokenFlag`).""" raise NotImplementedError() @property - def hardware_version(self): + def hardware_version(self) -> tuple[int, int]: """Hardware version (:class:`tuple`).""" raise NotImplementedError() @property - def firmware_version(self): + def firmware_version(self) -> tuple[int, int]: """Firmware version (:class:`tuple`).""" raise NotImplementedError() @property - def slot(self): + def slot(self) -> Slot: """The :class:`Slot` this token is installed in.""" raise NotImplementedError() @property - def label(self): + def label(self) -> str: """Label of this token (:class:`str`).""" raise NotImplementedError() @property - def serial(self): + def serial(self) -> bytes: """Serial number of this token (:class:`bytes`).""" raise NotImplementedError() @property - def manufacturer_id(self): + def manufacturer_id(self) -> str: """Manufacturer ID (:class:`str`).""" raise NotImplementedError() @property - def model(self): + def model(self) -> str: """Model name (:class:`str`).""" raise NotImplementedError() def open( self, - rw=False, - user_pin=None, - so_pin=None, - user_type=None, - attribute_mapper=None, - cancel_strategy=CancelStrategy.DEFAULT, - ): + rw: bool = False, + user_pin: str | bytes | object | None = None, + so_pin: str | bytes | object | None = None, + user_type: UserType | None = None, + attribute_mapper: AttributeMapper | None = None, + cancel_strategy: CancelStrategy = CancelStrategy.DEFAULT, + ) -> Session: """ Open a session on the token and optionally log in as a user or security officer (pass one of `user_pin` or `so_pin`). Pass PROTECTED_AUTH to @@ -270,30 +294,41 @@ class Session(IdentifiedBy): __slots__ = () - def __enter__(self): + def __enter__(self) -> Session: return self - def __exit__(self, type_, value, traceback): + def __exit__( + self, + type_: type[BaseException] | None, + value: BaseException | None, + traceback: Any, + ) -> None: self.close() @property - def token(self): + def token(self) -> Token: """:class:`Token` this session is on.""" raise NotImplementedError() @property - def rw(self): + def rw(self) -> bool: """True if this is a read/write session.""" raise NotImplementedError() - def close(self): + def close(self) -> None: """Close the session.""" raise NotImplementedError() - def reaffirm_credentials(self, pin): + def reaffirm_credentials(self, pin: str | bytes) -> None: raise NotImplementedError() - def get_key(self, object_class=None, key_type=None, label=None, id=None): + def get_key( + self, + object_class: ObjectClass | None = None, + key_type: KeyType | None = None, + label: str | None = None, + id: bytes | None = None, + ) -> Key: """ Search for a key with any of `key_type`, `label` and/or `id`. @@ -314,7 +349,7 @@ def get_key(self, object_class=None, key_type=None, label=None, id=None): if object_class is None and key_type is None and label is None and id is None: raise ArgumentsBad("Must specify at least one search parameter.") - attrs = {} + attrs: dict[Attribute, Any] = {} if object_class is not None: attrs[Attribute.CLASS] = object_class @@ -330,7 +365,7 @@ def get_key(self, object_class=None, key_type=None, label=None, id=None): iterator = self.get_objects(attrs) try: - key = next(iterator) + obj = next(iterator) except StopIteration as ex: raise NoSuchKey("No key matching %s" % attrs) from ex @@ -339,9 +374,11 @@ def get_key(self, object_class=None, key_type=None, label=None, id=None): raise MultipleObjectsReturned("More than 1 key matches %s" % attrs) except StopIteration: pass - return key + # The caller expects a Key, but get_objects returns Object + # This is a runtime assumption that the returned object is a Key + return obj # type: ignore[return-value] - def get_objects(self, attrs=None): + def get_objects(self, attrs: dict[Attribute, Any] | None = None) -> Iterator[Object]: """ Search for objects matching `attrs`. Returns a generator. @@ -362,7 +399,7 @@ def get_objects(self, attrs=None): """ raise NotImplementedError() - def create_object(self, attrs): + def create_object(self, attrs: dict[Attribute, Any]) -> Object: """ Create a new object on the :class:`Token`. This is a low-level interface to create any type of object and can be used for importing @@ -389,7 +426,13 @@ def create_object(self, attrs): """ raise NotImplementedError() - def create_domain_parameters(self, key_type, attrs, local=False, store=False): + def create_domain_parameters( + self, + key_type: KeyType, + attrs: dict[Attribute, Any], + local: bool = False, + store: bool = False, + ) -> DomainParameters: """ Create a domain parameters object from known parameters. @@ -421,13 +464,13 @@ def create_domain_parameters(self, key_type, attrs, local=False, store=False): def generate_domain_parameters( self, - key_type, - param_length, - store=False, - mechanism=None, - mechanism_param=None, - template=None, - ): + key_type: KeyType, + param_length: int, + store: bool = False, + mechanism: Mechanism | None = None, + mechanism_param: bytes | None = None, + template: dict[Attribute, Any] | None = None, + ) -> DomainParameters: """ Generate domain parameters. @@ -455,16 +498,16 @@ def generate_domain_parameters( def generate_key( self, - key_type, - key_length=None, - id=None, - label=None, - store=False, - capabilities=None, - mechanism=None, - mechanism_param=None, - template=None, - ): + key_type: KeyType, + key_length: int | None = None, + id: bytes | None = None, + label: str | None = None, + store: bool = False, + capabilities: MechanismFlag | None = None, + mechanism: Mechanism | None = None, + mechanism_param: bytes | None = None, + template: dict[Attribute, Any] | None = None, + ) -> SecretKey: """ Generate a single key (e.g. AES, DES). @@ -498,7 +541,12 @@ def generate_key( """ raise NotImplementedError() - def generate_keypair(self, key_type, key_length=None, **kwargs): + def generate_keypair( + self, + key_type: KeyType, + key_length: int | None = None, + **kwargs: Any, + ) -> tuple[PublicKey, PrivateKey]: """ Generate a asymmetric keypair (e.g. RSA). @@ -527,10 +575,15 @@ def generate_keypair(self, key_type, key_length=None, **kwargs): else: return self._generate_keypair(key_type, key_length=key_length, **kwargs) - def _generate_keypair(self, key_type, key_length=None, **kwargs): + def _generate_keypair( + self, + key_type: KeyType, + key_length: int | None = None, + **kwargs: Any, + ) -> tuple[PublicKey, PrivateKey]: raise NotImplementedError() - def seed_random(self, seed): + def seed_random(self, seed: bytes) -> None: """ Mix additional seed material into the RNG (if supported). @@ -538,7 +591,7 @@ def seed_random(self, seed): """ raise NotImplementedError() - def generate_random(self, nbits): + def generate_random(self, nbits: int) -> bytes: """ Generate `length` bits of random or pseudo-random data (if supported). @@ -547,7 +600,7 @@ def generate_random(self, nbits): """ raise NotImplementedError() - def digest(self, data, **kwargs): + def digest(self, data: str | bytes | Key | Iterator[bytes | Key], **kwargs: Any) -> bytes: """ Digest `data` using `mechanism`. @@ -572,15 +625,15 @@ def digest(self, data, **kwargs): return self._digest(data, **kwargs) elif isinstance(data, Key): - data = (data,) + return self._digest_generator(iter((data,)), **kwargs) return self._digest_generator(data, **kwargs) - def set_pin(self, old_pin, new_pin): + def set_pin(self, old_pin: str | bytes, new_pin: str | bytes) -> None: """Change the user pin.""" raise NotImplementedError() - def init_pin(self, pin): + def init_pin(self, pin: str | bytes) -> None: """ Initializes the user PIN. @@ -589,10 +642,20 @@ def init_pin(self, pin): """ raise NotImplementedError() - def _digest(self, data, mechanism=None, mechanism_param=None): + def _digest( + self, + data: bytes, + mechanism: Mechanism | None = None, + mechanism_param: bytes | None = None, + ) -> bytes: raise NotImplementedError() - def _digest_generator(self, data, mechanism=None, mechanism_param=None): + def _digest_generator( + self, + data: Iterator[bytes | Key], + mechanism: Mechanism | None = None, + mechanism_param: bytes | None = None, + ) -> bytes: raise NotImplementedError() @@ -608,27 +671,27 @@ class Object(IdentifiedBy): :exc:`pkcs11.exceptions.AttributeTypeInvalid`. """ - object_class = None + object_class: ObjectClass | None = None """:class:`pkcs11.constants.ObjectClass` of this Object.""" @property - def session(self): + def session(self) -> Session: raise NotImplementedError() @property - def handle(self): + def handle(self) -> int | None: raise NotImplementedError() - def __getitem__(self, key): + def __getitem__(self, key: Attribute) -> Any: raise NotImplementedError() - def __setitem__(self, key, value): + def __setitem__(self, key: Attribute, value: Any) -> None: raise NotImplementedError() - def get_attributes(self, keys): + def get_attributes(self, keys: list[Attribute]) -> dict[Attribute, Any]: raise NotImplementedError() - def copy(self, attrs): + def copy(self, attrs: dict[Attribute, Any]) -> Object: """ Make a copy of the object with new attributes `attrs`. @@ -649,7 +712,7 @@ def copy(self, attrs): """ raise NotImplementedError() - def destroy(self): + def destroy(self) -> None: """ Destroy the object. @@ -672,7 +735,7 @@ class DomainParameters(Object): """ @cached_property - def key_type(self): + def key_type(self) -> KeyType: """ Key type (:class:`pkcs11.mechanisms.KeyType`) these parameters can be used to generate. @@ -681,15 +744,15 @@ def key_type(self): def generate_keypair( self, - id=None, - label=None, - store=False, - capabilities=None, - mechanism=None, - mechanism_param=None, - public_template=None, - private_template=None, - ): + id: bytes | None = None, + label: str | None = None, + store: bool = False, + capabilities: MechanismFlag | None = None, + mechanism: Mechanism | None = None, + mechanism_param: bytes | None = None, + public_template: dict[Attribute, Any] | None = None, + private_template: dict[Attribute, Any] | None = None, + ) -> tuple[PublicKey, PrivateKey]: """ Generate a key pair from these domain parameters (e.g. for Diffie-Hellman. @@ -710,34 +773,37 @@ def generate_keypair( class LocalDomainParameters(DomainParameters): - def __init__(self, session, params): + _session: Session + params: dict[Attribute, Any] + + def __init__(self, session: Session, params: dict[Attribute, Any]) -> None: self._session = session self.params = params @property - def session(self): + def session(self) -> Session: return self._session @property - def handle(self): + def handle(self) -> None: return None - def __getitem__(self, key): + def __getitem__(self, key: Attribute) -> Any: try: return self.params[key] except KeyError as ex: raise AttributeTypeInvalid from ex - def get_attributes(self, keys): + def get_attributes(self, keys: list[Attribute]) -> dict[Attribute, Any]: return {key: self.params[key] for key in keys if key in self.params} - def __setitem__(self, key, value): + def __setitem__(self, key: Attribute, value: Any) -> None: self.params[key] = value class HasKeyType(Object): @cached_property - def key_type(self): + def key_type(self) -> KeyType: """Key type (:class:`pkcs11.mechanisms.KeyType`).""" return self[Attribute.KEY_TYPE] @@ -746,29 +812,29 @@ class Key(HasKeyType): """Base class for all key :class:`Object` types.""" @property - def key_length(self): + def key_length(self) -> int: """Key length in bits.""" raise NotImplementedError @cached_property - def id(self): + def id(self) -> bytes: """Key id (:class:`bytes`).""" return self[Attribute.ID] @cached_property - def label(self): + def label(self) -> str: """Key label (:class:`str`).""" return self[Attribute.LABEL] @cached_property - def _key_description(self): + def _key_description(self) -> str: """A description of the key.""" try: return "%s-bit %s" % (self.key_length, self.key_type.name) except AttributeTypeInvalid: return self.key_type.name - def __repr__(self): + def __repr__(self) -> str: return "<%s label='%s' id='%s' %s>" % ( type(self).__name__, self.label, @@ -783,10 +849,10 @@ class SecretKey(Key): (symmetric encryption key). """ - object_class = ObjectClass.SECRET_KEY + object_class: ObjectClass = ObjectClass.SECRET_KEY @cached_property - def key_length(self): + def key_length(self) -> int: """Key length in bits.""" return self[Attribute.VALUE_LEN] * 8 @@ -801,10 +867,10 @@ class PublicKey(Key): :func:`pkcs11.util.rsa.encode_rsa_public_key` respectively. """ - object_class = ObjectClass.PUBLIC_KEY + object_class: ObjectClass = ObjectClass.PUBLIC_KEY @cached_property - def key_length(self): + def key_length(self) -> int: """Key length in bits.""" return self[Attribute.MODULUS_BITS] @@ -823,10 +889,10 @@ class PrivateKey(Key): private key should be considered insecure. """ - object_class = ObjectClass.PRIVATE_KEY + object_class: ObjectClass = ObjectClass.PRIVATE_KEY @cached_property - def key_length(self): + def key_length(self) -> int: """Key length in bits.""" return len(self[Attribute.MODULUS]) * 8 @@ -844,10 +910,10 @@ class Certificate(Object): from a certificate to create the object. """ - object_class = ObjectClass.CERTIFICATE + object_class: ObjectClass = ObjectClass.CERTIFICATE @cached_property - def certificate_type(self): + def certificate_type(self) -> CertificateType: """ The type of certificate. @@ -861,7 +927,12 @@ class EncryptMixin(HasKeyType): This :class:`Object` supports the encrypt capability. """ - def encrypt(self, data, buffer_size=8192, **kwargs): + def encrypt( + self, + data: str | bytes | Iterator[bytes], + buffer_size: int = 8192, + **kwargs: Any, + ) -> bytes | Iterator[bytes]: """ Encrypt some `data`. @@ -940,13 +1011,36 @@ def encrypt_file(file_in, file_out, buffer_size=8192): else: return self._encrypt_generator(data, buffer_size=buffer_size, **kwargs) + def _encrypt( + self, + data: bytes, + mechanism: Mechanism | None = None, + mechanism_param: bytes | None = None, + buffer_size: int = 8192, + ) -> bytes: + raise NotImplementedError() + + def _encrypt_generator( + self, + data: Iterator[bytes], + mechanism: Mechanism | None = None, + mechanism_param: bytes | None = None, + buffer_size: int = 8192, + ) -> Iterator[bytes]: + raise NotImplementedError() + class DecryptMixin(HasKeyType): """ This :class:`Object` supports the decrypt capability. """ - def decrypt(self, data, buffer_size=8192, **kwargs): + def decrypt( + self, + data: bytes | Iterator[bytes], + buffer_size: int = 8192, + **kwargs: Any, + ) -> bytes | Iterator[bytes]: """ Decrypt some `data`. @@ -972,13 +1066,33 @@ def decrypt(self, data, buffer_size=8192, **kwargs): else: return self._decrypt_generator(data, buffer_size=buffer_size, **kwargs) + def _decrypt( + self, + data: bytes, + mechanism: Mechanism | None = None, + mechanism_param: bytes | None = None, + pin: str | bytes | None = None, + buffer_size: int = 8192, + ) -> bytes: + raise NotImplementedError() + + def _decrypt_generator( + self, + data: Iterator[bytes], + mechanism: Mechanism | None = None, + mechanism_param: bytes | None = None, + pin: str | bytes | None = None, + buffer_size: int = 8192, + ) -> Iterator[bytes]: + raise NotImplementedError() + class SignMixin(HasKeyType): """ This :class:`Object` supports the sign capability. """ - def sign(self, data, **kwargs): + def sign(self, data: str | bytes | Iterator[bytes], **kwargs: Any) -> bytes: """ Sign some `data`. @@ -1009,13 +1123,37 @@ def sign(self, data, **kwargs): else: return self._sign_generator(data, **kwargs) + def _sign( + self, + data: bytes, + mechanism: Mechanism | None = None, + mechanism_param: bytes | None = None, + pin: str | bytes | None = None, + buffer_size: int = 8192, + ) -> bytes: + raise NotImplementedError() + + def _sign_generator( + self, + data: Iterator[bytes], + mechanism: Mechanism | None = None, + mechanism_param: bytes | None = None, + pin: str | bytes | None = None, + ) -> bytes: + raise NotImplementedError() + class VerifyMixin(HasKeyType): """ This :class:`Object` supports the verify capability. """ - def verify(self, data, signature, **kwargs): + def verify( + self, + data: str | bytes | Iterator[bytes], + signature: bytes, + **kwargs: Any, + ) -> bool: """ Verify some `data`. @@ -1053,13 +1191,36 @@ def verify(self, data, signature, **kwargs): except (SignatureInvalid, SignatureLenRange): return False + def _verify( + self, + data: bytes, + signature: bytes, + mechanism: Mechanism | None = None, + mechanism_param: bytes | None = None, + ) -> None: + raise NotImplementedError() + + def _verify_generator( + self, + data: Iterator[bytes], + signature: bytes, + mechanism: Mechanism | None = None, + mechanism_param: bytes | None = None, + ) -> None: + raise NotImplementedError() + class WrapMixin(HasKeyType): """ This :class:`Object` supports the wrap capability. """ - def wrap_key(self, key, mechanism=None, mechanism_param=None): + def wrap_key( + self, + key: Key, + mechanism: Mechanism | None = None, + mechanism_param: bytes | None = None, + ) -> bytes: """ Use this key to wrap (i.e. encrypt) `key` for export. Returns an encrypted version of `key`. @@ -1082,17 +1243,17 @@ class UnwrapMixin(HasKeyType): def unwrap_key( self, - object_class, - key_type, - key_data, - id=None, - label=None, - mechanism=None, - mechanism_param=None, - store=False, - capabilities=None, - template=None, - ): + object_class: ObjectClass, + key_type: KeyType, + key_data: bytes, + id: bytes | None = None, + label: str | None = None, + mechanism: Mechanism | None = None, + mechanism_param: bytes | None = None, + store: bool = False, + capabilities: MechanismFlag | None = None, + template: dict[Attribute, Any] | None = None, + ) -> Key: """ Use this key to unwrap (i.e. decrypt) and import `key_data`. @@ -1121,16 +1282,16 @@ class DeriveMixin(HasKeyType): def derive_key( self, - key_type, - key_length, - id=None, - label=None, - store=False, - capabilities=None, - mechanism=None, - mechanism_param=None, - template=None, - ): + key_type: KeyType, + key_length: int, + id: bytes | None = None, + label: str | None = None, + store: bool = False, + capabilities: MechanismFlag | None = None, + mechanism: Mechanism | None = None, + mechanism_param: bytes | tuple[Any, ...] | None = None, + template: dict[Attribute, Any] | None = None, + ) -> SecretKey: """ Derive a new key from this key. Used to create session keys from a PKCS key exchange. diff --git a/pkcs11/util/__init__.py b/pkcs11/util/__init__.py index 8acc6bd..a47f152 100644 --- a/pkcs11/util/__init__.py +++ b/pkcs11/util/__init__.py @@ -1,4 +1,9 @@ -def biginteger(value): +from __future__ import annotations + +from typing import SupportsInt + + +def biginteger(value: SupportsInt) -> bytes: """ Returns a PKCS#11 biginteger bytestream from a Python integer or similar type (e.g. :class:`asn1crypto.core.Integer`). @@ -7,6 +12,6 @@ def biginteger(value): :rtype: bytes """ - value = int(value) # In case it's a asn1 type or similar + value_int = int(value) # In case it's a asn1 type or similar - return value.to_bytes((value.bit_length() + 7) // 8, byteorder="big") + return value_int.to_bytes((value_int.bit_length() + 7) // 8, byteorder="big") diff --git a/pkcs11/util/dh.py b/pkcs11/util/dh.py index 89f1a0e..97ae880 100644 --- a/pkcs11/util/dh.py +++ b/pkcs11/util/dh.py @@ -2,14 +2,21 @@ Key handling utilities for Diffie-Hellman keys. """ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + from asn1crypto.algos import DHParameters from asn1crypto.core import Integer from pkcs11.constants import Attribute from pkcs11.util import biginteger +if TYPE_CHECKING: + from pkcs11.types import DomainParameters, PublicKey + -def decode_dh_domain_parameters(der): +def decode_dh_domain_parameters(der: bytes) -> dict[Attribute, Any]: """ Decode DER-encoded Diffie-Hellman domain parameters. @@ -25,7 +32,7 @@ def decode_dh_domain_parameters(der): } -def encode_dh_domain_parameters(obj): +def encode_dh_domain_parameters(obj: DomainParameters) -> bytes: """ Encode DH domain parameters into DER-encoded format. @@ -45,7 +52,7 @@ def encode_dh_domain_parameters(obj): return asn1.dump() -def encode_dh_public_key(key): +def encode_dh_public_key(key: PublicKey) -> bytes: """ Encode DH public key into RFC 3279 DER-encoded format. @@ -58,7 +65,7 @@ def encode_dh_public_key(key): return asn1.dump() -def decode_dh_public_key(der): +def decode_dh_public_key(der: bytes) -> bytes: """ Decode a DH public key from RFC 3279 DER-encoded format. diff --git a/pkcs11/util/dsa.py b/pkcs11/util/dsa.py index 261bfd9..a91e13c 100644 --- a/pkcs11/util/dsa.py +++ b/pkcs11/util/dsa.py @@ -2,6 +2,10 @@ Key handling utilities for DSA keys, domain parameters and signatures.. """ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + from asn1crypto.algos import DSASignature from asn1crypto.core import Integer from asn1crypto.keys import DSAParams @@ -9,8 +13,11 @@ from pkcs11.constants import Attribute from pkcs11.util import biginteger +if TYPE_CHECKING: + from pkcs11.types import DomainParameters, PublicKey + -def decode_dsa_domain_parameters(der): +def decode_dsa_domain_parameters(der: bytes) -> dict[Attribute, Any]: """ Decode RFC 3279 DER-encoded Dss-Params. @@ -27,7 +34,7 @@ def decode_dsa_domain_parameters(der): } -def encode_dsa_domain_parameters(obj): +def encode_dsa_domain_parameters(obj: DomainParameters) -> bytes: """ Encode RFC 3279 DER-encoded Dss-Params. @@ -45,7 +52,7 @@ def encode_dsa_domain_parameters(obj): return asn1.dump() -def encode_dsa_public_key(key): +def encode_dsa_public_key(key: PublicKey) -> bytes: """ Encode DSA public key into RFC 3279 DER-encoded format. @@ -58,7 +65,7 @@ def encode_dsa_public_key(key): return asn1.dump() -def decode_dsa_public_key(der): +def decode_dsa_public_key(der: bytes) -> bytes: """ Decode a DSA public key from RFC 3279 DER-encoded format. @@ -73,7 +80,7 @@ def decode_dsa_public_key(der): return biginteger(asn1) -def encode_dsa_signature(signature): +def encode_dsa_signature(signature: bytes) -> bytes: """ Encode a signature (generated by :meth:`pkcs11.SignMixin.sign`) into DER-encoded ASN.1 (Dss_Sig_Value) format. @@ -87,7 +94,7 @@ def encode_dsa_signature(signature): return asn1.dump() -def decode_dsa_signature(der): +def decode_dsa_signature(der: bytes) -> bytes: """ Decode a DER-encoded ASN.1 (Dss_Sig_Value) signature (as generated by OpenSSL/X.509) into PKCS #11 format. diff --git a/pkcs11/util/ec.py b/pkcs11/util/ec.py index dd9594d..455516d 100644 --- a/pkcs11/util/ec.py +++ b/pkcs11/util/ec.py @@ -3,6 +3,10 @@ signatures. """ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + from asn1crypto.algos import DSASignature from asn1crypto.core import OctetString from asn1crypto.keys import ( @@ -15,8 +19,11 @@ from pkcs11.constants import Attribute, ObjectClass from pkcs11.mechanisms import KeyType +if TYPE_CHECKING: + from pkcs11.types import PublicKey + -def encode_named_curve_parameters(oid): +def encode_named_curve_parameters(oid: str) -> bytes: """ Return DER-encoded ANSI X.62 EC parameters for a named curve. @@ -33,7 +40,10 @@ def encode_named_curve_parameters(oid): ).dump() -def decode_ec_public_key(der, encode_ec_point=True): +def decode_ec_public_key( + der: bytes, + encode_ec_point: bool = True, +) -> dict[Attribute, Any]: """ Decode a DER-encoded EC public key as stored by OpenSSL into a dictionary of attributes able to be passed to :meth:`pkcs11.Session.create_object`. @@ -67,7 +77,7 @@ def decode_ec_public_key(der, encode_ec_point=True): } -def decode_ec_private_key(der): +def decode_ec_private_key(der: bytes) -> dict[Attribute, Any]: """ Decode a DER-encoded EC private key as stored by OpenSSL into a dictionary of attributes able to be passed to :meth:`pkcs11.Session.create_object`. @@ -86,7 +96,7 @@ def decode_ec_private_key(der): } -def encode_ec_public_key(key): +def encode_ec_public_key(key: PublicKey) -> bytes: """ Encode a DER-encoded EC public key as stored by OpenSSL. @@ -108,7 +118,7 @@ def encode_ec_public_key(key): ).dump() -def encode_ecdsa_signature(signature): +def encode_ecdsa_signature(signature: bytes) -> bytes: """ Encode a signature (generated by :meth:`pkcs11.SignMixin.sign`) into DER-encoded ASN.1 (ECDSA_Sig_Value) format. @@ -120,7 +130,7 @@ def encode_ecdsa_signature(signature): return DSASignature.from_p1363(signature).dump() -def decode_ecdsa_signature(der): +def decode_ecdsa_signature(der: bytes) -> bytes: """ Decode a DER-encoded ASN.1 (ECDSA_Sig_Value) signature (as generated by OpenSSL/X.509) into PKCS #11 format. diff --git a/pkcs11/util/rsa.py b/pkcs11/util/rsa.py index 418e4d8..97fc926 100644 --- a/pkcs11/util/rsa.py +++ b/pkcs11/util/rsa.py @@ -2,6 +2,10 @@ Key handling utilities for RSA keys (PKCS#1). """ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + from asn1crypto.keys import RSAPrivateKey, RSAPublicKey from pkcs11.constants import Attribute, MechanismFlag, ObjectClass @@ -9,8 +13,14 @@ from pkcs11.mechanisms import KeyType from pkcs11.util import biginteger +if TYPE_CHECKING: + from pkcs11.types import PublicKey -def decode_rsa_private_key(der, capabilities=None): + +def decode_rsa_private_key( + der: bytes, + capabilities: MechanismFlag | int | None = None, +) -> dict[Attribute, Any]: """ Decode a RFC2437 (PKCS#1) DER-encoded RSA private key into a dictionary of attributes able to be passed to :meth:`pkcs11.Session.create_object`. @@ -19,8 +29,7 @@ def decode_rsa_private_key(der, capabilities=None): :param MechanismFlag capabilities: Optional key capabilities :rtype: dict(Attribute,*) """ - if capabilities is None: - capabilities = DEFAULT_KEY_CAPABILITIES[KeyType.RSA] + caps: MechanismFlag | int = capabilities or DEFAULT_KEY_CAPABILITIES[KeyType.RSA] key = RSAPrivateKey.load(der) @@ -35,13 +44,16 @@ def decode_rsa_private_key(der, capabilities=None): Attribute.EXPONENT_1: biginteger(key["exponent1"]), Attribute.EXPONENT_2: biginteger(key["exponent2"]), Attribute.COEFFICIENT: biginteger(key["coefficient"]), - Attribute.DECRYPT: MechanismFlag.DECRYPT in capabilities, - Attribute.SIGN: MechanismFlag.SIGN in capabilities, - Attribute.UNWRAP: MechanismFlag.UNWRAP in capabilities, + Attribute.DECRYPT: MechanismFlag.DECRYPT & caps != 0, + Attribute.SIGN: MechanismFlag.SIGN & caps != 0, + Attribute.UNWRAP: MechanismFlag.UNWRAP & caps != 0, } -def decode_rsa_public_key(der, capabilities=None): +def decode_rsa_public_key( + der: bytes, + capabilities: MechanismFlag | int | None = None, +) -> dict[Attribute, Any]: """ Decode a RFC2437 (PKCS#1) DER-encoded RSA public key into a dictionary of attributes able to be passed to :meth:`pkcs11.Session.create_object`. @@ -50,9 +62,7 @@ def decode_rsa_public_key(der, capabilities=None): :param MechanismFlag capabilities: Optional key capabilities :rtype: dict(Attribute,*) """ - - if capabilities is None: - capabilities = DEFAULT_KEY_CAPABILITIES[KeyType.RSA] + caps: MechanismFlag | int = capabilities or DEFAULT_KEY_CAPABILITIES[KeyType.RSA] key = RSAPublicKey.load(der) return { @@ -60,13 +70,13 @@ def decode_rsa_public_key(der, capabilities=None): Attribute.KEY_TYPE: KeyType.RSA, Attribute.MODULUS: biginteger(key["modulus"]), Attribute.PUBLIC_EXPONENT: biginteger(key["public_exponent"]), - Attribute.ENCRYPT: MechanismFlag.ENCRYPT in capabilities, - Attribute.VERIFY: MechanismFlag.VERIFY in capabilities, - Attribute.WRAP: MechanismFlag.WRAP in capabilities, + Attribute.ENCRYPT: MechanismFlag.ENCRYPT & caps != 0, + Attribute.VERIFY: MechanismFlag.VERIFY & caps != 0, + Attribute.WRAP: MechanismFlag.WRAP & caps != 0, } -def encode_rsa_public_key(key): +def encode_rsa_public_key(key: PublicKey) -> bytes: """ Encode an RSA public key into PKCS#1 DER-encoded format. diff --git a/pkcs11/util/x509.py b/pkcs11/util/x509.py index 4b3aa06..78e1cab 100644 --- a/pkcs11/util/x509.py +++ b/pkcs11/util/x509.py @@ -2,6 +2,10 @@ Certificate handling utilities for X.509 (SSL) certificates. """ +from __future__ import annotations + +from typing import Any + from asn1crypto.core import OctetString from asn1crypto.x509 import Certificate @@ -9,7 +13,7 @@ from pkcs11.mechanisms import KeyType -def decode_x509_public_key(der): +def decode_x509_public_key(der: bytes) -> dict[Attribute, Any]: """ Decode a DER-encoded X.509 certificate's public key into a set of attributes able to be passed to :meth:`pkcs11.Session.create_object`. @@ -33,7 +37,7 @@ def decode_x509_public_key(der): "ec": KeyType.EC, }[key_info.algorithm] - attrs = { + attrs: dict[Attribute, Any] = { Attribute.CLASS: ObjectClass.PUBLIC_KEY, Attribute.KEY_TYPE: key_type, } @@ -72,7 +76,10 @@ def decode_x509_public_key(der): return attrs -def decode_x509_certificate(der, extended_set=False): +def decode_x509_certificate( + der: bytes, + extended_set: bool = False, +) -> dict[Attribute, Any]: """ Decode a DER-encoded X.509 certificate into a dictionary of attributes able to be passed to :meth:`pkcs11.Session.create_object`. @@ -95,7 +102,7 @@ def decode_x509_certificate(der, extended_set=False): issuer = x509.issuer serial = x509["tbs_certificate"]["serial_number"] - template = { + template: dict[Attribute, Any] = { Attribute.CLASS: ObjectClass.CERTIFICATE, Attribute.CERTIFICATE_TYPE: CertificateType.X_509, Attribute.SUBJECT: subject.dump(), diff --git a/pyproject.toml b/pyproject.toml index 79b3e86..49d2472 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,9 @@ archs = ["universal2"] [tool.setuptools.packages.find] include = ["pkcs11*"] +[tool.setuptools.package-data] +pkcs11 = ["py.typed"] + [tool.coverage.run] plugins = ["Cython.Coverage"] @@ -106,6 +109,7 @@ docs = [ ] lint = [ "ruff>=0.8.3", + "ty>=0.0.5", ] release = [ "setuptools>=80.8", diff --git a/tests/test_rsa.py b/tests/test_rsa.py index f108abd..296ad64 100644 --- a/tests/test_rsa.py +++ b/tests/test_rsa.py @@ -2,8 +2,11 @@ PKCS#11 RSA Public Key Cryptography """ +import unittest + import pkcs11 -from pkcs11 import MGF, Attribute, KeyType, Mechanism, ObjectClass +from pkcs11 import MGF, Attribute, KeyType, Mechanism, MechanismFlag, ObjectClass +from pkcs11.util.rsa import decode_rsa_private_key, decode_rsa_public_key from . import FIXME, TOKEN_PIN, TestCase, requires @@ -248,3 +251,88 @@ def test_encrypt_too_much_data(self): # This should ideally throw DataLen but you can't trust it with self.assertRaises(pkcs11.PKCS11Error): self.public.encrypt(data) + + +class RSAUtilTests(unittest.TestCase): + """Tests for RSA utility functions (no HSM required).""" + + # RSA 2048-bit Private Key (PKCS#1 DER format) + # Generated with: python3 generate_hex_for_test.py + RSA_PRIVATE_KEY_DER = bytes.fromhex( + "308204a40201000282010100d7765d0172639bf18e98049dcbc3e8083aa284f2" + "64321cc105d581c0c05042fdc4222e0b91625b9ce66e770028094e5b6f2658fd" + "8857b4290d4dc6fb62ae326948f10c554660367ad13de7b47ea8f14afc76cb08" + "c1ea0c880a7123d708b7fad7d45577d02604e7fda235fe089d5f2abb0417cdee" + "b4a46613cf8d0dd07bdbaf1eefafaf0b924b7893b7942925a67783a367141720" + "e76f4fa2c476d8f6367b8b0283411f9baa0c6ac32fac82ba2428ef19fcbb7069" + "e3f5ea382f055e42045bc30bbcf2f7b7d3a0cf7f86534ac5236cebc99ceecc34" + "1057712f9c5102e6c8aa0c9a9e46e198ecd0f8f18d0d77511ce8403c15f5b1df" + "7e8ef3ad7964ad18356fa4fd0203010001028201001d11c11692e25185d3a13a" + "ee3731a53a86feaa4531b37921a9b1d6a1b4d09f59317f130b488026b0127ed0" + "db5a8b76e0eb2c17518d7597befa268634206a342ef442615197ff1f1a8ee475" + "406ade4c3fbbb4234c792d24a7ae10f9aee7643b19a772288a12b712bdab86f1" + "51243a54bf8a9bd392e3185315552948b5da20178e2b8f0c5abe1a217aa596ef" + "21460043abf2a28e3fb1255801cd5d091dbd04064eef5c540fa770a173aa68b6" + "c4b476b241cec815be4eabffe51512926d64e2f693cfbb84d7a2c669c6f089b1" + "0d89b1193778fe7a2cf5470805af44acddb2e679f32ae86f3050771b7dd7b150" + "e4cd2d918796129d8b52303e4334dca6483e14575902818100f1330624abb27a" + "818fe2eb323022c5b3e6875a5367cfb444619ef4d0567258acba48a6ffa29835" + "05672f87334eba1e1eafc8959c0cf6fc76978d0ab40e5a007f356ba4f84fd323" + "5ca744321cf78d67720cd3741592454d61d0bf8a0e3cc6b5bf8fa0100fffae0a" + "f5ecb7ad27b97d4dfbff41faaac38d07975b66930b4052d1f302818100e4af0a" + "3f405366f91be44b3492b7d3e025f896fbb97fb9bc62878ffd29ee1dcf0c73a7" + "1baacc06d44ff9d9ba0b5c35338dfac5c5e50ed61976d2449dac573e71b5d07f" + "c3de6c8c89e041b7b00663cdfb329749322364277d361a96993463492474ddc4" + "99abc7269641ea6a7f9b84764c9b96d4f8a1e39fe614bb5c8ee307794f028181" + "00864ea034193b7805df263f4b220caac403310975fa0f6954ce7b21dd44d5c5" + "54e1220583c17939c4f97138bab432e504b7635d1399108b024a5f6a3f5ae278" + "f65cbbc50fd3fb40ec9de3567854cc7376c977916355a0ab77353302dfecadc5" + "9496984d796b28f1c780f9c23ca58805bdb5a47abd4dc8a11a81f5bb197fc6de" + "4b028181008ae8ac7fc952200d975cb0360a1d31cd49235c8b219dad33fa61c0" + "1c16d936302baf20c5d494c45d390b5aaf00f18cbb7935e7e69281d599782cb7" + "535379574bf915e2561708b6c1958035d4edbcb8452af0ec9c5115284b8d8ecf" + "05d6e5ac6b41b5e813345def597c46a95444224d3db1910862d2eb92984ee594" + "8e92e75a4f0281803793f2163018e7f5c39724ad650036f2f3bd707bbc9273a2" + "5fd579e73f8b22894aae8fc2b6445d11977fc572f31cedb23be5bcb4a51f3ba2" + "a0d4c34d8b4eee8ea21cf3182d6ed1a548c3adfa683e515a291cec9d4694873e" + "4ddd8ff0666fa0e3ae3e5ebcc7db78775efcd70a433648a3181afea42f31c5cb" + "cefc72489b2a557c" + ) + + # RSA 2048-bit Public Key (PKCS#1 DER format) + # Generated with: python3 generate_hex_for_test.py + RSA_PUBLIC_KEY_DER = bytes.fromhex( + "3082010a0282010100d7765d0172639bf18e98049dcbc3e8083aa284f264321c" + "c105d581c0c05042fdc4222e0b91625b9ce66e770028094e5b6f2658fd8857b4" + "290d4dc6fb62ae326948f10c554660367ad13de7b47ea8f14afc76cb08c1ea0c" + "880a7123d708b7fad7d45577d02604e7fda235fe089d5f2abb0417cdeeb4a466" + "13cf8d0dd07bdbaf1eefafaf0b924b7893b7942925a67783a367141720e76f4f" + "a2c476d8f6367b8b0283411f9baa0c6ac32fac82ba2428ef19fcbb7069e3f5ea" + "382f055e42045bc30bbcf2f7b7d3a0cf7f86534ac5236cebc99ceecc34105771" + "2f9c5102e6c8aa0c9a9e46e198ecd0f8f18d0d77511ce8403c15f5b1df7e8ef3" + "ad7964ad18356fa4fd0203010001" + ) + + def test_decode_rsa_private_key_with_capabilities(self): + """Test decode_rsa_private_key with explicit capabilities parameter.""" + # Test with explicit capabilities + caps = MechanismFlag.SIGN | MechanismFlag.DECRYPT + result = decode_rsa_private_key(self.RSA_PRIVATE_KEY_DER, capabilities=caps) + + self.assertEqual(result[Attribute.CLASS], ObjectClass.PRIVATE_KEY) + self.assertEqual(result[Attribute.KEY_TYPE], KeyType.RSA) + self.assertTrue(result[Attribute.SIGN]) + self.assertTrue(result[Attribute.DECRYPT]) + self.assertFalse(result[Attribute.UNWRAP]) + + def test_decode_rsa_public_key_with_capabilities(self): + """Test decode_rsa_public_key with explicit capabilities parameter.""" + # Test with explicit capabilities + caps = MechanismFlag.ENCRYPT | MechanismFlag.VERIFY + result = decode_rsa_public_key(self.RSA_PUBLIC_KEY_DER, capabilities=caps) + + self.assertEqual(result[Attribute.CLASS], ObjectClass.PUBLIC_KEY) + self.assertEqual(result[Attribute.KEY_TYPE], KeyType.RSA) + self.assertTrue(result[Attribute.ENCRYPT]) + self.assertTrue(result[Attribute.VERIFY]) + self.assertFalse(result[Attribute.WRAP]) diff --git a/uv.lock b/uv.lock index 9ce7ddb..5977b54 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.9" resolution-markers = [ "python_full_version >= '3.11'", @@ -641,6 +641,7 @@ dev = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx-rtd-theme" }, + { name = "ty" }, ] docs = [ { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -657,6 +658,7 @@ docs-build = [ ] lint = [ { name = "ruff" }, + { name = "ty" }, ] release = [ { name = "cython" }, @@ -691,6 +693,7 @@ dev = [ { name = "setuptools-scm", specifier = ">=8.3.1" }, { name = "sphinx", specifier = ">=7.4.7" }, { name = "sphinx-rtd-theme", specifier = ">=3.0.2" }, + { name = "ty", specifier = ">=0.0.5" }, ] docs = [ { name = "sphinx", specifier = ">=7.4.7" }, @@ -701,7 +704,10 @@ docs-build = [ { name = "sphinx", specifier = ">=7.4.7" }, { name = "sphinx-rtd-theme", specifier = ">=3.0.2" }, ] -lint = [{ name = "ruff", specifier = ">=0.8.3" }] +lint = [ + { name = "ruff", specifier = ">=0.8.3" }, + { name = "ty", specifier = ">=0.0.5" }, +] release = [ { name = "cython" }, { name = "setuptools", specifier = ">=80.8" }, @@ -1013,6 +1019,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] +[[package]] +name = "ty" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/db/6299d478000f4f1c6f9bf2af749359381610ffc4cbe6713b66e436ecf6e7/ty-0.0.5.tar.gz", hash = "sha256:983da6330773ff71e2b249810a19c689f9a0372f6e21bbf7cde37839d05b4346", size = 4806218, upload-time = "2025-12-20T21:19:17.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/98/c1f61ba378b4191e641bb36c07b7fcc70ff844d61be7a4bf2fea7472b4a9/ty-0.0.5-py3-none-linux_armv6l.whl", hash = "sha256:1594cd9bb68015eb2f5a3c68a040860f3c9306dc6667d7a0e5f4df9967b460e2", size = 9785554, upload-time = "2025-12-20T21:19:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f9/b37b77c03396bd779c1397dae4279b7ad79315e005b3412feed8812a4256/ty-0.0.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7c0140ba980233d28699d9ddfe8f43d0b3535d6a3bbff9935df625a78332a3cf", size = 9603995, upload-time = "2025-12-20T21:19:15.256Z" }, + { url = "https://files.pythonhosted.org/packages/7d/70/4e75c11903b0e986c0203040472627cb61d6a709e1797fb08cdf9d565743/ty-0.0.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:15de414712cde92048ae4b1a77c4dc22920bd23653fe42acaf73028bad88f6b9", size = 9145815, upload-time = "2025-12-20T21:19:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/89/05/93983dfcf871a41dfe58e5511d28e6aa332a1f826cc67333f77ae41a2f8a/ty-0.0.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:438aa51ad6c5fae64191f8d58876266e26f9250cf09f6624b6af47a22fa88618", size = 9619849, upload-time = "2025-12-20T21:19:19.084Z" }, + { url = "https://files.pythonhosted.org/packages/82/b6/896ab3aad59f846823f202e94be6016fb3f72434d999d2ae9bd0f28b3af9/ty-0.0.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b3d373fd96af1564380caf153600481c676f5002ee76ba8a7c3508cdff82ee0", size = 9606611, upload-time = "2025-12-20T21:19:24.583Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ae/098e33fc92330285ed843e2750127e896140c4ebd2d73df7732ea496f588/ty-0.0.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8453692503212ad316cf8b99efbe85a91e5f63769c43be5345e435a1b16cba5a", size = 10029523, upload-time = "2025-12-20T21:19:07.055Z" }, + { url = "https://files.pythonhosted.org/packages/04/5a/f4b4c33758b9295e9aca0de9645deca0f4addd21d38847228723a6e780fc/ty-0.0.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2e4c454139473abbd529767b0df7a795ed828f780aef8d0d4b144558c0dc4446", size = 10870892, upload-time = "2025-12-20T21:19:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c5/4e3e7e88389365aa1e631c99378711cf0c9d35a67478cb4720584314cf44/ty-0.0.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:426d4f3b82475b1ec75f3cc9ee5a667c8a4ae8441a09fcd8e823a53b706d00c7", size = 10599291, upload-time = "2025-12-20T21:19:26.557Z" }, + { url = "https://files.pythonhosted.org/packages/c1/5d/138f859ea87bd95e17b9818e386ae25a910e46521c41d516bf230ed83ffc/ty-0.0.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5710817b67c6b2e4c0224e4f319b7decdff550886e9020f6d46aa1ce8f89a609", size = 10413515, upload-time = "2025-12-20T21:19:11.094Z" }, + { url = "https://files.pythonhosted.org/packages/27/21/1cbcd0d3b1182172f099e88218137943e0970603492fb10c7c9342369d9a/ty-0.0.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23c55ef08882c7c5ced1ccb90b4eeefa97f690aea254f58ac0987896c590f76", size = 10144992, upload-time = "2025-12-20T21:19:13.225Z" }, + { url = "https://files.pythonhosted.org/packages/ad/30/fdac06a5470c09ad2659a0806497b71f338b395d59e92611f71b623d05a0/ty-0.0.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b9e4c1a28a23b14cf8f4f793f4da396939f16c30bfa7323477c8cc234e352ac4", size = 9606408, upload-time = "2025-12-20T21:19:09.212Z" }, + { url = "https://files.pythonhosted.org/packages/09/93/e99dcd7f53295192d03efd9cbcec089a916f49cad4935c0160ea9adbd53d/ty-0.0.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4e9ebb61529b9745af662e37c37a01ad743cdd2c95f0d1421705672874d806cd", size = 9630040, upload-time = "2025-12-20T21:19:38.165Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f8/6d1e87186e4c35eb64f28000c1df8fd5f73167ce126c5e3dd21fd1204a23/ty-0.0.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5eb191a8e332f50f56dfe45391bdd7d43dd4ef6e60884710fd7ce84c5d8c1eb5", size = 9754016, upload-time = "2025-12-20T21:19:32.79Z" }, + { url = "https://files.pythonhosted.org/packages/28/e6/20f989342cb3115852dda404f1d89a10a3ce93f14f42b23f095a3d1a00c9/ty-0.0.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:92ed7451a1e82ee134a2c24ca43b74dd31e946dff2b08e5c34473e6b051de542", size = 10252877, upload-time = "2025-12-20T21:19:20.787Z" }, + { url = "https://files.pythonhosted.org/packages/57/9d/fc66fa557443233dfad9ae197ff3deb70ae0efcfb71d11b30ef62f5cdcc3/ty-0.0.5-py3-none-win32.whl", hash = "sha256:71f6707e4c1c010c158029a688a498220f28bb22fdb6707e5c20e09f11a5e4f2", size = 9212640, upload-time = "2025-12-20T21:19:30.817Z" }, + { url = "https://files.pythonhosted.org/packages/68/b6/05c35f6dea29122e54af0e9f8dfedd0a100c721affc8cc801ebe2bc2ed13/ty-0.0.5-py3-none-win_amd64.whl", hash = "sha256:2b8b754a0d7191e94acdf0c322747fec34371a4d0669f5b4e89549aef28814ae", size = 10034701, upload-time = "2025-12-20T21:19:28.311Z" }, + { url = "https://files.pythonhosted.org/packages/df/ca/4201ed5cb2af73912663d0c6ded927c28c28b3c921c9348aa8d2cfef4853/ty-0.0.5-py3-none-win_arm64.whl", hash = "sha256:83bea5a5296caac20d52b790ded2b830a7ff91c4ed9f36730fe1f393ceed6654", size = 9566474, upload-time = "2025-12-20T21:19:22.518Z" }, +] + [[package]] name = "typing-extensions" version = "4.14.0"