"""
Manage X.509 certificates
=========================

.. versionadded:: 3006.0

    This module represents a complete rewrite of the original ``x509`` modules
    and is named ``x509_v2`` since it introduces breaking changes.

:depends: cryptography

.. note::

    All parameters that take a public key, private key, certificate,
    CSR or CRL can be specified either as a PEM/hex/base64 string or
    a path to a local file encoded in all supported formats for the type.

Configuration instructions and general remarks are documented
in the :ref:`execution module docs <x509-setup>`.

For the list of breaking changes versus the previous ``x509`` modules,
please also refer to the :ref:`execution module docs <x509-setup>`.

About
-----
This module can enable managing a complete PKI infrastructure, including creating
private keys, CAs, certificates and CRLs. It includes the ability to generate a
private key on a server, and have the corresponding public key sent to a remote
CA to create a CA signed certificate. This can be done in a secure manner, where
private keys are always generated locally and never moved across the network.

Example
-------
Here is a simple example scenario. In this example ``ca`` is the ca server,
and ``www`` is a web server that needs a certificate signed by ``ca``.

.. note::

    Remote signing requires the setup of :term:`Peer Communication` and signing
    policies. Please see the :ref:`execution module docs <x509-setup>`.


/srv/salt/top.sls

.. code-block:: yaml

    base:
      '*':
        - cert
      'ca':
        - ca
      'www':
        - www

This state creates the CA key, certificate and signing policy. It also publishes
the certificate to the mine, where it can be easily retrieved by other minions.

.. code-block:: yaml

    # /srv/salt/ca.sls

    Configure the x509 module:
      file.managed:
        - name: /etc/salt/minion.d/x509.conf
        - source: salt://x509.conf

    Restart Salt minion:
      cmd.run:
        - name: 'salt-call service.restart salt-minion'
        - bg: true
        - onchanges:
          - file: /etc/salt/minion.d/x509.conf

    Ensure PKI directories exist:
      file.directory:
        - name: /etc/pki/issued_certs
        - makedirs: true

    Create CA private key:
      x509.private_key_managed:
        - name: /etc/pki/ca.key
        - keysize: 4096
        - backup: true
        - require:
          - file: /etc/pki/issued_certs

    Create self-signed CA certificate:
      x509.certificate_managed:
        - name: /etc/pki/ca.crt
        - signing_private_key: /etc/pki/ca.key
        - CN: ca.example.com
        - C: US
        - ST: Utah
        - L: Salt Lake City
        - basicConstraints: "critical, CA:true"
        - keyUsage: "critical, cRLSign, keyCertSign"
        - subjectKeyIdentifier: hash
        - authorityKeyIdentifier: keyid:always,issuer
        - days_valid: 3650
        - days_remaining: 0
        - backup: true
        - require:
          - x509: /etc/pki/ca.key

.. code-block:: yaml

    # /srv/salt/x509.conf

    # enable x509_v2
    features:
      x509_v2: true

    # publish the CA certificate to the mine
    mine_functions:
      x509.get_pem_entries: [/etc/pki/ca.crt]

    # define at least one signing policy for remote signing
    x509_signing_policies:
      www:
        - minions: 'www'
        - signing_private_key: /etc/pki/ca.key
        - signing_cert: /etc/pki/ca.crt
        - C: US
        - ST: Utah
        - L: Salt Lake City
        - basicConstraints: "critical CA:false"
        - keyUsage: "critical keyEncipherment"
        - subjectKeyIdentifier: hash
        - authorityKeyIdentifier: keyid:always,issuer
        - days_valid: 30
        - copypath: /etc/pki/issued_certs/


This example state will instruct all minions to trust certificates signed by
our new CA. Mind that this example works for Debian-based OS only.
Also note the Jinja call to encode the string to JSON, which will avoid
YAML issues with newline characters.

.. code-block:: jinja

    # /srv/salt/cert.sls

    Ensure the CA trust bundle exists:
      file.directory:
        - name: /usr/local/share/ca-certificates

    Ensure our self-signed CA certificate is included:
      x509.pem_managed:
        - name: /usr/local/share/ca-certificates/myca.crt
        - text: {{ salt["mine.get"]("ca", "x509.get_pem_entries")["ca"]["/etc/pki/ca.crt"] | json }}

This state creates a private key, then requests a certificate signed by our CA
according to the www policy.

.. code-block:: yaml

    # /srv/salt/www.sls

    Ensure PKI directory exists:
      file.directory:
        - name: /etc/pki

    Create private key for the certificate:
      x509.private_key_managed:
        - name: /etc/pki/www.key
        - keysize: 4096
        - backup: true
        - require:
          - file: /etc/pki

    Request certificate:
      x509.certificate_managed:
        - name: /etc/pki/www.crt
        - ca_server: ca
        - signing_policy: www
        - private_key: /etc/pki/www.key
        - CN: www.example.com
        - days_remaining: 7
        - backup: true
        - require:
          - x509: /etc/pki/www.key
"""

import base64
import logging
import os.path
from datetime import datetime, timedelta, timezone

import salt.utils.atomicfile
import salt.utils.platform
from salt.exceptions import CommandExecutionError, SaltInvocationError
from salt.state import STATE_INTERNAL_KEYWORDS as _STATE_INTERNAL_KEYWORDS

try:
    import cryptography.x509 as cx509
    from cryptography.exceptions import UnsupportedAlgorithm

    import salt.utils.x509 as x509util

    HAS_CRYPTOGRAPHY = True
except ImportError:
    HAS_CRYPTOGRAPHY = False


log = logging.getLogger(__name__)

__virtualname__ = "x509"


def __virtual__():
    if not HAS_CRYPTOGRAPHY:
        return (False, "Could not load cryptography")
    if not __opts__["features"].get("x509_v2", True):
        return (False, "x509_v2 modules were explicitly disabled in `features:x509_v2`")
    return __virtualname__


