# gemato: OpenPGP signature support tests
# (c) 2017-2023 Michał Górny
# SPDX-License-Identifier: GPL-2.0-or-later

import base64
import contextlib
import datetime
import io
import logging
import os
import shlex
import signal
import tempfile

import pytest

import gemato.cli
from gemato.compression import open_potentially_compressed_path
from gemato.exceptions import (
    ManifestUnsignedData,
    ManifestSyntaxError,
    OpenPGPNoImplementation,
    OpenPGPVerificationFailure,
    OpenPGPExpiredKeyFailure,
    OpenPGPRevokedKeyFailure,
    OpenPGPKeyImportError,
    OpenPGPKeyRefreshError,
    OpenPGPRuntimeError,
    OpenPGPUntrustedSigFailure,
    ManifestInsecureHashes,
    )
from gemato.manifest import ManifestFile
from gemato.openpgp import (
    SystemGPGEnvironment,
    IsolatedGPGEnvironment,
    PGPyEnvironment,
    get_wkd_url,
    OpenPGPSignatureList,
    OpenPGPSignatureData,
    OpenPGPSignatureStatus,
    )
from gemato.recursiveloader import ManifestRecursiveLoader

from tests.keydata import (
    PRIVATE_KEY_ID, KEY_FINGERPRINT, OTHER_KEY_FINGERPRINT, VALID_PUBLIC_KEY,
    VALID_KEY_NOEMAIL, VALID_KEY_NONUTF, COMBINED_PUBLIC_KEYS,
    VALID_KEY_SUBKEY, PRIVATE_KEY, EXPIRED_PUBLIC_KEY, REVOKED_PUBLIC_KEY,
    OTHER_VALID_PUBLIC_KEY, UNSIGNED_PUBLIC_KEY, FORGED_PUBLIC_KEY,
    UNSIGNED_SUBKEY, FORGED_SUBKEY, SUBKEY_FINGERPRINT,
    UNEXPIRE_PUBLIC_KEY, OLD_UNEXPIRE_PUBLIC_KEY,
    FORGED_UNEXPIRE_KEY, TWO_SIGNATURE_PUBLIC_KEYS, SECOND_KEY_FINGERPRINT,
    SECOND_VALID_PUBLIC_KEY, TWO_KEYS_ONE_EXPIRED,
    )
from tests.test_recursiveloader import INSECURE_HASH_TESTS
from tests.testutil import HKPServer


MALFORMED_PUBLIC_KEY = b'''
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQENBFnwXJMBCACgaTVz+d10TGL9zR920sb0GBFsitAJ5ZFzO4E0cg3SHhwI+reM
JQ6LLKmHowY/E1dl5FBbnJoRMxXP7/eScQ7HlhYj1gMPN5XiS2pkPwVkmJKBDV42
DLwoytC+ot0frRTJvSdEPCX81BNMgFiBSpkeZfXqb9XmU03bh6mFnrdd4CsHpTQG
csVXHK8QKhaxuqmHTALdpSzKCb/r0N/Z3sQExZhfLcBf/9UUVXj44Nwc6ooqZLRi
zHydxwQdxNu0aOFGEBn9WTi8Slf7MfR/pF0dI8rs9w6zMzVEq0lhDPpKFGDveoGf
g/+TpvBNXZ7DWH23GM4kID3pk4LLMc24U1PhABEBAAG0D2dlbWF0byB0ZXN0IGtl
eYkBRgQTAQoAMBYhBIHhLBa9jc1gvhgIRRNogOcqexOEBQJZ8FyTAhsDBQsJCg0E
AxUKCAIeAQIXgAAKCRATaIDnKnsThCnkB/0fhTH230idhlfZhFbVgTLxrj4rpsGg
20K8HkMaWzshsONdKkqYaYuRcm2UQZ0Kg5rm9jQsGYuAnzH/7XwmOleY95ycVfBk
je9aXF6BEoGick6C/AK5w77vd1kcBtJDrT4I7vwD4wRkyUdCkpVMVT4z4aZ7lHJ4
ECrrrI/mg0b+sGRyHfXPvIPp7F2959L/dpbhBZDfMOFC0A9LBQBJldKFbQLg3xzX
4tniz/BBrp7KjTOMKU0sufsedI50xc6cvCYCwJElqo86vv69klZHahE/k9nJaUAM
jCvJNJ7pU8YnJSRTQDH0PZEupAdzDU/AhGSrBz5+Jr7N0pQIxq4duE/Q
=r7JK
-----END PGP PUBLIC KEY BLOCK-----
'''

COMMON_MANIFEST_TEXT = """\
TIMESTAMP 2017-10-22T18:06:41Z
MANIFEST eclass/Manifest 0 MD5 d41d8cd98f00b204e9800998ecf8427e\
 SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709
IGNORE local
DATA myebuild-0.ebuild 0 MD5 d41d8cd98f00b204e9800998ecf8427e\
 SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709
MISC metadata.xml 0 MD5 d41d8cd98f00b204e9800998ecf8427e\
 SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709
DIST mydistfile.tar.gz 0 MD5 d41d8cd98f00b204e9800998ecf8427e\
 SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709\
"""

SIGNED_MANIFEST = f"""
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

{COMMON_MANIFEST_TEXT}
-----BEGIN PGP SIGNATURE-----

iQEzBAEBCAAdFiEEgeEsFr2NzWC+GAhFE2iA5yp7E4QFAloCx+YACgkQE2iA5yp7
E4TYrwf+JxjkVDNtvSN3HjQmdtcayLsaliw/2kqjoaQKs0lZD8+NRe7xPmwSm4bP
XKfoouJ0+/s87vuYJpBBCjtUDA9C9yZIeRTo8+eW6XsZbRRUmUD5ylTS+FpSsUrS
bEyYk4yZQMYrat+GQ1QBv+625nqnSDv5LZHBBZ/rG36GGlwHPbIKIishnDfdG2QQ
zuxkqepNq4Inzp//ES7Bv4qbTzyBI//HzfY31vOgdhhs5N5Ytez3Xxv/KNOTYdi1
ZIfqeaQ4NoefmxQunyEjT+8X2DMaEeHQni7dwjQc+FiN4ReV9aWbLo2O2cArqEHR
mkkhTd2Auao4D2K74BePBuiZ9+eDQA==
=khff
-----END PGP SIGNATURE-----
"""

