lists.arthurdejong.org
RSS feed

python-pskc branch master updated. 1.3-12-gf10acff

[Date Prev][Date Next] [Thread Prev][Thread Next]

python-pskc branch master updated. 1.3-12-gf10acff



This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "python-pskc".

The branch, master has been updated
       via  f10acff73e025a998fe31de85a88e9da3a8d7729 (commit)
       via  02c35e70d8470b25924c1e6e582ff1bce2a7f546 (commit)
      from  b6b593c18abb3cd5b36ea7fa8cbd848820749cc6 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
https://arthurdejong.org/git/python-pskc/commit/?id=f10acff73e025a998fe31de85a88e9da3a8d7729

commit f10acff73e025a998fe31de85a88e9da3a8d7729
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Wed Dec 24 20:07:45 2025 +0100

    Include type information in documentation
    
    This also includes a few other cleanups moving some documentation to
    docstrings.

diff --git a/docs/encryption.rst b/docs/encryption.rst
index 23836db..0e730a3 100644
--- a/docs/encryption.rst
+++ b/docs/encryption.rst
@@ -46,11 +46,12 @@ The Encryption class
 
 .. class:: Encryption
 
-   .. attribute:: id
+   .. autoattribute:: id
 
       Optional identifier of the encryption key.
 
    .. attribute:: algorithm
+      :type: str | None
 
       A URI of the encryption algorithm used. See the section
       :ref:`encryption-algorithms` below for a list of algorithms URIs.
@@ -62,22 +63,24 @@ The Encryption class
 
 
    .. attribute:: is_encrypted
+      :type: bool
 
       An indicator of whether the PSKC file requires an additional pre-shared
       key or passphrase to decrypt the contents of the file. Will be ``True``
       if a key or passphrase is needed, ``False`` otherwise.
 
-   .. attribute:: key_names
+   .. autoattribute:: key_names
 
       List of names provided for the encryption key.
 
    .. attribute:: key_name
+      :type: str | None
 
       Since usually only one name is defined for a key but the schema allows
       for multiple names, this is a shortcut for accessing the first value of
       :attr:`key_names`. It will return ``None`` if no name is available.
 
-   .. attribute:: key
+   .. autoattribute:: key
 
       The binary value of the encryption key. In the case of pre-shared keys
       this value should be set before trying to access encrypted information
@@ -86,75 +89,20 @@ The Encryption class
       When using key derivation the secret key is available in this attribute
       after calling :func:`derive_key`.
 
-   .. function:: derive_key(password)
+   .. automethod:: derive_key
 
-      Derive a key from the supplied password and information in the PSKC
-      file (generally algorithm, salt, etc.).
-
-      This function may raise a :exc:`~pskc.exceptions.KeyDerivationError`
-      exception if key derivation fails for some reason.
-
-   .. attribute:: fields
+   .. autoattribute:: fields
 
       A list of :class:`~pskc.key.Key` instance field names that will be
       encrypted when the PSKC file is written. List values can contain
       ``secret``, ``counter``, ``time_offset``, ``time_interval`` and
       ``time_drift``.
 
-   .. function:: setup_preshared_key(...)
-
-      Configure pre-shared key encryption when writing the file.
-
-      :param bytes key: the encryption key to use
-      :param str id: encryption key identifier
-      :param str algorithm: encryption algorithm
-      :param int key_length: encryption key length in bytes
-      :param str key_name: a name for the key
-      :param list key_names: a number of names for the key
-      :param list fields: a list of fields to encrypt
-
-      This is a utility function to easily set up encryption. Encryption can
-      also be set up by manually by setting the
-      :class:`~pskc.encryption.Encryption` properties.
-
-      This method will generate a key if required and set the passed values.
-      By default AES128-CBC encryption will be configured and unless a key is
-      specified one of the correct length will be generated. If the algorithm
-      does not provide integrity checks (e.g. CBC-mode algorithms) integrity
-      checking in the PSKC file will be set up using
-      :func:`~pskc.mac.MAC.setup()`.
-
-      By default only the :attr:`~pskc.key.Key.secret` property will be
-      encrypted when writing the file.
-
-   .. function:: setup_pbkdf2(...)
-
-      Configure password-based PSKC encryption when writing the file.
-
-      :param str password: the password to use (required)
-      :param str id: encryption key identifier
-      :param str algorithm: encryption algorithm
-      :param int key_length: encryption key length in bytes
-      :param str key_name: a name for the key
-      :param list key_names: a number of names for the key
-      :param list fields: a list of fields to encrypt
-      :param bytes salt: PBKDF2 salt
-      :param int salt_length: used when generating random salt
-      :param int iterations: number of PBKDF2 iterations
-      :param function prf: PBKDF2 pseudorandom function
-
-      Defaults for the above parameters are similar to those for
-      :func:`setup_preshared_key()` but the password parameter is required.
-
-      By default 12000 iterations will be used and a random salt with the
-      length of the to-be-generated encryption key will be used.
+   .. automethod:: setup_preshared_key
 
-   .. function:: remove_encryption()
+   .. automethod:: setup_pbkdf2
 
-      Decrypt all data stored in the PSKC file and remove the encryption
-      configuration. This can be used to read and encrypted PSKC file,
-      decrypt the file, remove the encryption and output an unencrypted PSKC
-      file or to replace the encryption algorithm.
+   .. automethod:: remove_encryption
 
 
 .. _encryption-algorithms:
diff --git a/docs/exceptions.rst b/docs/exceptions.rst
index 81a73b2..11e875e 100644
--- a/docs/exceptions.rst
+++ b/docs/exceptions.rst
@@ -6,36 +6,12 @@ only raise exceptions on wildly invalid PSKC files.
 
 .. module:: pskc.exceptions
 
-.. exception:: PSKCError
+.. autoexception:: PSKCError
 
-   The base class for all exceptions that the module will raise. In some
-   cases third-party code may raise additional exceptions.
+.. autoexception:: ParseError
 
-.. exception:: ParseError
+.. autoexception:: EncryptionError
 
-   Raised when the PSKC file cannot be correctly read due to invalid XML or
-   some required element or attribute is missing. This exception should only
-   be raised when parsing the file (i.e. when the :class:`~pskc.PSKC` class is
-   instantiated).
+.. autoexception:: DecryptionError
 
-.. .. exception:: EncryptionError
-
-   Raised when encrypting a value is not possible due to key length issues,
-   missing or wrong length plain text, or other issues.
-
-.. exception:: DecryptionError
-
-   Raised when decrypting a value fails due to missing or incorrect key,
-   unsupported decryption or MAC algorithm, failed message authentication
-   check or other error.
-
-   This exception is generally raised when accessing encrypted information
-   (i.e. the :attr:`~pskc.key.Key.secret`, :attr:`~pskc.key.Key.counter`,
-   :attr:`~pskc.key.Key.time_offset`, :attr:`~pskc.key.Key.time_interval` or
-   :attr:`~pskc.key.Key.time_drift` attributes of the :class:`~pskc.key.Key`
-   class).
-
-.. exception:: KeyDerivationError
-
-   Raised when key derivation fails due to an unsupported algorithm or
-   missing information in the PSKC file.
+.. autoexception:: KeyDerivationError
diff --git a/docs/mac.rst b/docs/mac.rst
index c9e7813..fea3514 100644
--- a/docs/mac.rst
+++ b/docs/mac.rst
@@ -27,6 +27,7 @@ The MAC class
 .. class:: MAC
 
    .. attribute:: algorithm
+      :type: str | None
 
       A URI of the MAC algorithm used for message authentication. See the
       section :ref:`mac-algorithms` below for a list of algorithm URIs.
@@ -37,25 +38,14 @@ The MAC class
       ``http://www.w3.org/2001/04/xmldsig-more#hmac-sha256``.
 
    .. attribute:: key
+      :type: bytes | None
 
       For HMAC checking, this contains the binary value of the MAC key. The
       MAC key is generated specifically for each PSKC file and encrypted with
       the PSKC encryption key, so the PSKC file should be decrypted first
       (see :doc:`encryption`).
 
-   .. function:: setup(...)
-
-      Configure an encrypted MAC key for creating a new PSKC file.
-
-      :param str algorithm: encryption algorithm
-      :param bytes key: the encryption key to use
-
-      None of the arguments are required. By default HMAC-SHA1 will be used
-      as a MAC algorithm. If no key is configured a random key will be
-      generated with the length of the output of the configured hash.
-
-      This function will automatically be called when the configured
-      encryption algorithm requires a message authentication code.
+   .. automethod:: setup
 
 
 .. _mac-algorithms:
diff --git a/docs/policy.rst b/docs/policy.rst
index 3d1d8f7..70e1503 100644
--- a/docs/policy.rst
+++ b/docs/policy.rst
@@ -19,72 +19,75 @@ The Policy class
 
 .. class:: Policy
 
-   .. attribute:: start_date
+   .. autoattribute:: start_date
 
       :class:`datetime.datetime` value that indicates that the key must not
       be used before this date.
 
-   .. attribute:: expiry_date
+   .. autoattribute:: expiry_date
 
       :class:`datetime.datetime` value that indicates that the key must not
       be used after this date. Systems should not rely upon the device to
       enforce key usage date restrictions, as some devices do not have an
       internal clock.
 
-   .. attribute:: number_of_transactions
+   .. autoattribute:: number_of_transactions
 
       The value indicates the maximum number of times a key carried within
       the PSKC document may be used by an application after having received
       it.
 
-   .. attribute:: key_usage
+   .. autoattribute:: key_usage
 
       A list of `valid usage scenarios
       <https://www.iana.org/assignments/pskc/#key-usage>`__ for the
       key that the recipient should check against the intended usage of the
       key. Also see :func:`may_use` and :ref:`key-use-constants` below.
 
-   .. attribute:: pin_key_id
+   .. autoattribute:: pin_key_id
 
       The unique `id` of the key within the PSKC file that contains the value
       of the PIN that protects this key.
 
    .. attribute:: pin_key
+      :type: Key | None
 
       Instance of the :class:`~pskc.key.Key` (if any) that contains the value
       of the PIN referenced by :attr:`pin_key_id`.
 
    .. attribute:: pin
+      :type: str | None
 
       PIN value referenced by :attr:`pin_key_id` (if any). The value is
       transparently decrypted if possible.
 
-   .. attribute:: pin_usage
+   .. autoattribute:: pin_usage
 
       Describe how the PIN is used during the usage of the key. See
       :ref:`pin-use-constants` below.
 
-   .. attribute:: pin_max_failed_attempts
+   .. autoattribute:: pin_max_failed_attempts
 
       The maximum number of times the PIN may be entered wrongly before it
       MUST NOT be possible to use the key any more.
 
-   .. attribute:: pin_min_length
+   .. autoattribute:: pin_min_length
 
       The minimum length of a PIN that can be set to protect the associated
       key.
 
-   .. attribute:: pin_max_length
+   .. autoattribute:: pin_max_length
 
       The maximum length of a PIN that can be set to protect this key.
 
-   .. attribute:: pin_encoding
+   .. autoattribute:: pin_encoding
 
       The encoding of the PIN which is one of ``DECIMAL``, ``HEXADECIMAL``,
       ``ALPHANUMERIC``, ``BASE64``, or ``BINARY`` (see
       :attr:`~pskc.key.Key.challenge_encoding`).
 
    .. attribute:: unknown_policy_elements
+      :type: bool
 
       Boolean that is set to ``True`` if the PSKC policy information contains
       unknown or unsupported definitions or values. A conforming
@@ -92,11 +95,7 @@ The Policy class
       value is ``True`` to ensure that the lack of understanding of certain
       extensions does not lead to unintended key usage.
 
-   .. function:: may_use(usage=None, now=None)
-
-      Check whether the key may be used for the provided purpose. The key
-      :attr:`start_date` and :attr:`expiry_date` are also checked. The `now`
-      argument can be used to specify another point in time to check against.
+   .. automethod:: may_use
 
 .. _key-use-constants:
 
diff --git a/docs/signatures.rst b/docs/signatures.rst
index 8d5a185..963a130 100644
--- a/docs/signatures.rst
+++ b/docs/signatures.rst
@@ -31,84 +31,54 @@ The Signature class
 .. class:: Signature
 
    .. attribute:: is_signed
+      :type: bool
 
       A boolan value that indicates whether an XML signature is present in
       the PSKC file. This property does not indicate whether the signature
       is validated.
 
    .. attribute:: algorithm
+      :type: str | None
 
       A URI of the signing algorithm used.
       Assigned values to this attribute will be converted to the canonical
       URI for the algorithm if it is known.
 
-   .. attribute:: canonicalization_method
+   .. autoattribute:: canonicalization_method
 
       A URI that is used to identify the XML canonicalization method used.
 
-   .. attribute:: digest_algorithm
+   .. autoattribute:: digest_algorithm
 
       A URI that identifies that hashing algorithm that is used to construct
       the signature.
 
-   .. attribute:: issuer
+   .. autoattribute:: issuer
 
       A distinguished name of the issuer of the certificate that belongs to
       the key that is used for the signature.
 
-   .. attribute:: serial
+   .. autoattribute:: serial
 
       A serial number of the certificate that belongs to the key that is used
       for the signature.
 
-   .. attribute:: key
+   .. autoattribute:: key
 
       A PEM encoded key that will be used to create the signed PSKC file.
 
-   .. attribute:: certificate
+   .. autoattribute:: certificate
 
       A PEM encoded certificate that is embedded inside the signature that
       can be used to validate the signature.
 
    .. attribute:: signed_pskc
+      :type: PSKC
 
       A :class:`~pskc.PSKC` instance that contains the signed contents of the
       PSKC file. It is usually required to call :func:`verify` before
       accessing this attribute without raising an exception.
 
-   .. function:: verify(certificate=None, ca_pem_file=None)
+   .. automethod:: verify
 
-      Verify the validity of the embedded XML signature. This function will
-      raise an exception when the validation fails.
-
-      :param bytes certificate: a PEM encoded certificate that is used for 
verification
-      :param str ca_pem_file: the name of a file that contains a CA certificate
-
-      The signature can be verified in three ways:
-
-      * The signature was made with a key that has a certificate that is
-        signed by a CA that is configured in the system CA store. In this
-        case neither `certificate` or `ca_pem_file` need to be specified (but
-        a certificate needs to be embedded inside the PSKC file).
-      * The signature was made with a key and a certificate for the key was
-        transmitted out-of-band. In this case the `certificate` argument
-        needs to be present.
-      * The signature was made with a key and has a certificate that is
-        signed by a specific CA who's certificate was transmitted
-        out-of-band. In this case the `ca_pem_file` is used to point to a CA
-        certificate file (but a certificate needs to be embedded inside the
-        PSKC file).
-
-      After calling this function a verified version of the PSKC file will
-      be present in the :attr:`signed_pskc` attribute.
-
-   .. function:: sign(key, certificate=None)
-
-      Set up a key and optionally a certificate that will be used to create an
-      embedded XML signature when writing the file.
-
-      :param bytes key: PEM encoded key used for signing
-      :param bytes certificate: PEM encoded certificate that will be embedded
-
-      This is a utility function that is used to configure the properties
-      needed to create a signed PSKC file.
+   .. automethod:: sign
diff --git a/docs/usage.rst b/docs/usage.rst
index ce9fc87..89150d2 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -67,7 +67,7 @@ The PSKC class
 
 .. module:: pskc
 
-.. class:: PSKC([filename])
+.. class:: PSKC(filename: str | bytes | PathLike[str] | PathLike[bytes] | 
IO[str] | IO[bytes] | None = None)
 
    The :class:`PSKC` class is used as a wrapper to access information from a
    PSKC file.
@@ -79,60 +79,52 @@ The PSKC class
 
    Instances of this class provide the following attributes and functions:
 
-   .. attribute:: version
+   .. autoattribute:: version
 
       The PSKC format version used. Only version ``1.0`` is currently
       specified in
       `RFC 6030 <https://tools.ietf.org/html/rfc6030#section-1.2>`__
       and supported.
 
-   .. attribute:: id
+   .. autoattribute:: id
 
       A unique identifier for the container.
 
-   .. attribute:: devices
+   .. autoattribute:: devices
 
       A list of :class:`~pskc.device.Device` instances that represent the key
       containers within the PSKC file.
 
    .. attribute:: keys
+      :type: list[Key]
 
       A list of :class:`~pskc.key.Key` instances that represent the keys
       within the PSKC file.
 
    .. attribute:: encryption
+      :type: Encryption
 
       :class:`~pskc.encryption.Encryption` instance that handles PSKC file
       encryption. See :doc:`encryption` for more information.
 
    .. attribute:: mac
+      :type: MAC
 
       :class:`~pskc.mac.MAC` instance for handling integrity checking.
       See :doc:`mac` for more information.
 
    .. attribute:: signature