def certificate_managed(
    name,
    days_remaining=None,
    ca_server=None,
    signing_policy=None,
    encoding="pem",
    append_certs=None,
    digest="sha256",
    signing_private_key=None,
    signing_private_key_passphrase=None,
    signing_cert=None,
    public_key=None,
    private_key=None,
    private_key_passphrase=None,
    csr=None,
    subject=None,
    serial_number=None,
    not_before=None,
    not_after=None,
    days_valid=None,
    pkcs12_passphrase=None,
    pkcs12_encryption_compat=False,
    pkcs12_friendlyname=None,
    **kwargs,
):
    """
    Ensure an X.509 certificate is present as specified.

    This function accepts the same arguments as :py:func:`x509.create_certificate <salt.modules.x509_v2.create_certificate>`,
    as well as most ones for :py:func:`file.managed <salt.states.file.managed>`.

    .. note::

        Since ``file.managed`` also has an ``encoding`` param, it can be passed
        as ``file_encoding`` instead.

    name
        The path the certificate should be present at.

    days_remaining
        The certificate will be recreated once the remaining certificate validity
        period is less than this number of days.
        Defaults to ``90`` (until v3009) or ``7`` (from v3009 onwards).

    ca_server
        Request a remotely signed certificate from ca_server. For this to
        work, a ``signing_policy`` must be specified, and that same policy
        must be configured on the ca_server.  Also, the Salt master must
        permit peers to call the ``x509.sign_remote_certificate`` function.
        See the :ref:`execution module docs <x509-setup>` for details.

    signing_policy
        The name of a configured signing policy. Parameters specified in there
        are hardcoded and cannot be overridden. This is required for remote signing,
        otherwise optional.

    encoding
        Specify the encoding of the resulting certificate. It can be serialized
        as a ``pem`` (or ``pkcs7_pem``) text file or in several binary formats
        (``der``, ``pkcs7_der``, ``pkcs12``). Defaults to ``pem``.

    append_certs
        A list of additional certificates to append to the new one, e.g. to create a CA chain.

        .. note::

            Mind that when ``der`` encoding is in use, appending certificatees is prohibited.

    copypath
        Create a copy of the issued certificate in PEM format in this directory.
        The file will be named ``<serial_number>.crt`` if prepend_cn is false.

    prepend_cn
        When ``copypath`` is set, prepend the common name of the certificate to
        the file name like so: ``<CN>-<serial_number>.crt``. Defaults to false.

    digest
        The hashing algorithm to use for the signature. Valid values are:
        sha1, sha224, sha256, sha384, sha512, sha512_224, sha512_256, sha3_224,
        sha3_256, sha3_384, sha3_512. Defaults to ``sha256``.
        Ignored for ``ed25519`` and ``ed448`` key types.

    signing_cert
        The CA certificate to be used for signing the issued certificate.

        Leave empty to create a self-signed certificate.

    signing_private_key
        The private key to be used for signing the new certificate. Required.

        Usually, this is the private key corresponding to the public key in ``signing_cert``.
        When creating self-signed certificates (missing ``signing_cert``), derives
        the new certificate's embedded public key from this private key.

    signing_private_key_passphrase
        If ``signing_private_key`` is encrypted, the passphrase to decrypt it.

    private_key
        A **private key**, which is used to derive the public key the certificate
        is issued for. If this is unset, checks ``public_key`` or ``csr`` to derive it.

        Ignored when creating self-signed certificates (missing ``signing_cert``).

        .. hint::
            When ``encoding`` is ``pkcs12``, this private key is embedded into
            the resulting container.

    private_key_passphrase
        If ``private_key`` is specified and encrypted, the passphrase to decrypt it.

    public_key
        A **public key**, which is used as the public key the certificate is issued for,
        but only if ``private_key`` is **not** specified. If this is unset, checks ``csr`` to derive it.

        Ignored when creating self-signed certificates (missing ``signing_cert``).

    csr
        A **certificate signing request** to use as a base for generating the certificate:

        - Extensions not otherwise specified (arguments, signing_policy) are copied.
        - If ``private_key`` and ``public_key`` are both unspecified, copies the embedded
          public key into the certificate. This step is skipped when creating self-signed
          certificates (missing ``signing_cert``).

    subject
        The subject's distinguished name embedded in the certificate. This is one way of
        passing this information (see ``kwargs`` below for the other).
        This argument will be preferred and allows to control the order of RDNs in the DN
        as well as to embed RDNs with multiple attributes.
        This can be specified as a RFC4514-encoded string (``CN=example.com,O=Example Inc,C=US``,
        mind that the rendered order is reversed from what is embedded), a list
        of RDNs encoded as in RFC4514 (``["C=US", "O=Example Inc", "CN=example.com"]``)
        or a dictionary (``{"CN": "example.com", "C": "US", "O": "Example Inc"}``,
        default ordering).
        Multiple name attributes per RDN are concatenated with a ``+``.

        .. note::

            Parsing of RFC4514 strings requires at least cryptography release 37.

    serial_number
        A serial number to be embedded in the certificate. If unspecified, will
        autogenerate one. This should be an integer, either in decimal or
        hexadecimal notation.

    not_before
        Set a specific date the certificate should not be valid before.
        The format should follow ``%Y-%m-%d %H:%M:%S`` and will be interpreted as GMT/UTC.
        Defaults to the time of issuance.

    not_after
        Set a specific date the certificate should not be valid after.
        The format should follow ``%Y-%m-%d %H:%M:%S`` and will be interpreted as GMT/UTC.
        If unspecified, defaults to the current time plus ``days_valid`` days.

    days_valid
        If ``not_after`` is unspecified, the number of days from the time of issuance
        the certificate should be valid for.
        Defaults to ``365`` (until v3009) or ``30`` (from v3009 onwards).

    pkcs12_passphrase
        When encoding a certificate as ``pkcs12``, encrypt it with this passphrase.

        .. note::

            PKCS12 encryption is very weak and `should not be relied on for security <https://cryptography.io/en/stable/hazmat/primitives/asymmetric/serialization/#cryptography.hazmat.primitives.serialization.pkcs12.serialize_key_and_certificates>`_.

    pkcs12_encryption_compat
        OpenSSL 3 and cryptography v37 switched to a much more secure default
        encryption for PKCS12, which might be incompatible with some systems.
        This forces the legacy encryption. Defaults to False.

    pkcs12_friendlyname
        When encoding a certificate as ``pkcs12``, a name for the certificate can be included.

    kwargs
        Embedded X.509v3 extensions and the subject's distinguished name can be
        controlled via supplemental keyword arguments. See
        :py:func:`x509.create_certificate <salt.modules.x509_v2.create_certificate>`
        for an overview.
    """
    # Deprecation checks vs the old x509 module
    if days_valid is None and not_after is None:
        try:
            salt.utils.versions.warn_until(
                3009,
                "The default value for `days_valid` will change to 30. Please adapt your code accordingly.",
            )
            days_valid = 365
        except RuntimeError:
            days_valid = 30

    if days_remaining is None:
        try:
            salt.utils.versions.warn_until(
                3009,
                "The default value for `days_remaining` will change to 7. Please adapt your code accordingly.",
            )
            days_remaining = 90
        except RuntimeError:
            days_remaining = 7

    if "algorithm" in kwargs:
        salt.utils.versions.warn_until(
            3009,
            "`algorithm` has been renamed to `digest`. Please update your code.",
        )
        digest = kwargs.pop("algorithm")
    kwargs = x509util.ensure_cert_kwargs_compat(kwargs)

    ret = {
        "name": name,
        "changes": {},
        "result": True,
        "comment": "The certificate is in the correct state",
    }
    current = None
    changes = {}
    verb = "create"
    file_args, cert_args = x509util.split_file_kwargs(
        _filter_state_internal_kwargs(kwargs)
    )
    append_certs = append_certs or []
    if not isinstance(append_certs, list):
        append_certs = [append_certs]

    try:
        # check file.managed changes early to avoid using unnecessary resources
        file_managed_test = _file_managed(name, test=True, replace=False, **file_args)
        if file_managed_test["result"] is False:
            ret["result"] = False
            ret["comment"] = (
                "Problem while testing file.managed changes, see its output"
            )
            _add_sub_state_run(ret, file_managed_test)
            return ret

        if "is not present and is not set for creation" in file_managed_test["comment"]:
            _add_sub_state_run(ret, file_managed_test)
            return ret

        real_name = name
        replace = False

        # handle follow_symlinks
        if __salt__["file.is_link"](name):
            if file_args.get("follow_symlinks", True):
                real_name = os.path.realpath(name)
            else:
                # workaround https://github.com/saltstack/salt/issues/31802
                __salt__["file.remove"](name)
                replace = True

        if __salt__["file.file_exists"](real_name):
            signing_policy_contents = __salt__["x509.get_signing_policy"](
                signing_policy, ca_server=ca_server
            )
            (
                current,
                checked_changes,
                replace,
                private_key_loaded,
            ) = x509util.check_cert_changes(
                real_name,
                days_remaining=days_remaining,
                days_valid=days_valid,
                not_before=not_before,
                not_after=not_after,
                ca_server=ca_server,
                signing_policy_contents=signing_policy_contents,
                encoding=encoding,
                append_certs=append_certs,
                digest=digest,
                signing_private_key=signing_private_key,
                signing_private_key_passphrase=signing_private_key_passphrase,
                signing_cert=signing_cert,
                public_key=public_key,
                private_key=private_key,
                private_key_passphrase=private_key_passphrase,
                csr=csr,
                subject=subject,
                serial_number=serial_number,
                pkcs12_passphrase=pkcs12_passphrase,
                pkcs12_encryption_compat=pkcs12_encryption_compat,
                pkcs12_friendlyname=pkcs12_friendlyname,
                **cert_args,
            )
            changes.update(checked_changes)
        else:
            changes["created"] = name

        if replace:
            changes["replaced"] = name

        if (
            not changes
            and file_managed_test["result"]
            and not file_managed_test["changes"]
        ):
            _add_sub_state_run(ret, file_managed_test)
            return ret

        ret["changes"] = changes
        if current and changes:
            verb = "recreate"

        if __opts__["test"]:
            ret["result"] = None if changes else True
            ret["comment"] = (
                f"The certificate would have been {verb}d"
                if changes
                else ret["comment"]
            )
            _add_sub_state_run(ret, file_managed_test)
            return ret

        if changes:
            if not set(changes) - {
                "additional_certs",
                "encoding",
                "pkcs12_friendlyname",
            }:
                # do not reissue if only metaparameters changed
                if encoding == "pkcs12":
                    cert = __salt__["x509.encode_certificate"](
                        current,
                        append_certs=append_certs,
                        encoding=encoding,
                        private_key=private_key_loaded,
                        pkcs12_passphrase=pkcs12_passphrase,
                        pkcs12_encryption_compat=pkcs12_encryption_compat,
                        pkcs12_friendlyname=pkcs12_friendlyname,
                    )
                else:
                    cert = __salt__["x509.encode_certificate"](
                        current, encoding=encoding, append_certs=append_certs
                    )
            else:
                # request a new certificate otherwise
                cert = __salt__["x509.create_certificate"](
                    ca_server=ca_server,
                    signing_policy=signing_policy,
                    encoding=encoding,
                    append_certs=append_certs,
                    pkcs12_passphrase=pkcs12_passphrase,
                    pkcs12_encryption_compat=pkcs12_encryption_compat,
                    pkcs12_friendlyname=pkcs12_friendlyname,
                    digest=digest,
                    signing_private_key=signing_private_key,
                    signing_private_key_passphrase=signing_private_key_passphrase,
                    signing_cert=signing_cert,
                    public_key=public_key,
                    private_key=private_key,
                    private_key_passphrase=private_key_passphrase,
                    csr=csr,
                    subject=subject,
                    serial_number=serial_number,
                    not_before=not_before,
                    not_after=not_after,
                    days_valid=days_valid,
                    **cert_args,
                )
            ret["comment"] = f"The certificate has been {verb}d"
            if encoding not in ["pem", "pkcs7_pem"]:
                # file.managed does not support binary contents, so create
                # an empty file first (makedirs). This will not work with check_cmd!
                file_managed_ret = _file_managed(name, replace=False, **file_args)
                _add_sub_state_run(ret, file_managed_ret)
                if not _check_file_ret(file_managed_ret, ret, current):
                    return ret
                salt.utils.atomicfile.safe_atomic_write(
                    real_name,
                    base64.b64decode(cert),
                    __salt__["config.backup_mode"](file_args.get("backup", "")),
                    __opts__["cachedir"],
                )

        if not changes or encoding in ["pem", "pkcs7_pem"]:
            replace = bool(encoding in ["pem", "pkcs7_pem"] and changes)
            contents = cert if replace else None
            file_managed_ret = _file_managed(
                name, contents=contents, replace=replace, **file_args
            )
            _add_sub_state_run(ret, file_managed_ret)
            if not _check_file_ret(file_managed_ret, ret, current):
                return ret

    except (CommandExecutionError, SaltInvocationError) as err:
        ret["result"] = False
        ret["comment"] = str(err)
        ret["changes"] = {}
    return ret