POST_EXPIRATION_SIGNED_MANIFEST = f"""
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

{COMMON_MANIFEST_TEXT}
-----BEGIN PGP SIGNATURE-----

iQEzBAEBCAAdFiEEgeEsFr2NzWC+GAhFE2iA5yp7E4QFAmPsj28ACgkQE2iA5yp7
E4R0xAf8CC6uh8VMmv8xlFePEoBYEuSUtDa2hWHJv1sMn90QnszHGG6oo32g2Lje
H9NRyjOltAG9t0siF/pf57EiKCs9B+Z9zLGYuWlK4gvkHjMHzsoTipUymm2/saEo
AuoeZvhqNtfU0hCIJsWENtdyMb/hsJIxIOwBjVS/JT5cZlOGjhlyxVO0CS/7FsCp
GZCeLYPdYXPw2em2DR3Q3NDuNmUY7W3WhJCL14uC+AkU64SnHc13xQ9/go6TQ2ho
783Jm2f/4ZREYpKMvCgUJvOADSqnfY89hc6B/9JCXn+Zm8a31zgENlJ8DEhN0JMN
le/JaXEH/AhO6xCOmk8tNQ3QXcNF5w==
=UGgA
-----END PGP SIGNATURE-----
"""

DASH_ESCAPED_SIGNED_MANIFEST = '''
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

- TIMESTAMP 2017-10-22T18:06:41Z
- MANIFEST eclass/Manifest 0 MD5 d41d8cd98f00b204e9800998ecf8427e\
 SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709
IGNORE local
- DATA myebuild-0.ebuild 0 MD5 d41d8cd98f00b204e9800998ecf8427e\
 SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709
MISC metadata.xml 0 MD5 d41d8cd98f00b204e9800998ecf8427e\
 SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709
- DIST mydistfile.tar.gz 0 MD5 d41d8cd98f00b204e9800998ecf8427e\
 SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709
-----BEGIN PGP SIGNATURE-----

iQEzBAEBCAAdFiEEgeEsFr2NzWC+GAhFE2iA5yp7E4QFAloCx+YACgkQE2iA5yp7
E4TYrwf+JxjkVDNtvSN3HjQmdtcayLsaliw/2kqjoaQKs0lZD8+NRe7xPmwSm4bP
XKfoouJ0+/s87vuYJpBBCjtUDA9C9yZIeRTo8+eW6XsZbRRUmUD5ylTS+FpSsUrS
bEyYk4yZQMYrat+GQ1QBv+625nqnSDv5LZHBBZ/rG36GGlwHPbIKIishnDfdG2QQ
zuxkqepNq4Inzp//ES7Bv4qbTzyBI//HzfY31vOgdhhs5N5Ytez3Xxv/KNOTYdi1
ZIfqeaQ4NoefmxQunyEjT+8X2DMaEeHQni7dwjQc+FiN4ReV9aWbLo2O2cArqEHR
mkkhTd2Auao4D2K74BePBuiZ9+eDQA==
=khff
-----END PGP SIGNATURE-----
'''

MODIFIED_SIGNED_MANIFEST = '''
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

TIMESTAMP 2017-10-22T18:06:41Z
MANIFEST eclass/Manifest 0 MD5 d41d8cd98f00b204e9800998ecf8427e\
 SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709
IGNORE local
DATA myebuild-0.ebuild 32
MISC metadata.xml 0 MD5 d41d8cd98f00b204e9800998ecf8427e\
 SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709
DIST mydistfile.tar.gz 0 MD5 d41d8cd98f00b204e9800998ecf8427e\
 SHA1 da39a3ee5e6b4b0d3255bfef95601890afd80709
-----BEGIN PGP SIGNATURE-----

iQEzBAEBCAAdFiEEgeEsFr2NzWC+GAhFE2iA5yp7E4QFAloCx+YACgkQE2iA5yp7
E4TYrwf+JxjkVDNtvSN3HjQmdtcayLsaliw/2kqjoaQKs0lZD8+NRe7xPmwSm4bP
XKfoouJ0+/s87vuYJpBBCjtUDA9C9yZIeRTo8+eW6XsZbRRUmUD5ylTS+FpSsUrS
bEyYk4yZQMYrat+GQ1QBv+625nqnSDv5LZHBBZ/rG36GGlwHPbIKIishnDfdG2QQ
zuxkqepNq4Inzp//ES7Bv4qbTzyBI//HzfY31vOgdhhs5N5Ytez3Xxv/KNOTYdi1
ZIfqeaQ4NoefmxQunyEjT+8X2DMaEeHQni7dwjQc+FiN4ReV9aWbLo2O2cArqEHR
mkkhTd2Auao4D2K74BePBuiZ9+eDQA==
=khff
-----END PGP SIGNATURE-----
'''

EXPIRED_SIGNED_MANIFEST = f"""
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

{COMMON_MANIFEST_TEXT}
-----BEGIN PGP SIGNATURE-----

iQE5BAEBCAAjFiEEgeEsFr2NzWC+GAhFE2iA5yp7E4QFAlnxCXcFgwABUYAACgkQ
E2iA5yp7E4SDpQgAizTfQ6HJ1mawgElYV1LsOKGT8ivC6CAeU3Cs1E8zYpitKuy7
Yu5WrqUgck5GkXfswxHISkV+oWzrA/j0bUV768o+fY2JmlKuc/VWeyYDGnDtgDPz
NXYoqlQ1z3TDeaRktHcblECghf/A9Hbw0L4i0DVvDdk9APtIswgL/RmpXAQS1Bl7
sE1aFIy8CMBf3itco7NGjPpCxRt7ckS+UIKNgzrfnS7WEXHIirykEsMYKTLfuN2u
HSxRUCkTK1jBuP/v/rjdqUJw3LXAbjxFl9SyUX4AgCgHqgso3IZwjAprQRKNSObO
t5pTRGhLWgdLUrs7vRB7wf7F8h4sci/YBKJRFA==
=VGMV
-----END PGP SIGNATURE-----
"""

SUBKEY_SIGNED_MANIFEST = f"""
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

{COMMON_MANIFEST_TEXT}
-----BEGIN PGP SIGNATURE-----

iLMEAQEIAB0WIQR+nd48vkfkN0GN90A4udL3bMgzzAUCX0UGrAAKCRA4udL3bMgz
zH8MA/93/oNkXaA8+ZX7s8umhNMHiovdLJMna7Bl2C/tEdLfOoyp9o3lChhnB49v
g7VRUc//lz5sDUShdUUlTYjCPGLaYf2rBZHqd5POGJOsbzu1Tmtd8uhWFWnl8Kip
n4XmpdPvu+UdAHpQIGzKoNOEDJpZ5CzPLhYa5KgZiJhpYsDXgg==
=lpJi
-----END PGP SIGNATURE-----
"""

