From 0ff2d2b7a8068883e075969226b51ac4873aef39 Mon Sep 17 00:00:00 2001 From: jeanluc Date: Thu, 2 Feb 2023 17:07:31 +0100 Subject: [PATCH] Add signature verification to file.managed/archive.extracted --- changelog/63143.added | 1 + salt/modules/file.py | 248 ++++++++++++++++++ salt/states/archive.py | 133 +++++++++- salt/states/file.py | 125 +++++++++ .../files/file/base/custom.tar.gz.SHA256 | 1 + .../files/file/base/custom.tar.gz.SHA256.asc | 8 + .../base/custom.tar.gz.SHA256.clearsign.asc | 12 + .../files/file/base/custom.tar.gz.asc | 8 + .../files/file/base/grail/scene33.SHA256 | 1 + .../files/file/base/grail/scene33.SHA256.asc | 17 ++ .../base/grail/scene33.SHA256.clearsign.asc | 12 + .../files/file/base/grail/scene33.asc | 17 ++ .../file/base/grail/scene33.clearsign.asc | 107 ++++++++ .../functional/states/file/conftest.py | 10 + .../functional/states/file/test_managed.py | 229 +++++++++++++++- .../pytests/functional/states/test_archive.py | 230 ++++++++++++++++ 16 files changed, 1155 insertions(+), 4 deletions(-) create mode 100644 changelog/63143.added create mode 100644 tests/integration/files/file/base/custom.tar.gz.SHA256 create mode 100644 tests/integration/files/file/base/custom.tar.gz.SHA256.asc create mode 100644 tests/integration/files/file/base/custom.tar.gz.SHA256.clearsign.asc create mode 100644 tests/integration/files/file/base/custom.tar.gz.asc create mode 100644 tests/integration/files/file/base/grail/scene33.SHA256 create mode 100644 tests/integration/files/file/base/grail/scene33.SHA256.asc create mode 100644 tests/integration/files/file/base/grail/scene33.SHA256.clearsign.asc create mode 100644 tests/integration/files/file/base/grail/scene33.asc create mode 100644 tests/integration/files/file/base/grail/scene33.clearsign.asc diff --git a/changelog/63143.added b/changelog/63143.added new file mode 100644 index 00000000000..f8e8d115a7a --- /dev/null +++ b/changelog/63143.added @@ -0,0 +1 @@ +Added signature verification to file.managed/archive.extraced diff --git a/salt/modules/file.py b/salt/modules/file.py index 47bdf410ac4..3c76e1bd9bd 100644 --- a/salt/modules/file.py +++ b/salt/modules/file.py @@ -766,6 +766,11 @@ def get_source_sum( source_hash_name=None, saltenv="base", verify_ssl=True, + source_hash_sig=None, + signed_by_any=None, + signed_by_all=None, + keyring=None, + gnupghome=None, ): """ .. versionadded:: 2016.11.0 @@ -806,6 +811,39 @@ def get_source_sum( .. versionadded:: 3002 + source_hash_sig + When ``source_hash`` is a file, ensure a valid GPG signature exists on + the source hash file. + Set this to ``true`` for an inline (clearsigned) signature, or to a file URI + retrievable by :py:func:`cp.cache_file ` + for a detached one. + + .. versionadded:: 3007 + + signed_by_any + When verifying ``source_hash_sig``, require at least one valid signature + from one of a list of key fingerprints. This is passed to :py:func:`gpg.verify + `. + + .. versionadded:: 3007 + + signed_by_all + When verifying ``source_hash_sig``, require a valid signature from each + of the key fingerprints in this list. This is passed to :py:func:`gpg.verify + `. + + .. versionadded:: 3007 + + keyring + When verifying ``source_hash_sig``, use this keyring. + + .. versionadded:: 3007 + + gnupghome + When verifying ``source_hash_sig``, use this GnuPG home. + + .. versionadded:: 3007 + CLI Example: .. code-block:: bash @@ -846,6 +884,20 @@ def get_source_sum( raise CommandExecutionError( f"Source hash file {source_hash} not found" ) + if source_hash_sig: + _check_sig( + hash_fn, + signature=source_hash_sig + if isinstance(source_hash_sig, str) + else None, + signed_by_any=signed_by_any, + signed_by_all=signed_by_all, + keyring=keyring, + gnupghome=gnupghome, + saltenv=saltenv, + verify_ssl=verify_ssl, + ) + else: if proto != "": # Some unsupported protocol (e.g. foo://) is being used. @@ -967,6 +1019,54 @@ def check_hash(path, file_hash): return get_hash(path, hash_type) == hash_value +def _check_sig( + on_file, + signature=None, + signed_by_any=None, + signed_by_all=None, + keyring=None, + gnupghome=None, + saltenv="base", + verify_ssl=True, +): + try: + verify = __salt__["gpg.verify"] + except KeyError: + raise CommandExecutionError( + "Signature verification requires the gpg module, " + "which could not be found. Make sure you have the " + "necessary tools and libraries intalled (gpg, python-gnupg)" + ) + sig = None + if signature is not None: + # Fetch detached signature + sig = __salt__["cp.cache_file"](signature, saltenv, verify_ssl=verify_ssl) + if not sig: + raise CommandExecutionError( + f"Detached signature file {signature} not found" + ) + + res = verify( + filename=on_file, + signature=sig, + keyring=keyring, + gnupghome=gnupghome, + signed_by_any=signed_by_any, + signed_by_all=signed_by_all, + ) + + if res["res"] is True: + return + # Ensure detached signature and file are deleted from cache + # on signature verification failure. + if sig: + salt.utils.files.safe_rm(sig) + salt.utils.files.safe_rm(on_file) + raise CommandExecutionError( + f"The file's signature could not be verified: {res['message']}" + ) + + def find(path, *args, **kwargs): """ Approximate the Unix ``find(1)`` command and return a list of paths that @@ -4595,6 +4695,11 @@ def get_managed( skip_verify=False, verify_ssl=True, use_etag=False, + source_hash_sig=None, + signed_by_any=None, + signed_by_all=None, + keyring=None, + gnupghome=None, **kwargs, ): """ @@ -4660,6 +4765,39 @@ def get_managed( .. versionadded:: 3005 + source_hash_sig + When ``source_hash`` is a file and ``skip_verify`` is not true and ``use_etag`` + is not true, ensure a valid GPG signature exists on the source hash file. + Set this to ``true`` for an inline (clearsigned) signature, or to a file URI + retrievable by ``cp.cache_file`` for a detached one. The cached file + will be deleted if the signature verification fails. + + .. versionadded:: 3007 + + signed_by_any + When verifying ``source_hash_sig``, require at least one valid signature + from one of a list of key fingerprints. This is passed to :py:func:`gpg.verify + `. + + .. versionadded:: 3007 + + signed_by_all + When verifying ``source_hash_sig``, require a valid signature from each + of the key fingerprints in this list. This is passed to :py:func:`gpg.verify + `. + + .. versionadded:: 3007 + + keyring + When verifying ``source_hash_sig``, use this keyring. + + .. versionadded:: 3007 + + gnupghome + When verifying ``source_hash_sig``, use this GnuPG home. + + .. versionadded:: 3007 + CLI Example: .. code-block:: bash @@ -4728,6 +4866,11 @@ def get_managed( source_hash_name, saltenv, verify_ssl=verify_ssl, + source_hash_sig=source_hash_sig, + signed_by_any=signed_by_any, + signed_by_all=signed_by_all, + keyring=keyring, + gnupghome=gnupghome, ) except CommandExecutionError as exc: return "", {}, exc.strerror @@ -5978,6 +6121,12 @@ def manage_file( serange=None, verify_ssl=True, use_etag=False, + signature=None, + source_hash_sig=None, + signed_by_any=None, + signed_by_all=None, + keyring=None, + gnupghome=None, **kwargs, ): """ @@ -6107,6 +6256,53 @@ def manage_file( .. versionadded:: 3005 + signature + Ensure a valid GPG signature exists on the selected ``source`` file. + Set this to true for inline signatures, or to a file URI retrievable by + ``cp.cache_file`` for a detached one. The cached file will be deleted + if the signature verification fails. + + .. note:: + + This signature will be enforced regardless of source type and will be + required on the final output, therefore this does not lend itself well + when templates are rendered. + + .. versionadded:: 3007 + + source_hash_sig + When ``source_hash`` is a file and ``skip_verify`` is not true and ``use_etag`` + is not true, ensure a valid GPG signature exists on the source hash file. + Set this to ``true`` for an inline (clearsigned) signature, or to a file URI + retrievable by ``cp.cache_file`` for a detached one. The cached file + will be deleted if the signature verification fails. + + .. versionadded:: 3007 + + signed_by_any + When verifying signatures either on the managed file or its source hash file, + require at least one valid signature from one of a list of key fingerprints. + This is passed to :py:func:`gpg.verify `. + + .. versionadded:: 3007 + + signed_by_all + When verifying signatures either on the managed file or its source hash file, + require a valid signature from each of the key fingerprints in this list. + This is passed to :py:func:`gpg.verify `. + + .. versionadded:: 3007 + + keyring + When verifying signatures, use this keyring. + + .. versionadded:: 3007 + + gnupghome + When verifying signatures, use this GnuPG home. + + .. versionadded:: 3007 + CLI Example: .. code-block:: bash @@ -6192,6 +6388,23 @@ def manage_file( ret["result"] = False return ret + if signature: + try: + _check_sig( + sfn, + signature=signature if isinstance(signature, str) else None, + signed_by_any=signed_by_any, + signed_by_all=signed_by_all, + keyring=keyring, + gnupghome=gnupghome, + saltenv=saltenv, + verify_ssl=verify_ssl, + ) + except CommandExecutionError as err: + ret["result"] = False + ret["comment"] = f"Failed checking new file's signature: {err}" + return ret + # Print a diff equivalent to diff -u old new if __salt__["config.option"]("obfuscate_templates"): ret["changes"]["diff"] = "" @@ -6286,6 +6499,23 @@ def manage_file( ret["result"] = False return ret + if signature: + try: + _check_sig( + sfn, + signature=signature if isinstance(signature, str) else None, + signed_by_any=signed_by_any, + signed_by_all=signed_by_all, + keyring=keyring, + gnupghome=gnupghome, + saltenv=saltenv, + verify_ssl=verify_ssl, + ) + except CommandExecutionError as err: + ret["result"] = False + ret["comment"] = f"Failed checking new file's signature: {err}" + return ret + try: salt.utils.files.copyfile( sfn, @@ -6394,6 +6624,24 @@ def manage_file( ) ret["result"] = False return ret + + if signature: + try: + _check_sig( + sfn, + signature=signature if isinstance(signature, str) else None, + signed_by_any=signed_by_any, + signed_by_all=signed_by_all, + keyring=keyring, + gnupghome=gnupghome, + saltenv=saltenv, + verify_ssl=verify_ssl, + ) + except CommandExecutionError as err: + ret["result"] = False + ret["comment"] = f"Failed checking new file's signature: {err}" + return ret + # It is a new file, set the diff accordingly ret["changes"]["diff"] = "New file" if not os.path.isdir(contain_dir): diff --git a/salt/states/archive.py b/salt/states/archive.py index f3405b6ea0a..179fa5eff2f 100644 --- a/salt/states/archive.py +++ b/salt/states/archive.py @@ -165,6 +165,52 @@ def _cleanup_destdir(name): pass +def _check_sig( + on_file, + signature, + signed_by_any=None, + signed_by_all=None, + keyring=None, + gnupghome=None, +): + try: + verify = __salt__["gpg.verify"] + except KeyError: + raise CommandExecutionError( + "Signature verification requires the gpg module, " + "which could not be found. Make sure you have the " + "necessary tools and libraries intalled (gpg, python-gnupg)" + ) + sig = None + if signature is not None: + # fetch detached signature + sig = __salt__["cp.cache_file"](signature, __env__) + if not sig: + raise CommandExecutionError( + f"Detached signature file {signature} not found" + ) + + res = verify( + filename=on_file, + signature=sig, + keyring=keyring, + gnupghome=gnupghome, + signed_by_any=signed_by_any, + signed_by_all=signed_by_all, + ) + + if res["res"] is True: + return + # Ensure detached signature and file are deleted from cache + # on signature verification failure. + if sig: + salt.utils.files.safe_rm(sig) + salt.utils.files.safe_rm(on_file) + raise CommandExecutionError( + f"The file's signature could not be verified: {res['message']}" + ) + + def extracted( name, source, @@ -190,7 +236,13 @@ def extracted( enforce_ownership_on=None, archive_format=None, use_etag=False, - **kwargs + signature=None, + source_hash_sig=None, + signed_by_any=None, + signed_by_all=None, + keyring=None, + gnupghome=None, + **kwargs, ): """ .. versionadded:: 2014.1.0 @@ -671,6 +723,47 @@ def extracted( .. versionadded:: 3005 + signature + Ensure a valid GPG signature exists on the selected ``source`` file. + This needs to be a file URI retrievable by ``cp.cache_file`` which + identifies a detached signature. + This signature will be enforced regardless of source type. + + .. versionadded:: 3007 + + source_hash_sig + When ``source_hash`` is a file and ``skip_verify`` is not true and ``use_etag`` + is not true, ensure a valid GPG signature exists on the source hash file. + Set this to ``true`` for an inline (clearsigned) signature, or to a file URI + retrievable by ``cp.cache_file`` for a detached one. The cached file + will be deleted if the signature verification fails. + + .. versionadded:: 3007 + + signed_by_any + When verifying signatures either on the managed file or its source hash file, + require at least one valid signature from one of a list of key fingerprints. + This is passed to ``gpg.verify``. + + .. versionadded:: 3007 + + signed_by_all + When verifying signatures either on the managed file or its source hash file, + require a valid signature from each of the key fingerprints in this list. + This is passed to ``gpg.verify``. + + .. versionadded:: 3007 + + keyring + When verifying signatures, use this keyring. + + .. versionadded:: 3007 + + gnupghome + When verifying signatures, use this GnuPG home. + + .. versionadded:: 3007 + **Examples** 1. tar with lmza (i.e. xz) compression: @@ -824,6 +917,16 @@ def extracted( "'source_hash' is not also specified." ) + if signature or source_hash_sig: + # Fail early in case the gpg module is not present + try: + __salt__["gpg.verify"] + except KeyError: + ret[ + "comment" + ] = "Cannot verify signatures because the gpg module was not loaded" + return ret + try: source_match = __salt__["file.source_list"](source, source_hash, __env__)[0] except CommandExecutionError as exc: @@ -983,6 +1086,11 @@ def extracted( source_hash=source_hash, source_hash_name=source_hash_name, saltenv=__env__, + source_hash_sig=source_hash_sig, + signed_by_any=signed_by_any, + signed_by_all=signed_by_all, + keyring=keyring, + gnupghome=gnupghome, ) except CommandExecutionError as exc: ret["comment"] = exc.strerror @@ -1063,6 +1171,11 @@ def extracted( skip_verify=skip_verify, saltenv=__env__, use_etag=use_etag, + source_hash_sig=source_hash_sig, + signed_by_any=signed_by_any, + signed_by_all=signed_by_all, + keyring=keyring, + gnupghome=gnupghome, ) except Exception as exc: # pylint: disable=broad-except msg = "Failed to cache {}: {}".format( @@ -1084,6 +1197,20 @@ def extracted( ) return result + if signature: + try: + _check_sig( + cached, + signature, + signed_by_any=signed_by_any, + signed_by_all=signed_by_all, + keyring=keyring, + gnupghome=gnupghome, + ) + except CommandExecutionError as err: + ret["comment"] = f"Failed verifying the source file's signature: {err}" + return ret + existing_cached_source_sum = _read_cached_checksum(cached) if source_hash and source_hash_update and not skip_verify: @@ -1396,7 +1523,7 @@ def extracted( options=options, trim_output=trim_output, password=password, - **kwargs + **kwargs, ) except (CommandExecutionError, CommandNotFoundError) as exc: ret["comment"] = exc.strerror @@ -1409,7 +1536,7 @@ def extracted( trim_output=trim_output, password=password, extract_perms=extract_perms, - **kwargs + **kwargs, ) elif archive_format == "rar": try: diff --git a/salt/states/file.py b/salt/states/file.py index ad036acea27..cde3395c647 100644 --- a/salt/states/file.py +++ b/salt/states/file.py @@ -2315,6 +2315,12 @@ def managed( win_perms_reset=False, verify_ssl=True, use_etag=False, + signature=None, + source_hash_sig=None, + signed_by_any=None, + signed_by_all=None, + keyring=None, + gnupghome=None, **kwargs, ): r""" @@ -2911,6 +2917,54 @@ def managed( the ``source_hash`` parameter. .. versionadded:: 3005 + + signature + Ensure a valid GPG signature exists on the selected ``source`` file. + Set this to true for inline signatures, or to a file URI retrievable by + ``cp.cache_file`` for a detached one. + + .. note:: + + This signature will be enforced regardless of source type and will be + required on the final output, therefore this does not lend itself well + when templates are rendered. + The file will not be modified, meaning inline signatures are not + removed. + + .. versionadded:: 3007 + + source_hash_sig + When ``source_hash`` is a file and ``skip_verify`` is not true and ``use_etag`` + is not true, ensure a valid GPG signature exists on the source hash file. + Set this to ``true`` for an inline (clearsigned) signature, or to a file URI + retrievable by ``cp.cache_file`` for a detached one. The cached file + will be deleted if the signature verification fails. + + .. versionadded:: 3007 + + signed_by_any + When verifying signatures either on the managed file or its source hash file, + require at least one valid signature from one of a list of key fingerprints. + This is passed to ``gpg.verify``. + + .. versionadded:: 3007 + + signed_by_all + When verifying signatures either on the managed file or its source hash file, + require a valid signature from each of the key fingerprints in this list. + This is passed to ``gpg.verify``. + + .. versionadded:: 3007 + + keyring + When verifying signatures, use this keyring. + + .. versionadded:: 3007 + + gnupghome + When verifying signatures, use this GnuPG home. + + .. versionadded:: 3007 """ if "env" in kwargs: # "env" is not supported; Use "saltenv". @@ -2932,6 +2986,15 @@ def managed( if selinux is not None and not salt.utils.platform.is_linux(): return _error(ret, "The 'selinux' option is only supported on Linux") + if signature or source_hash_sig: + # Fail early in case the gpg module is not present + try: + __salt__["gpg.verify"] + except KeyError: + _error( + ret, "Cannot verify signatures because the gpg module was not loaded" + ) + if selinux: seuser = selinux.get("seuser", None) serole = selinux.get("serole", None) @@ -3220,6 +3283,11 @@ def managed( serange=serange, verify_ssl=verify_ssl, follow_symlinks=follow_symlinks, + source_hash_sig=source_hash_sig, + signed_by_any=signed_by_any, + signed_by_all=signed_by_all, + keyring=keyring, + gnupghome=gnupghome, **kwargs, ) @@ -3283,6 +3351,11 @@ def managed( skip_verify, verify_ssl=verify_ssl, use_etag=use_etag, + source_hash_sig=source_hash_sig, + signed_by_any=signed_by_any, + signed_by_all=signed_by_all, + keyring=keyring, + gnupghome=gnupghome, **kwargs, ) except Exception as exc: # pylint: disable=broad-except @@ -3338,6 +3411,11 @@ def managed( setype=setype, serange=serange, use_etag=use_etag, + signature=signature, + signed_by_any=signed_by_any, + signed_by_all=signed_by_all, + keyring=keyring, + gnupghome=gnupghome, **kwargs, ) except Exception as exc: # pylint: disable=broad-except @@ -3417,6 +3495,11 @@ def managed( setype=setype, serange=serange, use_etag=use_etag, + signature=signature, + signed_by_any=signed_by_any, + signed_by_all=signed_by_all, + keyring=keyring, + gnupghome=gnupghome, **kwargs, ) except Exception as exc: # pylint: disable=broad-except @@ -8939,6 +9022,11 @@ def cached( skip_verify=False, saltenv="base", use_etag=False, + source_hash_sig=None, + signed_by_any=None, + signed_by_all=None, + keyring=None, + gnupghome=None, ): """ .. versionadded:: 2017.7.3 @@ -8996,6 +9084,38 @@ def cached( .. versionadded:: 3005 + source_hash_sig + When ``source_hash`` is a file and ``skip_verify`` is not true and ``use_etag`` + is not true, ensure a valid GPG signature exists on the source hash file. + Set this to ``true`` for an inline (clearsigned) signature, or to a file URI + retrievable by ``cp.cache_file`` for a detached one. The cached file + will be deleted if the signature verification fails. + + .. versionadded:: 3007 + + signed_by_any + When verifying signatures either on the managed file or its source hash file, + require at least one valid signature from one of a list of key fingerprints. + This is passed to ``gpg.verify``. + + .. versionadded:: 3007 + + signed_by_all + When verifying signatures either on the managed file or its source hash file, + require a valid signature from each of the key fingerprints in this list. + This is passed to ``gpg.verify``. + + .. versionadded:: 3007 + + keyring + When verifying signatures, use this keyring. + + .. versionadded:: 3007 + + gnupghome + When verifying signatures, use this GnuPG home. + + .. versionadded:: 3007 This state will in most cases not be useful in SLS files, but it is useful when writing a state or remote-execution module that needs to make sure @@ -9062,6 +9182,11 @@ def cached( source_hash=source_hash, source_hash_name=source_hash_name, saltenv=saltenv, + source_hash_sig=source_hash_sig, + signed_by_any=signed_by_any, + signed_by_all=signed_by_all, + keyring=keyring, + gnupghome=gnupghome, ) except CommandExecutionError as exc: ret["comment"] = exc.strerror diff --git a/tests/integration/files/file/base/custom.tar.gz.SHA256 b/tests/integration/files/file/base/custom.tar.gz.SHA256 new file mode 100644 index 00000000000..b3d793cb41a --- /dev/null +++ b/tests/integration/files/file/base/custom.tar.gz.SHA256 @@ -0,0 +1 @@ +9591159d86f0a180e4e0645b2320d0235e23e66c66797df61508bf185e0ac1d2 custom.tar.gz diff --git a/tests/integration/files/file/base/custom.tar.gz.SHA256.asc b/tests/integration/files/file/base/custom.tar.gz.SHA256.asc new file mode 100644 index 00000000000..22303afe43b --- /dev/null +++ b/tests/integration/files/file/base/custom.tar.gz.SHA256.asc @@ -0,0 +1,8 @@ +-----BEGIN PGP SIGNATURE----- + +iLMEAAEIAB0WIQT4zMjLXh2IaNqlhZqx+apXxJfnHAUCY9vQUwAKCRCx+apXxJfn +HPoFA/9NbyrFi+PtSXFsVqetAc4iLQVjgYoaq2UnJNkYg9peD+lvz0qdYpWVh/4E +z5Etr3ggs+2Ff5JEpMDBopyTxsE24Dfrk64JML5s+l9VFnbOgH3QBYzDtxHqBmG6 +CymTEWsFkdsWSzzBoVkym28FEJPH7q/grAtjDS9FZqqBsD4mfg== +=/ffy +-----END PGP SIGNATURE----- diff --git a/tests/integration/files/file/base/custom.tar.gz.SHA256.clearsign.asc b/tests/integration/files/file/base/custom.tar.gz.SHA256.clearsign.asc new file mode 100644 index 00000000000..117ed3b172f --- /dev/null +++ b/tests/integration/files/file/base/custom.tar.gz.SHA256.clearsign.asc @@ -0,0 +1,12 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA256 + +9591159d86f0a180e4e0645b2320d0235e23e66c66797df61508bf185e0ac1d2 custom.tar.gz +-----BEGIN PGP SIGNATURE----- + +iLMEAQEIAB0WIQT4zMjLXh2IaNqlhZqx+apXxJfnHAUCY9vQKwAKCRCx+apXxJfn +HKmyBADACOiaroBrJKmqy0vqrySy7iTiMdTAv75YheaXLeFgUrdktfWDzNU1kFkB +VTZRyX+og8yi7601ls1jpuDHyBz2sT93zOWvw2iIdYBRKSenh+fHS0aAeRN1Zc9b +DzivTpN6CBVJFpBBxF8Ro/gob9Pek1+rqVFt12azCdZH/hlsOw== +=9QTd +-----END PGP SIGNATURE----- diff --git a/tests/integration/files/file/base/custom.tar.gz.asc b/tests/integration/files/file/base/custom.tar.gz.asc new file mode 100644 index 00000000000..b773428d04e --- /dev/null +++ b/tests/integration/files/file/base/custom.tar.gz.asc @@ -0,0 +1,8 @@ +-----BEGIN PGP SIGNATURE----- + +iLMEAAEIAB0WIQT4zMjLXh2IaNqlhZqx+apXxJfnHAUCY9vP4wAKCRCx+apXxJfn +HCOLA/sEH71Nph/3HVcmXfey9Fv58ekoJ16eKyfiYziBLH/mpwsdE9m3Hp7VHKlb +I0mnwsbmQnOGytsSLeepa5kwUOXw15z523N2egMXdcxyv3Vn0RRVwFw3MCd7pWRr +p8xmp2RLGuQjzHA+ymVmFiEfna2l3EknISHktMXv46jCWLL/HQ== +=mJSJ +-----END PGP SIGNATURE----- diff --git a/tests/integration/files/file/base/grail/scene33.SHA256 b/tests/integration/files/file/base/grail/scene33.SHA256 new file mode 100644 index 00000000000..bf8667bf63d --- /dev/null +++ b/tests/integration/files/file/base/grail/scene33.SHA256 @@ -0,0 +1 @@ +2689de4b07720aafe8f709619f142021ae8dbdd2742a3c86701cd794f97f506c scene33 diff --git a/tests/integration/files/file/base/grail/scene33.SHA256.asc b/tests/integration/files/file/base/grail/scene33.SHA256.asc new file mode 100644 index 00000000000..b9bef093a8d --- /dev/null +++ b/tests/integration/files/file/base/grail/scene33.SHA256.asc @@ -0,0 +1,17 @@ +-----BEGIN PGP SIGNATURE----- + +iLMEAAEIAB0WIQT4zMjLXh2IaNqlhZqx+apXxJfnHAUCY9p1FwAKCRCx+apXxJfn +HBb4BACEdEmETIy6bo16qS+vh8U4WC0V6/toslO5dokBpKAaD2Xg1+mtyaUhmXTu +e6OCxqMGxVbUgmOpo4r4TX+FeqASQVNB4Kk9urwUuSa1FKZTngm+bKGnFBLbJKjm +SKZBmmvtc4iIUWZtucLJWgzbD2bv/fcEI8A/8euSrfM1ArWQHw== +=BsFN +-----END PGP SIGNATURE----- + +-----BEGIN PGP SIGNATURE----- + +iLMEAAEIAB0WIQSN8J3lSrZ/YDHXHW8h5Z/XBbOHgQUCY9p1KAAKCRAh5Z/XBbOH +gaoTA/0V75HNnbA/+nuaw7pBJr1EBUh74+qAb1QQvqNrNdJhgZGLlEwz43kPopUr +MxFVBaz82lQ4nxaZXT/06trjNqnaacLcvRD67iwCPTBO3UR5AEfjZlP1ahAkCRQD +DF6gLHkm00u7hXEBH3rfp5lkWt0sdHLzuiQ6YVIAwOP412fpoQ== +=2cF/ +-----END PGP SIGNATURE----- diff --git a/tests/integration/files/file/base/grail/scene33.SHA256.clearsign.asc b/tests/integration/files/file/base/grail/scene33.SHA256.clearsign.asc new file mode 100644 index 00000000000..6daa2d11731 --- /dev/null +++ b/tests/integration/files/file/base/grail/scene33.SHA256.clearsign.asc @@ -0,0 +1,12 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA256 + +2689de4b07720aafe8f709619f142021ae8dbdd2742a3c86701cd794f97f506c scene33 +-----BEGIN PGP SIGNATURE----- + +iLMEAQEIAB0WIQT4zMjLXh2IaNqlhZqx+apXxJfnHAUCY9p0PAAKCRCx+apXxJfn +HP9QBADbGuPV6iFeij6ArezoFkfp2CTUT/wX94KXeXuKGh/8o2wH3xPRx5cDj04E +L+iU6N/MTAfVwAF+aJwRJt1pjSGWLYnUHLqxRDGRq5CTliWksLCjsV2MK0dAEU58 +952bfcFq50ECOYq316Fjg/0cizaVcqyqOtwy+2ggGloqWUjkEA== +=LR1/ +-----END PGP SIGNATURE----- diff --git a/tests/integration/files/file/base/grail/scene33.asc b/tests/integration/files/file/base/grail/scene33.asc new file mode 100644 index 00000000000..fdce4908a0d --- /dev/null +++ b/tests/integration/files/file/base/grail/scene33.asc @@ -0,0 +1,17 @@ +-----BEGIN PGP SIGNATURE----- + +iLMEAAEIAB0WIQT4zMjLXh2IaNqlhZqx+apXxJfnHAUCY9p0owAKCRCx+apXxJfn +HJVmA/0QXQ8C/+XoUqzTh94+NQ8+hwaKz44LFKAAMfGAHUdKx6qGVa0rv9nIfRJT +O+4XgKiqOeUxtF4sXL9nNu5zuPkW3+prfEum1hAC/owm6z0O1WFvO9oFN25n/Q64 +cvuWQEXMjJHIXF2IdDKame2+JK3KHn/Ng9bxjqdg4KmlGhhajA== +=qQmG +-----END PGP SIGNATURE----- + +-----BEGIN PGP SIGNATURE----- + +iLMEAAEIAB0WIQSN8J3lSrZ/YDHXHW8h5Z/XBbOHgQUCY9p02QAKCRAh5Z/XBbOH +gRwQBAC7vGMkoDFImhwmXg98gGsaxpGY2SD7rkoN/uO1u4IWufviyDULHX2K6zxa +bYPZpt6NPB385Vr3t7sEGu+gNapUmUrLPBFughaEdDvOkJhnQtq31kL4qoFIVGNs +PI093+VRCQBG7cjpNMAeRRk4TlE3BtuFQwoIaKGEPhUC/VfThQ== +=KaYP +-----END PGP SIGNATURE----- diff --git a/tests/integration/files/file/base/grail/scene33.clearsign.asc b/tests/integration/files/file/base/grail/scene33.clearsign.asc new file mode 100644 index 00000000000..662c235a3ab --- /dev/null +++ b/tests/integration/files/file/base/grail/scene33.clearsign.asc @@ -0,0 +1,107 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA256 + +Scene 33 + + + [clop clop whinny] + KNIGHT: They're nervous, sire. + ARTHUR: Then we'd best leave them here and carry on on foot. Dis-mount! + TIM: Behold the cave of Kyre Banorg! + ARTHUR: Right! Keep me covered. + KNIGHT: What with? + ARTHUR: Just keep me covered. + TIM: Too late! + [chord] + ARTHUR: What? + TIM: There he is! + ARTHUR: Where? + TIM: There! + ARTHUR: What, behind the rabbit? + TIM: It is the {{ spam }}! + ARTHUR: You silly sod! You got us all worked up! + TIM: Well, that's no ordinary rabbit. That's the most foul, cruel, + and bad-tempered rodent you ever set eyes on. + ROBIN: You tit! I soiled my armor I was so scared! + TIM: Look, that rabbit's got a vicious streak a mile wide, it's a + killer! + KNIGHT: Get stuffed! + TIM: It'll do you a trick, mate! + KNIGHT: Oh, yeah? + ROBIN: You mangy Scot git! + TIM: I'm warning you! + ROBIN: What's he do, nibble your bum? + TIM: He's got huge, sharp-- he can leap about-- look at the bones! + ARTHUR: Go on, Boris. Chop his head off! + BORIS: Right! Silly little bleeder. One rabbit stew comin' right up! + TIM: Look! + [squeak] + BORIS: Aaaugh! + [chord] + ARTHUR: Jesus Christ! + TIM: I warned you! + ROBIN: I peed again! + TIM: I warned you! But did you listen to me? Oh, no, you knew it all, + didn't you? Oh, it's just a harmless little bunny, isn't it? Well, + it's always the same, I always-- + ARTHUR: Oh, shut up! + TIM: --But do they listen to me?-- + ARTHUR: Right! + TIM: -Oh, no-- + KNIGHTS: Charge! + [squeak squeak] + KNIGHTS: Aaaaugh! Aaaugh! etc. + KNIGHTS: Run away! Run away! + TIM: Haw haw haw. Haw haw haw. Haw haw. + ARTHUR: Right. How many did we lose? + KNIGHT: Gawain. + KNIGHT: Hector. + ARTHUR: And Boris. That's five. + GALAHAD: Three, sir. + ARTHUR: Three. Three. And we'd better not risk another frontal + assault, that rabbit's dynamite. + ROBIN: Would it help to confuse it if we run away more? + ARTHUR: Oh, shut up and go and change your armor. + GALAHAD: Let us taunt it! It may become so cross that it will make + a mistake. + ARTHUR: Like what? + GALAHAD: Well,.... + ARTHUR: Have we got bows? + KNIGHT: No. + LAUNCELOT: We have the Holy Hand Grenade. + ARTHUR: Yes, of course! The Holy Hand Grenade of Antioch! 'Tis one + of the sacred relics Brother Maynard carries with him! Brother Maynard! + Bring up the Holy Hand Grenade! + [singing] + How does it, uh... how does it work? + KNIGHT: I know not, my liege. + ARTHUR: Consult the Book of Armaments! + MAYNARD: Armaments, Chapter Two, Verses Nine to Twenty-One. + BROTHER: "And Saint Atila raised the hand grenade up on high, saying, + 'Oh, Lord, bless this thy hand grenade that with it thou mayest blow + thy enemies to tiny bits, in thy mercy.' And the Lord did grin, and + people did feast upon the lambs, and sloths, and carp, and anchovies, + and orangutans, and breakfast cereals, and fruit bats, and large --" + MAYNARD: Skip a bit, Brother. + BROTHER: "And the Lord spake, saying, 'First shalt thou take out the + Holy Pin. Then, shalt thou count to three, no more, no less. Three + shalt be the number thou shalt count, and the number of the counting + shalt be three. Four shalt thou not count, nor either count thou two, + excepting that thou then proceed to three. Five is right out. Once + the number three, being the third number, be reached, then lobbest thou + thy Holy Hand Grenade of Antioch towards thou foe, who being naughty + in my sight, shall snuff it.'" + MAYNARD: Amen. + ALL: Amen. + ARTHUR: Right! One... two... five! + KNIGHT: Three, sir! + ARTHUR: Three! + [boom] +-----BEGIN PGP SIGNATURE----- + +iLMEAQEIAB0WIQT4zMjLXh2IaNqlhZqx+apXxJfnHAUCY9p0bAAKCRCx+apXxJfn +HGt0BAC9HNKB8ticKt4xToPpiide8pa2L19msWJ9tbSp7INQiGFKU/bGBzf3Rg/j +41BCL4L9dszcmpoa147pG3/A7UPyYzbA9fT3AvjX9Cc2OIzgmWwzSaccbZVFyctC +Z9x6uIWrCY/AiSekoIWri4Xwi0u2TzTxRKaraHASxYzdcV111w== +=0u4u +-----END PGP SIGNATURE----- diff --git a/tests/pytests/functional/states/file/conftest.py b/tests/pytests/functional/states/file/conftest.py index 80cd930ab86..9d5022aa0f8 100644 --- a/tests/pytests/functional/states/file/conftest.py +++ b/tests/pytests/functional/states/file/conftest.py @@ -42,11 +42,21 @@ def grail_scene33_file(grail): return grail / "scene33" +@pytest.fixture +def grail_scene33_clearsign_file(grail_scene33_file): + return grail_scene33_file.with_suffix(".clearsign.asc") + + @pytest.fixture def grail_scene33_file_hash(grail_scene33_file): return hashlib.sha256(grail_scene33_file.read_bytes()).hexdigest() +@pytest.fixture +def grail_scene33_clearsign_file_hash(grail_scene33_clearsign_file): + return hashlib.sha256(grail_scene33_clearsign_file.read_bytes()).hexdigest() + + @pytest.fixture(scope="module") def state_file_account(): with pytest.helpers.create_account(create_group=True) as system_account: diff --git a/tests/pytests/functional/states/file/test_managed.py b/tests/pytests/functional/states/file/test_managed.py index 9678fb63432..a84478838cf 100644 --- a/tests/pytests/functional/states/file/test_managed.py +++ b/tests/pytests/functional/states/file/test_managed.py @@ -3,13 +3,22 @@ import hashlib import os import shutil import stat +import subprocess import types +import psutil import pytest import salt.utils.files import salt.utils.platform +try: + import gnupg as gnupglib + + HAS_GNUPG = True +except ImportError: + HAS_GNUPG = False + pytestmark = [ pytest.mark.windows_whitelisted, ] @@ -19,14 +28,125 @@ BINARY_FILE = b"GIF89a\x01\x00\x01\x00\x80\x00\x00\x05\x04\x04\x00\x00\x00,\x00\ @pytest.fixture -def remote_grail_scene33(webserver, grail_scene33_file, grail_scene33_file_hash): +def remote_grail_scene33( + webserver, + grail_scene33_file, + grail_scene33_file_hash, + grail_scene33_clearsign_file, + grail_scene33_clearsign_file_hash, +): return types.SimpleNamespace( file=grail_scene33_file, + file_clearsign=grail_scene33_clearsign_file, hash=grail_scene33_file_hash, + hash_clearsign=grail_scene33_clearsign_file_hash, + hash_file=grail_scene33_file.with_suffix(".SHA256"), url=webserver.url("grail/scene33"), + url_hash=webserver.url("grail/scene33.SHA256"), ) +@pytest.fixture +def gpghome(tmp_path): + root = tmp_path / "gpghome" + root.mkdir(mode=0o0700) + try: + yield root + finally: + # Make sure we don't leave any gpg-agents running behind + gpg_connect_agent = shutil.which("gpg-connect-agent") + if gpg_connect_agent: + gnupghome = root / ".gnupg" + if not gnupghome.is_dir(): + gnupghome = root + try: + subprocess.run( + [gpg_connect_agent, "killagent", "/bye"], + env={"GNUPGHOME": str(gnupghome)}, + shell=False, + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except subprocess.CalledProcessError: + # This is likely CentOS 7 or Amazon Linux 2 + pass + + # If the above errored or was not enough, as a last resort, let's check + # the running processes. + for proc in psutil.process_iter(): + try: + if "gpg-agent" in proc.name(): + for arg in proc.cmdline(): + if str(root) in arg: + proc.terminate() + except Exception: # pylint: disable=broad-except + pass + + +@pytest.fixture +def gnupg(gpghome): + return gnupglib.GPG(gnupghome=str(gpghome)) + + +@pytest.fixture +def a_pubkey(): + return """\ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mI0EY9pxawEEAPpBbXxRYFUm6np5h746Nch7+OrbLdtBxP8x7VDOockr/x7drssb +llVFuK4HmiJg+Nkyakn3XmVYBHY2yBIkN/MP+R1zRxiFmniKOTD15UuHSQaWZTqh +qac6XrLZ20BiWl1fKweCz1wGUcMZaOBs0WVB0sIupqfS90Ub93VC/+oxABEBAAG0 +JlNhbHRTdGFjayBBIFRlc3QgPGF0ZXN0QHNhbHRzdGFjay5jb20+iNEEEwEIADsW +IQT4zMjLXh2IaNqlhZqx+apXxJfnHAUCY9pxawIbAwULCQgHAgIiAgYVCgkICwIE +FgIDAQIeBwIXgAAKCRCx+apXxJfnHFDhA/47t5yYdCcjxXu/1Kn9sQwI+aq/S3x9 +/ZKE+RodlryqA43BUT7N6JLQ5zJO6p+kRhMwCcVfBeDNJANqVi63HEDp8q3633BF +q1Cbi3BG0ugBdCADIETYBwl/ytMSgYwRO8b4TkYCyhWuWAgliVF3ceX0AVsng8pF +o6Vh4A3SqosQgA== +=eHpb +-----END PGP PUBLIC KEY BLOCK-----""" + + +@pytest.fixture +def a_fp(): + return "F8CCC8CB5E1D8868DAA5859AB1F9AA57C497E71C" + + +@pytest.fixture +def b_pubkey(): + return """\ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mI0EY9pxigEEANbHCh566IEbp9Ez1WE3oEi+XXyf7H3GDgrVc8v9COMexpAFkJa1 +gG+yCm4bOZ5vHAXbP2rGvlOEcao3y3evj2TWahg0+05CDugRjL0pO4JcMUBV1mBZ +ynUGoQ5T+WtKilJ5k/JrSRpJW3y//46q0g5c470qVNn9ZX0YZW/b7DFXABEBAAG0 +JlNhbHRTdGFjayBCIFRlc3QgPGJ0ZXN0QHNhbHRzdGFjay5jb20+iNEEEwEIADsW +IQSN8J3lSrZ/YDHXHW8h5Z/XBbOHgQUCY9pxigIbAwULCQgHAgIiAgYVCgkICwIE +FgIDAQIeBwIXgAAKCRAh5Z/XBbOHgTCuA/9mYXAehM9avvq0Jm2dVbPidqxLstki +tgo3gCWmO1b5dXEBrhOZ8pZAktQ3WWoRrbwpNA7NAEIDF5l6uwMLLbGPQ5jreOdP +uzHpHONR1WWAzw2dj3v+5IcLDQ4sLi9VRgJqtMasTd8TpqMCVNcMArDBiy5hRF/e +XWEkf19Nb8qrdg== +=OEiT +-----END PGP PUBLIC KEY BLOCK-----""" + + +@pytest.fixture +def b_fp(): + return "8DF09DE54AB67F6031D71D6F21E59FD705B38781" + + +@pytest.fixture +def gpg_keys_present(gnupg, a_pubkey, b_pubkey, a_fp, b_fp): + pubkeys = [a_pubkey, b_pubkey] + fingerprints = [a_fp, b_fp] + gnupg.import_keys("\n".join(pubkeys)) + present_keys = gnupg.list_keys() + for fp in fingerprints: + assert any(x["fingerprint"] == fp for x in present_keys) + yield + # cleanup is taken care of by gpghome and tmp_path + + def _format_ids(key, value): return "{}={}".format(key, value) @@ -801,6 +921,113 @@ def test_verify_ssl_https_source(file, tmp_path, ssl_webserver, verify_ssl): assert name.exists() +@pytest.mark.skipif(HAS_GNUPG is False, reason="Needs python-gnupg library") +@pytest.mark.usefixtures("gpg_keys_present") +@pytest.mark.parametrize("signature", [True, ".asc"]) +def test_file_managed_signature( + file, tmp_path, signature, remote_grail_scene33, gpghome +): + name = tmp_path / "test_file_managed_signature.txt" + source = remote_grail_scene33.url + if signature is True: + source += ".clearsign.asc" + contents_file = remote_grail_scene33.file_clearsign + source_hash = remote_grail_scene33.hash_clearsign + else: + signature = source + signature + contents_file = remote_grail_scene33.file + source_hash = remote_grail_scene33.hash + ret = file.managed( + str(name), + source=source, + source_hash=source_hash, + signature=signature, + gnupghome=str(gpghome), + ) + assert ret.result is True + assert ret.changes + assert name.exists() + assert name.read_text() == contents_file.read_text() + + +@pytest.mark.skipif(HAS_GNUPG is False, reason="Needs python-gnupg library") +@pytest.mark.usefixtures("gpg_keys_present") +def test_file_managed_signature_fail( + file, tmp_path, remote_grail_scene33, gpghome, modules +): + name = tmp_path / "test_file_managed_signature_fail.txt" + source = remote_grail_scene33.url + signature = source + ".asc" + source_hash = remote_grail_scene33.hash + # although there are valid signatures, this will be denied since the one below is required + signed_by_all = ["DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"] + ret = file.managed( + str(name), + source=source, + source_hash=source_hash, + signature=signature, + gnupghome=str(gpghome), + signed_by_all=signed_by_all, + ) + assert ret.result is False + assert not ret.changes + assert not name.exists() + # Ensure that a new state run will attempt to redownload the source + # instead of verifying the invalid signature again + assert not modules.cp.is_cached(source) + assert not modules.cp.is_cached(signature) + + +@pytest.mark.skipif(HAS_GNUPG is False, reason="Needs python-gnupg library") +@pytest.mark.usefixtures("gpg_keys_present") +@pytest.mark.parametrize("sig", [True, ".asc"]) +def test_file_managed_source_hash_sig( + file, tmp_path, sig, remote_grail_scene33, gpghome +): + name = tmp_path / "test_file_managed_source_hash_sig.txt" + source = remote_grail_scene33.url + source_hash = remote_grail_scene33.url_hash + contents_file = remote_grail_scene33.file + if sig is True: + source_hash += ".clearsign.asc" + else: + sig = source_hash + sig + ret = file.managed( + str(name), + source=source, + source_hash=source_hash, + source_hash_sig=sig, + gnupghome=str(gpghome), + ) + assert ret.result is True + assert ret.changes + assert name.exists() + assert name.read_text() == contents_file.read_text() + + +@pytest.mark.skipif(HAS_GNUPG is False, reason="Needs python-gnupg library") +@pytest.mark.usefixtures("gpg_keys_present") +def test_file_managed_source_hash_sig_fail( + file, tmp_path, remote_grail_scene33, gpghome +): + name = tmp_path / "test_file_managed_source_hash_sig.txt" + source = remote_grail_scene33.url + source_hash = remote_grail_scene33.url_hash + sig = source_hash + ".asc" + signed_by_all = ["DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"] + ret = file.managed( + str(name), + source=source, + source_hash=source_hash, + source_hash_sig=sig, + gnupghome=str(gpghome), + signed_by_all=signed_by_all, + ) + assert ret.result is False + assert not ret.changes + assert not name.exists() + + def test_issue_60203( file, tmp_path, diff --git a/tests/pytests/functional/states/test_archive.py b/tests/pytests/functional/states/test_archive.py index 3e1a63442e0..acc76f6cd6f 100644 --- a/tests/pytests/functional/states/test_archive.py +++ b/tests/pytests/functional/states/test_archive.py @@ -6,12 +6,23 @@ import os import random import shutil import socket +import subprocess import sys from contextlib import closing +from pathlib import Path +import psutil import pytest import salt.utils.files +from tests.support.runtests import RUNTIME_VARS + +try: + import gnupg as gnupglib + + HAS_GNUPG = True +except ImportError: + HAS_GNUPG = False class TestRequestHandler(http.server.SimpleHTTPRequestHandler): @@ -224,3 +235,222 @@ def test_archive_extracted_web_source_etag_operation( # The modified time of the cached file now changes assert cached_file_mtime != os.path.getmtime(cached_file) + + +@pytest.fixture +def gpghome(tmp_path): + root = tmp_path / "gpghome" + root.mkdir(mode=0o0700) + try: + yield root + finally: + # Make sure we don't leave any gpg-agents running behind + gpg_connect_agent = shutil.which("gpg-connect-agent") + if gpg_connect_agent: + gnupghome = root / ".gnupg" + if not gnupghome.is_dir(): + gnupghome = root + try: + subprocess.run( + [gpg_connect_agent, "killagent", "/bye"], + env={"GNUPGHOME": str(gnupghome)}, + shell=False, + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except subprocess.CalledProcessError: + # This is likely CentOS 7 or Amazon Linux 2 + pass + + # If the above errored or was not enough, as a last resort, let's check + # the running processes. + for proc in psutil.process_iter(): + try: + if "gpg-agent" in proc.name(): + for arg in proc.cmdline(): + if str(root) in arg: + proc.terminate() + except Exception: # pylint: disable=broad-except + pass + + +@pytest.fixture +def gnupg(gpghome): + return gnupglib.GPG(gnupghome=str(gpghome)) + + +@pytest.fixture +def a_pubkey(): + return """\ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mI0EY9pxawEEAPpBbXxRYFUm6np5h746Nch7+OrbLdtBxP8x7VDOockr/x7drssb +llVFuK4HmiJg+Nkyakn3XmVYBHY2yBIkN/MP+R1zRxiFmniKOTD15UuHSQaWZTqh +qac6XrLZ20BiWl1fKweCz1wGUcMZaOBs0WVB0sIupqfS90Ub93VC/+oxABEBAAG0 +JlNhbHRTdGFjayBBIFRlc3QgPGF0ZXN0QHNhbHRzdGFjay5jb20+iNEEEwEIADsW +IQT4zMjLXh2IaNqlhZqx+apXxJfnHAUCY9pxawIbAwULCQgHAgIiAgYVCgkICwIE +FgIDAQIeBwIXgAAKCRCx+apXxJfnHFDhA/47t5yYdCcjxXu/1Kn9sQwI+aq/S3x9 +/ZKE+RodlryqA43BUT7N6JLQ5zJO6p+kRhMwCcVfBeDNJANqVi63HEDp8q3633BF +q1Cbi3BG0ugBdCADIETYBwl/ytMSgYwRO8b4TkYCyhWuWAgliVF3ceX0AVsng8pF +o6Vh4A3SqosQgA== +=eHpb +-----END PGP PUBLIC KEY BLOCK-----""" + + +@pytest.fixture +def a_fp(): + return "F8CCC8CB5E1D8868DAA5859AB1F9AA57C497E71C" + + +@pytest.fixture +def b_pubkey(): + return """\ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mI0EY9pxigEEANbHCh566IEbp9Ez1WE3oEi+XXyf7H3GDgrVc8v9COMexpAFkJa1 +gG+yCm4bOZ5vHAXbP2rGvlOEcao3y3evj2TWahg0+05CDugRjL0pO4JcMUBV1mBZ +ynUGoQ5T+WtKilJ5k/JrSRpJW3y//46q0g5c470qVNn9ZX0YZW/b7DFXABEBAAG0 +JlNhbHRTdGFjayBCIFRlc3QgPGJ0ZXN0QHNhbHRzdGFjay5jb20+iNEEEwEIADsW +IQSN8J3lSrZ/YDHXHW8h5Z/XBbOHgQUCY9pxigIbAwULCQgHAgIiAgYVCgkICwIE +FgIDAQIeBwIXgAAKCRAh5Z/XBbOHgTCuA/9mYXAehM9avvq0Jm2dVbPidqxLstki +tgo3gCWmO1b5dXEBrhOZ8pZAktQ3WWoRrbwpNA7NAEIDF5l6uwMLLbGPQ5jreOdP +uzHpHONR1WWAzw2dj3v+5IcLDQ4sLi9VRgJqtMasTd8TpqMCVNcMArDBiy5hRF/e +XWEkf19Nb8qrdg== +=OEiT +-----END PGP PUBLIC KEY BLOCK-----""" + + +@pytest.fixture +def b_fp(): + return "8DF09DE54AB67F6031D71D6F21E59FD705B38781" + + +@pytest.fixture +def gpg_keys_present(gnupg, a_pubkey, b_pubkey, a_fp, b_fp): + pubkeys = [a_pubkey, b_pubkey] + fingerprints = [a_fp, b_fp] + gnupg.import_keys("\n".join(pubkeys)) + present_keys = gnupg.list_keys() + for fp in fingerprints: + assert any(x["fingerprint"] == fp for x in present_keys) + yield + # cleanup is taken care of by gpghome and tmp_path + + +@pytest.fixture(scope="module", autouse=True) +def sig_files_present(web_root, modules): + base = Path(RUNTIME_VARS.BASE_FILES) + for file in [ + "custom.tar.gz", + "custom.tar.gz.asc", + "custom.tar.gz.SHA256", + "custom.tar.gz.SHA256.clearsign.asc", + "custom.tar.gz.SHA256.asc", + ]: + modules.file.copy(base / file, Path(web_root) / file) + + +@pytest.mark.skipif(HAS_GNUPG is False, reason="Needs python-gnupg library") +@pytest.mark.usefixtures("gpg_keys_present") +def test_archive_extracted_signature(tmp_path, gpghome, free_port, modules, states): + name = tmp_path / "test_archive_extracted_signature" + source = f"http://localhost:{free_port}/custom.tar.gz" + signature = source + ".asc" + source_hash = source + ".SHA256" + ret = states.archive.extracted( + str(name), + source=source, + source_hash=source_hash, + archive_format="tar", + options="z", + signature=signature, + gnupghome=str(gpghome), + ) + assert ret.result is True + assert ret.changes + assert name.exists() + assert modules.file.find(str(name)) + + +@pytest.mark.skipif(HAS_GNUPG is False, reason="Needs python-gnupg library") +@pytest.mark.usefixtures("gpg_keys_present") +def test_archive_extracted_signature_fail( + tmp_path, gpghome, free_port, modules, states +): + name = tmp_path / "test_archive_extracted_signature_fail" + source = f"http://localhost:{free_port}/custom.tar.gz" + signature = source + ".asc" + source_hash = source + ".SHA256" + # although there are valid signatures, this will be denied since the one below is required + signed_by_all = ["DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"] + ret = states.archive.extracted( + str(name), + source=source, + source_hash=source_hash, + archive_format="tar", + options="z", + signature=signature, + signed_by_all=signed_by_all, + gnupghome=str(gpghome), + ) + assert ret.result is False + assert not ret.changes + assert not name.exists() + assert not modules.cp.is_cached(source) + assert not modules.cp.is_cached(signature) + + +@pytest.mark.skipif(HAS_GNUPG is False, reason="Needs python-gnupg library") +@pytest.mark.usefixtures("gpg_keys_present") +@pytest.mark.parametrize("sig", [True, ".asc"]) +def test_archive_extracted_source_hash_sig( + tmp_path, sig, gpghome, free_port, modules, states +): + name = tmp_path / "test_archive_extracted_source_hash_sig" + source = f"http://localhost:{free_port}/custom.tar.gz" + source_hash = source + ".SHA256" + if sig is True: + source_hash += ".clearsign.asc" + else: + sig = source_hash + sig + ret = states.archive.extracted( + str(name), + source=source, + source_hash=source_hash, + archive_format="tar", + options="z", + source_hash_sig=sig, + gnupghome=str(gpghome), + ) + assert ret.result is True + assert ret.changes + assert name.exists() + assert modules.file.find(str(name)) + + +@pytest.mark.skipif(HAS_GNUPG is False, reason="Needs python-gnupg library") +@pytest.mark.usefixtures("gpg_keys_present") +@pytest.mark.parametrize("sig", [True, ".asc"]) +def test_archive_extracted_source_hash_sig_fail( + tmp_path, sig, gpghome, free_port, modules, states +): + name = tmp_path / "test_archive_extracted_source_hash_sig_fail" + source = f"http://localhost:{free_port}/custom.tar.gz" + source_hash = source + ".SHA256.clearsign.asc" + signed_by_any = ["DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"] + ret = states.archive.extracted( + str(name), + source=source, + source_hash=source_hash, + archive_format="tar", + options="z", + source_hash_sig=True, + signed_by_any=signed_by_any, + gnupghome=str(gpghome), + ) + assert ret.result is False + assert not ret.changes + assert not name.exists() + assert not modules.cp.is_cached(source) + assert not modules.cp.is_cached(source_hash)