def crl_managed(
    name,
    signing_private_key,
    revoked,
    days_remaining=None,
    signing_cert=None,
    signing_private_key_passphrase=None,
    include_expired=False,
    days_valid=None,
    digest="sha256",
    encoding="pem",
    extensions=None,
    **kwargs,
):
    """
    Ensure a certificate revocation list is present as specified.

    This function accepts the same arguments as :py:func:`x509.create_crl <salt.modules.x509_v2.create_crl>`,
    as well as most ones for `:py:func:`file.managed <salt.states.file.managed>`.

    name
        The path the certificate revocation list should be present at.

    signing_private_key
        Your certificate authority's private key. It will be used to sign
        the CRL. Required.

    revoked
        A list of dicts containing all the certificates to revoke. Each dict
        represents one certificate. A dict must contain either the key
        ``serial_number`` with the value of the serial number to revoke, or
        ``certificate`` with some reference to the certificate to revoke.

        The dict can optionally contain the ``revocation_date`` key. If this
        key is omitted, the revocation date will be set to now. It should be a
        string in the format ``%Y-%m-%d %H:%M:%S``.

        The dict can also optionally contain the ``not_after`` key. This is
        redundant if the ``certificate`` key is included. If the
        ``certificate`` key is not included, this can be used for the logic
        behind the ``include_expired`` parameter. It should be a string in
        the format ``%Y-%m-%d %H:%M:%S``.

        The dict can also optionally contain the ``extensions`` key, which
        allows to set CRL entry-specific extensions. The following extensions
        are supported:

        certificateIssuer
            Identifies the certificate issuer associated with an entry in an
            indirect CRL. The format is the same as for subjectAltName.

        CRLReason
            Identifies the reason for certificate revocation.
            Available choices are ``unspecified``, ``keyCompromise``, ``CACompromise``,
            ``affiliationChanged``, ``superseded``, ``cessationOfOperation``,
            ``certificateHold``, ``privilegeWithdrawn``, ``aACompromise`` and
            ``removeFromCRL``.

        invalidityDate
            Provides the date on which the certificate became invalid.
            The value should be a string in the same format as ``revocation_date``.

    days_remaining
        The certificate revocation list will be recreated once the remaining
        CRL validity period is less than this number of days.
        Defaults to ``30`` (until v3009) or ``3`` (from v3009 onwards).
        Set to 0 to disable automatic renewal without anything changing.

    signing_cert
        The CA certificate to be used for signing the issued certificate.

    signing_private_key_passphrase
        If ``signing_private_key`` is encrypted, the passphrase to decrypt it.

    include_expired
        Also include already expired certificates in the CRL. Defaults to false.

    days_valid
        The number of days that the CRL should be valid for. This sets the ``Next Update``
        field in the CRL. Defaults to ``100`` (until v3009) or ``7`` (from v3009 onwards).

    digest
        The hashing algorithm to use for the signature. Valid values are:
        sha1, sha224, sha256, sha384, sha512, sha512_224, sha512_256, sha3_224,
        sha3_256, sha3_384, sha3_512. Defaults to ``sha256``.
        Ignored for ``ed25519`` and ``ed448`` key types.

    encoding
        Specify the encoding of the resulting certificate revocation list.
        It can be serialized as a ``pem`` text or binary ``der`` file.
        Defaults to ``pem``.

    extensions
        Add CRL extensions. See :py:func:`x509.create_crl <salt.modules.x509_v2.create_crl>`
        for details.

        .. note::

            For ``cRLNumber``, in addition the value ``auto`` is supported, which
            automatically increases the counter every time a new CRL is issued.

    Example:

    .. code-block:: yaml

        Manage CRL:
          x509.crl_managed:
            - name: /etc/pki/ca.crl
            - signing_private_key: /etc/pki/myca.key
            - signing_cert: /etc/pki/myca.crt
            - revoked:
              - certificate: /etc/pki/certs/badweb.crt
                revocation_date: 2022-11-01 00:00:00
                extensions:
                  CRLReason: keyCompromise
              - serial_number: D6:D2:DC:D8:4D:5C:C0:F4
                not_after: 2023-03-14 00:00:00
                revocation_date: 2022-10-25 00:00:00
                extensions:
                  CRLReason: cessationOfOperation
            - extensions:
                cRLNumber: auto
    """
    if "text" in kwargs:
        salt.utils.versions.kwargs_warn_until(["text"], "Potassium")
        kwargs.pop("text")

    if days_valid is None:
        try:
            salt.utils.versions.warn_until(
                3009,
                "The default value for `days_valid` will change to 7. Please adapt your code accordingly.",
            )
            days_valid = 100
        except RuntimeError:
            days_valid = 7

    if days_remaining is None:
        try:
            salt.utils.versions.warn_until(
                3009,
                "The default value for `days_remaining` will change to 3. Please adapt your code accordingly.",
            )
            days_remaining = 30
        except RuntimeError:
            days_remaining = 3

    revoked_parsed = []
    for rev in revoked:
        parsed = {}
        if len(rev) == 1 and isinstance(rev[next(iter(rev))], list):
            salt.utils.versions.warn_until(
                3009,
                "Revoked certificates should be specified as a simple list of dicts.",
            )
            for val in rev[next(iter(rev))]:
                parsed.update(val)
        if "reason" in (parsed or rev):
            salt.utils.versions.warn_until(
                3009,
                "The `reason` parameter for revoked certificates should be specified in extensions:CRLReason.",
            )
            salt.utils.dictupdate.set_dict_key_value(
                (parsed or rev), "extensions:CRLReason", (parsed or rev).pop("reason")
            )
        revoked_parsed.append(parsed or rev)
    revoked = revoked_parsed

    ret = {
        "name": name,
        "changes": {},
        "result": True,
        "comment": "The certificate revocation list is in the correct state",
    }
    current = current_encoding = None
    changes = {}
    verb = "create"
    file_args, extra_args = x509util.split_file_kwargs(
        _filter_state_internal_kwargs(kwargs)
    )
    extensions = extensions or {}
    if extra_args:
        raise SaltInvocationError(f"Unrecognized keyword arguments: {list(extra_args)}")

    try:
        # check file.managed changes early to avoid using unnecessary resources
        file_managed_test = _file_managed(name, test=True, replace=False, **file_args)

        if file_managed_test["result"] is False:
            ret["result"] = False
            ret["comment"] = (
                "Problem while testing file.managed changes, see its output"
            )
            _add_sub_state_run(ret, file_managed_test)
            return ret

        if "is not present and is not set for creation" in file_managed_test["comment"]:
            _add_sub_state_run(ret, file_managed_test)
            return ret

        real_name = name
        replace = False

        # handle follow_symlinks
        if __salt__["file.is_link"](name):
            if file_args.get("follow_symlinks", True):
                real_name = os.path.realpath(name)
            else:
                # workaround https://github.com/saltstack/salt/issues/31802
                __salt__["file.remove"](name)
                replace = True

        if __salt__["file.file_exists"](real_name):
            try:
                current, current_encoding = x509util.load_crl(
                    real_name, get_encoding=True
                )
            except x509util.CRLDeserializationError:
                replace = True
            else:
                try:
                    if current.signature_hash_algorithm is not None and not isinstance(
                        current.signature_hash_algorithm,
                        type(x509util.get_hashing_algorithm(digest)),
                    ):
                        # ed25519, ed448 do not use a separate hash for signatures, hence algo is None
                        # although CA certificates should not be using those currently
                        changes["digest"] = digest
                except UnsupportedAlgorithm:
                    # this eg happens with sha3 digest in cryptography < v39
                    log.warning(
                        "Could not determine signature hash algorithm of '%s'. "
                        "Continuing anyways",
                        name,
                    )

                if encoding != current_encoding:
                    changes["encoding"] = encoding
                try:
                    curr_next_update = current.next_update_utc
                except AttributeError:
                    # naive datetime object, release <42 (it's always UTC)
                    curr_next_update = current.next_update.replace(tzinfo=timezone.utc)
                if days_remaining and (
                    curr_next_update
                    < datetime.now(tz=timezone.utc) + timedelta(days=days_remaining)
                ):
                    changes["expiration"] = True

                # "auto" is a value that is managed in this function and cannot not be compared
                crl_auto = extensions.get("cRLNumber") == "auto"
                if crl_auto:
                    extensions.pop("cRLNumber")

                builder, sig_privkey = x509util.build_crl(
                    signing_private_key,
                    revoked,
                    signing_cert=signing_cert,
                    signing_private_key_passphrase=signing_private_key_passphrase,
                    include_expired=include_expired,
                    days_valid=days_valid,
                    extensions=extensions,
                )
                changes.update(_compare_crl(current, builder, sig_privkey.public_key()))
                if crl_auto:
                    # put cRLNumber = auto back if it was set
                    extensions["cRLNumber"] = "auto"
                    changes["extensions"]["removed"].pop(
                        changes["extensions"]["removed"].index("cRLNumber")
                    )
                    if not any(changes["extensions"].values()):
                        changes.pop("extensions")
        else:
            changes["created"] = name

        if replace:
            changes["replaced"] = name

        if (
            not changes
            and file_managed_test["result"]
            and not file_managed_test["changes"]
        ):
            _add_sub_state_run(ret, file_managed_test)
            return ret

        ret["changes"] = changes
        if current and changes:
            verb = "recreate"

        if __opts__["test"]:
            ret["result"] = None if changes else True
            ret["comment"] = (
                f"The certificate revocation list would have been {verb}d"
                if changes
                else ret["comment"]
            )
            _add_sub_state_run(ret, file_managed_test)
            return ret

        if changes:
            if not set(changes) - {"encoding"}:
                # do not regenerate if only metaparameters changed
                crl = __salt__["x509.encode_crl"](current, encoding=encoding)
            else:
                # autoincrease cRLNumber counter, if requested
                if extensions.get("cRLNumber") == "auto":
                    try:
                        extensions["cRLNumber"] = (
                            current.extensions.get_extension_for_class(
                                cx509.CRLNumber
                            ).value.crl_number
                            + 1
                        )
                    except (AttributeError, cx509.ExtensionNotFound):
                        extensions["cRLNumber"] = 1
                crl = __salt__["x509.create_crl"](
                    signing_private_key,
                    revoked,
                    signing_cert=signing_cert,
                    signing_private_key_passphrase=signing_private_key_passphrase,
                    include_expired=include_expired,
                    days_valid=days_valid,
                    digest=digest,
                    encoding=encoding,
                    extensions=extensions,
                )
            ret["comment"] = f"The certificate revocation list has been {verb}d"
            if encoding == "der":
                # file.managed does not support binary contents, so create
                # an empty file first (makedirs). This will not work with check_cmd!
                file_managed_ret = _file_managed(name, replace=False, **file_args)
                _add_sub_state_run(ret, file_managed_ret)
                if not _check_file_ret(file_managed_ret, ret, current):
                    return ret
                salt.utils.atomicfile.safe_atomic_write(
                    real_name,
                    base64.b64decode(crl),
                    __salt__["config.backup_mode"](file_args.get("backup", "")),
                    __opts__["cachedir"],
                )

        if not changes or encoding == "pem":
            replace = bool((encoding == "pem") and changes)
            contents = crl if replace else None
            file_managed_ret = _file_managed(
                name, contents=contents, replace=replace, **file_args
            )
            _add_sub_state_run(ret, file_managed_ret)
            if not _check_file_ret(file_managed_ret, ret, current):
                return ret
    except (CommandExecutionError, SaltInvocationError) as err:
        ret["result"] = False
        ret["comment"] = str(err)
        ret["changes"] = {}
    return ret