TWO_SIGNATURES = """
iQFHBAABCAAxFiEEgeEsFr2NzWC+GAhFE2iA5yp7E4QFAmPMHYQTHGdlbWF0b0Bl
eGFtcGxlLmNvbQAKCRATaIDnKnsThCDWB/95B9njv423M94uRdpPqSNqTpAokNhy
V0hjnhpiqnY85iFdL1Zc/rvhuxYbZezrig3dqctLseWYcx2mINBTLZqWHk5/NKEm
rd8iCdXZU1B7yo/HCfzUYR4HX5wISCiRjKimFFgkWKOg7KYGOqqrwLjAjaYJKmL5
L7R5joHpGbp87jix7c0ruSIMslQg5PbJ6/YAQWyOPTcZvqMFieJ8tqE/G2FabQcs
YRHEGu1x8wNY40rFzWd90ICR/hPjXZlCdCN2qk7hs+Coasb29n6pXjmt5L8/ICcL
zApRg8cetid6/SIzUSwiVqBt7i8noYWbgaazNt3HDlGq55v21dkOhmrXiIkEABYI
ADEWIQR1jj6cjPscaH2bJCVTcI9ps0i0zAUCY8wd6BMcc2Vjb25kQGV4YW1wbGUu
Y29tAAoJEFNwj2mzSLTMHKcA/0QbVl3PafYp45PFFo2e/knGKJKrm8D4bUH9wS5h
dchVAP0RSzkUQPP7Zs+2uHQItkqbXJyrBBHOqjGzeh39sWVuAw==
"""

TWO_SIGNATURE_MANIFEST = f"""
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

{COMMON_MANIFEST_TEXT}
-----BEGIN PGP SIGNATURE-----

{TWO_SIGNATURES}
=wG4b
-----END PGP SIGNATURE-----
"""


def strip_openpgp(text):
    lines = text.lstrip().splitlines()
    start = lines.index('')
    stop = lines.index('-----BEGIN PGP SIGNATURE-----')
    return '\n'.join(lines[start+1:stop-start+2]) + '\n'


# workaround pyflakes' limitations
_ = COMBINED_PUBLIC_KEYS
_ = EXPIRED_PUBLIC_KEY
_ = FORGED_PUBLIC_KEY
_ = FORGED_SUBKEY
_ = FORGED_UNEXPIRE_KEY
_ = OLD_UNEXPIRE_PUBLIC_KEY
_ = OTHER_VALID_PUBLIC_KEY
_ = SECOND_VALID_PUBLIC_KEY
_ = TWO_KEYS_ONE_EXPIRED
_ = TWO_SIGNATURE_PUBLIC_KEYS
_ = UNEXPIRE_PUBLIC_KEY
_ = UNSIGNED_PUBLIC_KEY
_ = UNSIGNED_SUBKEY
_ = VALID_KEY_NOEMAIL
_ = VALID_KEY_NONUTF
_ = VALID_KEY_SUBKEY


@pytest.mark.parametrize('manifest_var',
                         ["SIGNED_MANIFEST",
                          "DASH_ESCAPED_SIGNED_MANIFEST",
                          "SUBKEY_SIGNED_MANIFEST",
                          "MODIFIED_SIGNED_MANIFEST",
                          "EXPIRED_SIGNED_MANIFEST",
                          "TWO_SIGNATURE_MANIFEST",
                          "POST_EXPIRATION_SIGNED_MANIFEST",
                          ])
def test_noverify_goodish_manifest_load(manifest_var):
    """Test Manifest files that should succeed (OpenPGP disabled)"""
    m = ManifestFile()
    with io.StringIO(globals()[manifest_var]) as f:
        m.load(f, verify_openpgp=False)
    assert m.find_timestamp() is not None
    assert m.find_path_entry('myebuild-0.ebuild') is not None
    assert not m.openpgp_signed
    assert m.openpgp_signature is None


SIGNED_MANIFEST_JUNK_BEFORE = 'IGNORE test\n' + SIGNED_MANIFEST
SIGNED_MANIFEST_JUNK_AFTER = SIGNED_MANIFEST + 'IGNORE test\n'
SIGNED_MANIFEST_CUT_BEFORE_DATA = '\n'.join(
    SIGNED_MANIFEST.splitlines()[:3])
SIGNED_MANIFEST_CUT_BEFORE_SIGNATURE = '\n'.join(
    SIGNED_MANIFEST.splitlines()[:7])
SIGNED_MANIFEST_CUT_BEFORE_END = '\n'.join(
    SIGNED_MANIFEST.splitlines()[:15])


@pytest.mark.parametrize('manifest_var,expected',
                         [('SIGNED_MANIFEST_JUNK_BEFORE',
                           ManifestUnsignedData),
                          ('SIGNED_MANIFEST_JUNK_AFTER',
                           ManifestUnsignedData),
                          ('SIGNED_MANIFEST_CUT_BEFORE_DATA',
                           ManifestSyntaxError),
                          ('SIGNED_MANIFEST_CUT_BEFORE_SIGNATURE',
                           ManifestSyntaxError),
                          ('SIGNED_MANIFEST_CUT_BEFORE_END',
                           ManifestSyntaxError),
                          ])
def test_noverify_bad_manifest_load(manifest_var, expected):
    """Test Manifest files that should fail"""
    m = ManifestFile()
    with io.StringIO(globals()[manifest_var]) as f:
        with pytest.raises(expected):
            m.load(f, verify_openpgp=False)


@pytest.mark.parametrize('write_back', [False, True])
def test_noverify_recursive_manifest_loader(tmp_path, write_back):
    """Test reading signed Manifest"""
    with open(tmp_path / 'Manifest', 'w') as f:
        f.write(MODIFIED_SIGNED_MANIFEST)

    m = ManifestRecursiveLoader(tmp_path / 'Manifest',
                                verify_openpgp=False)
    assert not m.openpgp_signed
    assert m.openpgp_signature is None

    if write_back:
        m.save_manifest('Manifest')
        with open(tmp_path / 'Manifest') as f:
            assert f.read() == strip_openpgp(MODIFIED_SIGNED_MANIFEST)


def test_noverify_load_cli(tmp_path):
    """Test reading signed Manifest via CLI"""
    with open(tmp_path / 'Manifest', 'w') as f:
        f.write(MODIFIED_SIGNED_MANIFEST)
    os.mkdir(tmp_path / 'eclass')
    with open(tmp_path / 'eclass' / 'Manifest', 'w'):
        pass
    with open(tmp_path / 'myebuild-0.ebuild', 'wb') as f:
        f.write(b'12345678901234567890123456789012')
    with open(tmp_path / 'metadata.xml', 'wb'):
        pass

    assert 0 == gemato.cli.main(['gemato', 'verify',
                                 '--no-openpgp-verify', str(tmp_path)])