+      :type: Signature
 
       :class:`~pskc.signature.Signature` instance for handling embedded XML
       signatures in the file.
       See :doc:`signatures` for more information.
 
-   .. function:: add_device([**kwargs])
+   .. automethod:: add_device
 
-      Add a new key package to the PSKC instance. The keyword arguments may
-      refer to any attributes of the :class:`~pskc.device.Device` class with
-      which the new device is initialised.
+   .. automethod:: add_key
 
-   .. function:: add_key([**kwargs])
-
-      Add a new key to the PSKC instance. The keyword arguments may refer to
-      any attributes of the :class:`~pskc.key.Key` or
-      :class:`~pskc.device.Device` class with which the new key is
-      initialised.
-
-   .. function:: write(filename)
-
-      Write the PSKC object to the provided file. The `filename` argument can
-      be either the name of a file or a file-like object.
+   .. automethod:: write
 
 
 The Key class
@@ -140,17 +132,17 @@ The Key class
 
 .. module:: pskc.key
 
-.. class:: Key()
+.. class:: Key
 
    Instances of this class provide the following attributes and functions:
 
-   .. attribute:: id
+   .. autoattribute:: id
 
       A unique identifier for the key. If there are multiple interactions
       with the same key in multiple instances of PSKC files the `id` is
       supposed to remain the same.
 
-   .. attribute:: algorithm
+   .. autoattribute:: algorithm
 
       A URI that identifies the PSKC algorithm profile. The algorithm profile
       associates specific semantics to the key. Some `known profiles
@@ -170,6 +162,7 @@ The Key class
       
+------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+
 
    .. attribute:: secret
+      :type: bytes
 
       The binary value of the transported secret key. If the key information
       is encrypted in the PSKC file it is transparently decrypted if
@@ -177,12 +170,14 @@ The Key class
       :exc:`~pskc.exceptions.DecryptionError` if decryption fails.
 
    .. attribute:: counter
+      :type: int | None
 
       The event counter (integer) for event-based OTP algorithms. Will also be
       transparently decrypted and may also raise
       :exc:`~pskc.exceptions.DecryptionError`.
 
    .. attribute:: time_offset
+      :type: int | None
 
       The time offset (integer) for time-based OTP algorithms. If time
       intervals are used it carries the number of time intervals passed from
@@ -190,57 +185,60 @@ The Key class
       and may also raise :exc:`~pskc.exceptions.DecryptionError`.
 
    .. attribute:: time_interval
+      :type: int | None
 
       The time interval in seconds (integer) for time-based OTP algorithms
       (usually ``30`` or ``60``). Will also be transparently decrypted and may
       also raise :exc:`~pskc.exceptions.DecryptionError`.
 
    .. attribute:: time_drift
+      :type: int | None
 
       For time-based OTP algorithms this contains the device clock drift in
       number of intervals (integer). Will also be transparently decrypted and
       may also raise :exc:`~pskc.exceptions.DecryptionError`.
 
-   .. attribute:: issuer
+   .. autoattribute:: issuer
 
       The name of the party that issued the key. This may be different from
       the :attr:`~pskc.device.Device.manufacturer` of the device.
 
-   .. attribute:: key_profile
+   .. autoattribute:: key_profile
 
       A reference to a pre-shared key profile agreed upon between the sending
       and receiving parties. The profile information itself is not
       transmitted within the container.
       See `RFC 6030 <https://tools.ietf.org/html/rfc6030#section-4.4>`__.
 