def csr_managed(
    name,
    private_key,
    private_key_passphrase=None,
    digest="sha256",
    encoding="pem",
    subject=None,
    **kwargs,
):
    """
    Ensure a certificate signing request is present as specified.

    This function accepts the same arguments as :py:func:`x509.create_csr <salt.modules.x509_v2.create_csr>`,
    as well as most ones for :py:func:`file.managed <salt.states.file.managed>`.

    name
        The path the certificate signing request should be present at.

    private_key
        The private key corresponding to the public key the certificate should
        be issued for. The CSR will be signed by it. Required.

    private_key_passphrase
        If ``private_key`` is encrypted, the passphrase to decrypt it.

    digest
        The hashing algorithm to use for the signature. Valid values are:
        sha1, sha224, sha256, sha384, sha512, sha512_224, sha512_256, sha3_224,
        sha3_256, sha3_384, sha3_512. Defaults to ``sha256``.
        Ignored for ``ed25519`` and ``ed448`` key types.

    encoding
        Specify the encoding of the resulting certificate revocation list.
        It can be serialized as a ``pem`` text or binary ``der`` file.
        Defaults to ``pem``.

    kwargs
        Embedded X.509v3 extensions and the subject's distinguished name can be
        controlled via supplemental keyword arguments.
        See :py:func:`x509.create_certificate <salt.modules.x509_v2.create_certificate>`
        for an overview. Mind that some extensions are not available for CSR
        (``authorityInfoAccess``, ``authorityKeyIdentifier``,
        ``issuerAltName``, ``crlDistributionPoints``).
    """
    # Deprecation checks vs the old x509 module
    if "algorithm" in kwargs:
        salt.utils.versions.warn_until(
            3009,
            "`algorithm` has been renamed to `digest`. Please update your code.",
        )
        digest = kwargs.pop("algorithm")
    kwargs = x509util.ensure_cert_kwargs_compat(kwargs)

    ret = {
        "name": name,
        "changes": {},
        "result": True,
        "comment": "The certificate signing request is in the correct state",
    }
    current = current_encoding = None
    changes = {}
    verb = "create"
    file_args, csr_args = x509util.split_file_kwargs(
        _filter_state_internal_kwargs(kwargs)
    )

    try:
        # check file.managed changes early to avoid using unnecessary resources
        file_managed_test = _file_managed(name, test=True, replace=False, **file_args)

        if file_managed_test["result"] is False:
            ret["result"] = False
            ret["comment"] = (
                "Problem while testing file.managed changes, see its output"
            )
            _add_sub_state_run(ret, file_managed_test)
            return ret

        if "is not present and is not set for creation" in file_managed_test["comment"]:
            _add_sub_state_run(ret, file_managed_test)
            return ret

        real_name = name
        replace = False

        # handle follow_symlinks
        if __salt__["file.is_link"](name):
            if file_args.get("follow_symlinks", True):
                real_name = os.path.realpath(name)
            else:
                # workaround https://github.com/saltstack/salt/issues/31802
                __salt__["file.remove"](name)
                replace = True

        if __salt__["file.file_exists"](real_name):
            try:
                current, current_encoding = x509util.load_csr(
                    real_name, get_encoding=True
                )
            except x509util.CSRDeserializationError:
                replace = True
            except cx509.InvalidVersion:
                # by default, the previous x509 modules generated CSR with
                # invalid versions, which leads to an exception in cryptography >= v38
                changes["invalid_version"] = True
                replace = True
            else:
                try:
                    if current.signature_hash_algorithm is not None and not isinstance(
                        current.signature_hash_algorithm,
                        type(x509util.get_hashing_algorithm(digest)),
                    ):
                        # ed25519, ed448 do not use a separate hash for signatures, hence algo is None
                        changes["digest"] = digest
                except UnsupportedAlgorithm:
                    # this eg happens with sha3 digest in cryptography < v39
                    log.warning(
                        "Could not determine signature hash algorithm of '%s'. "
                        "Continuing anyways",
                        name,
                    )

                if encoding != current_encoding:
                    changes["encoding"] = encoding

                builder, privkey = x509util.build_csr(
                    private_key,
                    private_key_passphrase=private_key_passphrase,
                    subject=subject,
                    **csr_args,
                )
                if not x509util.is_pair(current.public_key(), privkey):
                    changes["private_key"] = True
                changes.update(_compare_csr(current, builder))
        else:
            changes["created"] = name

        if replace:
            changes["replaced"] = name

        if (
            not changes
            and file_managed_test["result"]
            and not file_managed_test["changes"]
        ):
            _add_sub_state_run(ret, file_managed_test)
            return ret

        ret["changes"] = changes
        if current and changes:
            verb = "recreate"

        if __opts__["test"]:
            ret["result"] = None if changes else True
            ret["comment"] = (
                f"The certificate signing request would have been {verb}d"
                if changes
                else ret["comment"]
            )
            _add_sub_state_run(ret, file_managed_test)
            return ret

        if changes:
            if not set(changes) - {"encoding"}:
                # do not regenerate if only metaparameters changed
                csr = __salt__["x509.encode_csr"](current, encoding=encoding)
            else:
                csr = __salt__["x509.create_csr"](
                    private_key,
                    private_key_passphrase=private_key_passphrase,
                    digest=digest,
                    encoding=encoding,
                    subject=subject,
                    **csr_args,
                )
            ret["comment"] = f"The certificate signing request has been {verb}d"
            if encoding == "der":
                # file.managed does not support binary contents, so create
                # an empty file first (makedirs). This will not work with check_cmd!
                file_managed_ret = _file_managed(name, replace=False, **file_args)
                _add_sub_state_run(ret, file_managed_ret)
                if not _check_file_ret(file_managed_ret, ret, current):
                    return ret
                salt.utils.atomicfile.safe_atomic_write(
                    real_name,
                    base64.b64decode(csr),
                    __salt__["config.backup_mode"](file_args.get("backup", "")),
                    __opts__["cachedir"],
                )
        if not changes or encoding == "pem":
            replace = bool((encoding == "pem") and changes)
            contents = csr if replace else None
            file_managed_ret = _file_managed(
                name, contents=contents, replace=replace, **file_args
            )
            _add_sub_state_run(ret, file_managed_ret)
            if not _check_file_ret(file_managed_ret, ret, current):
                return ret

    except (CommandExecutionError, SaltInvocationError) as err:
        ret["result"] = False
        ret["comment"] = str(err)
        ret["changes"] = {}
    return ret