class MockedSystemGPGEnvironment(SystemGPGEnvironment):
    """System environment variant mocked to use isolated GNUPGHOME"""
    def __init__(self, *args, **kwargs):
        self._tmpdir = tempfile.TemporaryDirectory()
        self._home = self._tmpdir.name
        os.environ['GNUPGHOME'] = self._tmpdir.name
        super().__init__(*args, **kwargs)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, exc_cb):
        self.close()

    def close(self):
        if self._tmpdir is not None:
            IsolatedGPGEnvironment.close(self)
            # we need to recreate it to make cleanup() happy
            os.mkdir(self._tmpdir.name)
            self._tmpdir.cleanup()
            self._tmpdir = None
            os.environ.pop('GNUPGHOME', None)

    def import_key(self, keyfile, trust=True):
        IsolatedGPGEnvironment.import_key(self, keyfile, trust=trust)


@pytest.fixture(params=[IsolatedGPGEnvironment,
                        MockedSystemGPGEnvironment,
                        PGPyEnvironment,
                        ])
def openpgp_env(request):
    """OpenPGP environment fixture"""
    try:
        env = request.param()
    except OpenPGPNoImplementation as e:
        pytest.skip(str(e))
    yield env
    env.close()


@pytest.fixture(params=[IsolatedGPGEnvironment,
                        ])
def openpgp_env_with_refresh(request):
    """OpenPGP environments that support refreshing keys"""
    env = request.param()
    yield env
    env.close()


MANIFEST_VARIANTS = [
    # manifest, key, expected fpr/exception
    # == good manifests ==
    ('SIGNED_MANIFEST', 'VALID_PUBLIC_KEY', None),
    ('SIGNED_MANIFEST', 'VALID_KEY_NOEMAIL', None),
    ('SIGNED_MANIFEST', 'VALID_KEY_NONUTF', None),
    ('SIGNED_MANIFEST', 'COMBINED_PUBLIC_KEYS', None),
    ('DASH_ESCAPED_SIGNED_MANIFEST', 'VALID_PUBLIC_KEY', None),
    ('SUBKEY_SIGNED_MANIFEST', 'VALID_KEY_SUBKEY', None),
    ("POST_EXPIRATION_SIGNED_MANIFEST", "VALID_PUBLIC_KEY", None),
    # == Manifest signed before the key expired ==
    ("SIGNED_MANIFEST", "EXPIRED_PUBLIC_KEY", None),
    # == Manifest with two signatures ==
    ("TWO_SIGNATURE_MANIFEST", "TWO_SIGNATURE_PUBLIC_KEYS", None),
    ("TWO_SIGNATURE_MANIFEST", "VALID_PUBLIC_KEY", OpenPGPVerificationFailure),
    ("TWO_SIGNATURE_MANIFEST", "SECOND_VALID_PUBLIC_KEY",
     OpenPGPVerificationFailure),
    ("TWO_SIGNATURE_MANIFEST", "TWO_KEYS_ONE_EXPIRED",
     OpenPGPExpiredKeyFailure),
    # == using private key ==
    ('SIGNED_MANIFEST', 'PRIVATE_KEY', None),
    # == bad manifests ==
    ('MODIFIED_SIGNED_MANIFEST', 'VALID_PUBLIC_KEY',
     OpenPGPVerificationFailure),
    ('EXPIRED_SIGNED_MANIFEST', 'VALID_PUBLIC_KEY',
     OpenPGPVerificationFailure),
    # == bad keys ==
    ('SIGNED_MANIFEST', None,
     OpenPGPVerificationFailure),
    ("POST_EXPIRATION_SIGNED_MANIFEST", "EXPIRED_PUBLIC_KEY",
     OpenPGPExpiredKeyFailure),
    ('SIGNED_MANIFEST', 'REVOKED_PUBLIC_KEY',
     OpenPGPRevokedKeyFailure),
    ('SIGNED_MANIFEST', 'OTHER_VALID_PUBLIC_KEY',
     OpenPGPVerificationFailure),
    ('SIGNED_MANIFEST', 'UNSIGNED_PUBLIC_KEY',
     OpenPGPKeyImportError),
    ('SIGNED_MANIFEST', 'FORGED_PUBLIC_KEY',
     OpenPGPKeyImportError),
    ('SUBKEY_SIGNED_MANIFEST', 'UNSIGNED_SUBKEY',
     OpenPGPVerificationFailure),
    ('SUBKEY_SIGNED_MANIFEST', 'FORGED_SUBKEY',
     OpenPGPVerificationFailure),
]


def assert_signature(sig: OpenPGPSignatureList,
                     manifest_var: str,
                     expect_both: bool = True,
                     ) -> None:
    """Make assertions about the signature"""
    if manifest_var == "TWO_SIGNATURE_MANIFEST":
        no_key_sig = OpenPGPSignatureData(
            sig_status=OpenPGPSignatureStatus.NO_PUBLIC_KEY)
        assert sorted(sig) == sorted([
            OpenPGPSignatureData(
                fingerprint=KEY_FINGERPRINT,
                timestamp=datetime.datetime(2023, 1, 21, 17, 14, 44),
                primary_key_fingerprint=KEY_FINGERPRINT,
                sig_status=OpenPGPSignatureStatus.GOOD,
                trusted_sig=True,
                valid_sig=True,
                ),
            OpenPGPSignatureData(
                fingerprint=SECOND_KEY_FINGERPRINT,
                timestamp=datetime.datetime(2023, 1, 21, 17, 16, 24),
                primary_key_fingerprint=SECOND_KEY_FINGERPRINT,
                sig_status=OpenPGPSignatureStatus.GOOD,
                trusted_sig=True,
                valid_sig=True,
                ) if expect_both else no_key_sig,
        ])
    elif manifest_var == 'SUBKEY_SIGNED_MANIFEST':
        assert len(sig) == 1
        assert sig.fingerprint == SUBKEY_FINGERPRINT
        assert sig.timestamp == datetime.datetime(2020, 8, 25, 12, 40, 12)
        assert sig.expire_timestamp is None
        assert sig.primary_key_fingerprint == KEY_FINGERPRINT
    elif manifest_var == "POST_EXPIRATION_SIGNED_MANIFEST":
        assert len(sig) == 1
        assert sig.fingerprint == KEY_FINGERPRINT
        assert sig.timestamp == datetime.datetime(2023, 2, 15, 7, 53, 19)
        assert sig.expire_timestamp is None
        assert sig.primary_key_fingerprint == KEY_FINGERPRINT
    else:
        assert len(sig) == 1
        assert sig.fingerprint == KEY_FINGERPRINT
        assert sig.timestamp == datetime.datetime(2017, 11, 8, 9, 1, 26)
        assert sig.expire_timestamp is None
        assert sig.primary_key_fingerprint == KEY_FINGERPRINT


@pytest.mark.parametrize('manifest_var,key_var,expected',
                         MANIFEST_VARIANTS)