-   .. attribute:: key_reference
+   .. autoattribute:: key_reference
 
       A reference to an external key that is not contained within the PSKC
       file (e.g., a PKCS #11 key label). If this attribute is present, the
       :attr:`secret` attribute will generally be missing.
 
-   .. attribute:: friendly_name
+   .. autoattribute:: friendly_name
 
       A human-readable name for the secret key.
 
-   .. attribute:: key_userid
+   .. autoattribute:: key_userid
 
       The distinguished name of the user associated with the key.
       Also see :attr:`~pskc.device.Device.device_userid`.
 
    .. attribute:: userid
+      :type: str | None
 
       The distinguished name of the user associated with the key or the device,
       taken from :attr:`key_userid` or 
:attr:`~pskc.device.Device.device_userid`
       whichever one is defined.
 
-   .. attribute:: algorithm_suite
+   .. autoattribute:: algorithm_suite
 
       Additional algorithm-specific characteristics. For example, in an
       HMAC-based algorithm it could specify the hash algorithm used (SHA1
       or SHA256).
 
-   .. attribute:: challenge_encoding
+   .. autoattribute:: challenge_encoding
 
       Encoding of the challenge accepted by the device for challenge-response
       authentication. One of:
@@ -251,41 +249,42 @@ The Key class
       * ``BASE64``: base-64 encoded
       * ``BINARY``: binary data
 
-   .. attribute:: challenge_min_length
+   .. autoattribute:: challenge_min_length
 
       The minimum size of the challenge accepted by the device.
 
-   .. attribute:: challenge_max_length
+   .. autoattribute:: challenge_max_length
 
       The maximum size of the challenge accepted by the device.
 
-   .. attribute:: challenge_check
+   .. autoattribute:: challenge_check
 
       Boolean that indicates whether the device will check an embedded
       `Luhn check digit 
<https://arthurdejong.org/python-stdnum/doc/stdnum.luhn.html>`_
       contained in the challenge.
 
-   .. attribute:: response_encoding
+   .. autoattribute:: response_encoding
 
       Format of the response that is generated by the device. If must be one
       of the values as described under :attr:`challenge_encoding`.
 
-   .. attribute:: response_length
+   .. autoattribute:: response_length
 
       The length of the response generated by the device.
 
-   .. attribute:: response_check
+   .. autoattribute:: response_check
 
       Boolean that indicates whether the device will append a
       `Luhn check digit 
<https://arthurdejong.org/python-stdnum/doc/stdnum.luhn.html>`_
       to the response.
 
    .. attribute:: policy
+      :type: Policy
 
       :class:`~pskc.policy.Policy` instance that provides key and PIN policy
       information. See :doc:`policy`.
 
-   .. function:: check()
+   .. method:: check()
 
       Check if any MACs in the key data embedded in the PSKC file are valid.
       This will return None if there is no MAC to be checked. It will return
@@ -302,25 +301,20 @@ The Device class
 
 .. module:: pskc.device
 
-.. class:: Device()
+.. class:: Device
 
    Instances of this class provide the following attributes and functions:
 
-   .. attribute:: keys
+   .. autoattribute:: keys
 
       A list of :class:`~pskc.key.Key` instances that represent the keys that
       are linked to this device. Most PSKC files only allow one key per
       device which is why all :class:`~pskc.device.Device` attributes are
       available in :class:`~pskc.key.Key`.
 
-   .. function:: add_key([**kwargs])
-
-      Add a new key to the device. The keyword arguments may refer to
-      any attributes of the :class:`~pskc.key.Key` or
-      :class:`~pskc.device.Device` class with which the new key is
-      initialised.
+   .. automethod:: add_key
 
-   .. attribute:: manufacturer
+   .. autoattribute:: manufacturer
 
       The name of the manufacturer of the device to which the key is
       provisioned.
@@ -333,45 +327,45 @@ The Device class
       The value may be different from the :attr:`~pskc.key.Key.issuer` of
       the key on the device.
 
-   .. attribute:: serial
+   .. autoattribute:: serial
 
       The serial number of the device to which the key is provisioned.
       Together with :attr:`manufacturer` (and possibly :attr:`issue_no`) this
       should uniquely identify the device.
 
-   .. attribute:: model
+   .. autoattribute:: model
 
       A manufacturer-specific description of the model of the device.
 
-   .. attribute:: issue_no
+   .. autoattribute:: issue_no
 
       The issue number in case there are devices with the same :attr:`serial`
       number so that they can be distinguished by different issue numbers.
 
-   .. attribute:: device_binding
+   .. autoattribute:: device_binding
 
       Reference to a device identifier (e.g. IMEI) that allows a provisioning
       server to ensure that the key is going to be loaded into a specific
       device.
 
-   .. attribute:: start_date
+   .. autoattribute:: start_date
 
       :class:`datetime.datetime` value that indicates that the device should
       only be used after this date.
 
-   .. attribute:: expiry_date
+   .. autoattribute:: expiry_date
 
       :class:`datetime.datetime` value that indicates that the device should
       only be used before this date. Systems should not rely upon the device
       to enforce key usage date restrictions, as some devices do not have an
       internal clock.
 
-   .. attribute:: device_userid
+   .. autoattribute:: device_userid
 
       The distinguished name of the user associated with the device.
       Also see :attr:`~pskc.key.Key.key_userid`.
 
-   .. attribute:: crypto_module
+   .. autoattribute:: crypto_module
 
       Implementation specific unique identifier of the cryptographic module
       on the device to which the keys have been (or will be) provisioned.
diff --git a/pskc/__init__.py b/pskc/__init__.py
index 46e772d..b35ef33 100644
--- a/pskc/__init__.py
+++ b/pskc/__init__.py
@@ -122,6 +122,10 @@ class PSKC:
 
         The new key is initialised with properties from the provided keyword
         arguments if any.
+
+        The arguments may be any valid attribute of :class:`~pskc.key.Key` or
+        :class:`~pskc.device.Device` or be of the special form 
`policy.attribute`
+        or `policy__attribute` to set any attribute of 
:class:`~pskc.policy.Policy`.
         """
         from pskc.device import update_attributes
         device = self.add_device()
@@ -130,7 +134,10 @@ class PSKC:
         return key
 
     def write(self, filename: str | bytes | PathLike[str] | PathLike[bytes] | 
IO[str] | IO[bytes]) -> None:
-        """Write the PSKC file to the provided file."""
+        """Write the PSKC file to the provided file.
+
+        The `filename` argument can be either the name of a file or a 
file-like object.
+        """
         from pskc.serialiser import PSKCSerialiser
         if hasattr(filename, 'write'):
             PSKCSerialiser.serialise_file(self, filename)  # type: ignore 
[arg-type]
diff --git a/pskc/device.py b/pskc/device.py
index 995453e..a382654 100644
--- a/pskc/device.py
+++ b/pskc/device.py
@@ -78,6 +78,10 @@ class Device:
 
         The new key is initialised with properties from the provided keyword
         arguments if any.
+
+        The arguments may be any valid attribute of :class:`~pskc.key.Key` or
+        be of the special form `policy.attribute` or `policy__attribute` to set
+        any attribute of :class:`~pskc.policy.Policy`.
         """
         from pskc.key import Key
         key = Key(self)
diff --git a/pskc/encryption.py b/pskc/encryption.py
index d16c151..9d76b95 100644
--- a/pskc/encryption.py
+++ b/pskc/encryption.py
@@ -306,7 +306,7 @@ class Encryption:
       fields: a list of Key fields that will be encrypted on writing
 
     The key can either be assigned to the key property or derived using the
-    derive_key() method.
+    `derive_key()` method.
     """
 
     def __init__(self, pskc: PSKC) -> None:
@@ -358,7 +358,14 @@ class Encryption:
         return False
 
     def derive_key(self, password: str | bytes | bytearray) -> None:
-        """Derive a key from the password."""
+        """Derive a key from the password.
+
+        The supplied password, together with the information embedded in the 
PSKC
+        file (generally algorithm, salt, etc.) is used to create a decryption 
key.
+
+        This function may raise a :exc:`~pskc.exceptions.KeyDerivationError`
+        exception if key derivation fails.
+        """
         self.key = self.derivation.derive(password)
 
     def _setup_encryption(
@@ -403,17 +410,27 @@ class Encryption:
     ) -> None:
         """Configure pre-shared key encryption when writing the file.
 
-        The following arguments may be supplied:
-          key: the encryption key to use
-          id: encryption key identifier
-          algorithm: encryption algorithm
-          key_length: encryption key length in bytes
-          key_name: a name for the key
-          key_names: a number of names for the key
-          fields: a list of fields to encrypt
-
-        None of the arguments are required, reasonable defaults will be
-        chosen for missing arguments.
+        :param key: the encryption key to use
+        :param id: encryption key identifier
+        :param algorithm: encryption algorithm
+        :param key_length: encryption key length in bytes
+        :param key_name: a name for the key
+        :param key_names: a number of names for the key
+        :param fields: a list of fields to encrypt
+
+        This is a utility function to easily set up encryption. Encryption can
+        also be set up by manually by setting the correct
+        :class:`~pskc.encryption.Encryption` properties.
+
+        This method will generate a key if required and set the passed values.
+        By default AES128-CBC encryption will be configured and unless a key is
+        specified one of the correct length will be generated. If the algorithm
+        does not provide integrity checks (e.g. CBC-mode algorithms) integrity
+        checking in the PSKC file will be set up using
+        :func:`~pskc.mac.MAC.setup()`.
+
+        By default only the :attr:`~pskc.key.Key.secret` property will be
+        encrypted when writing the file.
         """
         self._setup_encryption(
             id=id,
@@ -444,21 +461,23 @@ class Encryption:
     ) -> None:
         """Configure password-based PSKC encryption when writing the file.
 
-        The following arguments may be supplied:
-          password: the password to use (required)
-          id: encryption key identifier
-          algorithm: encryption algorithm
-          key_name: a name for the key
-          key_names: a number of names for the key
-          key_length: encryption key length in bytes
-          fields: a list of fields to encrypt
-          salt: PBKDF2 salt
-          salt_length: used when generating random salt
-          iterations: number of PBKDF2 iterations
-          prf: PBKDF2 pseudorandom function
-
-        Only password is required, for the other arguments reasonable
-        defaults will be chosen.
+        :param password: the password to use (required)
+        :param id: encryption key identifier
+        :param algorithm: encryption algorithm
+        :param key_length: encryption key length in bytes
+        :param key_name: a name for the key
+        :param key_names: a number of names for the key
+        :param fields: a list of fields to encrypt
+        :param salt: PBKDF2 salt
+        :param salt_length: used when generating random salt
+        :param iterations: number of PBKDF2 iterations
+        :param prf: PBKDF2 pseudorandom function
+
+        Defaults for the above parameters are similar to those for
+        :func:`setup_preshared_key()` but the password parameter is required.
+
+        By default 12000 iterations will be used and a random salt with the
+        length of the to-be-generated encryption key will be used.
         """
         self._setup_encryption(
             id=id,
@@ -494,7 +513,12 @@ class Encryption:
         return cipher_value
 
     def remove_encryption(self) -> None:
-        """Decrypt all values and remove the encryption from the PSKC file."""
+        """Decrypt all values and remove the encryption from the PSKC file.
+
+        This can be used to read and encrypted PSKC file, decrypt the file,
+        remove the encryption and output an unencrypted PSKC file or to replace
+        the encryption algorithm.
+        """
         # decrypt all values and store decrypted values
         for key in self.pskc.keys:
             key.secret = key.secret
diff --git a/pskc/exceptions.py b/pskc/exceptions.py
index 5cdbae0..39338e3 100644
--- a/pskc/exceptions.py
+++ b/pskc/exceptions.py
@@ -24,7 +24,11 @@ from __future__ import annotations
 
 
 class PSKCError(Exception):
-    """General top-level exception."""
+    """General top-level exception.
+
+    The base class for all exceptions that the module will raise. In some
+    cases third-party code may raise additional exceptions.
+    """
 
     pass
 
@@ -32,15 +36,21 @@ class PSKCError(Exception):
 class ParseError(PSKCError):
     """Something went wrong with parsing the PSKC file.
 
-    Either the file is invalid XML or required elements or attributes are
-    missing.
+    Raised when the PSKC file cannot be correctly read due to invalid XML or
+    some required element or attribute is missing. This exception should only
+    be raised when parsing the file (i.e. when the :class:`~pskc.PSKC` class is
+    instantiated).
     """
 
     pass
 
 
 class EncryptionError(PSKCError):
-    """There was a problem encrypting the value."""
+    """There was a problem encrypting the value.
+
+    Raised when encrypting a value is not possible due to key length issues,
+    missing or wrong length plain text, or other issues.
+    """
 
     pass
 
@@ -48,14 +58,25 @@ class EncryptionError(PSKCError):
 class DecryptionError(PSKCError):
     """There was a problem decrypting the value.
 
-    The encrypted value as available but something went wrong with decrypting
-    it.
+    Raised when decrypting a value fails due to missing or incorrect key,
+    unsupported decryption or MAC algorithm, failed message authentication
+    check or other error.
+
+    This exception is generally raised when accessing encrypted information
+    (i.e. the :attr:`~pskc.key.Key.secret`, :attr:`~pskc.key.Key.counter`,
+    :attr:`~pskc.key.Key.time_offset`, :attr:`~pskc.key.Key.time_interval` or
+    :attr:`~pskc.key.Key.time_drift` attributes of the :class:`~pskc.key.Key`
+    class).
     """
 
     pass
 
 
 class KeyDerivationError(PSKCError):
-    """There was a problem performing the key derivation."""
+    """There was a problem performing the key derivation.
+
+    Raised when key derivation fails due to an unsupported algorithm or
+    missing information in the PSKC file.
+    """
 
     pass
diff --git a/pskc/key.py b/pskc/key.py
index 8653536..0c51aab 100644
--- a/pskc/key.py
+++ b/pskc/key.py
@@ -205,7 +205,12 @@ class Key:
     crypto_module = DeviceProperty('crypto_module')
 
     def check(self) -> bool:
-        """Check if all MACs in the message are valid."""
+        """Check if all MACs in the message are valid.
+
+        This will return `None` if there is no MAC to be checked. It will 
return
+        True if all the MACs match. If any MAC fails a
+        `DecryptionError` exception is raised.
+        """
         if all(x is not False for x in (
                 self.secret, self.counter, self.time_offset,
                 self.time_interval, self.time_drift)):
diff --git a/pskc/mac.py b/pskc/mac.py
index 7fe5d68..6f81dae 100644
--- a/pskc/mac.py
+++ b/pskc/mac.py
@@ -127,12 +127,12 @@ class MAC:
     def setup(self, key: bytes | None = None, algorithm: str | None = None) -> 
None:
         """Configure an encrypted MAC key.
 
-        The following arguments may be supplied:
-          key: the MAC key to use
-          algorithm: MAC algorithm
+        None of the arguments are required. By default HMAC-SHA1 will be used
+        as a MAC algorithm. If no key is configured a random key will be
+        generated with the length of the output of the configured hash.
 
-        None of the arguments are required, reasonable defaults will be
-        chosen for missing arguments.
+        This function will automatically be called when the configured
+        encryption algorithm requires a message authentication code.
         """
         if key:
             self.key = key
diff --git a/pskc/policy.py b/pskc/policy.py
index 46ddf46..2508e33 100644
--- a/pskc/policy.py
+++ b/pskc/policy.py
@@ -139,7 +139,11 @@ class Policy:
         self.pin_max_failed_attempts = value
 
     def may_use(self, usage: str | None = None, now: datetime.datetime | None 
= None) -> bool:
-        """Check whether the key may be used for the provided purpose."""
+        """Check whether the key may be used for the provided purpose.
+
+        The key :attr:`start_date` and :attr:`expiry_date` are also checked. 
The `now`
+        argument can be used to specify another point in time to check against.
+        """
         import datetime
         import dateutil.tz
         if self.unknown_policy_elements:
diff --git a/pskc/signature.py b/pskc/signature.py
index fbe0029..80ed766 100644
--- a/pskc/signature.py
+++ b/pskc/signature.py
@@ -66,14 +66,31 @@ def sign_x509(
 def verify_x509(tree: _Element, certificate: str | None = None, ca_pem_file: 
str | None = None) -> _Element:
     """Verify signature in PSKC data against a trusted X.509 certificate.
 
-    If a certificate is supplied it is used to validate the signature,
-    otherwise any embedded certificate is used and validated against a
-    certificate in ca_pem_file if it specified and otherwise the operating
-    system CA certificates.
+    :param certificate: a PEM encoded certificate that is used for verification
+    :param ca_pem_file: the name of a file that contains a CA certificate
+
+    The signature can be verified in three ways:
+
+    * The signature has an embedded certificate that is signed by a CA that is
+      configured in the system CA store. In this case neither `certificate` or
+      `ca_pem_file` need to be specified
+    * The signature was made  and a certificate was transmitted out-of-band.
+      In this case the `certificate` argument needs to be present.
+    * The signature has a certificate that is signed by a specific CA who's
+      certificate was transmitted out-of-band. In this case the `ca_pem_file`
+      is used to point to a CA certificate file (but a certificate needs to be
+      embedded inside the PSKC file).
+
+    This function will raise an exception when the validation fails.
+
+    After calling this function a verified version of the PSKC file will
+    be present in the :attr:`signed_pskc` attribute.
     """
     from signxml import XMLVerifier  # type: ignore[attr-defined]
-    return XMLVerifier().verify(
-        tree, x509_cert=certificate, ca_pem_file=ca_pem_file).signed_xml  # 
type: ignore[union-attr,return-value]
+    return XMLVerifier().verify(  # type: ignore[union-attr,return-value]
+        tree, x509_cert=certificate,
+        ca_pem_file=ca_pem_file,
+    ).signed_xml
 
 
 class Signature:
@@ -133,11 +150,26 @@ class Signature:
         return self._signed_pskc
 
     def verify(self, certificate: str | None = None, ca_pem_file: str | None = 
None) -> bool:
-        """Check that the signature was made with the specified certificate.
-
-        If no certificate is provided the signature is expected to contain a
-        signature that is signed by the CA certificate (or the CA standard CA
-        certificates when ca_pem_file is absent).
+        """Verify signature in PSKC data against a trusted X.509 certificate.
+
+        The signature can be verified in three ways:
+
+        * The signature has an embedded certificate that is signed by a CA 
that is
+          configured in the system CA store. In this case neither 
`certificate` or
+          `ca_pem_file` need to be specified
+        * The signature was made  and a certificate was transmitted 
out-of-band.
+          In this case the `certificate` argument needs to be present.
+        * The signature has a certificate that is signed by a specific CA who's
+          certificate was transmitted out-of-band. In this case the 
`ca_pem_file`
+          is used to point to a CA certificate file (but a certificate needs 
to be
+          embedded inside the PSKC file).
+
+        This function will raise an exception when the validation fails. The 
`certificate`
+        is expected to be passed as a PEM encoded string. The `ca_pem_file` 
should point to a CA
+        certificate store (PEM encoded file).
+
+        After calling this function a verified version of the PSKC file will
+        be present in the :attr:`signed_pskc` attribute.
         """
         from pskc import PSKC
         from pskc.parser import PSKCParser
@@ -149,7 +181,14 @@ class Signature:
         return True
 
     def sign(self, key: bytes, certificate: str | None = None) -> None:
-        """Add an XML signature to the file."""
+        """Add an XML signature to the file.
+
+        Set up a key and optionally a certificate that will be used to create 
an
+        embedded XML signature when writing the file.
+
+        This is a utility function that is used to configure the properties
+        needed to create a signed PSKC file.
+        """
         self.key = key
         self.certificate = certificate
 
diff --git a/tox.ini b/tox.ini
index 65c4764..a85365c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -53,5 +53,6 @@ deps = codespell
 commands = codespell {posargs}
 
 [testenv:docs]
+use_develop = true
 deps = Sphinx
 commands = sphinx-build -N -b html docs {envtmpdir}/sphinx -W

https://arthurdejong.org/git/python-pskc/commit/?id=02c35e70d8470b25924c1e6e582ff1bce2a7f546

commit 02c35e70d8470b25924c1e6e582ff1bce2a7f546
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Wed Dec 24 00:46:42 2025 +0100

    Introduce type hints
    
    This ensures that the module includes type hints for everything and also
    runs mypy from tox.
    
    This minimises the number of functional changes but a few functions go
    from using `kwargs` to explicity named argumnets.

diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 98578a2..2c355d5 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -51,7 +51,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        tox_job: [docs, flake8, codespell]
+        tox_job: [docs, flake8, mypy, codespell]
     steps:
       - uses: actions/checkout@v3
       - name: Set up Python
diff --git a/pskc/__init__.py b/pskc/__init__.py
index aa0a9d3..46e772d 100644
--- a/pskc/__init__.py
+++ b/pskc/__init__.py
@@ -1,7 +1,7 @@
 # __init__.py - main module
 # coding: utf-8
 #
-# Copyright (C) 2014-2024 Arthur de Jong
+# Copyright (C) 2014-2025 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -49,15 +49,25 @@ The following generates an encrypted PSKC file:
 The module should be able to handle most common PSKC files.
 """
 
+from __future__ import annotations
+
 
 __all__ = ['PSKC', '__version__']
 
+from typing import IO, Sequence, TYPE_CHECKING
+
+if TYPE_CHECKING:  # pragma: no cover (only for mypy)
+    import datetime
+    from os import PathLike
+
+    from pskc.device import Device
+    from pskc.key import Key
 
 # the version number of the library
 __version__ = '1.3'
 
 
-class PSKC(object):
+class PSKC:
     """Wrapper module for parsing a PSKC file.
 
     Instances of this class provide the following attributes:
@@ -70,16 +80,20 @@ class PSKC(object):
       keys: list of keys (Key instances)
     """
 
-    def __init__(self, filename=None):
+    def __init__(
+        self,
+        filename: str | bytes | PathLike[str] | PathLike[bytes] | IO[str] | 
IO[bytes] | None = None,
+    ) -> None:
         from pskc.encryption import Encryption
         from pskc.signature import Signature
+        from pskc.device import Device
         from pskc.mac import MAC
-        self.version = None
-        self.id = None
+        self.version: str | None = None
+        self.id: str | None = None
         self.encryption = Encryption(self)
         self.signature = Signature(self)
         self.mac = MAC(self)
-        self.devices = []
+        self.devices: list[Device] = []
         if filename is not None:
             from pskc.parser import PSKCParser
             PSKCParser.parse_file(self, filename)
@@ -87,11 +101,11 @@ class PSKC(object):
             self.version = '1.0'
 
     @property
-    def keys(self):
+    def keys(self) -> Sequence[Key]:
         """Provide a list of keys."""
         return tuple(key for device in self.devices for key in device.keys)
 
-    def add_device(self, **kwargs):
+    def add_device(self, **kwargs: str | int | datetime.datetime | None) -> 
Device:
         """Create a new device instance for the PSKC file.
 
         The device is initialised with properties from the provided keyword
@@ -103,7 +117,7 @@ class PSKC(object):
         update_attributes(device, **kwargs)
         return device
 
-    def add_key(self, **kwargs):
+    def add_key(self, **kwargs: str | bytes | int | datetime.datetime | None) 
-> Key:
         """Create a new key instance for the PSKC file.
 
         The new key is initialised with properties from the provided keyword
@@ -115,11 +129,11 @@ class PSKC(object):
         update_attributes(key, **kwargs)
         return key
 
-    def write(self, filename):
+    def write(self, filename: str | bytes | PathLike[str] | PathLike[bytes] | 
IO[str] | IO[bytes]) -> None:
         """Write the PSKC file to the provided file."""
         from pskc.serialiser import PSKCSerialiser
         if hasattr(filename, 'write'):
-            PSKCSerialiser.serialise_file(self, filename)
+            PSKCSerialiser.serialise_file(self, filename)  # type: ignore 
[arg-type]
         else:
             with open(filename, 'wb') as output:
                 self.write(output)
diff --git a/pskc/algorithms.py b/pskc/algorithms.py
index 9947520..26091ae 100644
--- a/pskc/algorithms.py
+++ b/pskc/algorithms.py
@@ -1,7 +1,7 @@
 # algorithms.py - module for handling algorithm URIs
 # coding: utf-8
 #
-# Copyright (C) 2016-2017 Arthur de Jong
+# Copyright (C) 2016-2025 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -20,6 +20,8 @@
 
 """Utility module that handles algorithm URIs."""
 
+from __future__ import annotations
+
 
 # canonical URIs of known algorithms
 # Note that even if a URI is listed here it does not mean that
@@ -89,7 +91,7 @@ _algorithm_aliases = {
 }
 
 
-def normalise_algorithm(algorithm):
+def normalise_algorithm(algorithm: str | None) -> str | None:
     """Return the canonical URI for the provided algorithm."""
     if not algorithm or algorithm.lower() == 'none':
         return None
diff --git a/pskc/crypto/__init__.py b/pskc/crypto/__init__.py
index ac96d67..cf0ea44 100644
--- a/pskc/crypto/__init__.py
+++ b/pskc/crypto/__init__.py
@@ -1,7 +1,7 @@
 # __init__.py - general crypto utility functions
 # coding: utf-8
 #
-# Copyright (C) 2017 Arthur de Jong
+# Copyright (C) 2017-2025 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -19,3 +19,5 @@
 # 02110-1301 USA
 
 """Implement crypto utility functions."""
+
+from __future__ import annotations
diff --git a/pskc/crypto/aeskw.py b/pskc/crypto/aeskw.py
index fbe070f..20545fa 100644
--- a/pskc/crypto/aeskw.py
+++ b/pskc/crypto/aeskw.py
@@ -1,7 +1,7 @@
 # aeskw.py - implementation of AES key wrapping
 # coding: utf-8
 #
-# Copyright (C) 2014-2024 Arthur de Jong
+# Copyright (C) 2014-2025 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -20,21 +20,29 @@
 
 """Implement key wrapping as described in RFC 3394 and RFC 5649."""
 
+from __future__ import annotations
+
 import binascii
 import struct
+from typing import TYPE_CHECKING
 
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
 
 from pskc.exceptions import DecryptionError, EncryptionError
 
+if TYPE_CHECKING:  # pragma: no cover (only for mypy)
+    from typing import Type
+
+    from cryptography.hazmat.primitives.ciphers import BlockCipherAlgorithm
+
 
-def _strxor(a, b):
+def _strxor(a: bytes, b: bytes) -> bytes:
     """Return a XOR b."""
     return bytes(x ^ y for (x, y) in zip(a, b))
 
 
-def _split(value):
+def _split(value: bytes) -> tuple[bytes, bytes]:
     return value[:8], value[8:]
 
 
@@ -42,7 +50,13 @@ RFC3394_IV = binascii.a2b_hex('a6a6a6a6a6a6a6a6')
 RFC5649_IV = binascii.a2b_hex('a65959a6')
 
 
-def wrap(plaintext, key, iv=None, pad=None, algorithm=algorithms.AES):
+def wrap(
+    plaintext: bytes,
+    key: bytes,
+    iv: bytes | None = None,
+    pad: bool | None = None,
+    algorithm: Type[BlockCipherAlgorithm] = algorithms.AES,
+) -> bytes:
     """Apply the AES key wrap algorithm to the plaintext.
 
     The iv can specify an initial value, otherwise the value from RFC 3394 or
@@ -69,7 +83,7 @@ def wrap(plaintext, key, iv=None, pad=None, 
algorithm=algorithms.AES):
         else:
             iv = RFC3394_IV
 
-    cipher = Cipher(algorithm(key), modes.ECB(), default_backend())
+    cipher = Cipher(algorithm(key), modes.ECB(), default_backend())  # type: 
ignore[call-arg]
     encryptor = cipher.encryptor()
     n = len(plaintext) // 8
 
@@ -87,7 +101,13 @@ def wrap(plaintext, key, iv=None, pad=None, 
algorithm=algorithms.AES):
     return A + b''.join(R)
 
 
-def unwrap(ciphertext, key, iv=None, pad=None, algorithm=algorithms.AES):
+def unwrap(
+    ciphertext: bytes,
+    key: bytes,
+    iv: bytes | None = None,
+    pad: bool | None = None,
+    algorithm: Type[BlockCipherAlgorithm] = algorithms.AES,
+) -> bytes:
     """Apply the AES key unwrap algorithm to the ciphertext.
 
     The iv can specify an initial value, otherwise the value from RFC 3394 or
@@ -102,7 +122,7 @@ def unwrap(ciphertext, key, iv=None, pad=None, 
algorithm=algorithms.AES):
     if len(ciphertext) % 8 != 0 or (pad is False and len(ciphertext) < 24):
         raise DecryptionError('Ciphertext length wrong')
 
-    cipher = Cipher(algorithm(key), modes.ECB(), default_backend())
+    cipher = Cipher(algorithm(key), modes.ECB(), default_backend())  # type: 
ignore[call-arg]
     decryptor = cipher.decryptor()
     n = len(ciphertext) // 8 - 1
 
diff --git a/pskc/crypto/tripledeskw.py b/pskc/crypto/tripledeskw.py
index 0314fc9..391ffed 100644
--- a/pskc/crypto/tripledeskw.py
+++ b/pskc/crypto/tripledeskw.py
@@ -1,7 +1,7 @@
 # tripledeskw.py - implementation of Triple DES key wrapping
 # coding: utf-8
 #
-# Copyright (C) 2014-2017 Arthur de Jong
+# Copyright (C) 2014-2025 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -20,6 +20,8 @@
 
 """Implement Triple DES key wrapping as described in RFC 3217."""
 
+from __future__ import annotations
+
 import binascii
 import hashlib
 import os
@@ -30,7 +32,7 @@ from cryptography.hazmat.primitives.ciphers import Cipher, 
algorithms, modes
 from pskc.exceptions import DecryptionError, EncryptionError
 
 
-def _cms_hash(value):
+def _cms_hash(value: bytes) -> bytes:
     """Return the key hash algorithm described in RFC 3217 section 2."""
     return hashlib.sha1(value).digest()[:8]
 
@@ -38,7 +40,7 @@ def _cms_hash(value):
 RFC3217_IV = binascii.a2b_hex('4adda22c79e82105')
 
 
-def wrap(plaintext, key, iv=None):
+def wrap(plaintext: bytes, key: bytes, iv: bytes | None = None) -> bytes:
     """Wrap one key (typically a Triple DES key) with another Triple DES key.
 
     This uses the algorithm from RFC 3217 to encrypt the plaintext (the key
@@ -60,7 +62,7 @@ def wrap(plaintext, key, iv=None):
     return encryptor.update(tmp[::-1]) + encryptor.finalize()
 
 
-def unwrap(ciphertext, key):
+def unwrap(ciphertext: bytes, key: bytes) -> bytes:
     """Unwrap a key (typically Triple DES key ) with another Triple DES key.
 
     This uses the algorithm from RFC 3217 to decrypt the ciphertext (the
diff --git a/pskc/device.py b/pskc/device.py
index fc29413..995453e 100644
--- a/pskc/device.py
+++ b/pskc/device.py
@@ -1,7 +1,7 @@
 # device.py - module for handling device info from pskc files
 # coding: utf-8
 #
-# Copyright (C) 2016-2018 Arthur de Jong
+# Copyright (C) 2016-2025 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -20,19 +20,28 @@
 
 """Module that handles device information stored in PSKC files."""
 
+from __future__ import annotations
 
-def update_attributes(obj, **kwargs):
+import datetime
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:  # pragma: no cover (only for mypy)
+    from pskc import PSKC
+    from pskc.key import Key
+
+
+def update_attributes(obj: Key | Device, **kwargs: Any) -> None:
     """Update object with provided properties."""
     for k, v in kwargs.items():
-        k = k.split('.') if '.' in k else k.split('__')
+        parts = k.split('.') if '.' in k else k.split('__')
         o = obj
-        for name in k[:-1]:
+        for name in parts[:-1]:
             o = getattr(o, name)
-        getattr(o, k[-1])  # raise exception for non-existing properties
-        setattr(o, k[-1], v)
+        getattr(o, parts[-1])  # raise exception for non-existing properties
+        setattr(o, parts[-1], v)
 
 
-class Device(object):
+class Device:
     """Representation of a single key from a PSKC file.
 
     Instances of this class provide the following properties:
@@ -48,23 +57,23 @@ class Device(object):
       crypto_module: id of module to which keys are provisioned within device
     """
 
-    def __init__(self, pskc):
+    def __init__(self, pskc: PSKC) -> None:
 
         self.pskc = pskc
 
-        self.manufacturer = None
-        self.serial = None
-        self.model = None
-        self.issue_no = None
-        self.device_binding = None
-        self.start_date = None
-        self.expiry_date = None
-        self.device_userid = None
-        self.crypto_module = None
+        self.manufacturer: str | None = None
+        self.serial: str | None = None
+        self.model: str | None = None
+        self.issue_no: str | int | None = None
+        self.device_binding: str | None = None
+        self.start_date: datetime.datetime | None = None
+        self.expiry_date: datetime.datetime | None = None
+        self.device_userid: str | None = None
+        self.crypto_module: str | None = None
 
-        self.keys = []
+        self.keys: list[Key] = []
 
-    def add_key(self, **kwargs):
+    def add_key(self, **kwargs: str | bytes | int | datetime.datetime | None) 
-> Key:
         """Create a new key instance for the device.
 
         The new key is initialised with properties from the provided keyword
diff --git a/pskc/encryption.py b/pskc/encryption.py
index bdb3537..d16c151 100644
--- a/pskc/encryption.py
+++ b/pskc/encryption.py
@@ -26,12 +26,20 @@ algorithms and decryption.
 The encryption key can be derived using the KeyDerivation class.
 """
 
+from __future__ import annotations
 
 import os
 import re
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Type
 
+if TYPE_CHECKING:  # pragma: no cover (only for mypy)
+    from cryptography.hazmat.primitives.ciphers import BlockCipherAlgorithm
 
-def algorithm_key_lengths(algorithm):
+    from pskc import PSKC
+
+
+def algorithm_key_lengths(algorithm: str | None) -> Sequence[int]:
     """Return the possible key lengths for the configured algorithm."""
     from pskc.exceptions import DecryptionError
     if algorithm is None:
@@ -59,19 +67,24 @@ def algorithm_key_lengths(algorithm):
         raise DecryptionError('Unsupported algorithm: %r' % algorithm)
 
 
-def _decrypt_cbc(algorithm, key, ciphertext, iv=None):
+def _decrypt_cbc(
+    algorithm: Type[BlockCipherAlgorithm],
+    key: bytes,
+    ciphertext: bytes,
+    iv: bytes | None = None,
+) -> bytes:
     """Decrypt the ciphertext and return the plaintext value."""
     from cryptography.hazmat.backends import default_backend
     from cryptography.hazmat.primitives import padding
     from cryptography.hazmat.primitives.ciphers import Cipher, modes
     from pskc.exceptions import DecryptionError
     if not iv:
-        iv = ciphertext[:algorithm.block_size // 8]
-        ciphertext = ciphertext[algorithm.block_size // 8:]
+        iv = ciphertext[:algorithm.block_size // 8]  # type: ignore[operator]
+        ciphertext = ciphertext[algorithm.block_size // 8:]  # type: 
ignore[operator]
     cipher = Cipher(
-        algorithm(key), modes.CBC(iv), backend=default_backend())
+        algorithm(key), modes.CBC(iv), backend=default_backend())  # type: 
ignore[call-arg]
     decryptor = cipher.decryptor()
-    unpadder = padding.PKCS7(algorithm.block_size).unpadder()
+    unpadder = padding.PKCS7(algorithm.block_size).unpadder()  # type: 
ignore[arg-type]
     try:
         return unpadder.update(
             decryptor.update(ciphertext) +
@@ -80,7 +93,7 @@ def _decrypt_cbc(algorithm, key, ciphertext, iv=None):
         raise DecryptionError('Invalid padding')
 
 
-def decrypt(algorithm, key, ciphertext, iv=None):
+def decrypt(algorithm: str | None, key: bytes | None, ciphertext: bytes, iv: 
bytes | None = None) -> bytes:
     """Decrypt the ciphertext and return the plaintext value."""
     from cryptography.hazmat.primitives.ciphers import algorithms
     from pskc.exceptions import DecryptionError
@@ -99,11 +112,11 @@ def decrypt(algorithm, key, ciphertext, iv=None):
     elif algorithm.endswith('#kw-aes128') or \
             algorithm.endswith('#kw-aes192') or \
             algorithm.endswith('#kw-aes256'):
-        from pskc.crypto.aeskw import unwrap
-        return unwrap(ciphertext, key)
+        from pskc.crypto.aeskw import unwrap as easkw_unwrap
+        return easkw_unwrap(ciphertext, key)
     elif algorithm.endswith('#kw-tripledes'):
-        from pskc.crypto.tripledeskw import unwrap
-        return unwrap(ciphertext, key)
+        from pskc.crypto.tripledeskw import unwrap as tripledeskw_unwrap
+        return tripledeskw_unwrap(ciphertext, key)
     elif (algorithm.endswith('#camellia128-cbc') or
             algorithm.endswith('#camellia192-cbc') or
             algorithm.endswith('#camellia256-cbc')):
@@ -111,28 +124,29 @@ def decrypt(algorithm, key, ciphertext, iv=None):
     elif (algorithm.endswith('#kw-camellia128') or  # pragma: no branch
             algorithm.endswith('#kw-camellia192') or
             algorithm.endswith('#kw-camellia256')):
-        from pskc.crypto.aeskw import unwrap
-        return unwrap(ciphertext, key, algorithm=algorithms.Camellia)
+        from pskc.crypto.aeskw import unwrap as easkw_unwrap
+        return easkw_unwrap(ciphertext, key, algorithm=algorithms.Camellia)
     # no fallthrough because algorithm_key_lengths() fails with unknown algo
+    assert False  # pragma: no cover (only for mypy)  # noqa: B011
 
 
-def _encrypt_cbc(algorithm, key, plaintext, iv=None):
+def _encrypt_cbc(algorithm: Type[BlockCipherAlgorithm], key: bytes, plaintext: 
bytes, iv: bytes | None = None) -> bytes:
     """Encrypt the provided value with the key using the algorithm."""
     from cryptography.hazmat.backends import default_backend
     from cryptography.hazmat.primitives import padding
     from cryptography.hazmat.primitives.ciphers import Cipher, modes
-    iv = iv or os.urandom(algorithm.block_size // 8)
+    iv = iv or os.urandom(algorithm.block_size // 8)  # type: ignore[operator]
     cipher = Cipher(
-        algorithm(key), modes.CBC(iv), backend=default_backend())
+        algorithm(key), modes.CBC(iv), backend=default_backend())  # type: 
ignore[call-arg]
     encryptor = cipher.encryptor()
-    padder = padding.PKCS7(algorithm.block_size).padder()
+    padder = padding.PKCS7(algorithm.block_size).padder()  # type: 
ignore[arg-type]
     return (
         iv + encryptor.update(
             padder.update(plaintext) + padder.finalize()) +
         encryptor.finalize())
 
 
-def encrypt(algorithm, key, plaintext, iv=None):
+def encrypt(algorithm: str | None, key: bytes | None, plaintext: bytes, iv: 
bytes | None = None) -> bytes:
     """Encrypt the provided value with the key using the algorithm."""
     from cryptography.hazmat.primitives.ciphers import algorithms
     from pskc.exceptions import EncryptionError
@@ -151,11 +165,11 @@ def encrypt(algorithm, key, plaintext, iv=None):
     elif algorithm.endswith('#kw-aes128') or \
             algorithm.endswith('#kw-aes192') or \
             algorithm.endswith('#kw-aes256'):
-        from pskc.crypto.aeskw import wrap
-        return wrap(plaintext, key)
+        from pskc.crypto.aeskw import wrap as aeskw_wrap
+        return aeskw_wrap(plaintext, key)
     elif algorithm.endswith('#kw-tripledes'):
-        from pskc.crypto.tripledeskw import wrap
-        return wrap(plaintext, key)
+        from pskc.crypto.tripledeskw import wrap as tripledeskw_wrap
+        return tripledeskw_wrap(plaintext, key)
     elif (algorithm.endswith('#camellia128-cbc') or
             algorithm.endswith('#camellia192-cbc') or
             algorithm.endswith('#camellia256-cbc')):
@@ -163,12 +177,13 @@ def encrypt(algorithm, key, plaintext, iv=None):
     elif (algorithm.endswith('#kw-camellia128') or  # pragma: no branch
             algorithm.endswith('#kw-camellia192') or
             algorithm.endswith('#kw-camellia256')):
-        from pskc.crypto.aeskw import wrap
-        return wrap(plaintext, key, algorithm=algorithms.Camellia)
+        from pskc.crypto.aeskw import wrap as aeskw_wrap
+        return aeskw_wrap(plaintext, key, algorithm=algorithms.Camellia)
     # no fallthrough because algorithm_key_lengths() fails with unknown algo
+    assert False  # pragma: no cover (only for mypy)  # noqa: B011
 
 
-class KeyDerivation(object):
+class KeyDerivation:
     """Handle key derivation.
 
     The algorithm property contains the key derivation algorithm to use. For
@@ -180,37 +195,39 @@ class KeyDerivation(object):
       pbkdf2_prf: name of pseudorandom function used
     """
 
-    def __init__(self):
-        self._algorithm = None
+    def __init__(self) -> None:
+        self._algorithm: str | None = None
         # PBKDF2 properties
-        self.pbkdf2_salt = None
-        self.pbkdf2_iterations = None
-        self.pbkdf2_key_length = None
-        self._pbkdf2_prf = None
+        self.pbkdf2_salt: bytes | None = None
+        self.pbkdf2_iterations: int | None = None
+        self.pbkdf2_key_length: int | None = None
+        self._pbkdf2_prf: str | None = None
 
     @property
-    def algorithm(self):
+    def algorithm(self) -> str | None:
         """Provide the key derivation algorithm used."""
         if self._algorithm:
             return self._algorithm
+        return None
 
     @algorithm.setter
-    def algorithm(self, value):
+    def algorithm(self, value: str | None) -> None:
         from pskc.algorithms import normalise_algorithm
         self._algorithm = normalise_algorithm(value)
 
     @property
-    def pbkdf2_prf(self):
+    def pbkdf2_prf(self) -> str | None:
         """Provide the PBKDF2 pseudorandom function used."""
         if self._pbkdf2_prf:
             return self._pbkdf2_prf
+        return None
 
     @pbkdf2_prf.setter
-    def pbkdf2_prf(self, value):
+    def pbkdf2_prf(self, value: str | None) -> None:
         from pskc.algorithms import normalise_algorithm
         self._pbkdf2_prf = normalise_algorithm(value)
 
-    def derive_pbkdf2(self, password):
+    def derive_pbkdf2(self, password: str | bytes | bytearray) -> bytes:
         """Derive an encryption key from the provided password."""
         from hashlib import pbkdf2_hmac
         from pskc.exceptions import KeyDerivationError
@@ -228,16 +245,16 @@ class KeyDerivation(object):
             raise KeyDerivationError('Incomplete PBKDF2 configuration')
         # force conversion to bytestring
         if not isinstance(password, type(b'')):
-            password = password.encode()
+            password = password.encode()  # type: ignore[union-attr]
         try:
             return pbkdf2_hmac(
-                prf, password, self.pbkdf2_salt, self.pbkdf2_iterations,
+                prf, password, self.pbkdf2_salt, self.pbkdf2_iterations,  # 
type: ignore[arg-type]
                 self.pbkdf2_key_length)
         except ValueError:
             raise KeyDerivationError(
                 'Pseudorandom function unsupported: %r' % self.pbkdf2_prf)
 
-    def derive(self, password):
+    def derive(self, password: str | bytes | bytearray) -> bytes:
         """Derive a key from the password."""
         from pskc.exceptions import KeyDerivationError
         if self.algorithm is None:
@@ -248,8 +265,15 @@ class KeyDerivation(object):
             raise KeyDerivationError(
                 'Unsupported algorithm: %r' % self.algorithm)
 
-    def setup_pbkdf2(self, password, salt=None, salt_length=16,
-                     key_length=None, iterations=None, prf=None):
+    def setup_pbkdf2(
+        self,
+        password: str | bytes | bytearray,
+        salt: bytes | None = None,
+        salt_length: int = 16,
+        key_length: int | None = None,
+        iterations: int | None = None,
+        prf: str | None = None,
+    ) -> bytes:
         """Configure PBKDF2 key derivation properties."""
         self.algorithm = 'pbkdf2'
         if salt is None:
@@ -266,7 +290,7 @@ class KeyDerivation(object):
         return self.derive_pbkdf2(password)
 
 
-class Encryption(object):
+class Encryption:
     """Class for handling encryption keys that are used in the PSKC file.
 
     Encryption generally uses a symmetric key that is used to encrypt some
@@ -285,42 +309,44 @@ class Encryption(object):
     derive_key() method.
     """
 
-    def __init__(self, pskc):
+    def __init__(self, pskc: PSKC) -> None:
         self.pskc = pskc
-        self.id = None
-        self._algorithm = None
-        self.key_names = []
-        self.key = None
-        self.iv = None
+        self.id: str | None = None
+        self._algorithm: str | None = None
+        self.key_names: list[str] = []
+        self.key: bytes | None = None
+        self.iv: bytes | None = None
         self.derivation = KeyDerivation()
-        self.fields = []
+        self.fields: list[str] = []
 
     @property
-    def key_name(self):
+    def key_name(self) -> str | None:
         """Provide the name of the (first) key."""
         if self.key_names:
             return self.key_names[0]
+        return None
 
     @key_name.setter
-    def key_name(self, value):
+    def key_name(self, value: str | None) -> None:
         if value:
             self.key_names = [value]
         else:
             self.key_names = []
 
     @property
-    def algorithm(self):
+    def algorithm(self) -> str | None:
         """Provide the encryption algorithm used."""
         if self._algorithm:
             return self._algorithm
+        return None
 
     @algorithm.setter
-    def algorithm(self, value):
+    def algorithm(self, value: str | None) -> None:
         from pskc.algorithms import normalise_algorithm
         self._algorithm = normalise_algorithm(value)
 
     @property
-    def is_encrypted(self):
+    def is_encrypted(self) -> bool:
         """Test whether the PSKC file requires a decryption key."""
         from pskc.exceptions import DecryptionError
         try:
@@ -331,15 +357,29 @@ class Encryption(object):
             return True
         return False
 
-    def derive_key(self, password):
+    def derive_key(self, password: str | bytes | bytearray) -> None:
         """Derive a key from the password."""
         self.key = self.derivation.derive(password)
 
-    def _setup_encryption(self, kwargs):
-        for k in ('id', 'algorithm', 'key_name', 'key_names', 'fields'):
-            v = kwargs.pop(k, None)
-            if v is not None:
-                setattr(self, k, v)
+    def _setup_encryption(
+        self,
+        *,
+        id: str | None = None,
+        algorithm: str | None = None,
+        key_name: str | None = None,
+        key_names: list[str] | None = None,
+        fields: list[str] | None = None,
+    ) -> None:
+        if id is not None:
+            self.id = id
+        if algorithm is not None:
+            self.algorithm = algorithm
+        if key_name is not None:
+            self.key_name = key_name
+        if key_names is not None:
+            self.key_names = key_names
+        if fields is not None:
+            self.fields = fields
         # default encryption to AES128-CBC
         if not self.algorithm:
             self.algorithm = 'aes128-cbc'
@@ -350,7 +390,17 @@ class Encryption(object):
         if self.algorithm.endswith('-cbc'):
             self.pskc.mac.setup()
 
-    def setup_preshared_key(self, **kwargs):
+    def setup_preshared_key(
+        self,
+        *,
+        key: bytes | None = None,
+        id: str | None = None,
+        algorithm: str | None = None,
+        key_length: int | None = None,
+        key_name: str | None = None,
+        key_names: list[str] | None = None,
+        fields: list[str] | None = None,
+    ) -> None:
         """Configure pre-shared key encryption when writing the file.
 
         The following arguments may be supplied:
@@ -365,22 +415,42 @@ class Encryption(object):
         None of the arguments are required, reasonable defaults will be
         chosen for missing arguments.
         """
-        self._setup_encryption(kwargs)
-        self.key = kwargs.pop('key', self.key)
-        if not self.key:
-            self.key = os.urandom(
-                kwargs.pop('key_length', self.algorithm_key_lengths[-1]))
-
-    def setup_pbkdf2(self, password, **kwargs):
+        self._setup_encryption(
+            id=id,
+            algorithm=algorithm,
+            key_name=key_name,
+            key_names=key_names,
+            fields=fields,
+        )
+        if not key:
+            key_length = key_length or self.algorithm_key_lengths[-1]
+            key = os.urandom(key_length)
+        self.key = key
+
+    def setup_pbkdf2(
+        self,
+        password: str | bytes | bytearray,
+        *,
+        id: str | None = None,
+        algorithm: str | None = None,
+        key_name: str | None = None,
+        key_names: list[str] | None = None,
+        key_length: int | None = None,
+        fields: list[str] | None = None,
+        salt: bytes | None = None,
+        salt_length: int = 16,
+        iterations: int | None = None,
+        prf: str | None = None,
+    ) -> None:
         """Configure password-based PSKC encryption when writing the file.
 
         The following arguments may be supplied:
           password: the password to use (required)
           id: encryption key identifier
           algorithm: encryption algorithm
-          key_length: encryption key length in bytes
           key_name: a name for the key
           key_names: a number of names for the key
+          key_length: encryption key length in bytes
           fields: a list of fields to encrypt
           salt: PBKDF2 salt
           salt_length: used when generating random salt
@@ -390,29 +460,40 @@ class Encryption(object):
         Only password is required, for the other arguments reasonable
         defaults will be chosen.
         """
-        self._setup_encryption(kwargs)
-        # pass a key length to PBKDF2
-        kwargs.setdefault('key_length', self.algorithm_key_lengths[-1])
-        self.key = self.derivation.setup_pbkdf2(password, **kwargs)
+        self._setup_encryption(
+            id=id,
+            algorithm=algorithm,
+            key_name=key_name,
+            key_names=key_names,
+            fields=fields,
+        )
+        self.key = self.derivation.setup_pbkdf2(
+            password,
+            salt=salt,
+            salt_length=salt_length,
+            key_length=key_length or self.algorithm_key_lengths[-1],
+            iterations=iterations,
+            prf=prf,
+        )
 
     @property
-    def algorithm_key_lengths(self):
+    def algorithm_key_lengths(self) -> Sequence[int]:
         """Provide the possible key lengths for the configured algorithm."""
         return algorithm_key_lengths(self.algorithm)
 
-    def decrypt_value(self, cipher_value, algorithm=None):
+    def decrypt_value(self, cipher_value: bytes, algorithm: str | None = None) 
-> bytes:
         """Decrypt the cipher_value and return the plaintext value."""
         return decrypt(
             algorithm or self.algorithm, self.key, cipher_value, self.iv)
 
-    def encrypt_value(self, plaintext):
+    def encrypt_value(self, plaintext: bytes) -> bytes:
         """Encrypt the provided value and return the cipher_value."""
         cipher_value = encrypt(self.algorithm, self.key, plaintext, self.iv)
         if self.iv:
             cipher_value = cipher_value[len(self.iv):]
         return cipher_value
 
-    def remove_encryption(self):
+    def remove_encryption(self) -> None:
         """Decrypt all values and remove the encryption from the PSKC file."""
         # decrypt all values and store decrypted values
         for key in self.pskc.keys:
diff --git a/pskc/exceptions.py b/pskc/exceptions.py
index e5e4436..5cdbae0 100644
--- a/pskc/exceptions.py
+++ b/pskc/exceptions.py
@@ -1,7 +1,7 @@
 # exceptions.py - collection of pskc exceptions
 # coding: utf-8
 #
-# Copyright (C) 2014 Arthur de Jong
+# Copyright (C) 2014-2025 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -20,6 +20,8 @@
 
 """Collection of exceptions."""
 
+from __future__ import annotations
+
 
 class PSKCError(Exception):
     """General top-level exception."""
diff --git a/pskc/key.py b/pskc/key.py
index 499f273..8653536 100644
--- a/pskc/key.py
+++ b/pskc/key.py
@@ -1,7 +1,7 @@
 # key.py - module for handling keys from pskc files
 # coding: utf-8
 #
-# Copyright (C) 2014-2024 Arthur de Jong
+# Copyright (C) 2014-2025 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -20,38 +20,45 @@
 
 """Module that handles keys stored in PSKC files."""
 
+from __future__ import annotations
+
 
 import array
 import binascii
+from typing import Any, TYPE_CHECKING, cast
 
 from pskc.policy import Policy
 
+if TYPE_CHECKING:  # pragma: no cover (only for mypy)
+    from pskc import PSKC
+    from pskc.device import Device
+
 
-class EncryptedValue(object):
+class EncryptedValue:
     """A container for an encrypted value."""
 
-    def __init__(self, cipher_value, mac_value, algorithm):
+    def __init__(self, cipher_value: bytes, mac_value: bytes | None, 
algorithm: str | None) -> None:
         self.cipher_value = cipher_value
         self.mac_value = mac_value
         self.algorithm = algorithm
 
     @classmethod
-    def create(cls, pskc, value):
+    def create(cls, pskc: PSKC, value: bytes | bytearray | str) -> 
EncryptedValue:
         """Construct an encrypted value from a plaintext value."""
         # force conversion to bytestring
-        if not isinstance(value, (type(b''), bytearray)):
+        if not isinstance(value, (bytes, bytearray)):
             value = value.encode()
-        cipher_value = pskc.encryption.encrypt_value(value)
+        cipher_value = pskc.encryption.encrypt_value(cast(bytes, value))
         mac_value = None
         if pskc.mac.algorithm:
             mac_value = pskc.mac.generate_mac(cipher_value)
+        assert pskc.encryption.algorithm
         return cls(cipher_value, mac_value, pskc.encryption.algorithm)
 
-    def get_value(self, pskc):
+    def get_value(self, pskc: PSKC) -> bytes:
         """Provide the decrypted value."""
         from pskc.exceptions import DecryptionError
-        plaintext = pskc.encryption.decrypt_value(
-            self.cipher_value, self.algorithm)
+        plaintext = pskc.encryption.decrypt_value(self.cipher_value, 
self.algorithm)
         # allow MAC over plaintext or ciphertext
         # (RFC 6030 implies MAC over ciphertext but older draft used
         # MAC over plaintext)
@@ -66,14 +73,14 @@ class EncryptedIntegerValue(EncryptedValue):
     """Class representing an encrypted integer value."""
 
     @classmethod
-    def create(cls, pskc, value):
+    def create(cls, pskc: PSKC, value: int) -> EncryptedValue:  # type: 
ignore[override]
         """Construct an encrypted value from a plaintext value."""
-        value = '%x' % value
-        n = len(value)
-        value = binascii.unhexlify(value.zfill(n + (n & 1)))
-        return super(EncryptedIntegerValue, cls).create(pskc, value)
+        str_value = '%x' % value
+        n = len(str_value)
+        bytes_value = binascii.unhexlify(str_value.zfill(n + (n & 1)))
+        return super(EncryptedIntegerValue, cls).create(pskc, bytes_value)
 
-    def get_value(self, pskc):
+    def get_value(self, pskc: PSKC) -> int:  # type: ignore[override]
         """Provide the decrypted integer value."""
         value = super(EncryptedIntegerValue, self).get_value(pskc)
         # try to handle value as ASCII representation
@@ -86,38 +93,38 @@ class EncryptedIntegerValue(EncryptedValue):
         return result
 
 
-class DataTypeProperty(object):
+class DataTypeProperty:
     """A data descriptor that delegates actions to DataType instances."""
 
-    def __init__(self, name, doc):
+    def __init__(self, name: str, doc: str) -> None:
         self.name = name
         self.__doc__ = doc
 
-    def __get__(self, obj, objtype):
+    def __get__(self, obj: Key, objtype: type | None) -> Any:
         value = getattr(obj, '_' + self.name, None)
-        if hasattr(value, 'get_value'):
+        if isinstance(value, EncryptedValue):
             return value.get_value(obj.device.pskc)
         else:
             return value
 
-    def __set__(self, obj, val):
+    def __set__(self, obj: Key, val: Any) -> None:
         setattr(obj, '_' + self.name, val)
 
 
-class DeviceProperty(object):
+class DeviceProperty:
     """A data descriptor that delegates actions to the Device instance."""
 
-    def __init__(self, name):
+    def __init__(self, name: str) -> None:
         self.name = name
 
-    def __get__(self, obj, objtype):
+    def __get__(self, obj: Key, objtype: type | None) -> Any:
         return getattr(obj.device, self.name)
 
-    def __set__(self, obj, val):
+    def __set__(self, obj: Key, val: Any) -> None:
         setattr(obj.device, self.name, val)
 
 
-class Key(object):
+class Key:
     """Representation of a single key from a PSKC file.
 
     Instances of this class provide the following properties:
@@ -149,42 +156,42 @@ class Key(object):
     crypto_module properties of the Device class.
     """
 
-    def __init__(self, device):
+    def __init__(self, device: Device) -> None:
 
         self.device = device
 
-        self.id = None
-        self.algorithm = None
+        self.id: str | None = None
+        self.algorithm: str | None = None
 
-        self.issuer = None
-        self.key_profile = None
-        self.key_reference = None
-        self.friendly_name = None
-        self.key_userid = None
+        self.issuer: str | None = None
+        self.key_profile: str | None = None
+        self.key_reference: str | None = None
+        self.friendly_name: str | None = None
+        self.key_userid: str | None = None
 
-        self.algorithm_suite = None
+        self.algorithm_suite: str | None = None
 
-        self.challenge_encoding = None
-        self.challenge_min_length = None
-        self.challenge_max_length = None
-        self.challenge_check = None
+        self.challenge_encoding: str | None = None
+        self.challenge_min_length: int | None = None
+        self.challenge_max_length: int | None = None
+        self.challenge_check: bool | None = None
 
-        self.response_encoding = None
-        self.response_length = None
-        self.response_check = None
+        self.response_encoding: str | None = None
+        self.response_length: int | None = None
+        self.response_check: bool | None = None
 
         self.policy = Policy(self)
 
-    secret = DataTypeProperty(
+    secret: bytes | None = DataTypeProperty(  # type: ignore[assignment]
         'secret', 'The secret key itself.')
-    counter = DataTypeProperty(
+    counter: int | None = DataTypeProperty(  # type: ignore[assignment]
         'counter', 'An event counter for event-based OTP.')
-    time_offset = DataTypeProperty(
+    time_offset: int | None = DataTypeProperty(  # type: ignore[assignment]
         'time_offset',
         'A time offset for time-based OTP (number of intervals).')
-    time_interval = DataTypeProperty(
+    time_interval: int | None = DataTypeProperty(  # type: ignore[assignment]
         'time_interval', 'A time interval in seconds.')
-    time_drift = DataTypeProperty(
+    time_drift: int | None = DataTypeProperty(  # type: ignore[assignment]
         'time_drift', 'Device clock drift value (number of time intervals).')
 
     manufacturer = DeviceProperty('manufacturer')
@@ -197,14 +204,15 @@ class Key(object):
     device_userid = DeviceProperty('device_userid')
     crypto_module = DeviceProperty('crypto_module')
 
-    def check(self):
+    def check(self) -> bool:
         """Check if all MACs in the message are valid."""
         if all(x is not False for x in (
                 self.secret, self.counter, self.time_offset,
                 self.time_interval, self.time_drift)):
             return True
+        assert False  # pragma: no cover (only for mypy)  # noqa: B011
 
     @property
-    def userid(self):
+    def userid(self) -> str:
         """User identifier (either the key or device userid)."""
         return self.key_userid or self.device_userid
diff --git a/pskc/mac.py b/pskc/mac.py
index 38552dd..7fe5d68 100644
--- a/pskc/mac.py
+++ b/pskc/mac.py
@@ -1,7 +1,7 @@
 # mac.py - module for checking value signatures
 # coding: utf-8
 #
-# Copyright (C) 2014-2018 Arthur de Jong
+# Copyright (C) 2014-2025 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -28,17 +28,21 @@ The MAC key is generated specifically for each PSKC file 
and encrypted
 with the PSKC encryption key.
 """
 
+from __future__ import annotations
 
+import hashlib
 import os
 import re
 
+from pskc import PSKC
+from pskc.key import EncryptedValue
+
 
 _hmac_url_re = re.compile(r'^(.*#)?hmac-(?P<hash>[a-z0-9-]+)$')
 
 
-def _get_hash_obj(algorithm, *args):
+def _get_hash_obj(algorithm: str | None, *args: bytes) -> hashlib._Hash:
     """Return an instantiated hash object."""
-    import hashlib
     from pskc.algorithms import normalise_algorithm
     from pskc.exceptions import DecryptionError
     match = _hmac_url_re.search(normalise_algorithm(algorithm) or '')
@@ -50,15 +54,16 @@ def _get_hash_obj(algorithm, *args):
     raise DecryptionError('Unsupported MAC algorithm: %r' % algorithm)
 
 
-def mac(algorithm, key, value):
+def mac(algorithm: str | None, key: bytes | None, value: bytes) -> bytes:
     """Generate the MAC value over the specified value."""
     import hmac
+    assert key
     return hmac.new(
         key, value,
         lambda *args: _get_hash_obj(algorithm, *args)).digest()
 
 
-def mac_key_length(algorithm):
+def mac_key_length(algorithm: str | None) -> int:
     """Recommended minimal key length in bytes for the set algorithm."""
     # https://tools.ietf.org/html/rfc2104#section-3
     # an HMAC key should be at least as long as the hash output length
@@ -69,7 +74,7 @@ def mac_key_length(algorithm):
         return 16  # fallback value
 
 
-class MAC(object):
+class MAC:
     """Class describing the MAC algorithm to use and how to get the key.
 
     Instances of this class provide the following attributes:
@@ -78,15 +83,15 @@ class MAC(object):
       key: the binary value of the MAC key if it can be decrypted
     """
 
-    def __init__(self, pskc):
+    def __init__(self, pskc: PSKC) -> None:
         self.pskc = pskc
-        self._algorithm = None
+        self._algorithm: str | None = None
 
     @property
-    def key(self):
+    def key(self) -> bytes | None:
         """Provide access to the MAC key binary value if available."""
-        value = getattr(self, '_key', None)
-        if hasattr(value, 'get_value'):
+        value: bytes | EncryptedValue | None = getattr(self, '_key', None)
+        if isinstance(value, EncryptedValue):
             return value.get_value(self.pskc)
         elif value:
             return value
@@ -95,30 +100,31 @@ class MAC(object):
             return self.pskc.encryption.key
 
     @key.setter
-    def key(self, value):
+    def key(self, value: bytes | EncryptedValue | None) -> None:
         self._key = value
 
     @property
-    def algorithm(self):
+    def algorithm(self) -> str | None:
         """Provide the MAC algorithm used."""
         if self._algorithm:
             return self._algorithm
+        return None
 
     @algorithm.setter
-    def algorithm(self, value):
+    def algorithm(self, value: str | None) -> None:
         from pskc.algorithms import normalise_algorithm
         self._algorithm = normalise_algorithm(value)
 
     @property
-    def algorithm_key_length(self):
+    def algorithm_key_length(self) -> int:
         """Recommended minimal key length in bytes for the set algorithm."""
         return mac_key_length(self.algorithm)
 
-    def generate_mac(self, value):
+    def generate_mac(self, value: bytes) -> bytes:
         """Generate the MAC over the specified value."""
         return mac(self.algorithm, self.key, value)
 
-    def setup(self, key=None, algorithm=None):
+    def setup(self, key: bytes | None = None, algorithm: str | None = None) -> 
None:
         """Configure an encrypted MAC key.
 
         The following arguments may be supplied:
diff --git a/pskc/parser.py b/pskc/parser.py
index 5f82b78..5ea66f1 100644
--- a/pskc/parser.py
+++ b/pskc/parser.py
@@ -20,19 +20,34 @@
 
 """Module for parsing PSKC files."""
 
+from __future__ import annotations
 
 import array
 import base64
 import copy
+from typing import Any, IO, TYPE_CHECKING, cast
 
+from pskc.encryption import Encryption, KeyDerivation
 from pskc.exceptions import ParseError
 from pskc.key import EncryptedIntegerValue, EncryptedValue
+from pskc.mac import MAC
 from pskc.xml import (
     find, findall, findbin, findint, findtext, findtime, getbool, getint,
     parse, remove_namespaces)
 
+if TYPE_CHECKING:  # pragma: no cover (only for mypy)
+    from os import PathLike
 
-def plain2int(value):
+    from lxml.etree import _Element
+
+    from pskc import PSKC
+    from pskc.device import Device
+    from pskc.key import Key
+    from pskc.policy import Policy
+    from pskc.signature import Signature
+
+
+def plain2int(value: int | str | bytes) -> int:
     """Convert a plain text value to an int."""
     # try normal integer string parsing
     try:
@@ -40,7 +55,7 @@ def plain2int(value):
     except ValueError:
         pass
     # fall back to base64 decoding
-    value = base64.b64decode(value)
+    value = base64.b64decode(cast(bytes, value))
     # try to handle value as ASCII representation
     if value.isdigit():
         return int(value)
@@ -51,11 +66,15 @@ def plain2int(value):
     return result
 
 
-class PSKCParser(object):
+class PSKCParser:
     """Class to read various PSKC XML files into a PSKC structure."""
 
     @classmethod
-    def parse_file(cls, pskc, filename):
+    def parse_file(
+        cls,
+        pskc: PSKC,
+        filename: str | bytes | PathLike[str] | PathLike[bytes] | IO[str] | 
IO[bytes],
+    ) -> None:
         """Parse the provided file and store data in the PSKC instance."""
         try:
             tree = parse(filename)
@@ -66,7 +85,7 @@ class PSKCParser(object):
         cls.parse_document(pskc, tree.getroot())
 
     @classmethod
-    def parse_document(cls, pskc, container):
+    def parse_document(cls, pskc: PSKC, container: _Element) -> None:
         """Read information from the provided <KeyContainer> tree."""
         remove_namespaces(container)
         if container.tag not in ('KeyContainer', 'SecretContainer'):
@@ -97,7 +116,7 @@ class PSKCParser(object):
         cls.parse_signature(pskc.signature, find(container, 'Signature'))
 
     @classmethod
-    def parse_encryption(cls, encryption, key_info):
+    def parse_encryption(cls, encryption: Encryption, key_info: _Element | 
None) -> None:
         """Read encryption information from the <EncryptionKey> XML tree."""
         if key_info is None:
             return
@@ -109,7 +128,7 @@ class PSKCParser(object):
         for name in findall(key_info,
                             'KeyName', 'DerivedKey/MasterKeyName',
                             'DerivedKey/CarriedKeyName'):
-            encryption.key_names.append(findtext(name, '.'))
+            encryption.key_names.append(findtext(name, '.'))  # type: 
ignore[arg-type]
         encryption.iv = findbin(key_info, 'IV') or encryption.iv
         cls.parse_key_derivation(encryption.derivation, find(
             key_info, 'DerivedKey/KeyDerivationMethod'))
@@ -128,7 +147,7 @@ class PSKCParser(object):
                 encryption.algorithm_key_lengths[0])
 
     @classmethod
-    def parse_key_derivation(cls, derivation, key_derivation):
+    def parse_key_derivation(cls, derivation: KeyDerivation, key_derivation: 
_Element | None) -> None:
         """Read derivation parameters from a <KeyDerivationMethod> element."""
         if key_derivation is None:
             return
@@ -148,7 +167,7 @@ class PSKCParser(object):
                 derivation.pbkdf2_prf = prf.get('Algorithm')
 
     @classmethod
-    def parse_mac_method(cls, mac, mac_method):
+    def parse_mac_method(cls, mac: MAC, mac_method: _Element | None) -> None:
         """Read MAC information from the <MACMethod> XML tree."""
         if mac_method is None:
             return
@@ -161,7 +180,7 @@ class PSKCParser(object):
             mac.key = EncryptedValue(cipher_value, None, algorithm)
 
     @classmethod
-    def parse_key_package(cls, device, key_package):
+    def parse_key_package(cls, device: Device, key_package: _Element) -> None:
         """Read key information from the provided <KeyPackage> tree."""
         # find basic device information
         info = find(key_package, 'DeviceInfo', 'DeviceId')
@@ -181,7 +200,7 @@ class PSKCParser(object):
             cls.parse_key(device.add_key(), key_elm)
 
     @classmethod
-    def parse_key(cls, key, key_elm):
+    def parse_key(cls, key: Key, key_elm: _Element) -> None:
         """Read key information from the provided <KeyPackage> tree."""
         # get key basic information
         key.id = (
@@ -202,7 +221,7 @@ class PSKCParser(object):
         for data in findall(key_elm, 'Data'):
             name = data.get('Name')
             if name:
-                cls.parse_data(key, dict(
+                cls.parse_data(key, dict(  # type: ignore[arg-type]
                     secret='secret',
                     counter='counter',
                     time='time_offset',
@@ -264,10 +283,10 @@ class PSKCParser(object):
             findtime(key_elm, 'ExpiryDate') or key.policy.expiry_date)
 
     @classmethod
-    def parse_encrypted_value(cls, encrypted_value):
+    def parse_encrypted_value(cls, encrypted_value: _Element) -> tuple[str | 
None, bytes]:
         """Read encryption value from <EncryptedValue> element."""
         algorithm = None
-        cipher_value = findbin(encrypted_value, 'CipherData/CipherValue')
+        cipher_value: bytes = findbin(encrypted_value, 
'CipherData/CipherValue')  # type: ignore[assignment]
         encryption_method = find(encrypted_value, 'EncryptionMethod')
         if encryption_method is not None:
             algorithm = encryption_method.attrib.get('Algorithm')
@@ -278,7 +297,7 @@ class PSKCParser(object):
         return (algorithm, cipher_value)
 
     @classmethod
-    def parse_data(cls, key, field, element):
+    def parse_data(cls, key: Key, field: str, element: _Element | None) -> 
None:
         """Read information from the provided element.
 
         The element is expected to contain <PlainValue>, <EncryptedValue>
@@ -288,12 +307,12 @@ class PSKCParser(object):
         if element is None:
             return
         pskc = key.device.pskc
-        plain_value = None
+        plain_value: int | str | bytes | None = None
         cipher_value = None
         algorithm = None
         # get the plain2value function and encryption storage
         if field == 'secret':
-            plain2value = base64.b64decode
+            plain2value: Any = base64.b64decode
             encrypted_value_cls = EncryptedValue
         else:
             plain2value = plain2int
@@ -324,10 +343,10 @@ class PSKCParser(object):
             setattr(key, field, plain_value)
         elif cipher_value:
             setattr(key, field,
-                    encrypted_value_cls(cipher_value, mac_value, algorithm))
+                    encrypted_value_cls(cipher_value, mac_value, cast(str, 
algorithm)))
 
     @classmethod
-    def parse_policy(cls, policy, policy_elm):
+    def parse_policy(cls, policy: Policy, policy_elm: _Element | None) -> None:
         """Read key policy information from the provided <Policy> tree."""
         if policy_elm is None:
             return
@@ -337,7 +356,7 @@ class PSKCParser(object):
         policy.number_of_transactions = findint(
             policy_elm, 'NumberOfTransactions')
         for key_usage in findall(policy_elm, 'KeyUsage'):
-            policy.key_usage.append(findtext(key_usage, '.'))
+            policy.key_usage.append(findtext(key_usage, '.'))  # type: 
ignore[arg-type]
 
         pin_policy_elm = find(policy_elm, 'PINPolicy')
         if pin_policy_elm is not None:
@@ -367,7 +386,7 @@ class PSKCParser(object):
                 policy.unknown_policy_elements = True
 
     @classmethod
-    def parse_signature(cls, signature, signature_elm):
+    def parse_signature(cls, signature: Signature, signature_elm: _Element | 
None) -> None:
         """Read signature information from the <Signature> element."""
         if signature_elm is None:
             return
@@ -387,9 +406,9 @@ class PSKCParser(object):
         certificate = findbin(
             signature_elm, 'KeyInfo/X509Data/X509Certificate')
         if certificate:
-            certificate = base64.b64encode(certificate).decode('ascii')
+            certificate_str = base64.b64encode(certificate).decode('ascii')
             signature.certificate = '\n'.join(
                 ['-----BEGIN CERTIFICATE-----'] +
-                [certificate[i:i + 64]
-                 for i in range(0, len(certificate), 64)] +
+                [certificate_str[i:i + 64]
+                 for i in range(0, len(certificate_str), 64)] +
                 ['-----END CERTIFICATE-----'])
diff --git a/pskc/policy.py b/pskc/policy.py
index a66e983..46ddf46 100644
--- a/pskc/policy.py
+++ b/pskc/policy.py
@@ -1,7 +1,7 @@
 # policy.py - module for handling PSKC policy information
 # coding: utf-8
 #
-# Copyright (C) 2014-2017 Arthur de Jong
+# Copyright (C) 2014-2025 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -20,10 +20,17 @@
 
 """Module that provides PSKC key policy information."""
 
+from __future__ import annotations
+
+import datetime
 import warnings
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:  # pragma: no cover (only for mypy)
+    from pskc.key import Key
 
 
-def _make_aware(d):
+def _make_aware(d: datetime.datetime) -> datetime.datetime:
     """Make the specified datetime timezone aware."""
     import dateutil.tz
     if not d.tzinfo:
@@ -31,7 +38,7 @@ def _make_aware(d):
     return d
 
 
-class Policy(object):
+class Policy:
     """Representation of a policy that describes key and pin usage.
 
     Instances of this class provide attributes that describe limits that
@@ -101,23 +108,23 @@ class Policy(object):
     # The PIN is used in the algorithm computation.
     PIN_USE_ALGORITHMIC = 'Algorithmic'
 
-    def __init__(self, key=None):
+    def __init__(self, key: Key | None = None) -> None:
         """Create a new policy, optionally linked to the key and parsed."""
         self.key = key
-        self.start_date = None
-        self.expiry_date = None
-        self.number_of_transactions = None
-        self.key_usage = []
-        self.pin_key_id = None
-        self.pin_usage = None
-        self.pin_max_failed_attempts = None
-        self.pin_min_length = None
-        self.pin_max_length = None
-        self.pin_encoding = None
+        self.start_date: datetime.datetime | None = None
+        self.expiry_date: datetime.datetime | None = None
+        self.number_of_transactions: int | None = None
+        self.key_usage: list[str] = []
+        self.pin_key_id: str | None = None
+        self.pin_usage: str | None = None
+        self.pin_max_failed_attempts: int | None = None
+        self.pin_min_length: int | None = None
+        self.pin_max_length: int | None = None
+        self.pin_encoding: str | None = None
         self.unknown_policy_elements = False
 
     @property
-    def pin_max_failed_attemtps(self):
+    def pin_max_failed_attemtps(self) -> int | None:
         """Provide access to deprecated name."""
         warnings.warn(
             'The pin_max_failed_attemtps property has been renamed to '
@@ -125,13 +132,13 @@ class Policy(object):
         return self.pin_max_failed_attempts
 
     @pin_max_failed_attemtps.setter
-    def pin_max_failed_attemtps(self, value):
+    def pin_max_failed_attemtps(self, value: int | None) -> None:
         warnings.warn(
             'The pin_max_failed_attemtps property has been renamed to '
             'pin_max_failed_attempts.', DeprecationWarning, stacklevel=2)
         self.pin_max_failed_attempts = value
 
-    def may_use(self, usage=None, now=None):
+    def may_use(self, usage: str | None = None, now: datetime.datetime | None 
= None) -> bool:
         """Check whether the key may be used for the provided purpose."""
         import datetime
         import dateutil.tz
@@ -152,16 +159,18 @@ class Policy(object):
         return True
 
     @property
-    def pin_key(self):
+    def pin_key(self) -> Key | None:
         """Provide the PSKC Key that holds the PIN (if any)."""
         if self.pin_key_id and self.key and self.key.device.pskc:
             for key in self.key.device.pskc.keys:
                 if key.id == self.pin_key_id:
                     return key
+        return None
 
     @property
-    def pin(self):
+    def pin(self) -> str | None:
         """Provide the PIN value referenced by PINKeyId if any."""
         key = self.pin_key
-        if key:
+        if key and key.secret:
             return str(key.secret.decode())
+        return None
diff --git a/pskc/py.typed b/pskc/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/pskc/scripts/__init__.py b/pskc/scripts/__init__.py
index 0d4006e..03c4b09 100644
--- a/pskc/scripts/__init__.py
+++ b/pskc/scripts/__init__.py
@@ -1,7 +1,7 @@
 # __init__.py - collection of command-line scripts
 # coding: utf-8
 #
-# Copyright (C) 2018 Arthur de Jong
+# Copyright (C) 2018-2025 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -19,3 +19,5 @@
 # 02110-1301 USA
 
 """Collection of command-line scripts."""
+
+from __future__ import annotations
diff --git a/pskc/scripts/csv2pskc.py b/pskc/scripts/csv2pskc.py
index fdcc533..370bfbc 100644
--- a/pskc/scripts/csv2pskc.py
+++ b/pskc/scripts/csv2pskc.py
@@ -1,6 +1,6 @@
 # csv2pskc.py - script to convert a CSV file to PSKC
 #
-# Copyright (C) 2018 Arthur de Jong
+# Copyright (C) 2018-2025 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -19,11 +19,15 @@
 
 """Script to convert a CSV file to PSKC."""
 
+from __future__ import annotations
+
 import argparse
 import base64
 import csv
+import datetime
 import sys
 from binascii import a2b_hex
+from typing import Any, Iterator, TextIO
 
 import dateutil.parser
 
@@ -79,21 +83,21 @@ parser.add_argument(
     default='hex')
 
 
-def from_column(key, value, args):
+def from_column(key: str, value: str, secret_encoding: str) -> bytes | str | 
datetime.datetime:
     """Convert a key value read from a CSV file in a format for PSKC."""
     # decode encoded secret
     if key == 'secret':
-        return encodings[args.secret_encoding](value)
+        return encodings[secret_encoding](value)  # type: 
ignore[no-any-return,operator]
     # convert dates to timestamps
     if key.endswith('_date'):
         return dateutil.parser.parse(value)
     return value
 
 
-def open_csvfile(inputfile):
+def open_csvfile(inputfile: TextIO) -> Iterator[Any]:
     """Open the CSV file, trying to detect the dialect."""
     # Guess dialect if possible and open the CSV file
-    dialect = 'excel'
+    dialect: str | type[csv.Dialect] = 'excel'
     try:
         # seek before read to skip sniffing on non-seekable files
         inputfile.seek(0)
@@ -107,7 +111,7 @@ def open_csvfile(inputfile):
     return csv.reader(inputfile, dialect)
 
 
-def main():
+def main() -> None:
     """Convert a CSV file to PSKC."""
     # parse command-line arguments
     args = parser.parse_args()
@@ -138,7 +142,7 @@ def main():
         for column, value in zip(columns, row):
             for key in column.split('+'):
                 if value and key not in ('', '-'):
-                    data[key] = from_column(key, value, args)
+                    data[key] = from_column(key, value, args.secret_encoding)
         pskcfile.add_key(**data)
     # encrypt the file if needed
     if args.secret:
diff --git a/pskc/scripts/pskc2csv.py b/pskc/scripts/pskc2csv.py
index f482d5b..892dc57 100644
--- a/pskc/scripts/pskc2csv.py
+++ b/pskc/scripts/pskc2csv.py
@@ -1,6 +1,6 @@
 # pskc2csv.py - script to convert a PSKC file to CSV
 #
-# Copyright (C) 2014-2018 Arthur de Jong
+# Copyright (C) 2014-2025 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -19,6 +19,8 @@
 
 """Script to convert a PSKC file to CSV."""
 
+from __future__ import annotations
+
 import argparse
 import base64
 import csv
@@ -28,6 +30,7 @@ import sys
 from binascii import b2a_hex
 
 import pskc
+from pskc.key import Key
 from pskc.scripts.util import (
     OutputFile, VersionAction, get_key, get_password)
 
@@ -74,16 +77,16 @@ parser.add_argument(
     default='hex')
 
 
-def get_column(key, column, encoding):
+def get_column(key: Key, column: str, secret_encoding: str) -> str:
     """Return a string value for the given column."""
     value = operator.attrgetter(column)(key)
     if column == 'secret':
         # Python 3 compatible construct for outputting a string
-        return str(encodings[encoding](value).decode())
-    return value
+        return str(encodings[secret_encoding](value).decode())  # type: 
ignore[operator]
+    return value  # type: ignore[no-any-return]
 
 
-def main():
+def main() -> None:
     """Convert a PSKC file to CSV."""
     # parse command-line arguments
     args = parser.parse_args()
diff --git a/pskc/scripts/pskc2pskc.py b/pskc/scripts/pskc2pskc.py
index 0a80a44..b6a02e3 100644
--- a/pskc/scripts/pskc2pskc.py
+++ b/pskc/scripts/pskc2pskc.py
@@ -1,6 +1,6 @@
 # pskc2pskc.py - script to convert a PSKC file to another PSKC file
 #
-# Copyright (C) 2018 Arthur de Jong
+# Copyright (C) 2018-2025 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -19,6 +19,8 @@
 
 """Script to convert a PSKC file to another PSKC file."""
 
+from __future__ import annotations
+
 import argparse
 
 import pskc
@@ -60,7 +62,7 @@ parser.add_argument(
     help='hex encoded encryption key or a file containing the binary key')
 
 
-def main():
+def main() -> None:
     """Convert a PSKC file to another PSKC file."""
     # parse command-line arguments
     args = parser.parse_args()
diff --git a/pskc/scripts/util.py b/pskc/scripts/util.py
index c3adf54..e350265 100644
--- a/pskc/scripts/util.py
+++ b/pskc/scripts/util.py
@@ -1,6 +1,6 @@
 # util.py - utility functions for command-line scripts
 #
-# Copyright (C) 2014-2018 Arthur de Jong
+# Copyright (C) 2014-2025 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -19,10 +19,13 @@
 
 """Utility functions for command-line scripts."""
 
+from __future__ import annotations
+
 import argparse
 import os.path
 import sys
 from binascii import a2b_hex
+from typing import Any, TextIO
 
 import pskc
 
@@ -31,7 +34,7 @@ version_string = '''
 %s (python-pskc) %s
 Written by Arthur de Jong.
 
-Copyright (C) 2014-2018 Arthur de Jong
+Copyright (C) 2014-2025 Arthur de Jong
 This is free software; see the source for copying conditions.  There is NO
 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 '''.lstrip()
@@ -40,22 +43,33 @@ warranty; not even for MERCHANTABILITY or FITNESS FOR A 
PARTICULAR PURPOSE.
 class VersionAction(argparse.Action):
     """Define --version argparse action."""
 
-    def __init__(self, option_strings, dest,
-                 help='output version information and exit'):
+    def __init__(
+        self,
+        option_strings: list[str],
+        dest: str,
+        help: str = 'output version information and exit',
+    ) -> None:
         super(VersionAction, self).__init__(
             option_strings=option_strings,
             dest=argparse.SUPPRESS,
             default=argparse.SUPPRESS,
             nargs=0,
-            help=help)
-
-    def __call__(self, parser, namespace, values, option_string=None):
+            help=help,
+        )
+
+    def __call__(
+        self,
+        parser: argparse.ArgumentParser,
+        namespace: argparse.Namespace,
+        values: Any,
+        option_string: str | None = None,
+    ) -> None:
         """Output version information and exit."""
         sys.stdout.write(version_string % (parser.prog, pskc.__version__))
         parser.exit()
 
 
-def get_key(argument):
+def get_key(argument: str) -> bytes:
     """Get the key from a file or a hex-encoded string."""
     if os.path.isfile(argument):
         with open(argument, 'rb') as keyfile:
@@ -64,7 +78,7 @@ def get_key(argument):
         return a2b_hex(argument)
 
 
-def get_password(argument):
+def get_password(argument: str) -> str:
     """Get the password from a file or as a string."""
     if os.path.isfile(argument):
         with open(argument, 'r') as passfile:
@@ -73,17 +87,17 @@ def get_password(argument):
         return argument
 
 
-class OutputFile(object):
+class OutputFile:
     """Wrapper around output file to also fall back to stdout."""
 
-    def __init__(self, output):
+    def __init__(self, output: str) -> None:
         self.output = output
 
-    def __enter__(self):
+    def __enter__(self) -> TextIO:
         self.file = open(self.output, 'w') if self.output else sys.stdout
         return self.file
 
-    def __exit__(self, *args):
+    def __exit__(self, *args: Any) -> None:
         if self.output:
             self.file.close()
         else:  # we are using stdout
diff --git a/pskc/serialiser.py b/pskc/serialiser.py
index 863d907..95122a0 100644
--- a/pskc/serialiser.py
+++ b/pskc/serialiser.py
@@ -1,7 +1,7 @@
 # serialiser.py - PSKC file parsing functions
 # coding: utf-8
 #
-# Copyright (C) 2016-2024 Arthur de Jong
+# Copyright (C) 2016-2025 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -20,37 +20,54 @@
 
 """Module for serialising PSKC files to XML."""
 
+from __future__ import annotations
 
 import base64
+from typing import Callable, IO, Sequence, TYPE_CHECKING
 
 from pskc.key import EncryptedIntegerValue, EncryptedValue
 from pskc.xml import find, mk_elem, move_namespaces, reformat, tostring
 
+if TYPE_CHECKING:  # pragma: no cover (only for mypy)
+    from lxml.etree import _Element
 
-def my_b64encode(value):
+    from pskc import PSKC
+    from pskc.device import Device
+    from pskc.encryption import Encryption, KeyDerivation
+    from pskc.key import Key
+    from pskc.mac import MAC
+    from pskc.policy import Policy
+    from pskc.signature import Signature
+
+
+def my_b64encode(value: bytes | str) -> str:
     """Wrap around b64encode to handle types correctly."""
-    if not isinstance(value, type(b'')):
+    if not isinstance(value, bytes):
         value = value.encode()
     return base64.b64encode(value).decode()
 
 
-class PSKCSerialiser(object):
+class PSKCSerialiser:
     """Class for serialising a PSKC structure to PSKC 1.0 XML."""
 
     @classmethod
-    def serialise_file(cls, pskc, output):
+    def serialise_file(
+        cls,
+        pskc: PSKC,
+        output: IO[str] | IO[bytes],
+    ) -> None:
         """Write the PSKC structure to the specified output file."""
         xml = tostring(cls.serialise_document(pskc))
         try:
-            output.write(xml)
+            output.write(xml)  # type: ignore[arg-type]
         except TypeError:
             # fall back to writing as string
-            output.write(xml.decode('utf-8'))
+            output.write(xml.decode('utf-8'))  # type: ignore[call-overload]
 
     @classmethod
-    def serialise_document(cls, pskc):
+    def serialise_document(cls, pskc: PSKC) -> _Element:
         """Convert the PSKC structure to an element tree structure."""
-        container = mk_elem('pskc:KeyContainer', Version='1.0', Id=pskc.id)
+        container: _Element = mk_elem('pskc:KeyContainer', Version='1.0', 
Id=pskc.id)  # type: ignore[assignment]
         cls.serialise_encryption(pskc.encryption, container)
         cls.serialise_mac(pskc.mac, container)
         for device in pskc.devices:
@@ -58,14 +75,14 @@ class PSKCSerialiser(object):
         return cls.serialise_signature(pskc.signature, container)
 
     @classmethod
-    def serialise_encryption(cls, encryption, container):
+    def serialise_encryption(cls, encryption: Encryption, container: _Element) 
-> None:
         """Provide an XML element tree for the encryption information."""
         if all(x is None
                for x in (encryption.id, encryption.key_name, encryption.key,
                          encryption.derivation.algorithm)):
             return
-        encryption_key = mk_elem(container, 'pskc:EncryptionKey',
-                                 Id=encryption.id, empty=True)
+        encryption_key: _Element = mk_elem(  # type: ignore[assignment]
+            container, 'pskc:EncryptionKey', Id=encryption.id, empty=True)
         if encryption.derivation.algorithm:
             cls.serialise_key_derivation(
                 encryption.derivation, encryption_key, encryption.key_names)
@@ -74,8 +91,14 @@ class PSKCSerialiser(object):
                 mk_elem(encryption_key, 'ds:KeyName', name)
 
     @classmethod
-    def serialise_key_derivation(cls, derivation, encryption_key, key_names):
+    def serialise_key_derivation(
+        cls,
+        derivation: KeyDerivation,
+        encryption_key: _Element,
+        key_names: Sequence[str],
+    ) -> None:
         """Provide an XML structure for the key derivation properties."""
+        assert derivation.algorithm
         derived_key = mk_elem(encryption_key, 'xenc11:DerivedKey', empty=True)
         key_derivation = mk_elem(derived_key, 'xenc11:KeyDerivationMethod',
                                  Algorithm=derivation.algorithm)
@@ -94,7 +117,7 @@ class PSKCSerialiser(object):
             mk_elem(derived_key, 'xenc11:MasterKeyName', name)
 
     @classmethod
-    def serialise_mac(cls, mac, container):
+    def serialise_mac(cls, mac: MAC, container: _Element) -> None:
         """Provide an XML structure for the encrypted MAC key."""
         key_value = getattr(mac, '_key', None) or mac.pskc.encryption.key
         if not mac.algorithm and not key_value:
@@ -104,11 +127,12 @@ class PSKCSerialiser(object):
         if not key_value:
             return
         # encrypt the mac key if needed
-        if not hasattr(key_value, 'get_value'):
+        if not isinstance(key_value, EncryptedValue):
             key_value = EncryptedValue.create(mac.pskc, key_value)
         # construct encrypted MACKey
         algorithm = key_value.algorithm or mac.pskc.encryption.algorithm
         cipher_value = key_value.cipher_value
+        assert cipher_value
         if mac.pskc.encryption.iv:
             cipher_value = mac.pskc.encryption.iv + cipher_value
         mac_key = mk_elem(mac_method, 'pskc:MACKey', empty=True)
@@ -118,9 +142,10 @@ class PSKCSerialiser(object):
                 base64.b64encode(cipher_value).decode())
 
     @classmethod
-    def serialise_key_package(cls, device, container):
+    def serialise_key_package(cls, device: Device, container: _Element) -> 
None:
         """Provide an XML structure for key package."""
         key_package = mk_elem(container, 'pskc:KeyPackage', empty=True)
+        assert key_package is not None
         if any(x is not None
                for x in (device.manufacturer, device.serial, device.model,
                          device.issue_no, device.device_binding,
@@ -143,10 +168,10 @@ class PSKCSerialiser(object):
             cls.serialise_key(key, key_package)
 
     @classmethod
-    def serialise_key(cls, key, key_package):
+    def serialise_key(cls, key: Key, key_package: _Element) -> None:
         """Provide an XML structure for the key information."""
-        key_elm = mk_elem(key_package, 'pskc:Key', empty=True, Id=key.id,
-                          Algorithm=key.algorithm)
+        key_elm: _Element = mk_elem(  # type: ignore[assignment]
+            key_package, 'pskc:Key', empty=True, Id=key.id, 
Algorithm=key.algorithm)
         mk_elem(key_elm, 'pskc:Issuer', key.issuer)
         if any((key.algorithm_suite, key.challenge_encoding,
                 key.response_encoding, key.response_length)):
@@ -179,16 +204,17 @@ class PSKCSerialiser(object):
         cls.serialise_policy(key.policy, key_elm)
 
     @classmethod
-    def serialise_data(cls, key, field, key_elm, tag):
+    def serialise_data(cls, key: Key, field: str, key_elm: _Element, tag: str) 
-> None:
         """Provide an XML structure for the key material."""
-        value = getattr(key, '_%s' % field, None)
+        value: bytes | str | EncryptedValue | None = getattr(key, '_%s' % 
field, None)
         pskc = key.device.pskc
         # skip empty values
         if value in (None, ''):
             return
+        assert value is not None
         # get the value2text and encryption storage
         if field == 'secret':
-            value2text = my_b64encode
+            value2text: Callable[[bytes | str], str] = my_b64encode
             encrypted_value_cls = EncryptedValue
         else:
             value2text = str
@@ -199,11 +225,10 @@ class PSKCSerialiser(object):
             data = mk_elem(key_elm, 'pskc:Data', empty=True)
         element = mk_elem(data, tag, empty=True)
         # see if we should encrypt the value
-        if field in pskc.encryption.fields and not hasattr(
-                value, 'get_value'):
+        if field in pskc.encryption.fields and not isinstance(value, 
EncryptedValue):
             value = encrypted_value_cls.create(pskc, value)
         # write out value
-        if not hasattr(value, 'get_value'):
+        if not isinstance(value, EncryptedValue):
             # unencrypted value
             mk_elem(element, 'pskc:PlainValue', value2text(value))
         else:
@@ -225,7 +250,7 @@ class PSKCSerialiser(object):
                         base64.b64encode(value.mac_value).decode())
 
     @classmethod
-    def serialise_policy(cls, policy, key_elm):
+    def serialise_policy(cls, policy: Policy, key_elm: _Element) -> None:
         """Provide an XML structure with the key policy information."""
         # check if any policy attribute is set
         if not policy.key_usage and all(x is None for x in (
@@ -251,7 +276,7 @@ class PSKCSerialiser(object):
                 policy.number_of_transactions)
 
     @classmethod
-    def serialise_signature(cls, signature, container):
+    def serialise_signature(cls, signature: Signature, container: _Element) -> 
_Element:
         """Provide an XML structure for embedded XML signature."""
         if not signature.key:
             return container
diff --git a/pskc/signature.py b/pskc/signature.py
index b6f8454..fbe0029 100644
--- a/pskc/signature.py
+++ b/pskc/signature.py
@@ -1,7 +1,7 @@
 # signature.py - module for handling signed XML files
 # coding: utf-8
 #
-# Copyright (C) 2017-2018 Arthur de Jong
+# Copyright (C) 2017-2025 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -24,31 +24,46 @@ This module defines a Signature class that handles the 
signature checking,
 keys and certificates.
 """
 
+from __future__ import annotations
 
-def sign_x509(xml, key, certificate, algorithm=None, digest_algorithm=None,
-              canonicalization_method=None):
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:  # pragma: no cover (only for mypy)
+    from lxml.etree import _Element, _ElementTree
+
+    from pskc import PSKC
+
+
+def sign_x509(
+    xml: _Element,
+    key: bytes,
+    certificate: str,
+    algorithm: str | None = None,
+    digest_algorithm: str | None = None,
+    canonicalization_method: str | None = None,
+) -> _Element:
     """Sign PSKC data using X.509 certificate and private key.
 
     xml: an XML document
     key: the private key in binary format
     certificate: the X.509 certificate
     """
-    import signxml
+    from signxml import XMLSigner, XMLSignatureProcessor, methods  # type: 
ignore[attr-defined]
     algorithm = algorithm or 'rsa-sha256'
     digest_algorithm = digest_algorithm or 'sha256'
     canonicalization_method = (
         canonicalization_method or
-        getattr(signxml.XMLSignatureProcessor, 'default_c14n_algorithm', None) 
or
+        getattr(XMLSignatureProcessor, 'default_c14n_algorithm', None) or
         'http://www.w3.org/2006/12/xml-c14n11')
-    return signxml.XMLSigner(
-        method=signxml.methods.enveloped,
+    return XMLSigner(
+        method=methods.enveloped,
         signature_algorithm=algorithm.rsplit('#', 1)[-1].lower(),
         digest_algorithm=digest_algorithm.rsplit('#', 1)[-1].lower(),
         c14n_algorithm=canonicalization_method,
     ).sign(xml, key=key, cert=certificate)
 
 
-def verify_x509(tree, certificate=None, ca_pem_file=None):
+def verify_x509(tree: _Element, certificate: str | None = None, ca_pem_file: 
str | None = None) -> _Element:
     """Verify signature in PSKC data against a trusted X.509 certificate.
 
     If a certificate is supplied it is used to validate the signature,
@@ -56,12 +71,12 @@ def verify_x509(tree, certificate=None, ca_pem_file=None):
     certificate in ca_pem_file if it specified and otherwise the operating
     system CA certificates.
     """
-    from signxml import XMLVerifier
+    from signxml import XMLVerifier  # type: ignore[attr-defined]
     return XMLVerifier().verify(
-        tree, x509_cert=certificate, ca_pem_file=ca_pem_file).signed_xml
+        tree, x509_cert=certificate, ca_pem_file=ca_pem_file).signed_xml  # 
type: ignore[union-attr,return-value]
 
 
-class Signature(object):
+class Signature:
     """Class for handling signature checking of the PSKC file.
 
     Instances of this class provide the following properties:
@@ -77,18 +92,18 @@ class Signature(object):
       signed_pskc: a PSKC instance with the signed information
     """
 
-    def __init__(self, pskc):
+    def __init__(self, pskc: PSKC) -> None:
         self.pskc = pskc
-        self._algorithm = None
-        self.canonicalization_method = None
-        self.digest_algorithm = None
-        self.issuer = None
-        self.serial = None
-        self.key = None
-        self.certificate = None
+        self._algorithm: str | None = None
+        self.canonicalization_method: str | None = None
+        self.digest_algorithm: str | None = None
+        self.issuer: str | None = None
+        self.serial: str | None = None
+        self.key: bytes | None = None
+        self.certificate: str | None = None
 
     @property
-    def is_signed(self):
+    def is_signed(self) -> bool:
         """Test whether the PSKC file contains a signature.
 
         This method does not check whether the signature is valid but only if
@@ -99,24 +114,25 @@ class Signature(object):
             self.digest_algorithm or self.issuer or self.certificate)
 
     @property
-    def algorithm(self):
+    def algorithm(self) -> str | None:
         """Provide the signing algorithm used."""
         if self._algorithm:
             return self._algorithm
+        return None
 
     @algorithm.setter
-    def algorithm(self, value):
+    def algorithm(self, value: str | None) -> None:
         from pskc.algorithms import normalise_algorithm
         self._algorithm = normalise_algorithm(value)
 
     @property
-    def signed_pskc(self):
+    def signed_pskc(self) -> PSKC:
         """Provide the signed PSKC information."""
         if not hasattr(self, '_signed_pskc'):
             self.verify()
         return self._signed_pskc
 
-    def verify(self, certificate=None, ca_pem_file=None):
+    def verify(self, certificate: str | None = None, ca_pem_file: str | None = 
None) -> bool:
         """Check that the signature was made with the specified certificate.
 
         If no certificate is provided the signature is expected to contain a
@@ -125,19 +141,22 @@ class Signature(object):
         """
         from pskc import PSKC
         from pskc.parser import PSKCParser
+        self.tree: _Element | _ElementTree[_Element]
         signed_xml = verify_x509(self.tree, certificate, ca_pem_file)
         pskc = PSKC()
         PSKCParser.parse_document(pskc, signed_xml)
         self._signed_pskc = pskc
         return True
 
-    def sign(self, key, certificate=None):
+    def sign(self, key: bytes, certificate: str | None = None) -> None:
         """Add an XML signature to the file."""
         self.key = key
         self.certificate = certificate
 
-    def sign_xml(self, xml):
+    def sign_xml(self, xml: _Element) -> _Element:
         """Sign an XML document with the configured key and certificate."""
+        assert self.key
+        assert self.certificate
         return sign_x509(
             xml, self.key, self.certificate, self.algorithm,
             self.digest_algorithm, self.canonicalization_method)
diff --git a/pskc/xml.py b/pskc/xml.py
index 673d95a..7b969cc 100644
--- a/pskc/xml.py
+++ b/pskc/xml.py
@@ -1,7 +1,7 @@
 # xml.py - module for parsing and writing XML for PSKC files
 # coding: utf-8
 #
-# Copyright (C) 2014-2020 Arthur de Jong
+# Copyright (C) 2014-2025 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -23,21 +23,30 @@
 This module provides some utility functions for parsing XML files.
 """
 
-from __future__ import absolute_import
+from __future__ import absolute_import, annotations
 
+import datetime
 import sys
 from collections import OrderedDict
+from typing import IO, TYPE_CHECKING, cast
+
+if TYPE_CHECKING:  # pragma: no cover (only for mypy)
+    from collections.abc import Generator
+    from os import PathLike
+
+    from lxml.etree import _Element, _ElementTree
+
 
 # try to find a usable ElementTree implementation
 try:  # pragma: no cover (different implementations)
     from lxml.etree import parse as xml_parse, tostring as xml_tostring
     from lxml.etree import register_namespace, Element, SubElement
 except ImportError:  # pragma: no cover (different implementations)
-    from xml.etree.ElementTree import (
+    from xml.etree.ElementTree import (  # type: ignore[no-redef]
         parse as xml_parse, tostring as xml_tostring)
-    from xml.etree.ElementTree import register_namespace, Element, SubElement
+    from xml.etree.ElementTree import register_namespace, Element, SubElement  
# type: ignore[no-redef,assignment]
     try:
-        from defusedxml.ElementTree import parse as xml_parse  # noqa: F811
+        from defusedxml.ElementTree import parse as xml_parse  # type: 
ignore[no-redef]  # noqa: F811
     except ImportError:
         pass
 
@@ -57,7 +66,7 @@ namespaces = dict(
 )
 
 
-def register_namespaces():
+def register_namespaces() -> None:
     """Register the namespaces so the correct short names will be used."""
     for ns, namespace in namespaces.items():
         register_namespace(ns, namespace)
@@ -66,12 +75,14 @@ def register_namespaces():
 register_namespaces()
 
 
-def parse(source):
+def parse(
+    source: str | bytes | PathLike[str] | PathLike[bytes] | IO[str] | 
IO[bytes],
+) -> _ElementTree[_Element]:
     """Parse the provided file and return an element tree."""
     return xml_parse(sys.stdin if source == '-' else source)
 
 
-def remove_namespaces(tree):
+def remove_namespaces(tree: _Element | _ElementTree[_Element]) -> None:
     """Remove namespaces from all elements in the tree."""
     import re
     for elem in tree.iter():
@@ -79,59 +90,65 @@ def remove_namespaces(tree):
             elem.tag = re.sub(r'^\{[^}]*\}', '', elem.tag)
 
 
-def findall(tree, *matches):
+def findall(tree: _Element | _ElementTree[_Element], *matches: str) -> 
Generator[_Element]:
     """Find the child elements."""
     for match in matches:
         for element in tree.findall(match, namespaces=namespaces):
             yield element
 
 
-def find(tree, *matches):
+def find(tree: _Element | _ElementTree[_Element], *matches: str) -> _Element | 
None:
     """Find a child element that matches any of the patterns (or None)."""
     try:
         return next(findall(tree, *matches))
     except StopIteration:
         pass
+    return None
 
 
-def findtext(tree, *matches):
+def findtext(tree: _Element | _ElementTree[_Element], *matches: str) -> str | 
None:
     """Get the text value of an element (or None)."""
     element = find(tree, *matches)
-    if element is not None:
+    if element is not None and element.text:
         return element.text.strip()
+    return None
 
 
-def findint(tree, *matches):
+def findint(tree: _Element | _ElementTree[_Element], *matches: str) -> int | 
None:
     """Return an element value as an int (or None)."""
     value = findtext(tree, *matches)
     if value:
         return int(value)
+    return None
 
 
-def findtime(tree, *matches):
+def findtime(tree: _Element | _ElementTree[_Element], *matches: str) -> 
datetime.datetime | None:
     """Return an element value as a datetime (or None)."""
     value = findtext(tree, *matches)
     if value:
         import dateutil.parser
         return dateutil.parser.parse(value)
+    return None
 
 
-def findbin(tree, *matches):
+def findbin(tree: _Element | _ElementTree[_Element], *matches: str) -> bytes | 
None:
     """Return the binary element value base64 decoded."""
     value = findtext(tree, *matches)
     if value:
         import base64
         return base64.b64decode(value)
+    return None
 
 
-def getint(tree, attribute):
+def getint(tree: _Element, attribute: str) -> int | None:
     """Return an attribute value as an integer (or None)."""
     value = tree.get(attribute)
     if value:
         return int(value)
+    return None
 
 
-def getbool(tree, attribute, default=None):
+def getbool(tree: _Element, attribute: str, default: bool | None = None) -> 
bool | None:
     """Return an attribute value as a boolean (or None)."""
     value = tree.get(attribute)
     if value:
@@ -145,7 +162,7 @@ def getbool(tree, attribute, default=None):
     return default
 
 
-def _format(value):
+def _format(value: datetime.datetime | bool | int | str) -> str:
     import datetime
     if isinstance(value, datetime.datetime):
         value = value.isoformat()
@@ -159,25 +176,30 @@ def _format(value):
     return str(value)
 
 
-def mk_elem(parent, tag=None, text=None, empty=False, **kwargs):
+def mk_elem(
+    parent: _Element | str | None,
+    tag: str | None = None,
+    text: str | int | bool | datetime.datetime | None = None,
+    empty: bool = False,
+    **kwargs: str | int | bool | datetime.datetime | None,
+) -> _Element | None:
     """Add element as a child of parent."""
     # special-case the top-level element
     if tag is None:
-        tag = parent
+        tag = cast(str, parent)
         parent = None
         empty = True
     # don't create empty elements
-    if not empty and text is None and \
-       all(x is None for x in kwargs.values()):
-        return
+    if not empty and text is None and all(x is None for x in kwargs.values()):
+        return None
     # replace namespace identifier with URL
     if ':' in tag:
         ns, name = tag.split(':', 1)
         tag = '{%s}%s' % (namespaces[ns], name)
     if parent is None:
-        element = Element(tag, OrderedDict())
+        element: _Element = Element(tag, OrderedDict())
     else:
-        element = SubElement(parent, tag, OrderedDict())
+        element = SubElement(parent, tag, OrderedDict())  # type: 
ignore[assignment,type-var]
     # set text of element
     if text is not None:
         element.text = _format(text)
@@ -188,7 +210,7 @@ def mk_elem(parent, tag=None, text=None, empty=False, 
**kwargs):
     return element
 
 
-def move_namespaces(element):
+def move_namespaces(element: _Element) -> _Element:
     """Move the namespace declarations to the toplevel element."""
     if hasattr(element, 'nsmap'):  # pragma: no cover (only on lxml)
         # get all used namespaces
@@ -204,9 +226,9 @@ def move_namespaces(element):
     return element
 
 
-def reformat(element, indent=''):
+def reformat(element: _Element, indent: str = '') -> None:
     """Reformat the XML tree to have nice wrapping and indenting."""
-    tag = element.tag.split('}')[-1]
+    tag = cast(str, element.tag).split('}')[-1]
     # re-order attributes by alphabet
     attrib = sorted(element.attrib.items())
     element.attrib.clear()
@@ -216,7 +238,7 @@ def reformat(element, indent=''):
         if element.text:
             element.text = element.text.strip()
         if tag in ('X509Certificate', 'SignatureValue'):
-            element.text = ''.join(x for x in element.text if not x.isspace())
+            element.text = ''.join(x for x in element.text if not x.isspace()) 
 # type: ignore[union-attr]
     elif tag != 'SignedInfo':
         # indent children
         element.text = '\n ' + indent
@@ -227,7 +249,7 @@ def reformat(element, indent=''):
     element.tail = '\n' + indent
 
 
-def tostring(element):
+def tostring(element: _Element) -> bytes:
     """Return a serialised XML document for the element tree."""
     element = move_namespaces(element)
     reformat(element)
diff --git a/setup.cfg b/setup.cfg
index a35b935..d532fe4 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -41,3 +41,13 @@ extend-exclude =
 
 [codespell]
 skip = jquery*,*.egg-info,ChangeLog,./.git,./.tox,./build,./coverage,./std
+
+[mypy]
+python_version = 3.9
+strict = true
+warn_unreachable = true
+#ignore_imports = cryptography
+
+
+[mypy-cryptography.*]
+ignore_errors = true
diff --git a/setup.py b/setup.py
index 994405e..857e5f8 100755
--- a/setup.py
+++ b/setup.py
@@ -77,6 +77,7 @@ setup(
         'defuse': ['defusedxml'],
         'signature': ['signxml'],
     },
+    package_data={'': ['py.typed']},
     entry_points={
         'console_scripts': [
             'csv2pskc = pskc.scripts.csv2pskc:main',
diff --git a/tests/test_write.doctest b/tests/test_write.doctest
index 5e9b0f9..5ffd943 100644
--- a/tests/test_write.doctest
+++ b/tests/test_write.doctest
@@ -356,15 +356,17 @@ Set up an encrypted PSKC file and generate a pre-shared 
key for it.
 ...     id='1', serial='123456', secret=b'1234', counter=42)
 >>> pskc.encryption.setup_preshared_key(
 ...     algorithm='aes128-cbc',
+...     id='Foo',
 ...     key=a2b_hex('12345678901234567890123456789012'),
-...     key_name='Pre-shared KEY', fields = ['secret', 'counter'])
+...     key_name='Pre-shared KEY',
+...     fields=['secret', 'counter'])
 >>> f = tempfile.NamedTemporaryFile()
 >>> pskc.write(f.name)
 >>> with open(f.name, 'r') as r:
 ...     x = sys.stdout.write(r.read())  #doctest: +ELLIPSIS +REPORT_UDIFF
 <?xml version="1.0" encoding="UTF-8"?>
 <pskc:KeyContainer ... Version="1.0">
- <pskc:EncryptionKey>
+ <pskc:EncryptionKey Id="Foo">
   <ds:KeyName>Pre-shared KEY</ds:KeyName>
  </pskc:EncryptionKey>
  <pskc:MACMethod Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1";>
@@ -703,7 +705,8 @@ Check that we can add secrets as bytearray values
 >>> pskc.encryption.setup_preshared_key(
 ...     algorithm='aes128-cbc',
 ...     key=bytearray(a2b_hex('12345678901234567890123456789012')),
-...     key_name='Pre-shared KEY', fields = ['secret', 'counter'])
+...     key_names=['Pre-shared KEY'],
+...     fields=['secret', 'counter'])
 >>> f = tempfile.NamedTemporaryFile()
 >>> pskc.write(f.name)
 >>> with open(f.name, 'r') as r:
diff --git a/tox.ini b/tox.ini
index 0157b34..65c4764 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = 
py{38,39,310,312,313,py,py3}-signxml,py{38,39,310,311,312,313,py3}{-legacy,-legacy-defusedxml,-lxml},flake8,docs,codespell
+envlist = 
py{38,39,310,312,313,py,py3}-signxml,py{38,39,310,311,312,313,py3}{-legacy,-legacy-defusedxml,-lxml},flake8,mypy,docs,codespell
 skip_missing_interpreters = true
 
 [testenv]
@@ -33,6 +33,19 @@ deps = flake8<6.0
        flake8-tuple
        pep8-naming
 commands = flake8 .
+setenv=
+    PYTHONWARNINGS=ignore
+
+[testenv:mypy]
+skip_install = true
+deps = mypy
+       signxml
+       cryptography
+       types-defusedxml
+       types-lxml
+       types-python-dateutil
+commands =
+    mypy pskc
 
 [testenv:codespell]
 skip_install = true

-----------------------------------------------------------------------

Summary of changes:
 .github/workflows/test.yml |   2 +-
 docs/encryption.rst        |  74 ++---------
 docs/exceptions.rst        |  34 +----
 docs/mac.rst               |  16 +--
 docs/policy.rst            |  29 ++---
 docs/signatures.rst        |  52 ++------
 docs/usage.rst             | 100 +++++++-------
 pskc/__init__.py           |  45 +++++--
 pskc/algorithms.py         |   6 +-
 pskc/crypto/__init__.py    |   4 +-
 pskc/crypto/aeskw.py       |  34 ++++-
 pskc/crypto/tripledeskw.py |  10 +-
 pskc/device.py             |  51 +++++---
 pskc/encryption.py         | 315 ++++++++++++++++++++++++++++++---------------
 pskc/exceptions.py         |  39 ++++--
 pskc/key.py                | 111 +++++++++-------
 pskc/mac.py                |  50 +++----
 pskc/parser.py             |  67 ++++++----
 pskc/policy.py             |  55 +++++---
 pskc/py.typed              |   0
 pskc/scripts/__init__.py   |   4 +-
 pskc/scripts/csv2pskc.py   |  18 ++-
 pskc/scripts/pskc2csv.py   |  13 +-
 pskc/scripts/pskc2pskc.py  |   6 +-
 pskc/scripts/util.py       |  40 ++++--
 pskc/serialiser.py         |  79 ++++++++----
 pskc/signature.py          | 130 +++++++++++++------
 pskc/xml.py                |  82 +++++++-----
 setup.cfg                  |  10 ++
 setup.py                   |   1 +
 tests/test_write.doctest   |   9 +-
 tox.ini                    |  16 ++-
 32 files changed, 888 insertions(+), 614 deletions(-)
 create mode 100644 pskc/py.typed


hooks/post-receive
-- 
python-pskc