def pem_managed(name, text, **kwargs):
    """
    Manage the contents of a PEM file directly with the content in text,
    ensuring correct formatting.

    name
        The path to the file to manage.

    text
        The PEM-formatted text to write.

    kwargs
        Most arguments supported by :py:func:`file.managed <salt.states.file.managed>` are passed through.
    """
    file_args, extra_args = x509util.split_file_kwargs(kwargs)
    if extra_args:
        raise SaltInvocationError(f"Unrecognized keyword arguments: {list(extra_args)}")

    try:
        file_args["contents"] = __salt__["x509.get_pem_entry"](text=text)
    except (CommandExecutionError, SaltInvocationError) as err:
        return {"name": name, "result": False, "comment": str(err), "changes": {}}
    return _file_managed(name, **file_args)


def private_key_managed(
    name,
    algo="rsa",
    keysize=None,
    passphrase=None,
    encoding="pem",
    new=False,
    overwrite=False,
    pkcs12_encryption_compat=False,
    **kwargs,
):
    """
    Ensure a private key is present as specified.

    This function accepts the same arguments as :py:func:`x509.create_private_key <salt.modules.x509_v2.create_private_key>`,
    as well as most ones for :py:func:`file.managed <salt.states.file.managed>`.

    .. note::

        If ``mode`` is unspecified, it will default to ``0400``.

    name
        The path the private key should be present at.

    algo
        The digital signature scheme the private key should be based on.
        Available: ``rsa``, ``ec``, ``ed25519``, ``ed448``. Defaults to ``rsa``.

    keysize
        For ``rsa``, specifies the bitlength of the private key (2048, 3072, 4096).
        For ``ec``, specifies the NIST curve to use (256, 384, 521).
        Irrelevant for Edwards-curve schemes (``ed25519``, ``ed448``).
        Defaults to 2048 for RSA and 256 for EC.

    passphrase
        If this is specified, the private key will be encrypted using this
        passphrase. The encryption algorithm cannot be selected, it will be
        determined automatically as the best available one.

    encoding
        Specify the encoding of the resulting private key. It can be serialized
        as a ``pem`` text, binary ``der`` or ``pkcs12`` file.
        Defaults to ``pem``.

    new
        Always create a new key. Defaults to false.
        Combining new with :mod:`prereq <salt.states.requisites.prereq>`
        can allow key rotation whenever a new certificate is generated.

    overwrite
        Overwrite an existing private key if the provided passphrase cannot decrypt it.
        Defaults to false.

    pkcs12_encryption_compat
        Some operating systems are incompatible with the encryption defaults
        for PKCS12 used since OpenSSL v3. This switch triggers a fallback to
        ``PBESv1SHA1And3KeyTripleDESCBC``.
        Please consider the `notes on PKCS12 encryption <https://cryptography.io/en/stable/hazmat/primitives/asymmetric/serialization/#cryptography.hazmat.primitives.serialization.pkcs12.serialize_key_and_certificates>`_.

    Example:

    The Jinja templating in this example ensures a new private key is generated
    if the file does not exist and whenever the associated certificate
    is to be renewed.

    .. code-block:: jinja

        Manage www private key:
          x509.private_key_managed:
            - name: /etc/pki/www.key
            - keysize: 4096
            - new: true
        {%- if salt["file.file_exists"]("/etc/pki/www.key") %}
            - prereq:
              - x509: /etc/pki/www.crt
        {%- endif %}
    """
    # Deprecation checks vs the old x509 module
    if "bits" in kwargs:
        salt.utils.versions.warn_until(
            3009,
            "`bits` has been renamed to `keysize`. Please update your code.",
        )
        keysize = kwargs.pop("bits")

    ignored_params = {"cipher", "verbose", "text"}.intersection(
        kwargs
    )  # path, overwrite
    if ignored_params:
        salt.utils.versions.kwargs_warn_until(ignored_params, "Potassium")
        for x in ignored_params:
            kwargs.pop(x)

    ret = {
        "name": name,
        "changes": {},
        "result": True,
        "comment": "The private key is in the correct state",
    }
    current = current_encoding = None
    changes = {}
    verb = "create"
    file_args, extra_args = x509util.split_file_kwargs(
        _filter_state_internal_kwargs(kwargs)
    )

    if extra_args:
        raise SaltInvocationError(f"Unrecognized keyword arguments: {list(extra_args)}")

    if not file_args.get("mode") and not salt.utils.platform.is_windows():
        # ensure secure defaults
        file_args["mode"] = "0400"

    try:
        if keysize and algo in ["ed25519", "ed448"]:
            raise SaltInvocationError(f"keysize is an invalid parameter for {algo}")

        # check file.managed changes early to avoid using unnecessary resources
        file_managed_test = _file_managed(name, test=True, replace=False, **file_args)

        if file_managed_test["result"] is False:
            ret["result"] = False
            ret["comment"] = (
                "Problem while testing file.managed changes, see its output"
            )
            _add_sub_state_run(ret, file_managed_test)
            return ret

        if "is not present and is not set for creation" in file_managed_test["comment"]:
            _add_sub_state_run(ret, file_managed_test)
            return ret

        real_name = name
        replace = False

        # handle follow_symlinks
        if __salt__["file.is_link"](name):
            if file_args.get("follow_symlinks", True):
                real_name = os.path.realpath(name)
            else:
                # workaround https://github.com/saltstack/salt/issues/31802
                __salt__["file.remove"](name)
                replace = True

        file_exists = __salt__["file.file_exists"](real_name)

        if file_exists and not new:
            try:
                current, current_encoding, _ = x509util.load_privkey(
                    real_name, passphrase=passphrase, get_encoding=True
                )
            except x509util.SuperfluousPassword:
                changes["passphrase"] = True
                current, current_encoding, _ = x509util.load_privkey(
                    real_name, passphrase=None, get_encoding=True
                )
            except x509util.InvalidPassword as err:
                if not overwrite:
                    raise CommandExecutionError(
                        "The provided passphrase cannot decrypt the private key. "
                        "Pass overwrite: true to force regeneration"
                    ) from err
                changes["passphrase"] = True
            except x509util.MissingPassword as err:
                if not overwrite:
                    raise CommandExecutionError(
                        "The existing file is encrypted. Pass overwrite: true "
                        "to force regeneration without passphrase"
                    ) from err
                changes["passphrase"] = True
            except x509util.PrivDeserializationError as err:
                if not overwrite:
                    raise CommandExecutionError(
                        "The existing file does not seem to be a private key "
                        "formatted as DER, PEM or embedded in PKCS12. "
                        "Pass overwrite: true to force regeneration"
                    ) from err
                replace = True
        if current:
            key_type = x509util.get_key_type(current)
            check_keysize = keysize
            if check_keysize is None:
                if algo == "rsa":
                    check_keysize = 2048
                elif algo == "ec":
                    check_keysize = 256
            if any(
                (
                    (algo == "rsa" and not key_type == x509util.KEY_TYPE.RSA),
                    (algo == "ec" and not key_type == x509util.KEY_TYPE.EC),
                    (algo == "ed25519" and not key_type == x509util.KEY_TYPE.ED25519),
                    (algo == "ed448" and not key_type == x509util.KEY_TYPE.ED448),
                )
            ):
                changes["algo"] = algo
            if (
                "algo" not in changes
                and algo in ("rsa", "ec")
                and current.key_size != check_keysize
            ):
                changes["keysize"] = check_keysize
            if encoding != current_encoding:
                changes["encoding"] = encoding
        elif file_exists and new:
            changes["replaced"] = name
        else:
            changes["created"] = name

        if (
            not changes
            and file_managed_test["result"]
            and not file_managed_test["changes"]
        ):
            _add_sub_state_run(ret, file_managed_test)
            return ret

        ret["changes"] = changes
        if file_exists and changes:
            verb = "recreate"

        if __opts__["test"]:
            ret["result"] = None if changes else True
            ret["comment"] = (
                f"The private key would have been {verb}d"
                if changes
                else ret["comment"]
            )
            _add_sub_state_run(ret, file_managed_test)
            return ret

        if changes:
            if not set(changes) - {"encoding", "passphrase"}:
                # do not regenerate if only metaparameters changed
                pk = __salt__["x509.encode_private_key"](
                    current, passphrase=passphrase, encoding=encoding
                )
            else:
                pk = __salt__["x509.create_private_key"](
                    algo=algo,
                    keysize=keysize,
                    passphrase=passphrase,
                    encoding=encoding,
                    pkcs12_encryption_compat=pkcs12_encryption_compat,
                )
            ret["comment"] = f"The private key has been {verb}d"
            if encoding != "pem":
                # file.managed does not support binary contents, so create
                # an empty file first (makedirs). This will not work with check_cmd!
                file_managed_ret = _file_managed(name, replace=False, **file_args)
                _add_sub_state_run(ret, file_managed_ret)
                if not _check_file_ret(file_managed_ret, ret, current):
                    return ret
                salt.utils.atomicfile.safe_atomic_write(
                    real_name,
                    base64.b64decode(pk),
                    __salt__["config.backup_mode"](file_args.get("backup", "")),
                    __opts__["cachedir"],
                )

        if not changes or encoding == "pem":
            replace = bool((encoding == "pem") and changes)
            contents = pk if replace else None
            file_managed_ret = _file_managed(
                name, contents=contents, replace=replace, **file_args
            )
            _add_sub_state_run(ret, file_managed_ret)
            if not _check_file_ret(file_managed_ret, ret, current):
                return ret
    except (CommandExecutionError, SaltInvocationError) as err:
        ret["result"] = False
        ret["comment"] = str(err)
        ret["changes"] = {}
    return ret