def test_verify_manifest(openpgp_env, manifest_var, key_var, expected):
    """Test direct Manifest data verification"""
    if (isinstance(openpgp_env, PGPyEnvironment) and
            manifest_var == 'DASH_ESCAPED_SIGNED_MANIFEST'):
        pytest.xfail('dash escaping is known-broken in pgpy')

    try:
        with io.StringIO(globals()[manifest_var]) as f:
            if expected is None:
                if key_var is not None:
                    with io.BytesIO(globals()[key_var]) as kf:
                        openpgp_env.import_key(kf)

                sig = openpgp_env.verify_file(f)
                assert_signature(sig, manifest_var)
            else:
                with pytest.raises(expected):
                    if key_var is not None:
                        with io.BytesIO(globals()[key_var]) as kf:
                            openpgp_env.import_key(kf)

                    openpgp_env.verify_file(f)
    except OpenPGPNoImplementation as e:
        pytest.skip(str(e))


def test_verify_one_out_of_two():
    try:
        with MockedSystemGPGEnvironment() as openpgp_env:
            with io.BytesIO(VALID_PUBLIC_KEY) as f:
                openpgp_env.import_key(f)

            with io.StringIO(TWO_SIGNATURE_MANIFEST) as f:
                sig = openpgp_env.verify_file(f, require_all_good=False)

            assert_signature(sig, "TWO_SIGNATURE_MANIFEST", expect_both=False)
    except OpenPGPNoImplementation as e:
        pytest.skip(str(e))


def test_verify_untrusted_key():
    try:
        with MockedSystemGPGEnvironment() as openpgp_env:
            with io.BytesIO(VALID_PUBLIC_KEY) as f:
                openpgp_env.import_key(f, trust=False)

            with io.StringIO(SIGNED_MANIFEST) as f:
                with pytest.raises(OpenPGPUntrustedSigFailure):
                    openpgp_env.verify_file(f)
    except OpenPGPNoImplementation as e:
        pytest.skip(str(e))


@pytest.mark.parametrize('manifest_var,key_var,expected',
                         MANIFEST_VARIANTS)
def test_manifest_load(openpgp_env, manifest_var, key_var, expected):
    """Test Manifest verification via ManifestFile.load()"""
    if (isinstance(openpgp_env, PGPyEnvironment) and
            manifest_var == 'DASH_ESCAPED_SIGNED_MANIFEST'):
        pytest.xfail('dash escaping is known-broken in pgpy')

    try:
        key_loaded = False
        m = ManifestFile()
        with io.StringIO(globals()[manifest_var]) as f:
            if expected is None:
                if key_var is not None:
                    with io.BytesIO(globals()[key_var]) as kf:
                        openpgp_env.import_key(kf)

                key_loaded = True
                m.load(f, openpgp_env=openpgp_env)
                assert m.openpgp_signed
                assert_signature(m.openpgp_signature, manifest_var)
            else:
                with pytest.raises(expected):
                    if key_var is not None:
                        with io.BytesIO(globals()[key_var]) as kf:
                            openpgp_env.import_key(kf)

                    key_loaded = True
                    m.load(f, openpgp_env=openpgp_env)
                assert not m.openpgp_signed
                assert m.openpgp_signature is None

        if key_loaded:
            # Manifest entries should be loaded even if verification failed
            assert m.find_timestamp() is not None
            assert m.find_path_entry('myebuild-0.ebuild') is not None
    except OpenPGPNoImplementation as e:
        pytest.skip(str(e))


@pytest.mark.parametrize('filename', ['Manifest', 'Manifest.gz'])
@pytest.mark.parametrize('manifest_var,key_var,expected',
                         MANIFEST_VARIANTS)
def test_recursive_manifest_loader(tmp_path, openpgp_env, filename,
                                   manifest_var, key_var, expected):
    """Test Manifest verification via ManifestRecursiveLoader"""
    if (isinstance(openpgp_env, PGPyEnvironment) and
            manifest_var == 'DASH_ESCAPED_SIGNED_MANIFEST'):
        pytest.xfail('dash escaping is known-broken in pgpy')

    try:
        with open_potentially_compressed_path(tmp_path / filename, 'w') as cf:
            cf.write(globals()[manifest_var])

        if expected is None:
            if key_var is not None:
                with io.BytesIO(globals()[key_var]) as f:
                    openpgp_env.import_key(f)

            m = ManifestRecursiveLoader(tmp_path / filename,
                                        verify_openpgp=True,
                                        openpgp_env=openpgp_env)
            assert m.openpgp_signed
            assert_signature(m.openpgp_signature, manifest_var)
        else:
            with pytest.raises(expected):
                if key_var is not None:
                    with io.BytesIO(globals()[key_var]) as f:
                        openpgp_env.import_key(f)

                ManifestRecursiveLoader(tmp_path / filename,
                                        verify_openpgp=True,
                                        openpgp_env=openpgp_env)
    except OpenPGPNoImplementation as e:
        pytest.skip(str(e))


@pytest.fixture
def base_tree(tmp_path):
    os.mkdir(tmp_path / 'eclass')
    with open(tmp_path / 'eclass' / 'Manifest', 'w'):
        pass
    with open(tmp_path / 'myebuild-0.ebuild', 'wb'):
        pass
    with open(tmp_path / 'metadata.xml', 'wb'):
        pass
    return tmp_path


@pytest.mark.parametrize('manifest_var,key_var,expected',
                         [(m, k, e) for m, k, e in MANIFEST_VARIANTS
                          if k is not None])
def test_cli(base_tree, caplog, manifest_var, key_var, expected):
    """Test Manifest verification via CLI"""
    with open(base_tree / '.key.bin', 'wb') as f:
        f.write(globals()[key_var])
    with open(base_tree / 'Manifest', 'w') as f:
        f.write(globals()[manifest_var])
    if manifest_var == 'MODIFIED_SIGNED_MANIFEST':
        with open(base_tree / 'myebuild-0.ebuild', 'wb') as f:
            f.write(b'12345678901234567890123456789012')

    retval = gemato.cli.main(['gemato', 'verify',
                              '--openpgp-key',
                              str(base_tree / '.key.bin'),
                              '--no-refresh-keys',
                              '--require-signed-manifest',
                              # we verify this option separately
                              # and our test data currently sucks
                              '--no-require-secure-hashes',
                              str(base_tree)])
    if str(OpenPGPNoImplementation('install gpg')) in caplog.text:
        pytest.skip('OpenPGP implementation missing')

    eexit = 0 if expected is None else 1
    assert retval == eexit
    if expected is not None:
        assert str(expected('')) in caplog.text


EMPTY_DATA = b''


@pytest.mark.parametrize(
    'key_var,success',
    [('VALID_PUBLIC_KEY', True),
     ('VALID_KEY_NOEMAIL', True),
     ('VALID_KEY_NONUTF', True),
     ('MALFORMED_PUBLIC_KEY', False),
     ('EMPTY_DATA', False),
     ('FORGED_PUBLIC_KEY', False),
     ('UNSIGNED_PUBLIC_KEY', False),
     ])
