From 2776523b669b5f49986ee8d4322b34d2710a6a8f Mon Sep 17 00:00:00 2001 From: Hristo Voyvodov Date: Thu, 9 May 2024 11:44:41 +0300 Subject: [PATCH] fixup! initial draft for vault_pki --- .../modules/saltext.vault.modules.debug.rst | 5 - src/saltext/vault/modules/vault_pki.py | 65 ++--- src/saltext/vault/states/vault_pki.py | 245 +++++++++--------- src/saltext/vault/utils/vault/helpers.py | 42 +-- src/saltext/vault/utils/vault/pki.py | 134 ++++++++-- 5 files changed, 268 insertions(+), 223 deletions(-) delete mode 100644 docs/ref/modules/saltext.vault.modules.debug.rst diff --git a/docs/ref/modules/saltext.vault.modules.debug.rst b/docs/ref/modules/saltext.vault.modules.debug.rst deleted file mode 100644 index a84fd50a..00000000 --- a/docs/ref/modules/saltext.vault.modules.debug.rst +++ /dev/null @@ -1,5 +0,0 @@ -``debug`` -========= - -.. automodule:: saltext.vault.modules.debug - :members: diff --git a/src/saltext/vault/modules/vault_pki.py b/src/saltext/vault/modules/vault_pki.py index 02bb2fdb..49ac4d2a 100644 --- a/src/saltext/vault/modules/vault_pki.py +++ b/src/saltext/vault/modules/vault_pki.py @@ -6,13 +6,15 @@ .. important:: This module requires the general :ref:`Vault setup `. """ + import logging import salt.utils.x509 as x509util -import saltext.vault.utils.vault as vault from cryptography.hazmat.primitives import serialization from salt.exceptions import CommandExecutionError +import saltext.vault.utils.vault as vault + log = logging.getLogger(__name__) @@ -29,7 +31,7 @@ def list_roles(mount="pki"): salt '*' vault_pki.list_roles mount - The mount path the DB backend is mounted to. Defaults to ``pki``. + The mount path the PKI backend is mounted to. Defaults to ``pki``. """ endpoint = f"{mount}/roles" try: @@ -56,7 +58,7 @@ def read_role(name, mount="pki"): The name of the role. mount - The mount path the DB backend is mounted to. Defaults to ``pki``. + The mount path the PKI backend is mounted to. Defaults to ``pki``. """ endpoint = f"{mount}/roles/{name}" @@ -74,7 +76,7 @@ def read_role(name, mount="pki"): def write_role( name, mount="pki", - issuer_ref="default", + issuer_ref=None, ttl=None, max_ttl=None, allow_localhost=None, @@ -101,7 +103,7 @@ def write_role( The name of the role. mount - The mount path the DB backend is mounted to. Defaults to ``pki``. + The mount path the PKI backend is mounted to. Defaults to ``pki``. issuer_ref Name or id of the issuer which will be used with this role. If not set, default issuer will be used. @@ -153,7 +155,6 @@ def write_role( """ endpoint = f"{mount}/roles/{name}" - method = "POST" headers = {} @@ -163,7 +164,8 @@ def write_role( payload = {k: v for k, v in kwargs.items() if not k.startswith("_")} - payload["issuer_ref"] = issuer_ref + if issuer_ref is not None: + payload["issuer_ref"] = issuer_ref if ttl is not None: payload["ttl"] = ttl if max_ttl is not None: @@ -187,14 +189,15 @@ def write_role( if require_cn is not None: payload["require_cn"] = require_cn - for m in [method, "POST"]: - try: - vault.query(m, endpoint, __opts__, __context__, payload=payload, add_headers=headers) - return True - except vault.VaultUnsupportedOperationError: - continue - except vault.VaultException as err: - raise CommandExecutionError(f"{err.__class__}: {err}") from err + try: + vault.query(method, endpoint, __opts__, __context__, payload=payload, add_headers=headers) + return True + except vault.VaultUnsupportedOperationError as err: + raise CommandExecutionError( + f"Vault version too old. Please upgrade to v1.11.0+: {err}" + ) from err + except vault.VaultException as err: + raise CommandExecutionError(f"{err.__class__}: {err}") from err def delete_role(name, mount="pki"): @@ -213,7 +216,7 @@ def delete_role(name, mount="pki"): The name of the role. mount - The mount path the DB backend is mounted to. Defaults to ``pki``. + The mount path the PKI backend is mounted to. Defaults to ``pki``. """ endpoint = f"{mount}/roles/{name}" @@ -242,7 +245,7 @@ def list_issuers(mount="pki"): salt '*' vault_pki.list_issuers mount - The mount path the DB backend is mounted to. Defaults to ``pki``. + The mount path the PKI backend is mounted to. Defaults to ``pki``. """ endpoint = f"{mount}/issuers" @@ -272,13 +275,13 @@ def read_issuer(ref="default", mount="pki"): which means default issuer. Defaults to ``default``. mount - The mount path the DB backend is mounted to. Defaults to ``pki``. + The mount path the PKI backend is mounted to. Defaults to ``pki``. """ endpoint = f"{mount}/issuer/{ref}/json" try: - return vault.query("GET", endpoint, __opts__, __context__, is_unauthd=False)["data"] + return vault.query("GET", endpoint, __opts__, __context__, is_unauthd=True)["data"] except vault.VaultNotFoundError: return None except vault.VaultException as err: @@ -310,7 +313,7 @@ def update_issuer( which means default issuer. Defaults to ``default``. mount - The mount path the DB backend is mounted to. Defaults to ``pki``. + The mount path the PKI backend is mounted to. Defaults to ``pki``. manual_chain Chain of issuer references to build this issuer's computed CAChain field from, when non-empty. @@ -384,7 +387,7 @@ def read_issuer_certificate(name="default", mount="pki", include_chain=False): which means default issuer. Defaults to ``default``. mount - The mount path the DB backend is mounted to. Defaults to ``pki``. + The mount path the PKI backend is mounted to. Defaults to ``pki``. include_chain If set to true will append the CA chain to the certificate (in case of intermediate issuer) @@ -474,7 +477,7 @@ def generate_root( The common name to be used for the CA mount - The mount path the DB backend is mounted to. Defaults to ``pki``. + The mount path the PKI backend is mounted to. Defaults to ``pki``. type Specifies the type of the root to create. If exported, the private key will be returned in the response; @@ -557,7 +560,7 @@ def delete_key(ref, mount="pki"): Ref of the key. Could be name or key_id. mount - The mount path the DB backend is mounted to. Defaults to ``pki``. + The mount path the PKI backend is mounted to. Defaults to ``pki``. """ endpoint = f"{mount}/key/{ref}" @@ -586,7 +589,7 @@ def delete_issuer(ref, mount="pki"): Ref of the issuer. Could be name or issuer_id. mount - The mount path the DB backend is mounted to. Defaults to ``pki``. + The mount path the PKI backend is mounted to. Defaults to ``pki``. """ endpoint = f"{mount}/issuer/{ref}" @@ -618,7 +621,7 @@ def read_issuer_crl(ref="default", mount="pki", delta=False): Ref of the issuer. Could be name or issuer_id. Defaults to default issuer. mount - The mount path the DB backend is mounted to. Defaults to ``pki``. + The mount path the PKI backend is mounted to. Defaults to ``pki``. delta If set to true, will return delta CRL instead of complete one. @@ -662,7 +665,7 @@ def list_revoked_certificates(mount="pki"): salt '*' vault_pki.list_revoked_certificates mount - The mount path the DB backend is mounted to. Defaults to ``pki``. + The mount path the PKI backend is mounted to. Defaults to ``pki``. """ endpoint = f"{mount}/certs/revoked" @@ -685,7 +688,7 @@ def list_certificates(mount="pki"): salt '*' vault_pki.list_certificates mount - The mount path the DB backend is mounted to. Defaults to ``pki``. + The mount path the PKI backend is mounted to. Defaults to ``pki``. """ endpoint = f"{mount}/certs" @@ -716,7 +719,7 @@ def read_certificate(serial, mount="pki"): ``ca_chain`` for the default issuer's CA trust chain. mount - The mount path the DB backend is mounted to. Defaults to ``pki``. + The mount path the PKI backend is mounted to. Defaults to ``pki``. """ endpoint = f"{mount}/cert/{serial}" @@ -755,7 +758,7 @@ def issue_certificate( Common name to be set for the certificate. mount - The mount path the DB backend is mounted to. Defaults to ``pki``. + The mount path the PKI backend is mounted to. Defaults to ``pki``. issuer Override role's issuer. Can be issuer_name or issuer_id. @@ -839,7 +842,7 @@ def sign_certificate( Common name to be set for the certificate. mount - The mount path the DB backend is mounted to. Defaults to ``pki``. + The mount path the PKI backend is mounted to. Defaults to ``pki``. csr Pass the CSR which will be used for issuing the certificate. Either ``csr`` or ``private_key`` parameter can be set, not both. @@ -955,7 +958,7 @@ def revoke_certificate(serial=None, certificate=None, mount="pki"): Specifies the certificate (PEM or path) to revoke. Either ``serial`` or ``certificate`` must be specified. mount - The mount path the DB backend is mounted to. Defaults to ``pki``. + The mount path the PKI backend is mounted to. Defaults to ``pki``. """ endpoint = f"{mount}/revoke/" payload = {} diff --git a/src/saltext/vault/states/vault_pki.py b/src/saltext/vault/states/vault_pki.py index b61667a4..3fc72e69 100644 --- a/src/saltext/vault/states/vault_pki.py +++ b/src/saltext/vault/states/vault_pki.py @@ -1,21 +1,33 @@ -import copy +import base64 import logging import os -from datetime import datetime -from datetime import timedelta +import salt.utils.files import salt.utils.x509 as x509util from salt.exceptions import CommandExecutionError from salt.exceptions import SaltInvocationError -from salt.state import STATE_INTERNAL_KEYWORDS as _STATE_INTERNAL_KEYWORDS -from salt.utils import context as saltcontext -from saltext.vault.utils.vault.helpers import timestring_to_seconds -from saltext.vault.utils.vault.pki import compare_ca_chain -from saltext.vault.utils.vault.pki import compare_cert_signing -from saltext.vault.utils.vault.pki import encode_certificate + +from saltext.vault.utils.vault.helpers import filter_state_internal_kwargs +from saltext.vault.utils.vault.helpers import timestring_map +from saltext.vault.utils.vault.pki import check_cert_for_changes + +# from saltext.vault.utils.vault.pki import compare_ca_chain +# from saltext.vault.utils.vault.pki import encode_certificate log = logging.getLogger(__name__) +__virtualname__ = "vault_pki" + + +def __virtual__(): + if not __opts__.get("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, @@ -30,7 +42,7 @@ def certificate_managed( sign_verbatim=False, private_key=None, private_key_passphrase=None, - force=False, + reissue=False, **kwargs, ): """ @@ -95,15 +107,21 @@ def certificate_managed( private_key_passphrase Password for the private key if encrypted. - force - If true will force a new certificate to be issued no matter of current validity period. + reissue + If true will force a new certificate to be issued no matter of current state. Not considered, if + file doesn't exists. kwargs Any other parameter accepted by ``file_managed`` execution module or Vault PKI :obj:`sign_certificate ` execution module. """ - if timestring_to_seconds(ttl_remaining) >= timestring_to_seconds(ttl): + if encoding not in ["der", "pem", "pkcs7_der", "pkcs7_pem"]: + raise CommandExecutionError( + f"Invalid value '{encoding}' for encoding. Valid: " "der, pem, pkcs7_der, pkcs7_pem" + ) + + if timestring_map(ttl_remaining, cast=int) >= timestring_map(ttl, cast=int): raise CommandExecutionError("The ttl_remaning cannot be larger or equal to ttl.") ret = { @@ -113,11 +131,10 @@ def certificate_managed( "comment": "The certificate is in the correct state", } - current = current_encoding = None changes = {} ca_chain = [] verb = "create" - file_args, cert_args = _split_file_kwargs(_filter_state_internal_kwargs(kwargs)) + file_args, cert_args = _split_file_kwargs(filter_state_internal_kwargs(kwargs)) try: # check file.managed changes early to avoid using unnecessary resources @@ -132,99 +149,54 @@ def certificate_managed( _add_sub_state_run(ret, file_managed_test) return ret - real_name = os.path.realpath(name) - replace = False - - if force: - replace = True - - if __salt__["file.file_exists"](real_name): - try: - ( - current, - current_encoding, - current_chain, - _, - ) = x509util.load_cert(real_name, passphrase=None, get_encoding=True) - except SaltInvocationError as err: - if any( - ( - "Could not deserialize binary data" in str(err), - "Could not load PEM-encoded" in str(err), - ) - ): - replace = True - else: - raise + # handle follow_symlinks + if __salt__["file.is_link"](name): + if file_args.get("follow_symlinks", True): + name = os.path.realpath(name) else: - if encoding != current_encoding: - changes["encoding"] = { - "old": current_encoding, - "new": encoding, - } + # workaround https://github.com/saltstack/salt/issues/31802 + __salt__["file.remove"](name) + changes["replaced"] = True - for k, v in x509util.NAME_ATTRS_OID.items(): - # We have to ignore anything except CN in case of not sign-verbatim - # as they will be populated as in the role and we cannot control them. - if not sign_verbatim and not k == "CN": - continue - if k in kwargs: - current_attr = current.subject.get_attributes_for_oid(v) - if len(current_attr) > 0: - attr = current_attr[0] - if not kwargs[k] == attr.value: - changes.update( - {"subject": {k: {"old": attr.value, "new": kwargs[k]}}} - ) - else: - changes.update({"subject": {k: {"old": "", "new": kwargs[k]}}}) - - # Check if certificate should be renewed due to close to expiration - if current.not_valid_after < datetime.utcnow() + timedelta( - seconds=timestring_to_seconds(ttl_remaining) - ): - changes["expiration"] = True + replace = False + file_exists = __salt__["file.file_exists"](name) + if file_exists: + if reissue: + # No need to make any checks, just replace the cert + changes["replaced"] = True + else: if issuer is None: issuer = __salt__["vault_pki.read_role"](role_name, mount=mount)["issuer_ref"] issuer_info = __salt__["vault_pki.read_issuer"](issuer, mount=mount) if append_ca_chain: - ca_chain = [x509util.load_cert(x) for x in issuer_info["ca_chain"][:-1]] - for ca in ca_chain: - if ca.subject.rfc4514_string() == ca.issuer.rfc4514_string(): - # Self-signed CA. Shouldn't be in the chain. - ca_chain.remove(ca) - continue - - if not compare_ca_chain(current_chain, ca_chain): - changes["ca_chain"] = True - - ca = x509util.load_cert(issuer_info["certificate"]) - privKey = x509util.load_privkey(private_key, private_key_passphrase) - - changes.update( - compare_cert_signing( - current=current, - signing_ca=ca, - private_key=privKey, - ) + ca_chain = [x509util.load_cert(x) for x in issuer_info["ca_chain"]] + + changes = check_cert_for_changes( + current=name, + append_chain=ca_chain, + common_name=common_name, + encoding=encoding, + issuer=issuer_info["certificate"], + private_key=private_key, + private_key_passphrase=private_key_passphrase, + common_name_only=not sign_verbatim, + expire_tolerance=ttl_remaining, + **cert_args, ) else: - changes["created"] = name - - if replace: - changes["replaced"] = name + changes["create"] = True 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 = "reissued" + if changes and file_exists: + verb = "reissue" if __opts__["test"]: ret["result"] = None if changes else True @@ -240,8 +212,8 @@ def certificate_managed( "encoding", }: verb = "recreated" - cert = encode_certificate( - current, + cert = __salt__["x509.encode_certificate"]( + name, append_certs=ca_chain, encoding=encoding, ) @@ -258,21 +230,29 @@ def certificate_managed( remove_roots_from_chain=False, **cert_args, ) - cert = encode_certificate( + cert = __salt__["x509.encode_certificate"]( issued_cert["certificate"], append_certs=issued_cert["ca_chain"], encoding=encoding, ) - ret["comment"] = f"The certificate has been {verb}d" + 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, name): + return ret + _safe_atomic_write(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): + if not _check_file_ret(file_managed_ret, ret, file_exists): return ret except (CommandExecutionError, SaltInvocationError) as err: @@ -324,8 +304,8 @@ def _diff_params(current): nonlocal issuer_ref, ttl, max_ttl, kwargs diff_params = ( ("issuer_ref", issuer_ref), - ("ttl", timestring_to_seconds(ttl)), - ("max_ttl", timestring_to_seconds(max_ttl)), + ("ttl", timestring_map(ttl, cast=int)), + ("max_ttl", timestring_map(max_ttl, cast=int)), ) changed = {} for param, arg in diff_params: @@ -355,28 +335,35 @@ def _diff_params(current): current = __salt__["vault_pki.read_role"](name, mount=mount) changes = {} - if current: - changes = _diff_params(current) - if not changes: + try: + if current: + changes = _diff_params(current) + if not changes: + return ret + + if __opts__["test"]: + ret["result"] = None + ret["comment"] = ( + f"PKI role `{name}` would have been {'updated' if current else 'created'}" + ) + ret["changes"].update(changes) + if not current: + ret["changes"]["created"] = name return ret - if __opts__["test"]: - ret["result"] = None - ret["comment"] = f"PKI role `{name}` would have been {'updated' if current else 'created'}" - ret["changes"].update(changes) + __salt__["vault_pki.write_role"]( + name=name, mount=mount, issuer_ref=issuer_ref, ttl=ttl, max_ttl=max_ttl, **kwargs + ) + if not current: ret["changes"]["created"] = name - return ret - - __salt__["vault_pki.write_role"]( - name=name, mount=mount, issuer_ref=issuer_ref, ttl=ttl, max_ttl=max_ttl, **kwargs - ) - - if not current: - ret["changes"]["created"] = name - ret["changes"].update(changes) - ret["comment"] = f"PKI role `{name}` has been {'updated' if current else 'created'}" + ret["changes"].update(changes) + ret["comment"] = f"PKI role `{name}` has been {'updated' if current else 'created'}" + except (CommandExecutionError, SaltInvocationError) as err: + ret["result"] = False + ret["comment"] = str(err) + ret["changes"] = {} return ret @@ -426,12 +413,6 @@ def role_absent(name, mount="pki"): 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", @@ -481,13 +462,23 @@ 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 - opts = copy.deepcopy(__opts__) - opts["test"] = test or __opts__["test"] + test = test or __opts__["test"] + res = __salt__["state.single"]("file.managed", name, test=test, **kwargs) + return res[next(iter(res))] + - file_managed = __states__["file.managed"] - with saltcontext.func_globals_inject(file_managed, __opts__=opts): - with saltcontext.func_globals_inject(__salt__["file.manage_file"], __opts__=opts): - return file_managed(name, **kwargs) +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) def _check_file_ret(fret, ret, current): diff --git a/src/saltext/vault/utils/vault/helpers.py b/src/saltext/vault/utils/vault/helpers.py index b95c445d..008a87d1 100644 --- a/src/saltext/vault/utils/vault/helpers.py +++ b/src/saltext/vault/utils/vault/helpers.py @@ -8,6 +8,7 @@ from salt.exceptions import InvalidConfigError from salt.exceptions import SaltInvocationError +from salt.state import STATE_INTERNAL_KEYWORDS as _STATE_INTERNAL_KEYWORDS SALT_RUNTYPE_MASTER = 0 SALT_RUNTYPE_MASTER_IMPERSONATING = 1 @@ -128,23 +129,23 @@ def expand_pattern_lists(pattern, **mappings): return [pattern] -def timestring_map(val): +def timestring_map(val, cast=float): """ Turn a time string (like ``60m``) into a float with seconds as a unit. """ if val is None: return val if isinstance(val, (int, float)): - return float(val) + return cast(val) try: - return float(val) + return cast(val) except ValueError: pass if not isinstance(val, str): raise SaltInvocationError("Expected integer or time string") if not re.match(r"^\d+(?:\.\d+)?[smhd]$", val): raise SaltInvocationError(f"Invalid time string format: {val}") - raw, unit = float(val[:-1]), val[-1] + raw, unit = cast(val[:-1]), val[-1] if unit == "s": return raw raw *= 60 @@ -159,32 +160,7 @@ def timestring_map(val): raise RuntimeError("This path should not have been hit") -def timestring_to_seconds(val): - """ - Turn a time string (like ``60m``) into a integer with seconds as a unit. - """ - if val is None: - return val - if isinstance(val, (int, float)): - return int(val) - try: - return int(val) - except ValueError: - pass - if not isinstance(val, str): - raise SaltInvocationError("Expected integer or time string") - if not re.match(r"^\d+(?:\.\d+)?[smhd]$", val): - raise SaltInvocationError(f"Invalid time string format: {val}") - raw, unit = int(val[:-1]), val[-1] - if unit == "s": - return raw - raw *= 60 - if unit == "m": - return raw - raw *= 60 - if unit == "h": - return raw - raw *= 24 - if unit == "d": - return raw - raise RuntimeError("This path should not have been hit") +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} diff --git a/src/saltext/vault/utils/vault/pki.py b/src/saltext/vault/utils/vault/pki.py index 722584fd..4efc3c66 100644 --- a/src/saltext/vault/utils/vault/pki.py +++ b/src/saltext/vault/utils/vault/pki.py @@ -1,8 +1,114 @@ +from datetime import datetime +from datetime import timedelta +from datetime import timezone + import salt.utils.x509 as x509util from cryptography import x509 as cx509 from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives import serialization from salt.exceptions import CommandExecutionError +from salt.exceptions import SaltInvocationError + +from saltext.vault.utils.vault.helpers import timestring_map + + +def check_cert_for_changes( + current, + issuer, + private_key, + common_name: str, + encoding="pem", + common_name_only=False, + append_chain=None, + private_key_passphrase=None, + expire_tolerance=None, + **kwargs, +) -> dict: + + changes = {} + + expire_tolerance = expire_tolerance or 0 + + append_chain = append_chain or [] + if not isinstance(append_chain, list): + append_chain = [append_chain] + + try: + ( + current, + current_encoding, + current_chain, + _, + ) = x509util.load_cert(current, passphrase=None, get_encoding=True) + except SaltInvocationError as err: + if any( + ( + "Could not deserialize binary data" in str(err), + "Could not load PEM-encoded" in str(err), + ) + ): + changes["replaced"] = True + return changes + else: + raise + + if encoding != current_encoding: + changes["encoding"] = { + "old": current_encoding, + "new": encoding, + } + + # Check common_name. This is always checked as a major + # and required attribute for each certificate. + current_cn = current.subject.get_attributes_for_oid(x509util.NAME_ATTRS_OID["CN"])[0].value + if not current_cn == common_name: + changes.update({"subject": {"CN": {"old": current_cn, "new": common_name}}}) + + # If we need to compare Common Name only we can skip this one + if not common_name_only: + for k, v in x509util.NAME_ATTRS_OID.items(): + # Just in case ignore CN attribute if passed by mistake + if k == "CN": + continue + if k in kwargs: + current_attr = current.subject.get_attributes_for_oid(v) + if len(current_attr) > 0: + attr = current_attr[0] + if not kwargs[k] == attr.value: + changes.update({"subject": {k: {"old": attr.value, "new": kwargs[k]}}}) + else: + changes.update({"subject": {k: {"old": "", "new": kwargs[k]}}}) + + append_chain = [x509util.load_cert(x) for x in append_chain] + for ca in append_chain: + if ca.subject.rfc4514_string() == ca.issuer.rfc4514_string(): + # Self-signed CA. Shouldn't be in the chain. + append_chain.remove(ca) + continue + + if not compare_ca_chain(current_chain, append_chain): + changes["ca_chain"] = True + + ca = x509util.load_cert(issuer) + privKey = x509util.load_privkey(private_key, private_key_passphrase) + + changes.update( + compare_cert_signing( + current=current, + signing_ca=ca, + private_key=privKey, + ) + ) + + # Check if certificate should be renewed due to close to expiration + if current.not_valid_after_utc < datetime.now(timezone.utc) + timedelta( + seconds=timestring_map(expire_tolerance, cast=int) + ): + changes["expiration"] = { + "expire_in": (current.not_valid_after_utc - datetime.now(timezone.utc)).total_seconds(), + "toleration": timestring_map(expire_tolerance, cast=int), + } + + return changes def compare_cert_signing(current: cx509.Certificate, signing_ca: cx509.Certificate, private_key): @@ -33,32 +139,6 @@ def compare_ca_chain(current, new): return True -def encode_certificate( - certificate, - encoding="pem", - append_certs=None, -): - if encoding not in ["pem", "pkcs7_pem"]: - raise CommandExecutionError( - f"Invalid value '{encoding}' for encoding. Valid: " "pem, pkcs7_pem" - ) - - append_certs = append_certs or [] - if not isinstance(append_certs, list): - append_certs = [append_certs] - - cert = x509util.load_cert(certificate) - append_certs = [x509util.load_cert(x) for x in append_certs] - - crt_encoding = getattr(serialization.Encoding, encoding.upper()) - crt_bytes = cert.public_bytes(crt_encoding) - for append_cert in append_certs: - # this can only happen for PEM, checked in the beginning - crt_bytes += b"\n" + append_cert.public_bytes(crt_encoding) - - return crt_bytes.decode() - - def _getattr_safe(obj, attr): try: return getattr(obj, attr)