diff --git a/salt/salt/minion.sls b/salt/salt/minion.sls index ae2f9f487..9f2a6f3fa 100644 --- a/salt/salt/minion.sls +++ b/salt/salt/minion.sls @@ -8,6 +8,7 @@ include: - salt.python_modules + - salt.patch.x509_v2 - salt - systemd.reload - repo.client diff --git a/salt/salt/patch/x509_v2/init.sls b/salt/salt/patch/x509_v2/init.sls new file mode 100644 index 000000000..2483339a2 --- /dev/null +++ b/salt/salt/patch/x509_v2/init.sls @@ -0,0 +1,6 @@ +patch_x509_v2_state_module: + file.replace: + - name: /opt/saltstack/salt/lib/python3.10/site-packages/salt/states/x509_v2.py + - pattern: 'res = __salt__\["state.single"\]\("file.managed", name, test=test, \*\*kwargs\)' + - repl: 'res = __salt__["state.single"]("file.managed", name, test=test, concurrent=True, **kwargs)' + - backup: .bak diff --git a/setup/files/patch/states/x509_v2.py b/setup/files/patch/states/x509_v2.py deleted file mode 100644 index acc86045f..000000000 --- a/setup/files/patch/states/x509_v2.py +++ /dev/null @@ -1,1814 +0,0 @@ -""" -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 `. - -For the list of breaking changes versus the previous ``x509`` modules, -please also refer to the :ref:`execution module docs `. - -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 `. - - -/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 - - 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 copy -import datetime -import logging -import os.path - -import salt.utils.context -import salt.utils.files -from salt.exceptions import CommandExecutionError, SaltInvocationError -from salt.features import features -from salt.state import STATE_INTERNAL_KEYWORDS as _STATE_INTERNAL_KEYWORDS - -try: - import cryptography.x509 as cx509 - from cryptography.exceptions import UnsupportedAlgorithm - from cryptography.hazmat.primitives import hashes - - 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 features.get("x509_v2"): - return ( - False, - "x509_v2 needs to be explicitly enabled by setting `x509_v2: true` " - "in the minion configuration value `features` until Salt 3008 (Argon).", - ) - return __virtualname__ - - -def certificate_managed( - name, - days_remaining=None, - ca_server=None, - signing_policy=None, - encoding="pem", - append_certs=None, - copypath=None, - prepend_cn=False, - 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 `, - as well as most ones for `:py:func:`file.managed `. - - 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 ` 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 ``.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: ``-.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``. - This will be ignored for ``ed25519`` and ``ed448`` key types. - - signing_private_key - The private key corresponding to the public key in ``signing_cert``. Required. - - signing_private_key_passphrase - If ``signing_private_key`` is encrypted, the passphrase to decrypt it. - - signing_cert - The CA certificate to be used for signing the issued certificate. - - public_key - The public key the certificate should be issued for. Other ways of passing - the required information are ``private_key`` and ``csr``. If neither are set, - the public key of the ``signing_private_key`` will be included, i.e. - a self-signed certificate is generated. - - private_key - The private key corresponding to the public key the certificate should - be issued for. This is one way of specifying the public key that will - be included in the certificate, the other ones being ``public_key`` and ``csr``. - - private_key_passphrase - If ``private_key`` is specified and encrypted, the passphrase to decrypt it. - - csr - A certificate signing request to use as a base for generating the certificate. - The following information will be respected, depending on configuration: - - * public key - * extensions, if not otherwise specified (arguments, signing_policy) - - 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 `_. - - 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 ` - 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( - "Potassium", - "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( - "Potassium", - "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( - "Potassium", - "`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 = current_encoding = None - changes = {} - verb = "create" - file_args, cert_args = _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): - try: - ( - current, - current_encoding, - current_chain, - current_extra, - ) = x509util.load_cert( - real_name, passphrase=pkcs12_passphrase, get_encoding=True - ) - except SaltInvocationError as err: - if "Bad decrypt" in str(err): - changes["pkcs12_passphrase"] = True - elif any( - ( - "Could not deserialize binary data" in str(err), - "Could not load PEM-encoded" in str(err), - ) - ): - replace = True - else: - raise - else: - if encoding != current_encoding: - changes["encoding"] = encoding - elif encoding == "pkcs12" and current_extra.cert.friendly_name != ( - salt.utils.stringutils.to_bytes(pkcs12_friendlyname) - if pkcs12_friendlyname - else None - ): - changes["pkcs12_friendlyname"] = pkcs12_friendlyname - - if ( - current.not_valid_after - < datetime.datetime.utcnow() - + datetime.timedelta(days=days_remaining) - ): - changes["expiration"] = True - - current_chain = current_chain or [] - ca_chain = [x509util.load_cert(x) for x in append_certs] - if not _compare_ca_chain(current_chain, ca_chain): - changes["additional_certs"] = True - - ( - builder, - private_key_loaded, - signing_cert_loaded, - final_kwargs, - ) = _build_cert( - ca_server=ca_server, - signing_policy=signing_policy, - digest=digest, # passed because of signing_policy merging - 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, - ) - - try: - if current.signature_hash_algorithm is not None and not isinstance( - current.signature_hash_algorithm, - type(x509util.get_hashing_algorithm(final_kwargs["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 in cryptography < v39 - log.warning( - "Could not determine signature hash algorithm of '%s'. " - "Continuing anyways", - name, - ) - - changes.update( - _compare_cert( - current, - builder, - signing_cert=signing_cert_loaded, - serial_number=serial_number, - not_before=not_before, - not_after=not_after, - ) - ) - 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 - _safe_atomic_write( - real_name, base64.b64decode(cert), file_args.get("backup", "") - ) - - 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 `, - as well as most ones for `:py:func:`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``. - This will be 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 ` - 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( - "Potassium", - "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( - "Potassium", - "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( - "Potassium", - "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( - "Potassium", - "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 = _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 SaltInvocationError as err: - if any( - ( - "Could not load PEM-encoded" in str(err), - "Could not load DER-encoded" in str(err), - ) - ): - replace = True - else: - raise - 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 - if days_remaining and ( - current.next_update - < datetime.datetime.utcnow() - + datetime.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 - _safe_atomic_write( - real_name, base64.b64decode(crl), file_args.get("backup", "") - ) - - 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 `, - as well as most ones for :py:func:`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``. - This will be 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 ` - 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( - "Potassium", - "`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 = _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 SaltInvocationError as err: - if any( - ( - "Could not load PEM-encoded" in str(err), - "Could not load DER-encoded" in str(err), - ) - ): - replace = True - else: - raise - 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 - _safe_atomic_write( - real_name, base64.b64decode(csr), file_args.get("backup", "") - ) - 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 ` are passed through. - """ - file_args, extra_args = _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 `, - as well as most ones for :py:func:`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 ` - 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 `_. - - 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( - "Potassium", - "`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 = _split_file_kwargs(kwargs) - - if extra_args: - raise SaltInvocationError(f"Unrecognized keyword arguments: {list(extra_args)}") - - if not file_args.get("mode"): - # 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 SaltInvocationError as err: - if "Bad decrypt" in str(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 - elif any( - ( - "Could not deserialize binary data" in str(err), - "Could not load DER-encoded" in str(err), - "Could not load PEM-encoded" in str(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 - elif "Private key is unencrypted" in str(err): - changes["passphrase"] = True - current, current_encoding, _ = x509util.load_privkey( - real_name, passphrase=None, get_encoding=True - ) - elif "Private key is encrypted" in str(err) and not passphrase: - if not overwrite: - raise CommandExecutionError( - "The existing file is encrypted. Pass overwrite: true " - "to force regeneration without passphrase" - ) from err - changes["passphrase"] = True - else: - raise - 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"] = 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 - _safe_atomic_write( - real_name, base64.b64decode(pk), file_args.get("backup", "") - ) - - 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 _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 _split_file_kwargs(kwargs): - valid_file_args = [ - "user", - "group", - "mode", - "attrs", - "makedirs", - "dir_mode", - "backup", - "create", - "follow_symlinks", - "check_cmd", - "tmp_dir", - "tmp_ext", - "selinux", - "encoding", - "encoding_errors", - "win_owner", - "win_perms", - "win_deny_perms", - "win_inheritance", - "win_perms_reset", - ] - file_args = {"show_changes": False} - extra_args = {} - for k, v in kwargs.items(): - if k in valid_file_args: - file_args[k] = v - else: - extra_args[k] = v - return file_args, extra_args - - -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") - # work around https://github.com/saltstack/salt/issues/62590 - test = test or __opts__["test"] - file_managed = __states__["file.managed"] - if test: - # calls via __salt__["state.single"](..., test=test) - # can overwrite __opts__["test"] permanently. Workaround: - opts = __opts__ - if not __opts__["test"]: - opts = copy.copy(__opts__) - opts["test"] = test - with salt.utils.context.func_globals_inject(file_managed, __opts__=opts): - # The file execution module accesses __opts__["test"] as well - with salt.utils.context.func_globals_inject( - __salt__["file.check_perms"], __opts__=opts - ): - with salt.utils.context.func_globals_inject( - __salt__["file.manage_file"], __opts__=opts - ): - return file_managed(name, **kwargs) - return file_managed(name, **kwargs) - - -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 _build_cert( - ca_server=None, signing_policy=None, signing_private_key=None, **kwargs -): - final_kwargs = copy.deepcopy(kwargs) - x509util.merge_signing_policy( - __salt__["x509.get_signing_policy"](signing_policy, ca_server=ca_server), - final_kwargs, - ) - - builder, _, private_key_loaded, signing_cert = x509util.build_crt( - signing_private_key, - skip_load_signing_private_key=ca_server is not None, - **final_kwargs, - ) - return builder, private_key_loaded, signing_cert, final_kwargs - - -def _compare_cert(current, builder, signing_cert, serial_number, not_before, not_after): - changes = {} - - if ( - serial_number is not None - and _getattr_safe(builder, "_serial_number") != current.serial_number - ): - changes["serial_number"] = serial_number - - if not x509util.match_pubkey( - _getattr_safe(builder, "_public_key"), current.public_key() - ): - changes["private_key"] = True - - if signing_cert and not x509util.verify_signature( - current, signing_cert.public_key() - ): - changes["signing_private_key"] = True - - if _getattr_safe(builder, "_subject_name") != current.subject: - changes["subject_name"] = _getattr_safe( - builder, "_subject_name" - ).rfc4514_string() - - if _getattr_safe(builder, "_issuer_name") != current.issuer: - changes["issuer_name"] = _getattr_safe(builder, "_issuer_name").rfc4514_string() - - ext_changes = _compare_exts(current, builder) - if any(ext_changes.values()): - changes["extensions"] = ext_changes - return changes - - -def _compare_csr(current, builder): - changes = {} - - # if _getattr_safe(builder, "_subject_name") != current.subject: - if not _compareattr_safe(builder, "_subject_name", current.subject): - changes["subject_name"] = _getattr_safe( - builder, "_subject_name" - ).rfc4514_string() - - ext_changes = _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 _getattr_safe(builder, "_issuer_name") != current.issuer: - changes["issuer_name"] = _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 = _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 = _compare_exts(current, builder) - if any(ext_changes.values()): - changes["extensions"] = ext_changes - return changes - - -def _compare_exts(current, builder): - def getextname(ext): - try: - return ext.oid._name - except AttributeError: - return ext.oid.dotted_string - - added = [] - changed = [] - removed = [] - builder_extensions = cx509.Extensions(_getattr_safe(builder, "_extensions")) - - # iter is unnecessary, but avoids a pylint < 2.13.6 crash - for ext in iter(builder_extensions): - try: - cur_ext = current.extensions.get_extension_for_oid(ext.value.oid) - if cur_ext.critical != ext.critical or cur_ext.value != ext.value: - changed.append(getextname(ext)) - except cx509.ExtensionNotFound: - added.append(getextname(ext)) - - for ext in current.extensions: - try: - builder_extensions.get_extension_for_oid(ext.value.oid) - except cx509.ExtensionNotFound: - removed.append(getextname(ext)) - - return {"added": added, "changed": changed, "removed": removed} - - -def _compare_ca_chain(current, new): - if not len(current) == len(new): - return False - for i, new_cert in enumerate(new): - if new_cert.fingerprint(hashes.SHA256()) != current[i].fingerprint( - hashes.SHA256() - ): - return False - return True - - -def _getattr_safe(obj, attr): - try: - return getattr(obj, attr) - except AttributeError as err: - # Since we cannot get the certificate object without signing, - # we need to compare attributes marked as internal. At least - # convert possible exceptions into some description. - raise CommandExecutionError( - f"Could not get attribute {attr} from {obj.__class__.__name__}. " - "Did the internal API of cryptography change?" - ) from err - - -def _compareattr_safe(obj, attr, comp): - try: - return getattr(obj, attr) == comp - except AttributeError: - return False - - -def _safe_atomic_write(dst, data, backup): - """ - Create a temporary file with only user r/w perms and atomically - copy it to the destination, honoring ``backup``. - """ - tmp = salt.utils.files.mkstemp(prefix=salt.utils.files.TEMPFILE_PREFIX) - with salt.utils.files.fopen(tmp, "wb") as tmp_: - tmp_.write(data) - salt.utils.files.copyfile( - tmp, dst, __salt__["config.backup_mode"](backup), __opts__["cachedir"] - ) - salt.utils.files.safe_rm(tmp) diff --git a/setup/so-functions b/setup/so-functions index b97d4bb52..82dd9d375 100755 --- a/setup/so-functions +++ b/setup/so-functions @@ -1960,11 +1960,8 @@ salt_install_module_deps() { } salt_patch_x509_v2() { - # this can be removed when https://github.com/saltstack/salt/issues/64195 is resolved - if [ $SALTVERSION == "3006.1" ]; then - info "Salt version 3006.1 found. Patching /opt/saltstack/salt/lib/python3.10/site-packages/salt/states/x509_v2.py" - \cp -v ./files/patch/states/x509_v2.py /opt/saltstack/salt/lib/python3.10/site-packages/salt/states/x509_v2.py - fi + # this can be removed when https://github.com/saltstack/salt/issues/66929 is resolved + logCmd "salt-call state.apply salt.patch.x509_v2 --local --file-root=../salt/" } # Create an secrets pillar so that passwords survive re-install