def test_env_import_key(openpgp_env, key_var, success):
    """Test importing valid and invalid keys"""
    try:
        if success:
            openpgp_env.import_key(io.BytesIO(globals()[key_var]))
        else:
            with pytest.raises(OpenPGPKeyImportError):
                openpgp_env.import_key(io.BytesIO(globals()[key_var]))
    except OpenPGPNoImplementation as e:
        pytest.skip(str(e))


def test_env_double_close():
    """Test that env can be closed multiple times"""
    with IsolatedGPGEnvironment() as env:
        env.close()


def test_env_home_after_close():
    """Test that .home can not be referenced after closing"""
    with IsolatedGPGEnvironment() as env:
        env.close()
        with pytest.raises(AssertionError):
            env.home


@pytest.fixture(params=[IsolatedGPGEnvironment,
                        MockedSystemGPGEnvironment,
                        ])
def privkey_env(request):
    """Environment with private key loaded"""
    try:
        env = request.param()
        env.import_key(io.BytesIO(PRIVATE_KEY))
    except OpenPGPNoImplementation as e:
        pytest.skip(str(e))
    yield env
    env.close()


TEST_STRING = 'The quick brown fox jumps over the lazy dog'


@pytest.mark.parametrize('keyid', [None, PRIVATE_KEY_ID])
def test_sign_data(privkey_env, keyid):
    """Test signing data"""
    with io.StringIO(TEST_STRING) as f:
        with io.StringIO() as wf:
            privkey_env.clear_sign_file(f, wf, keyid=keyid)
            wf.seek(0)
            privkey_env.verify_file(wf)


@pytest.mark.parametrize('keyid', [None, PRIVATE_KEY_ID])
@pytest.mark.parametrize('sign', [None, False, True])
def test_dump_signed_manifest(privkey_env, keyid, sign):
    """Test dumping a signed Manifest"""
    m = ManifestFile()
    verify = True if sign is None else False
    with io.StringIO(SIGNED_MANIFEST) as f:
        m.load(f, verify_openpgp=verify, openpgp_env=privkey_env)
    assert m.openpgp_signed == verify

    with io.StringIO() as f:
        m.dump(f, openpgp_keyid=keyid, openpgp_env=privkey_env,
               sign_openpgp=sign)
        f.seek(0)
        m.load(f, openpgp_env=privkey_env)
    if sign is not False:
        assert m.openpgp_signed
        assert m.openpgp_signature is not None
    else:
        assert not m.openpgp_signed
        assert m.openpgp_signature is None


@pytest.mark.parametrize('filename', ['Manifest', 'Manifest.gz'])
@pytest.mark.parametrize('sign', [None, True])
def test_recursive_manifest_loader_save_manifest(tmp_path, privkey_env,
                                                 filename, sign):
    """Test signing Manifests via ManifestRecursiveLoader"""
    with open_potentially_compressed_path(tmp_path / filename, 'w') as cf:
        cf.write(SIGNED_MANIFEST)

    verify = not sign
    m = ManifestRecursiveLoader(tmp_path / filename,
                                verify_openpgp=verify,
                                sign_openpgp=sign,
                                openpgp_env=privkey_env)
    assert m.openpgp_signed == verify

    m.save_manifest(filename)
    m2 = ManifestFile()
    with open_potentially_compressed_path(tmp_path / filename, 'r') as cf:
        m2.load(cf, openpgp_env=privkey_env)
    assert m2.openpgp_signed
    assert m2.openpgp_signature is not None


def test_recursive_manifest_loader_save_submanifest(tmp_path, privkey_env):
    """Test that sub-Manifests are not signed"""
    with open(tmp_path / 'Manifest', 'w') as f:
        f.write(SIGNED_MANIFEST)
    os.mkdir(tmp_path / 'eclass')
    with open(tmp_path / 'eclass' / 'Manifest', 'w'):
        pass

    m = ManifestRecursiveLoader(tmp_path / 'Manifest',
                                verify_openpgp=False,
                                sign_openpgp=True,
                                openpgp_env=privkey_env)
    assert not m.openpgp_signed
    assert m.openpgp_signature is None

    m.load_manifest('eclass/Manifest')
    m.save_manifest('eclass/Manifest')

    m2 = ManifestFile()
    with open(tmp_path / 'eclass' / 'Manifest') as f:
        m2.load(f, openpgp_env=privkey_env)
    assert not m2.openpgp_signed
    assert m2.openpgp_signature is None


@pytest.mark.parametrize(
    'key_var,expected',
    [('VALID_PUBLIC_KEY', {KEY_FINGERPRINT: ['gemato@example.com']}),
     ('OTHER_VALID_PUBLIC_KEY',
      {OTHER_KEY_FINGERPRINT: ['gemato@example.com']}),
     ('VALID_KEY_SUBKEY', {KEY_FINGERPRINT: ['gemato@example.com']}),
     ('VALID_KEY_NOEMAIL', {KEY_FINGERPRINT: []}),
     ('VALID_KEY_NONUTF', {KEY_FINGERPRINT: ['gemato@example.com']}),
     ])
def test_list_keys(openpgp_env_with_refresh, key_var, expected):
    try:
        openpgp_env_with_refresh.import_key(io.BytesIO(globals()[key_var]))
    except OpenPGPNoImplementation as e:
        pytest.skip(str(e))
    assert openpgp_env_with_refresh.list_keys() == expected


@pytest.fixture(scope='module')
def global_hkp_server():
    """A fixture that starts a single HKP server instance for tests"""
    server = HKPServer()
    server.start()
    yield server
    server.stop()


@pytest.fixture
def hkp_server(global_hkp_server):
    """A fixture that resets the global HKP server with empty keys"""
    global_hkp_server.keys.clear()
    yield global_hkp_server