def certificate_managed_ssh(
    name, result, comment, changes, encoding=None, contents=None, **kwargs
):
    """
    Helper for the SSH wrapper module.
    This receives a base64/PEM-encoded certificate and dumps the data to the target.
    A ``file.managed`` sub-state run will be performed.
    """
    ret = {"name": name, "result": result, "comment": comment, "changes": changes}
    if not result:
        return ret
    file_managed_ret = _file_managed(name, replace=False, **kwargs)
    _add_sub_state_run(ret, file_managed_ret)
    if not _check_file_ret(file_managed_ret, ret, __salt__["file.file_exists"](name)):
        return ret
    if contents is not None:
        if __opts__["test"]:
            ret["comment"] += (
                '. The file was not actually updated - please pass test=opts.get("test") '
                "into the wrapper to enable proper test mode support."
            )
            return ret
        if encoding in ("pem", "pkcs7_pem"):
            contents = contents.encode()
        else:
            contents = base64.b64decode(contents)
        salt.utils.atomicfile.safe_atomic_write(
            name,
            contents,
            __salt__["config.backup_mode"](kwargs.get("backup", "")),
            __opts__["cachedir"],
        )
    return ret


def private_key_managed_ssh(name, result, comment, changes, tempfile=None, **kwargs):
    """
    Helper for the SSH wrapper module to report the correct return and
    perform a ``file.managed`` sub-state run.
    """
    ret = {"name": name, "result": result, "comment": comment, "changes": changes}
    if not result:
        return ret
    file_managed_ret = _file_managed(name, replace=False, **kwargs)
    if tempfile is not None:
        if __opts__["test"]:
            ret["comment"] += (
                '. The file was not actually updated - please pass test=opts.get("test") '
                "into the wrapper to enable proper test mode support."
            )
            try:
                __salt__["file.remove"](tempfile)
            except Exception:  # pylint: disable=broad-except
                pass
            return ret
        try:
            # This will replace symlinks with the file
            __salt__["file.move"](tempfile, name)
        except Exception as err:  # pylint: disable=broad-except
            ret["result"] = False
            ret["comment"] += f". But: Failed moving the private key into place: {err}"
            ret["changes"] = {}
            return ret
    _add_sub_state_run(ret, file_managed_ret)
    return ret


def _filter_state_internal_kwargs(kwargs):
    # check_cmd is a valid argument to file.managed
    ignore = set(_STATE_INTERNAL_KEYWORDS) - {"check_cmd"}
    return {k: v for k, v in kwargs.items() if k not in ignore}


def _add_sub_state_run(ret, sub):
    sub["low"] = {
        "name": ret["name"],
        "state": "file",
        "__id__": __low__["__id__"],
        "fun": "managed",
    }
    if "sub_state_run" not in ret:
        ret["sub_state_run"] = []
    ret["sub_state_run"].append(sub)


def _file_managed(name, test=None, **kwargs):
    if test not in [None, True]:
        raise SaltInvocationError("test param can only be None or True")
    test = test or __opts__["test"]
    res = __salt__["state.single"](
        "file.managed", name, test=test, concurrent=True, **kwargs
    )
    if not isinstance(res, dict):
        raise CommandExecutionError(
            f"Failed running file.managed in x509_v2 state: {res}"
        )
    return res[next(iter(res))]


def _check_file_ret(fret, ret, current):
    if fret["result"] is False:
        ret["result"] = False
        ret["comment"] = (
            f"Could not {'create' if not current else 'update'} file, see file.managed output"
        )
        ret["changes"] = {}
        return False
    return True