REFRESH_VARIANTS = [
    # manifest, key, server key fpr, server key, expected exception
    ('SIGNED_MANIFEST', 'VALID_PUBLIC_KEY', KEY_FINGERPRINT,
     'VALID_PUBLIC_KEY', None),
    ('SIGNED_MANIFEST', 'VALID_KEY_NONUTF', KEY_FINGERPRINT,
     'VALID_PUBLIC_KEY', None),
    ('SIGNED_MANIFEST', 'VALID_PUBLIC_KEY', KEY_FINGERPRINT,
     'REVOKED_PUBLIC_KEY', OpenPGPRevokedKeyFailure),
    # test fetching subkey for primary key
    ('SUBKEY_SIGNED_MANIFEST', 'VALID_PUBLIC_KEY', KEY_FINGERPRINT,
     'VALID_KEY_SUBKEY', None),
    # refresh should fail if key is not on server
    ('SIGNED_MANIFEST', 'VALID_PUBLIC_KEY', None, None,
     OpenPGPKeyRefreshError),
    # unrevocation should not be possible
    ('SIGNED_MANIFEST', 'REVOKED_PUBLIC_KEY', KEY_FINGERPRINT,
     'VALID_PUBLIC_KEY', OpenPGPRevokedKeyFailure),
    # unexpiration should be possible
    ('SIGNED_MANIFEST', 'EXPIRED_PUBLIC_KEY', KEY_FINGERPRINT,
     'UNEXPIRE_PUBLIC_KEY', None),
    # ...but only with a new signature
    ("POST_EXPIRATION_SIGNED_MANIFEST", "EXPIRED_PUBLIC_KEY", KEY_FINGERPRINT,
     "OLD_UNEXPIRE_PUBLIC_KEY", OpenPGPExpiredKeyFailure),
    # make sure server can't malicously inject or replace key
    ('SIGNED_MANIFEST', 'OTHER_VALID_PUBLIC_KEY', OTHER_KEY_FINGERPRINT,
     'VALID_PUBLIC_KEY', OpenPGPKeyRefreshError),
    ('SIGNED_MANIFEST', 'OTHER_VALID_PUBLIC_KEY', OTHER_KEY_FINGERPRINT,
     'COMBINED_PUBLIC_KEYS', OpenPGPRuntimeError),
    # test that forged keys are rejected
    ("POST_EXPIRATION_SIGNED_MANIFEST", "EXPIRED_PUBLIC_KEY", KEY_FINGERPRINT,
     "FORGED_UNEXPIRE_KEY", OpenPGPExpiredKeyFailure),
    ('SUBKEY_SIGNED_MANIFEST', 'VALID_PUBLIC_KEY', KEY_FINGERPRINT,
     'FORGED_SUBKEY', OpenPGPVerificationFailure),
    ('SUBKEY_SIGNED_MANIFEST', 'VALID_PUBLIC_KEY', KEY_FINGERPRINT,
     'UNSIGNED_SUBKEY', OpenPGPVerificationFailure),
]


@pytest.mark.parametrize(
    'manifest_var,key_var,server_key_fpr,server_key_var,expected',
    REFRESH_VARIANTS +
    [('SIGNED_MANIFEST', 'VALID_KEY_NOEMAIL', KEY_FINGERPRINT,
      'VALID_PUBLIC_KEY', None),
     ])
def test_refresh_hkp(openpgp_env_with_refresh, hkp_server, manifest_var,
                     key_var, server_key_fpr, server_key_var, expected):
    """Test refreshing against a HKP keyserver"""
    try:
        if key_var is not None:
            with io.BytesIO(globals()[key_var]) as f:
                openpgp_env_with_refresh.import_key(f)

        if server_key_var is not None:
            hkp_server.keys[server_key_fpr] = globals()[server_key_var]

        if expected is None:
            openpgp_env_with_refresh.refresh_keys(
                allow_wkd=False, keyserver=hkp_server.addr)
            with io.StringIO(globals()[manifest_var]) as f:
                openpgp_env_with_refresh.verify_file(f)
        else:
            with pytest.raises(expected):
                openpgp_env_with_refresh.refresh_keys(
                    allow_wkd=False, keyserver=hkp_server.addr)
                with io.StringIO(globals()[manifest_var]) as f:
                    openpgp_env_with_refresh.verify_file(f)
    except OpenPGPNoImplementation as e:
        pytest.skip(str(e))


@pytest.mark.parametrize(
    'manifest_var,key_var,server_key_fpr,server_key_var,expected,'
    'expect_hit',
    [args + (True,) for args in REFRESH_VARIANTS] +
    [('SIGNED_MANIFEST', 'VALID_KEY_NOEMAIL', KEY_FINGERPRINT,
      'VALID_PUBLIC_KEY', OpenPGPKeyRefreshError, False),
     ])
def test_refresh_wkd(openpgp_env_with_refresh,
                     manifest_var,
                     key_var,
                     server_key_fpr,
                     server_key_var,
                     expected,
                     expect_hit):
    """Test refreshing against WKD"""
    with pytest.importorskip('responses').RequestsMock(
            assert_all_requests_are_fired=expect_hit) as responses:
        try:
            if key_var is not None:
                with io.BytesIO(globals()[key_var]) as f:
                    openpgp_env_with_refresh.import_key(f)

            if server_key_var is not None:
                responses.add(
                    responses.GET,
                    'https://example.com/.well-known/openpgpkey/hu/'
                    '5x66h616iaskmnadrm86ndo6xnxbxjxb?l=gemato',
                    body=globals()[server_key_var],
                    content_type='application/pgp-keys')
            else:
                responses.add(
                    responses.GET,
                    'https://example.com/.well-known/openpgpkey/hu/'
                    '5x66h616iaskmnadrm86ndo6xnxbxjxb?l=gemato',
                    status=404)

            if expected is None:
                openpgp_env_with_refresh.refresh_keys(
                    allow_wkd=True, keyserver='hkps://block.invalid/')
                with io.StringIO(globals()[manifest_var]) as f:
                    openpgp_env_with_refresh.verify_file(f)
            else:
                with pytest.raises(expected):
                    openpgp_env_with_refresh.refresh_keys(
                        allow_wkd=True, keyserver='hkps://block.invalid/')
                    with io.StringIO(globals()[manifest_var]) as f:
                        openpgp_env_with_refresh.verify_file(f)
        except OpenPGPNoImplementation as e:
            pytest.skip(str(e))


@pytest.mark.parametrize('status', [401, 404, 500, ConnectionError])
def test_refresh_wkd_fallback_to_hkp(openpgp_env_with_refresh,
                                     hkp_server, caplog, status):
    """Test whether WKD refresh failure falls back to HKP"""
    with pytest.importorskip('responses').RequestsMock() as responses:
        try:
            with io.BytesIO(VALID_PUBLIC_KEY) as f:
                openpgp_env_with_refresh.import_key(f)
            hkp_server.keys[KEY_FINGERPRINT] = REVOKED_PUBLIC_KEY
            if status is not ConnectionError:
                responses.add(
                    responses.GET,
                    'https://example.com/.well-known/openpgpkey/hu/'
                    '5x66h616iaskmnadrm86ndo6xnxbxjxb?l=gemato',
                    status=status)

            caplog.set_level(logging.DEBUG)
            openpgp_env_with_refresh.refresh_keys(
                allow_wkd=True, keyserver=hkp_server.addr)
            assert 'failing due to failed request' in caplog.text

            with pytest.raises(OpenPGPRevokedKeyFailure):
                with io.StringIO(SIGNED_MANIFEST) as f:
                    openpgp_env_with_refresh.verify_file(f)
        except OpenPGPNoImplementation as e:
            pytest.skip(str(e))