def _compare_csr(current, builder):
    changes = {}

    if not _compareattr_safe(builder, "_subject_name", current.subject):
        changes["subject_name"] = x509util.getattr_safe(
            builder, "_subject_name"
        ).rfc4514_string()

    ext_changes = x509util.compare_exts(current, builder)
    if any(ext_changes.values()):
        changes["extensions"] = ext_changes
    return changes


def _compare_crl(current, builder, sig_pubkey):
    # these are necessary because the classes do not have the required method
    def _get_revoked_certificate_by_serial_number(revoked, serial):
        try:
            return [x for x in revoked if x.serial_number == serial][0]
        except IndexError:
            return None

    def _get_extension_for_oid(extensions, oid):
        try:
            return [x for x in extensions if x.oid == oid][0]
        except IndexError:
            return None

    changes = {}

    if x509util.getattr_safe(builder, "_issuer_name") != current.issuer:
        changes["issuer_name"] = x509util.getattr_safe(
            builder, "_issuer_name"
        ).rfc4514_string()
    if not current.is_signature_valid(sig_pubkey):
        changes["public_key"] = True

    rev_changes = {"added": [], "changed": [], "removed": []}
    revoked = x509util.getattr_safe(builder, "_revoked_certificates")
    for rev in revoked:
        cur = current.get_revoked_certificate_by_serial_number(rev.serial_number)
        if cur is None:
            # certificate was not revoked before
            rev_changes["added"].append(x509util.dec2hex(rev.serial_number))
            continue

        for ext in rev.extensions:
            cur_ext = _get_extension_for_oid(cur.extensions, ext.oid)
            # revoked certificate's extensions have changed (added/changed)
            if any(
                (
                    cur_ext is None,
                    cur_ext.critical != ext.critical,
                    cur_ext.value != ext.value,
                )
            ):
                rev_changes["changed"].append(x509util.dec2hex(rev.serial_number))

        for cur_ext in cur.extensions:
            if _get_extension_for_oid(rev.extensions, cur_ext.oid) is None:
                # an extension was removed from from the revoked certificate
                rev_changes["changed"].append(x509util.dec2hex(rev.serial_number))

    for rev in current:
        # certificate was removed from the CRL, probably because it was outdated anyways
        if (
            _get_revoked_certificate_by_serial_number(revoked, rev.serial_number)
            is None
        ):
            rev_changes["removed"].append(x509util.dec2hex(rev.serial_number))

    if any(rev_changes.values()):
        changes["revocations"] = rev_changes

    ext_changes = x509util.compare_exts(current, builder)
    if any(ext_changes.values()):
        changes["extensions"] = ext_changes
    return changes


def _compareattr_safe(obj, attr, comp):
    try:
        return getattr(obj, attr) == comp
    except AttributeError:
        return False