@pytest.mark.parametrize(
    'email,expected',
    [('gemato@example.com',
      'https://example.com/.well-known/openpgpkey/hu/'
      '5x66h616iaskmnadrm86ndo6xnxbxjxb?l=gemato'),
     ('Joe.Doe@Example.ORG',
      'https://example.org/.well-known/openpgpkey/hu/'
      'iy9q119eutrkn8s1mk4r39qejnbu3n5q?l=Joe.Doe'),
     ])
def test_get_wkd_url(email, expected):
    assert get_wkd_url(email) == expected


def signal_desc(sig):
    if hasattr(signal, 'strsignal'):
        return signal.strsignal(sig)
    else:
        return sig


@pytest.mark.parametrize(
    'command,expected,match',
    [('true', 0, None),
     ('false', 1, None),
     ('gpg --verify {tmp_path}/Manifest', 0, None),
     ('gpg --verify {tmp_path}/Manifest.subkey', 2, None),
     ('sh -c "kill $$"', -signal.SIGTERM,
      f'Child process terminated due to signal: '
      f'{signal_desc(signal.SIGTERM)}'),
     ('sh -c "kill -USR1 $$"', -signal.SIGUSR1,
      f'Child process terminated due to signal: '
      f'{signal_desc(signal.SIGUSR1)}'),
     ])
def test_cli_gpg_wrap(tmp_path, caplog, command, expected, match):
    with open(tmp_path / '.key.bin', 'wb') as f:
        f.write(VALID_PUBLIC_KEY)
    with open(tmp_path / 'Manifest', 'w') as f:
        f.write(SIGNED_MANIFEST)
    with open(tmp_path / 'Manifest.subkey', 'w') as f:
        f.write(SUBKEY_SIGNED_MANIFEST)

    command = [x.replace('{tmp_path}', str(tmp_path))
               for x in shlex.split(command)]
    retval = gemato.cli.main(['gemato', 'gpg-wrap',
                              '--openpgp-key',
                              str(tmp_path / '.key.bin'),
                              '--no-refresh-keys',
                              '--'] + command)
    if str(OpenPGPNoImplementation('install gpg')) in caplog.text:
        pytest.skip('OpenPGP implementation missing')

    assert retval == expected
    if match is not None:
        assert match in caplog.text


@pytest.mark.parametrize("hashes_arg,insecure", INSECURE_HASH_TESTS)
@pytest.mark.parametrize(
    "sign,require_secure",
    [(None, None),
     (False, None),
     (True, None),
     (None, False),
     (True, False),
     ])
def test_recursive_manifest_loader_require_secure(tmp_path, privkey_env,
                                                  hashes_arg, insecure,
                                                  sign, require_secure):
    with open(tmp_path / "Manifest", "w") as f:
        f.write(SIGNED_MANIFEST)

    ctx = (pytest.raises(ManifestInsecureHashes)
           if insecure is not None and sign is not False
           and require_secure is not False
           else contextlib.nullcontext())
    with ctx:
        m = ManifestRecursiveLoader(tmp_path / "Manifest",
                                    hashes=hashes_arg.split(),
                                    require_secure_hashes=require_secure,
                                    verify_openpgp=not sign,
                                    sign_openpgp=sign,
                                    openpgp_env=privkey_env)
        if not sign:
            assert m.openpgp_signed


@pytest.mark.parametrize("hashes_arg,insecure", INSECURE_HASH_TESTS)
@pytest.mark.parametrize(
    "sign,require_secure",
    [("", ""),
     ("--no-sign", ""),
     ("--sign", ""),
     ("", "--no-require-secure-hashes"),
     ("--sign", "--no-require-secure-hashes"),
     ])
def test_update_require_secure_cli(base_tree, caplog, hashes_arg,
                                   insecure, sign, require_secure):
    with open(base_tree / ".key.bin", "wb") as keyf:
        keyf.write(PRIVATE_KEY)
    with open(base_tree / "Manifest", "w") as f:
        f.write(SIGNED_MANIFEST)

    retval = gemato.cli.main(["gemato", "update",
                              "-K", str(base_tree / ".key.bin"),
                              "--hashes", hashes_arg,
                              str(base_tree)]
                             + f"{sign} {require_secure}".split())
    if str(OpenPGPNoImplementation('install gpg')) in caplog.text:
        pytest.skip('OpenPGP implementation missing')

    expected = (1 if insecure is not None and sign != "--no-sign"
                and require_secure != "--no-require-secure-hashes"
                else 0)
    assert retval == expected
    if expected == 1:
        assert str(ManifestInsecureHashes(insecure)) in caplog.text


@pytest.mark.parametrize(
    "require_secure", ["", "--no-require-secure-hashes"])
def test_verify_require_secure_cli(base_tree, caplog, require_secure):
    with open(base_tree / ".key.bin", "wb") as keyf:
        keyf.write(VALID_PUBLIC_KEY)
    with open(base_tree / "Manifest", "w") as f:
        f.write(SIGNED_MANIFEST)

    retval = gemato.cli.main(["gemato", "verify",
                              "--no-refresh-keys",
                              "--require-signed-manifest",
                              "-K", str(base_tree / ".key.bin"),
                              str(base_tree)]
                             + require_secure.split())
    if str(OpenPGPNoImplementation('install gpg')) in caplog.text:
        pytest.skip('OpenPGP implementation missing')

    expected = (1 if require_secure != "--no-require-secure-hashes"
                else 0)
    assert retval == expected
    if expected == 1:
        assert str(ManifestInsecureHashes(["MD5"])) in caplog.text


@pytest.mark.parametrize(
    "key_var,two_sigs",
    [("TWO_SIGNATURE_PUBLIC_KEYS", True),
     ("VALID_PUBLIC_KEY", False),
     ])
def test_verify_detached(tmp_path, key_var, two_sigs):
    try:
        with MockedSystemGPGEnvironment() as openpgp_env:
            with io.BytesIO(globals()[key_var]) as f:
                openpgp_env.import_key(f)

            with open(tmp_path / "data.bin", "wb") as f:
                f.write(b"\r\n".join(COMMON_MANIFEST_TEXT.encode("utf8")
                                     .splitlines()))
            with open(tmp_path / "sig.bin", "wb") as f:
                f.write(base64.b64decode(TWO_SIGNATURES))

            with open(tmp_path / "data.bin", "rb") as f:
                sig = openpgp_env.verify_detached(
                    tmp_path / "sig.bin", f,
                    require_all_good=two_sigs)

            assert_signature(sig, "TWO_SIGNATURE_MANIFEST",
                             expect_both=two_sigs)
    except OpenPGPNoImplementation as e:
        pytest.skip(str(e))
