import argparse
import base64
import hashlib
import io
import json
import logging
import os
import subprocess
import sys
import tempfile
import time
import zipfile

import requests
from cryptography.hazmat.primitives import serialization as crypto_serialization

from keylime import api_version as keylime_api_version
from keylime import ca_util, config, crypto, keylime_logging, measured_boot, registrar_client, signing, web_util
from keylime.agentstates import AgentAttestState
from keylime.cmd import user_data_encrypt
from keylime.common import algorithms, retry, states, validators
from keylime.ima import file_signatures, ima
from keylime.requests_client import RequestsClient
from keylime.tpm import tpm2_objects
from keylime.tpm.tpm_abstract import TPM_Utilities
from keylime.tpm.tpm_main import tpm

# setup logging
logger = keylime_logging.init_logging("tenant")

# special exception that suppresses stack traces when it happens
class UserError(Exception):
    pass


class Tenant:
    """Simple command processor example."""

    config = None

    verifier_ip = None
    verifier_port = None

    agent_ip = None
    cv_cloudagent_ip = None
    agent_port = None

    registrar_ip = None
    registrar_port = None
    registrar_data = None

    api_version = None

    uuid_service_generate_locally = None
    agent_uuid = None

    K = None
    V = None
    U = None
    auth_tag = None

    tpm_policy = None
    metadata = {}
    allowlist = {}
    ima_policy_name = ""
    ima_sign_verification_keys = []
    revocation_key = ""
    accept_tpm_hash_algs = []
    accept_tpm_encryption_algs = []
    accept_tpm_signing_algs = []
    mb_refstate = None
    supported_version = None

    client_cert = None
    client_key = None
    client_key_password = None
    trusted_server_ca = False
    enable_agent_mtls = False
    verify_server_cert = False
    verify_custom = None

    request_timeout = None

    # Context with the trusted CA certificates from the configuration
    tls_context = None

    # Context with the agent's mTLS certificate
    agent_tls_context = None

    payload = None

    tpm_instance = tpm()

    def __init__(self):
        """Set up required values and TLS"""
        self.nonce = None
        self.agent_ip = None
        self.agent_port = None
        self.verifier_id = None
        self.verifier_ip = config.get("tenant", "verifier_ip")
        self.verifier_port = config.get("tenant", "verifier_port")
        self.registrar_ip = config.get("tenant", "registrar_ip")
        self.registrar_port = config.get("tenant", "registrar_port")
        self.api_version = keylime_api_version.current_version()
        self.enable_agent_mtls = config.getboolean("tenant", "enable_agent_mtls")
        self.request_timeout = config.getint("tenant", "request_timeout", fallback=60)

        logger.info("Setting up client TLS...")
        (cert, key, trusted_ca, key_password), verify_server_cert = web_util.get_tls_options(
            "tenant", is_client=True, logger=logger
        )

        if not self.enable_agent_mtls:
            logger.warning(
                "Warning: agent mTLS is currently disabled, keys will be sent in the clear! This should only be used for testing."
            )

        if not verify_server_cert:
            logger.warning(
                "Warning: server certificate verification is disabled as 'trusted_server_ca' option is set to 'all'. This should only be used for testing."
            )

        if not trusted_ca:
            logger.warning("No certificates provided in 'trusted_server_ca'")

        if cert and not os.path.isfile(cert):
            logger.warning("Could not find file %s provided in 'client_cert'", cert)

        if key and not os.path.isfile(key):
            logger.warning("Could not find file %s provided in 'client_key'", key)

        if all([(cert and os.path.isfile(cert)), (key and os.path.isfile(key))]):
            self.client_cert = cert
            self.client_key = key
            self.client_key_password = key_password
            self.verify_server_cert = verify_server_cert
            self.trusted_server_ca = trusted_ca

            self.tls_context = web_util.generate_tls_context(
                cert, key, trusted_ca, key_password, verify_server_cert, is_client=True, logger=logger
            )

            logger.info("TLS is enabled.")
        else:
            logger.warning("TLS is disabled.")

    @property
    def verifier_base_url(self):
        return f"{self.verifier_ip}:{self.verifier_port}"

    def process_allowlist(self, args):
        # Set up PCR values
        if "tpm_policy" in args and args["tpm_policy"] is not None:
            tpm_policy = args["tpm_policy"]
        else:
            # Use default empty TPM policy
            tpm_policy = "{}"
        self.tpm_policy = TPM_Utilities.readPolicy(tpm_policy)
        logger.info("TPM PCR Mask from policy is %s", self.tpm_policy["mask"])

        if len(args.get("ima_sign_verification_keys")) > 0:
            # Auto-enable IMA (or-bit mask)
            self.tpm_policy["mask"] = hex(int(self.tpm_policy["mask"], 0) | (1 << config.IMA_PCR))

            # Add all IMA file signing verification keys to a keyring
            tenant_keyring = file_signatures.ImaKeyring()
            for filename in args["ima_sign_verification_keys"]:
                pubkey, keyidv2 = file_signatures.get_pubkey_from_file(filename)
                if not pubkey:
                    raise UserError(f"File '{filename}' is not a file with a key")
                tenant_keyring.add_pubkey(pubkey, keyidv2)
            self.ima_sign_verification_keys = tenant_keyring.to_string()

        # Read command-line path string allowlist
        al_data = None

        if "allowlist" in args and args["allowlist"] is not None:

            self.enforce_pcrs(list(self.tpm_policy.keys()), [config.IMA_PCR], "IMA")

            # Auto-enable IMA (or-bit mask)
            self.tpm_policy["mask"] = hex(int(self.tpm_policy["mask"], 0) | (1 << config.IMA_PCR))

            try:
                al_data = ima.read_allowlist(
                    args["allowlist"], args["allowlist_checksum"], args["allowlist_sig"], args["allowlist_sig_key"]
                )
            except Exception as ima_e:
                raise UserError(str(ima_e)) from ima_e

        # Read command-line path string IMA exclude list
        excl_data = None
        if "ima_exclude" in args and args["ima_exclude"] is not None:
            if isinstance(args["ima_exclude"], str):
                excl_data = ima.read_excllist(args["ima_exclude"])
            elif isinstance(args["ima_exclude"], list):
                excl_data = args["ima_exclude"]
            else:
                raise UserError("Invalid exclude list provided")

        # Set up IMA
        if TPM_Utilities.check_mask(self.tpm_policy["mask"], config.IMA_PCR):
            # Process allowlists
            al_data["excllist"] = excl_data
            self.allowlist = al_data

        # Store allowlist name
        if "allowlist_name" in args and args["allowlist_name"] is not None:
            self.ima_policy_name = args["allowlist_name"]
            # Auto-enable IMA (or-bit mask)
            self.tpm_policy["mask"] = hex(int(self.tpm_policy["mask"], 0) | (1 << config.IMA_PCR))

        # Read command-line path string TPM event log (measured boot) reference state
        mb_refstate_data = None
        if "mb_refstate" in args and args["mb_refstate"] is not None:

            self.enforce_pcrs(list(self.tpm_policy.keys()), config.MEASUREDBOOT_PCRS, "measured boot")

            # Auto-enable TPM event log mesured boot (or-bit mask)
            for _pcr in config.MEASUREDBOOT_PCRS:
                self.tpm_policy["mask"] = hex(int(self.tpm_policy["mask"], 0) | (1 << _pcr))

            logger.info(
                "TPM PCR Mask automatically modified is %s to include IMA/Event log PCRs", self.tpm_policy["mask"]
            )

            if isinstance(args["mb_refstate"], str):
                if args["mb_refstate"] == "default":
                    args["mb_refstate"] = config.get("tenant", "mb_refstate")
                mb_refstate_data = measured_boot.read_mb_refstate(args["mb_refstate"])
            else:
                raise UserError("Invalid measured boot reference state (intended state) provided")

        # Set up measured boot (TPM event log) reference state
        if TPM_Utilities.check_mask(self.tpm_policy["mask"], config.MEASUREDBOOT_PCRS[2]):
            # Process measured boot reference state
            self.mb_refstate = mb_refstate_data

    def init_add(self, args):
        """Set up required values. Command line options can overwrite these config values

        Arguments:
            args {[string]} -- agent_ip|agent_port|cv_agent_ip
        """
        if "agent_ip" in args:
            self.agent_ip = args["agent_ip"]

        if "agent_port" in args and args["agent_port"] is not None:
            self.agent_port = args["agent_port"]

        self.registrar_data = registrar_client.getData(
            self.registrar_ip, self.registrar_port, self.agent_uuid, self.tls_context
        )

        if self.registrar_data is None:
            raise UserError(f"Agent ${self.agent_uuid} data not found in the Registrar.")

        # try to get the port or ip from the registrar if it is missing
        if (self.agent_ip is None or self.agent_port is None) and self.registrar_data is not None:
            if self.agent_ip is None:
                if self.registrar_data["ip"] is not None:
                    self.agent_ip = self.registrar_data["ip"]
                else:
                    raise UserError("No Ip was specified or found in the Registrar")

            if self.agent_port is None and self.registrar_data["port"] is not None:
                self.agent_port = self.registrar_data["port"]

        # Check if a contact ip and port for the agent was found
        if self.agent_ip is None:
            raise UserError("The contact ip address for the agent was not specified.")

        if self.agent_port is None:
            raise UserError("The contact port for the agent was not specified.")

        # Auto-detection for API version
        self.supported_version = args["supported_version"]
        if self.supported_version is None:
            # Default to 1.0 if the agent did not send a mTLS certificate
            if self.registrar_data.get("mtls_cert", None) is None:
                self.supported_version = "1.0"
            else:
                # Try to connect to the agent to get supported version
                if self.registrar_data["mtls_cert"] == "disabled":
                    self.enable_agent_mtls = False
                    logger.warning(
                        "Warning: mTLS for agents is disabled: the identity of each node will be based on the properties of the TPM only. "
                        "Unless you have strict control of your network, it is strongly advised that remote code execution should be disabled, "
                        'by setting "payload_script=" and "extract_payload_zip=False" under "[agent]" in agent configuration file.'
                    )
                    tls_context = None
                else:
                    # Store the agent self-signed certificate as a string
                    self.verify_custom = self.registrar_data["mtls_cert"]

                    if not self.agent_tls_context:
                        self.agent_tls_context = web_util.generate_tls_context(
                            self.client_cert,
                            self.client_key,
                            self.trusted_server_ca,
                            self.client_key_password,
                            self.verify_server_cert,
                            is_client=True,
                            ca_cert_string=self.verify_custom,
                            logger=logger,
                        )
                    tls_context = self.agent_tls_context

                with RequestsClient(
                    f"{self.agent_ip}:{self.agent_port}",
                    tls_enabled=self.enable_agent_mtls,
                    tls_context=tls_context,
                ) as get_version:
                    res = get_version.get("/version")
                    if res and res.status_code == 200:
                        try:
                            data = res.json()
                            api_version = data["results"]["supported_version"]
                            if keylime_api_version.validate_version(api_version):
                                self.supported_version = api_version
                            else:
                                logger.warning("API version provided by the agent is not valid")
                        except (TypeError, KeyError):
                            pass

        if self.supported_version is None:
            api_version = keylime_api_version.current_version()
            logger.warning("Could not detect supported API version. Defaulting to %s", api_version)
            self.supported_version = api_version

        # Now set the cv_agent_ip
        if "cv_agent_ip" in args and args["cv_agent_ip"] is not None:
            self.cv_cloudagent_ip = args["cv_agent_ip"]
        else:
            self.cv_cloudagent_ip = self.agent_ip

        # Make sure all keys exist in dictionary
        if "file" not in args:
            args["file"] = None
        if "keyfile" not in args:
            args["keyfile"] = None
        if "payload" not in args:
            args["payload"] = None
        if "ca_dir" not in args:
            args["ca_dir"] = None
        if "incl_dir" not in args:
            args["incl_dir"] = None
        if "ca_dir_pw" not in args:
            args["ca_dir_pw"] = None

        # Set up accepted algorithms
        self.accept_tpm_hash_algs = config.getlist("tenant", "accept_tpm_hash_algs")
        self.accept_tpm_encryption_algs = config.getlist("tenant", "accept_tpm_encryption_algs")
        self.accept_tpm_signing_algs = config.getlist("tenant", "accept_tpm_signing_algs")

        self.process_allowlist(args)

        # if none
        if args["file"] is None and args["keyfile"] is None and args["ca_dir"] is None:
            raise UserError(
                "You must specify one of -k, -f, or --cert to specify the key/contents to be securely delivered to the agent"
            )

        if args["keyfile"] is not None:
            if args["file"] is not None or args["ca_dir"] is not None:
                raise UserError(
                    "You must specify one of -k, -f, or --cert to specify the key/contents to be securely delivered to the agent"
                )

            # read the keys in
            if isinstance(args["keyfile"], dict) and "data" in args["keyfile"]:
                if isinstance(args["keyfile"]["data"], list) and len(args["keyfile"]["data"]) == 1:
                    keyfile = args["keyfile"]["data"][0]
                    if keyfile is None:
                        raise UserError("Invalid key file contents")
                    f = io.StringIO(keyfile)
                else:
                    raise UserError("Invalid key file provided")
            else:
                f = open(args["keyfile"], encoding="utf-8")  # pylint: disable=consider-using-with
            self.K = base64.b64decode(f.readline())
            self.U = base64.b64decode(f.readline())
            self.V = base64.b64decode(f.readline())
            f.close()

            # read the payload in (opt.)
            if isinstance(args["payload"], dict) and "data" in args["payload"]:
                if isinstance(args["payload"]["data"], list) and len(args["payload"]["data"]) > 0:
                    self.payload = args["payload"]["data"][0]
            else:
                if args["payload"] is not None:
                    with open(args["payload"], "rb") as f:
                        self.payload = f.read()

        if args["file"] is not None:
            if args["keyfile"] is not None or args["ca_dir"] is not None:
                raise UserError(
                    "You must specify one of -k, -f, or --cert to specify the key/contents to be securely delivered to the agent"
                )

            if isinstance(args["file"], dict) and "data" in args["file"]:
                if isinstance(args["file"]["data"], list) and len(args["file"]["data"]) > 0:
                    contents = args["file"]["data"][0]
                    if contents is None:
                        raise UserError("Invalid file payload contents")
                else:
                    raise UserError("Invalid file payload provided")
            else:
                with open(args["file"], encoding="utf-8") as f:
                    contents = f.read()
            ret = user_data_encrypt.encrypt(contents)
            self.K = ret["k"]
            self.U = ret["u"]
            self.V = ret["v"]
            self.payload = ret["ciphertext"]

        if args["ca_dir"] is None and args["incl_dir"] is not None:
            raise UserError("--include option is only valid when used with --cert")
        if args["ca_dir"] is not None:
            if args["file"] is not None or args["keyfile"] is not None:
                raise UserError(
                    "You must specify one of -k, -f, or --cert to specify the key/contents to be securely delivered to the agent"
                )
            if args["ca_dir"] == "default":
                args["ca_dir"] = config.CA_WORK_DIR

            if "ca_dir_pw" in args and args["ca_dir_pw"] is not None:
                ca_util.setpassword(args["ca_dir_pw"])

            if not os.path.exists(args["ca_dir"]) or not os.path.exists(os.path.join(args["ca_dir"], "cacert.crt")):
                logger.warning("CA directory does not exist. Creating...")
                ca_util.cmd_init(args["ca_dir"])
            if not os.path.exists(os.path.join(args["ca_dir"], f"{self.agent_uuid}-private.pem")):
                ca_util.cmd_mkcert(args["ca_dir"], self.agent_uuid)

            cert_pkg, serial, subject = ca_util.cmd_certpkg(args["ca_dir"], self.agent_uuid)

            # support revocation
            if not os.path.exists(os.path.join(args["ca_dir"], "RevocationNotifier-private.pem")):
                ca_util.cmd_mkcert(args["ca_dir"], "RevocationNotifier")
            rev_package, _, _ = ca_util.cmd_certpkg(args["ca_dir"], "RevocationNotifier")

            # extract public and private keys from package
            sf = io.BytesIO(rev_package)
            with zipfile.ZipFile(sf) as zf:
                privkey = zf.read("RevocationNotifier-private.pem")
                cert = zf.read("RevocationNotifier-cert.crt")

            # put the cert of the revoker into the cert package
            sf = io.BytesIO(cert_pkg)
            with zipfile.ZipFile(sf, "a", compression=zipfile.ZIP_STORED) as zf:
                zf.writestr("RevocationNotifier-cert.crt", cert)

                # add additional files to zip
                if args["incl_dir"] is not None:
                    if isinstance(args["incl_dir"], dict) and "data" in args["incl_dir"] and "name" in args["incl_dir"]:
                        if isinstance(args["incl_dir"]["data"], list) and isinstance(args["incl_dir"]["name"], list):
                            if len(args["incl_dir"]["data"]) != len(args["incl_dir"]["name"]):
                                raise UserError("Invalid incl_dir provided")
                            for i in range(len(args["incl_dir"]["data"])):
                                zf.writestr(os.path.basename(args["incl_dir"]["name"][i]), args["incl_dir"]["data"][i])
                    else:
                        if os.path.exists(args["incl_dir"]):
                            files = next(os.walk(args["incl_dir"]))[2]
                            for filename in files:
                                with open(os.path.join(args["incl_dir"], filename), "rb") as f:
                                    zf.writestr(os.path.basename(f.name), f.read())
                        else:
                            logger.warning(
                                "Specified include directory %s does not exist. Skipping...", args["incl_dir"]
                            )

            cert_pkg = sf.getvalue()

            # put the private key into the data to be send to the CV
            self.revocation_key = privkey.decode("utf-8")

            # encrypt up the cert package
            ret = user_data_encrypt.encrypt(cert_pkg)
            self.K = ret["k"]
            self.U = ret["u"]
            self.V = ret["v"]
            self.metadata = {"cert_serial": serial, "subject": subject}
            self.payload = ret["ciphertext"]

        if self.payload is not None:
            max_payload_size = config.getint("tenant", "max_payload_size")
            if len(self.payload) > max_payload_size:
                raise UserError(f"Payload size {len(self.payload)} exceeds max size {max_payload_size}")

    def enforce_pcrs(self, policy_pcrs, protected_pcrs, pcr_use):
        policy_pcrs = list(self.tpm_policy.keys())
        policy_pcrs.remove("mask")

        for _pcr in policy_pcrs:
            if int(_pcr) in protected_pcrs:
                logger.error(
                    'WARNING: PCR %s is specified in "tpm_policy", but will in fact be used by %s. Please remove it from policy',
                    _pcr,
                    pcr_use,
                )
                sys.exit(1)

    def preloop(self):
        """encrypt the agent UUID as a check for delivering the correct key"""
        self.auth_tag = crypto.do_hmac(self.K, self.agent_uuid)
        # be very careful printing K, U, or V as they leak in logs stored on unprotected disks
        if config.INSECURE_DEBUG:
            logger.debug("K: %s", base64.b64encode(self.K))
            logger.debug("V: %s", base64.b64encode(self.V))
            logger.debug("U: %s", base64.b64encode(self.U))
            logger.debug("Auth Tag: %s", self.auth_tag)

    def check_ek(self, ekcert):
        """Check the Entity Key

        Arguments:
            ekcert {str} -- The endorsement key, either None, "emulator", or base64 encoded der cert

        Returns:
            [type] -- [description]
        """
        if config.getboolean("tenant", "require_ek_cert"):
            if ekcert == "emulator" and config.DISABLE_EK_CERT_CHECK_EMULATOR:
                logger.info("Not checking ekcert of TPM emulator")
            elif ekcert is None:
                logger.warning("No EK cert provided, require_ek_cert option in config set to True")
                return False
            elif not self.tpm_instance.verify_ek(base64.b64decode(ekcert)):
                logger.warning("Invalid EK certificate")
                return False

        return True

    def validate_tpm_quote(self, public_key, quote, hash_alg):
        """Validate TPM Quote received from the Agent

        Arguments:
            public_key {[type]} -- [description]
            quote {[type]} -- [description]
            hash_alg {bool} -- [description]

        Raises:
            UserError: [description]

        Returns:
            [type] -- [description]
        """
        if self.registrar_data is None:
            logger.warning("AIK not found in registrar, quote not validated")
            return False

        failure = self.tpm_instance.check_quote(
            AgentAttestState(self.agent_uuid),
            self.nonce,
            public_key,
            quote,
            self.registrar_data["aik_tpm"],
            hash_alg=hash_alg,
            compressed=(self.supported_version == "1.0"),
        )
        if failure:
            if self.registrar_data["regcount"] > 1:
                logger.error(
                    "WARNING: This UUID had more than one ek-ekcert registered to it! This might indicate that your system is misconfigured or a malicious host is present. Run 'regdelete' for this agent and restart"
                )
                sys.exit()
            return False

        if self.registrar_data["regcount"] > 1:
            logger.warning(
                "WARNING: This UUID had more than one ek-ekcert registered to it! This might indicate that your system is misconfigured. Run 'regdelete' for this agent and restart"
            )

        if not config.getboolean("tenant", "require_ek_cert") and config.get("tenant", "ek_check_script") == "":
            logger.warning(
                "DANGER: EK cert checking is disabled and no additional checks on EKs have been specified with ek_check_script option. Keylime is not secure!!"
            )
            return True

        # check EK cert and make sure it matches EK
        if not self.check_ek(self.registrar_data["ekcert"]):
            return False
        # if agent is virtual, check phyisical EK cert and make sure it matches phyiscal EK
        if "provider_keys" in self.registrar_data:
            if not self.check_ek(self.registrar_data["provider_keys"]["ekcert"]):
                return False

        # check all EKs with optional script:
        script = config.get("tenant", "ek_check_script")
        if not script:
            return True

        if script[0] != "/":
            script = os.path.join(config.WORK_DIR, script)

        logger.info("Checking EK with script %s", script)
        # now we need to exec the script with the ek and ek cert in vars
        env = os.environ.copy()
        env["AGENT_UUID"] = self.agent_uuid
        env["EK"] = tpm2_objects.pubkey_from_tpm2b_public(
            base64.b64decode(self.registrar_data["ek_tpm"]),
        ).public_bytes(
            crypto_serialization.Encoding.PEM,
            crypto_serialization.PublicFormat.SubjectPublicKeyInfo,
        )
        env["EK_TPM"] = self.registrar_data["ek_tpm"]
        if self.registrar_data["ekcert"] is not None:
            env["EK_CERT"] = self.registrar_data["ekcert"]
        else:
            env["EK_CERT"] = ""

        env["PROVKEYS"] = json.dumps(self.registrar_data.get("provider_keys", {}))
        with subprocess.Popen(
            script, env=env, shell=True, cwd=config.WORK_DIR, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
        ) as proc:
            retval = proc.wait()
            if retval != 0:
                raise UserError("External check script failed to validate EK")
            logger.debug("External check script successfully to validated EK")
            while True:
                line = proc.stdout.readline().decode()
                if line == "":
                    break
                logger.debug("ek_check output: %s", line.strip())
        return True

    def do_cv(self):
        """Initiate v, agent_id and ip and initiate the cloudinit sequence"""
        b64_v = base64.b64encode(self.V).decode("utf-8")
        logger.debug("b64_v: %s", b64_v)
        data = {
            "v": b64_v,
            "cloudagent_ip": self.cv_cloudagent_ip,
            "cloudagent_port": self.agent_port,
            "tpm_policy": json.dumps(self.tpm_policy),
            "ima_policy_bundle": json.dumps(self.allowlist),
            "ima_policy_name": self.ima_policy_name,
            "mb_refstate": json.dumps(self.mb_refstate),
            "ima_sign_verification_keys": json.dumps(self.ima_sign_verification_keys),
            "metadata": json.dumps(self.metadata),
            "revocation_key": self.revocation_key,
            "accept_tpm_hash_algs": self.accept_tpm_hash_algs,
            "accept_tpm_encryption_algs": self.accept_tpm_encryption_algs,
            "accept_tpm_signing_algs": self.accept_tpm_signing_algs,
            "ak_tpm": self.registrar_data["aik_tpm"],
            "mtls_cert": self.registrar_data.get("mtls_cert", None),
            "supported_version": self.supported_version,
        }
        json_message = json.dumps(data)
        do_cv = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
        response = do_cv.post(
            (f"/v{self.api_version}/agents/{self.agent_uuid}"), data=json_message, timeout=self.request_timeout
        )

        if response.status_code == 503:
            logger.error(
                "Cannot connect to Verifier at %s with Port %s. Connection refused.",
                self.verifier_ip,
                self.verifier_port,
            )
            sys.exit()
        elif response.status_code == 504:
            logger.error("Verifier at %s with Port %s timed out.", self.verifier_ip, self.verifier_port)
            sys.exit()

        if response.status_code == 409:
            # this is a conflict, need to update or delete it
            Tenant._print_json_response(response)
            sys.exit()
        elif response.status_code != 200:
            keylime_logging.log_http_response(logger, logging.ERROR, response.json())
            logger.error(
                "POST command response: %s Unexpected response from Cloud Verifier: %s",
                response.status_code,
                response.text,
            )
            sys.exit()

    def do_cvstatus(self):
        """Perform operational state look up for agent on the verifier"""

        do_cvstatus = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)

        response = do_cvstatus.get((f"/v{self.api_version}/agents/{self.agent_uuid}"), timeout=self.request_timeout)

        if response.status_code == 503:
            logger.error(
                "Cannot connect to Verifier at %s with Port %s. Connection refused.",
                self.verifier_ip,
                self.verifier_port,
            )
            return response.json()
        if response.status_code == 504:
            logger.error("Verifier at %s with Port %s timed out.", self.verifier_ip, self.verifier_port)
            return response.json()
        if response.status_code == 404:
            logger.info(
                "Verifier at %s with Port %s does not have agent %s.",
                self.verifier_ip,
                self.verifier_port,
                self.agent_uuid,
            )
            return response.json()
        if response.status_code == 200:
            response = response.json()

            res = response.pop("results")
            response["results"] = {self.agent_uuid: res}

            operational_state = states.state_to_str(response["results"][self.agent_uuid]["operational_state"])
            response["results"][self.agent_uuid]["operational_state"] = operational_state

            logger.info("Agent Info:\n%s", json.dumps(response["results"]))

            return response

        logger.info(
            "Status command response: %s. Unexpected response from Cloud Verifier %s on port %s. %s",
            response.status_code,
            self.verifier_ip,
            self.verifier_port,
            str(response),
        )
        return response

    def do_cvlist(self):
        """List all agent statuses in cloudverifier"""

        do_cvstatus = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)

        verifier_id = ""
        if self.verifier_id is not None:
            verifier_id = self.verifier_id

        response = do_cvstatus.get(f"/v{self.api_version}/agents/?verifier={verifier_id}", timeout=self.request_timeout)

        if response.status_code == 503:
            logger.error(
                "Cannot connect to Verifier at %s with Port %s. Connection refused.",
                self.verifier_ip,
                self.verifier_port,
            )
            return response.json()
        if response.status_code == 504:
            logger.error("Verifier at %s with Port %s timed out.", self.verifier_ip, self.verifier_port)
            return response.json()
        if response.status_code == 404:
            logger.info(
                "Verifier at %s with Port %s does not have agent %s.",
                self.verifier_ip,
                self.verifier_port,
                self.agent_uuid,
            )
            return response.json()
        if response.status_code == 200:
            response = response.json()

            logger.info('From verifier %s port %s retrieved: "%s"', self.verifier_ip, self.verifier_port, response)

            return response

        logger.info(
            "Status command response: %s. Unexpected response from Cloud Verifier %s on port %s. %s",
            response.status_code,
            self.verifier_ip,
            self.verifier_port,
            str(response),
        )
        return response

    def do_cvbulkinfo(self):
        """Perform operational state look up for agent"""

        do_cvstatus = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)

        verifier_id = ""
        if self.verifier_id is not None:
            verifier_id = self.verifier_id

        response = do_cvstatus.get(
            f"/v{self.api_version}/agents/?bulk={True}&verifier={verifier_id}", timeout=self.request_timeout
        )

        if response.status_code == 503:
            logger.error(
                "Cannot connect to Verifier at %s with Port %s. Connection refused.",
                self.verifier_ip,
                self.verifier_port,
            )
            return response.json()
        if response.status_code == 504:
            logger.error("Verifier at %s with Port %s timed out.", self.verifier_ip, self.verifier_port)
            return response.json()
        if response.status_code == 404:
            logger.info(
                "Verifier at %s with Port %s does not have agent %s.",
                self.verifier_ip,
                self.verifier_port,
                self.agent_uuid,
            )
            return response.json()
        if response.status_code == 200:
            response = response.json()

            for agent in response["results"].keys():
                response["results"][agent]["operational_state"] = states.state_to_str(
                    response["results"][agent]["operational_state"]
                )
            logger.info("Bulk Agent Info:\n%s", json.dumps(response["results"]))

            return response

        logger.info(
            "Status command response: %s. Unexpected response from Cloud Verifier %s on port %s. %s",
            response.status_code,
            self.verifier_ip,
            self.verifier_port,
            str(response),
        )
        return response

    def do_cvdelete(self, verifier_check=True):
        """Delete agent from Verifier."""
        if verifier_check:
            cvresponse = self.do_cvstatus()

            if not isinstance(cvresponse, dict):
                return cvresponse

            if cvresponse["code"] != 200:
                logger.error("Could not get status of agent %s from verifier %s.", self.agent_uuid, self.verifier_ip)
                return cvresponse

            self.verifier_ip = cvresponse["results"][self.agent_uuid]["verifier_ip"]
            self.verifier_port = cvresponse["results"][self.agent_uuid]["verifier_port"]

        do_cvdelete = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
        response = do_cvdelete.delete(f"/v{self.api_version}/agents/{self.agent_uuid}", timeout=self.request_timeout)

        response = response.json()

        if response["code"] == 503:
            logger.error(
                "Cannot connect to Verifier at %s with Port %s. Connection refused.",
                self.verifier_ip,
                self.verifier_port,
            )
            return response
        if response["code"] == 504:
            logger.error("Verifier at %s with Port %s timed out.", self.verifier_ip, self.verifier_port)
            return response
        if response["code"] == 202:
            deleted = False
            for _ in range(12):
                get_cvdelete = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
                response = get_cvdelete.get(
                    f"/v{self.api_version}/agents/{self.agent_uuid}", timeout=self.request_timeout
                )

                if response.status_code == 404:
                    deleted = True
                    break
                time.sleep(0.4)
            if deleted:
                logger.info("Agent %s deleted from the CV", self.agent_uuid)
                return response.json()
            logger.error("Timed out waiting for delete of agent %s to complete at CV", self.agent_uuid)
            return response.json()
        if response["code"] == 200:
            logger.info("Agent %s deleted from the CV", self.agent_uuid)
            return response

        keylime_logging.log_http_response(logger, logging.ERROR, response)
        return response

    def do_regstatus(self):
        agent_info = registrar_client.getData(self.registrar_ip, self.registrar_port, self.agent_uuid, self.tls_context)

        if not agent_info:
            logger.info(
                "Agent %s does not exist on the registrar. Please register the agent with the registrar.",
                self.agent_uuid,
            )
            response = {
                "code": 404,
                "status": f"Agent {self.agent_uuid} does not exist on "
                f"registrar {self.registrar_ip} port {self.registrar_port}.",
                "results": {},
            }
            logger.info(json.dumps(response))
            return response

        response = {
            "code": 200,
            "status": f"Agent {self.agent_uuid} exists on "
            f"registrar {self.registrar_ip} port {self.registrar_port}.",
            "results": {},
        }
        response["results"][self.agent_uuid] = agent_info
        response["results"][self.agent_uuid]["operational_state"] = states.state_to_str(states.REGISTERED)

        logger.info(json.dumps(response))

        return response

    def do_reglist(self):
        """List agents from Registrar"""
        response = registrar_client.doRegistrarList(
            self.registrar_ip, self.registrar_port, tls_context=self.tls_context
        )

        logger.info(
            "From registrar %s port %s retrieved %s", self.registrar_ip, self.registrar_port, json.dumps(response)
        )
        return response

    def do_regdelete(self):
        """Delete agent from Registrar"""
        response = registrar_client.doRegistrarDelete(
            self.registrar_ip, self.registrar_port, self.agent_uuid, tls_context=self.tls_context
        )

        return response

    def do_status(self):
        """Perform operational state look up for agent"""

        regresponse = self.do_regstatus()

        if regresponse["code"] == 404:
            return regresponse

        cvresponse = self.do_cvstatus()

        if not isinstance(cvresponse, dict):
            logger.error(
                "Unexpected response from Cloud Verifier %s on port %s. response %s",
                self.verifier_ip,
                self.verifier_port,
                str(cvresponse),
            )
            return cvresponse

        if regresponse["code"] == 200 and cvresponse["code"] == 200:
            return cvresponse
        if regresponse["code"] == 200 and cvresponse["code"] != 200:
            return regresponse

        logger.error(
            "Unknown inconsistent state between registrar %s on "
            "port %s and verifier %s on port %s occured. Got "
            "registrar response %s verifier response %s",
            self.verifier_ip,
            self.verifier_port,
            self.registrar_ip,
            self.registrar_port,
            str(regresponse),
            str(cvresponse),
        )

        return {"registrar": regresponse, "verifier": cvresponse}

    def do_cvreactivate(self, verifier_check=True):
        """Reactive Agent."""
        if verifier_check:
            agent_json = self.do_cvstatus()
            self.verifier_ip = agent_json["results"][self.agent_uuid]["verifier_ip"]
            self.verifier_port = agent_json["results"][self.agent_uuid]["verifier_port"]

        do_cvreactivate = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
        response = do_cvreactivate.put(
            f"/v{self.api_version}/agents/{self.agent_uuid}/reactivate",
            data=b"",
            timeout=self.request_timeout,
        )

        if response.status_code == 503:
            logger.error(
                "Cannot connect to Verifier at %s with Port %s. Connection refused.",
                self.verifier_ip,
                self.verifier_port,
            )
            return response.json()
        if response.status_code == 504:
            logger.error("Verifier at %s with Port %s timed out.", self.verifier_ip, self.verifier_port)
            return response.json()
        if response.status_code == 200:
            logger.info("Agent %s re-activated", self.agent_uuid)
            return response.json()

        response_body = response.json()
        keylime_logging.log_http_response(logger, logging.ERROR, response_body)
        logger.error("Update command response: %s Unexpected response from Cloud Verifier.", response.status_code)
        return response.json()

    def do_cvstop(self):
        """Stop declared active agent"""
        params = f"/v{self.api_version}/agents/{self.agent_uuid}/stop"
        do_cvstop = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
        response = do_cvstop.put(params, data=b"", timeout=self.request_timeout)
        if response.status_code == 503:
            logger.error(
                "Cannot connect to Verifier at %s with Port %s. Connection refused.",
                self.verifier_ip,
                self.verifier_port,
            )
            sys.exit()
        elif response.status_code == 504:
            logger.error("Verifier at %s with Port %s timed out.", self.verifier_ip, self.verifier_port)
            sys.exit()

        response_body = response.json()
        if response.status_code != 200:
            keylime_logging.log_http_response(logger, logging.ERROR, response_body)
        else:
            logger.info("Agent %s stopped", self.agent_uuid)

    def do_quote(self):
        """Perform TPM quote by GET towards Agent

        Raises:
            UserError: Connection handler
        """
        self.nonce = TPM_Utilities.random_password(20)

        numtries = 0
        response = None
        # Note: We need a specific retry handler (perhaps in common), no point having localised unless we have too.
        while True:
            try:
                params = f"/v{self.supported_version}/quotes/identity?nonce=%s" % (self.nonce)
                cloudagent_base_url = f"{self.agent_ip}:{self.agent_port}"

                if self.enable_agent_mtls and self.registrar_data and self.registrar_data["mtls_cert"]:
                    with RequestsClient(
                        cloudagent_base_url,
                        self.enable_agent_mtls,
                        tls_context=self.agent_tls_context,
                    ) as do_quote:
                        response = do_quote.get(params, timeout=self.request_timeout)
                else:
                    logger.warning("Connecting to agent without using mTLS!")
                    do_quote = RequestsClient(cloudagent_base_url, tls_enabled=False)
                    response = do_quote.get(params, timeout=self.request_timeout)

                print(response)
                response_body = response.json()

            except Exception as e:
                if response.status_code in (503, 504):
                    numtries += 1
                    maxr = config.getint("tenant", "max_retries")
                    if numtries >= maxr:
                        logger.error(
                            "Tenant cannot establish connection to agent on %s with port %s",
                            self.agent_ip,
                            self.agent_port,
                        )
                        sys.exit()
                    interval = config.getfloat("tenant", "retry_interval")
                    exponential_backoff = config.getboolean("tenant", "exponential_backoff")
                    next_retry = retry.retry_time(exponential_backoff, interval, numtries, logger)
                    logger.info(
                        "Tenant connection to agent at %s refused %s/%s times, trying again in %s seconds...",
                        self.agent_ip,
                        numtries,
                        maxr,
                        next_retry,
                    )
                    time.sleep(next_retry)
                    continue

                raise e
            break

        if response is not None and response.status_code != 200:
            raise UserError(f"Status command response: {response.status_code} Unexpected response from Cloud Agent.")

        if "results" not in response_body:
            raise UserError(f"Error: unexpected http response body from Cloud Agent: {str(response.status)}")

        quote = response_body["results"]["quote"]
        logger.debug("Agent_quote received quote: %s", quote)

        public_key = response_body["results"]["pubkey"]
        logger.debug("Agent_quote received public key: %s", public_key)

        # Ensure hash_alg is in accept_tpm_hash_algs list
        hash_alg = response_body["results"]["hash_alg"]
        logger.debug("Agent_quote received hash algorithm: %s", hash_alg)
        if not algorithms.is_accepted(
            hash_alg, config.getlist("tenant", "accept_tpm_hash_algs")
        ) or not algorithms.Hash.is_recognized(hash_alg):
            raise UserError(f"TPM Quote is using an unaccepted hash algorithm: {hash_alg}")

        # Ensure enc_alg is in accept_tpm_encryption_algs list
        enc_alg = response_body["results"]["enc_alg"]
        logger.debug("Agent_quote received encryption algorithm: %s", enc_alg)
        if not algorithms.is_accepted(enc_alg, config.getlist("tenant", "accept_tpm_encryption_algs")):
            raise UserError(f"TPM Quote is using an unaccepted encryption algorithm: {enc_alg}")

        # Ensure sign_alg is in accept_tpm_encryption_algs list
        sign_alg = response_body["results"]["sign_alg"]
        logger.debug("Agent_quote received signing algorithm: %s", sign_alg)
        if not algorithms.is_accepted(sign_alg, config.getlist("tenant", "accept_tpm_signing_algs")):
            raise UserError(f"TPM Quote is using an unaccepted signing algorithm: {sign_alg}")

        if not self.validate_tpm_quote(public_key, quote, algorithms.Hash(hash_alg)):
            raise UserError(f"TPM Quote from cloud agent is invalid for nonce: {self.nonce}")

        logger.info("Quote from %s validated", self.agent_ip)

        # encrypt U with the public key
        encrypted_U = crypto.rsa_encrypt(crypto.rsa_import_pubkey(public_key), self.U)

        b64_encrypted_u = base64.b64encode(encrypted_U)
        logger.debug("b64_encrypted_u: %s", b64_encrypted_u.decode("utf-8"))
        data = {"encrypted_key": b64_encrypted_u.decode("utf-8"), "auth_tag": self.auth_tag}

        if self.payload is not None:
            data["payload"] = self.payload.decode("utf-8")

        # post encrypted U back to CloudAgent
        params = f"/v{self.supported_version}/keys/ukey"
        cloudagent_base_url = f"{self.agent_ip}:{self.agent_port}"

        if self.enable_agent_mtls and self.registrar_data and self.registrar_data["mtls_cert"]:
            with RequestsClient(
                cloudagent_base_url,
                self.enable_agent_mtls,
                tls_context=self.agent_tls_context,
            ) as post_ukey:
                response = post_ukey.post(params, json=data, timeout=self.request_timeout)
        else:
            logger.warning("Connecting to agent without using mTLS!")
            post_ukey = RequestsClient(cloudagent_base_url, tls_enabled=False)
            response = post_ukey.post(params, json=data, timeout=self.request_timeout)

        if response.status_code == 503:
            logger.error(
                "Cannot connect to Agent at %s with Port %s. Connection refused.", self.agent_ip, self.agent_port
            )
            sys.exit()
        elif response.status_code == 504:
            logger.error("Verifier at %s with Port %s timed out.", self.verifier_ip, self.verifier_port)
            sys.exit()

        if response.status_code != 200:
            keylime_logging.log_http_response(logger, logging.ERROR, response_body)
            raise UserError(
                f"Posting of Encrypted U to the Cloud Agent failed with response code {response.status_code} ({response.text})"
            )

    def do_verify(self):
        """Perform verify using a random generated challenge"""
        challenge = TPM_Utilities.random_password(20)
        numtries = 0

        while True:
            response = None
            try:
                cloudagent_base_url = f"{self.agent_ip}:{self.agent_port}"

                if self.registrar_data and self.registrar_data["mtls_cert"]:
                    with RequestsClient(
                        cloudagent_base_url,
                        self.enable_agent_mtls,
                        tls_context=self.agent_tls_context,
                    ) as do_verify:
                        response = do_verify.get(
                            f"/v{self.supported_version}/keys/verify?challenge={challenge}",
                            timeout=self.request_timeout,
                        )
                else:
                    logger.warning("Connecting to agent without using mTLS!")
                    do_verify = RequestsClient(cloudagent_base_url, tls_enabled=False)
                    response = do_verify.get(
                        f"/v{self.supported_version}/keys/verify?challenge={challenge}", timeout=self.request_timeout
                    )

                response_body = response.json()
            except Exception as e:
                if response is not None and response.status_code in (503, 504):
                    numtries += 1
                    maxr = config.getint("tenant", "max_retries")
                    if numtries >= maxr:
                        logger.error(
                            "Cannot establish connection to agent on %s with port %s", self.agent_ip, self.agent_port
                        )
                        self.do_cvstop()
                        sys.exit()
                    interval = config.getfloat("tenant", "retry_interval")
                    exponential_backoff = config.getboolean("tenant", "exponential_backoff")
                    next_retry = retry.retry_time(exponential_backoff, interval, numtries, logger)
                    logger.info(
                        "Verifier connection to agent at %s refused %s/%s times, trying again in %s seconds...",
                        self.agent_ip,
                        numtries,
                        maxr,
                        next_retry,
                    )
                    time.sleep(next_retry)
                    continue
                self.do_cvstop()
                raise e
            if response.status_code == 200:
                if "results" not in response_body or "hmac" not in response_body["results"]:
                    logger.critical("Error: unexpected http response body from Cloud Agent: %s", response.status_code)
                    self.do_cvstop()
                    break
                mac = response_body["results"]["hmac"]

                ex_mac = crypto.do_hmac(self.K, challenge)

                if mac == ex_mac:
                    logger.info("Key derivation successful")
                else:
                    logger.error("Key derivation failed")
                    self.do_cvstop()
            else:
                keylime_logging.log_http_response(logger, logging.ERROR, response_body)
                numtries += 1
                maxr = config.getint("tenant", "max_retries")
                if numtries >= maxr:
                    logger.error("Agent on %s with port %s failed key derivation", self.agent_ip, self.agent_port)
                    self.do_cvstop()
                    sys.exit()
                interval = config.getfloat("tenant", "retry_interval")
                exponential_backoff = config.getboolean("tenant", "exponential_backoff")
                next_retry = retry.retry_time(exponential_backoff, interval, numtries, logger)
                logger.info(
                    "Key derivation not yet complete (retry %s/%s), trying again in %s seconds... (Ctrl-C to stop)",
                    numtries,
                    maxr,
                    next_retry,
                )
                time.sleep(next_retry)
                continue
            break

    def do_add_allowlist(self, args):
        if "allowlist_name" not in args or not args["allowlist_name"]:
            raise UserError("allowlist_name is required to add an allowlist")

        self.process_allowlist(args)
        data = {"tpm_policy": json.dumps(self.tpm_policy), "ima_policy_bundle": json.dumps(self.allowlist)}
        body = json.dumps(data)
        cv_client = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
        response = cv_client.post(
            f"/v{self.api_version}/allowlists/{self.ima_policy_name}", data=body, timeout=self.request_timeout
        )
        Tenant._print_json_response(response)

    def do_delete_allowlist(self, name):
        cv_client = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
        response = cv_client.delete(f"/v{self.api_version}/allowlists/{name}", timeout=self.request_timeout)
        Tenant._print_json_response(response)

    def do_show_allowlist(self, name):  # pylint: disable=unused-argument
        cv_client = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
        response = cv_client.get(f"/v{self.api_version}/allowlists/{name}", timeout=self.request_timeout)
        print(f"Show allowlist command response: {response.status_code}.")
        Tenant._print_json_response(response)

    @staticmethod
    def _print_json_response(response):
        try:
            json_response = response.json()
        except ValueError:
            json_response = "{}"
        print(json_response)


def write_to_namedtempfile(data, delete_tmp_files):
    temp = tempfile.NamedTemporaryFile(  # pylint: disable=consider-using-with
        prefix="keylime-", delete=delete_tmp_files
    )
    temp.write(data)
    temp.flush()
    return temp.name


def main(argv=sys.argv):  # pylint: disable=dangerous-default-value
    """[summary]

    Keyword Arguments:
        argv {[type]} -- [description] (default: {sys.argv})

    Raises:
        UserError: [description]
        UserError: [description]
        UserError: [description]
    """
    parser = argparse.ArgumentParser(argv[0])
    parser.add_argument(
        "-c",
        "--command",
        action="store",
        dest="command",
        default="add",
        help="valid commands are add,delete,update,"
        "regstatus,cvstatus,status,reglist,cvlist,reactivate,"
        "regdelete,bulkinfo,addallowlist,showallowlist,deleteallowlist. defaults to add",
    )
    parser.add_argument(
        "-t", "--targethost", action="store", dest="agent_ip", help="the IP address of the host to provision"
    )
    parser.add_argument(
        "-tp", "--targetport", action="store", dest="agent_port", help="the Port of the host to provision"
    )
    parser.add_argument(
        "-r",
        "--registrarhost",
        action="store",
        dest="registrar_ip",
        help="the IP address of the registrar where to retrieve the agents data from.",
    )
    parser.add_argument(
        "-rp", "--registrarport", action="store", dest="registrar_port", help="the port of the registrar."
    )
    parser.add_argument(
        "--cv_targethost",
        action="store",
        default=None,
        dest="cv_agent_ip",
        help="the IP address of the host to provision that the verifier will use (optional).  Use only if different than argument to option -t/--targethost",
    )
    parser.add_argument("-v", "--cv", action="store", dest="verifier_ip", help="the IP address of the cloud verifier")
    parser.add_argument("-vp", "--cvport", action="store", dest="verifier_port", help="the port of the cloud verifier")
    parser.add_argument(
        "-vi", "--cvid", action="store", dest="verifier_id", help="the unique identifier of a cloud verifier"
    )
    parser.add_argument(
        "-nvc",
        "--no-verifier-check",
        action="store_false",
        dest="verifier_check",
        default=True,
        help="Disable the check to confirm if the agent is being processed by the specified verifier. Use only with -c/--command delete or reactivate",
    )
    parser.add_argument("-u", "--uuid", action="store", dest="agent_uuid", help="UUID for the agent to provision")
    parser.add_argument(
        "-f", "--file", action="store", default=None, help="Deliver the specified plaintext to the provisioned agent"
    )
    parser.add_argument(
        "--cert",
        action="store",
        dest="ca_dir",
        default=None,
        help='Create and deliver a certificate using a CA created by ca-util. Pass in the CA directory or use "default" to use the standard dir',
    )
    parser.add_argument(
        "-k", "--key", action="store", dest="keyfile", help="an intermedia key file produced by user_data_encrypt"
    )
    parser.add_argument(
        "-p",
        "--payload",
        action="store",
        default=None,
        help="Specify the encrypted payload to deliver with encrypted keys specified by -k",
    )
    parser.add_argument(
        "--include",
        action="store",
        dest="incl_dir",
        default=None,
        help="Include additional files in provided directory in certificate zip file.  Must be specified with --cert",
    )
    parser.add_argument(
        "--allowlist", action="store", dest="allowlist", default=None, help="Specify the file path of an allowlist"
    )
    parser.add_argument(
        "--signature-verification-key",
        "--sign_verification_key",
        action="append",
        dest="ima_sign_verification_keys",
        default=[],
        help="Specify an IMA file signature verification key",
    )
    parser.add_argument(
        "--signature-verification-key-sig",
        action="append",
        dest="ima_sign_verification_key_sigs",
        default=[],
        help="Specify the GPG signature file for an IMA file signature verification key; pair this option with --signature-verification-key",
    )
    parser.add_argument(
        "--signature-verification-key-sig-key",
        action="append",
        dest="ima_sign_verification_key_sig_keys",
        default=[],
        help="Specify the GPG public key file use to validate the --signature-verification-key-sig; pair this option with --signature-verification-key",
    )
    parser.add_argument(
        "--signature-verification-key-url",
        action="append",
        dest="ima_sign_verification_key_urls",
        default=[],
        help="Specify the URL for a remote IMA file signature verification key",
    )
    parser.add_argument(
        "--signature-verification-key-sig-url",
        action="append",
        dest="ima_sign_verification_key_sig_urls",
        default=[],
        help="Specify the URL for the remote GPG signature of a remote IMA file signature verification key; pair this option with --signature-verification-key-url",
    )
    parser.add_argument(
        "--signature-verification-key-sig-url-key",
        action="append",
        dest="ima_sign_verification_key_sig_url_keys",
        default=[],
        help="Specify the GPG public key file used to validate the --signature-verification-key-sig-url; pair this option with --signature-verification-key-url",
    )
    parser.add_argument(
        "--mb_refstate",
        action="store",
        dest="mb_refstate",
        default=None,
        help="Specify the location of a measure boot reference state (intended state)",
    )
    parser.add_argument(
        "--allowlist-checksum",
        action="store",
        dest="allowlist_checksum",
        default=None,
        help="Specify the SHA-256 checksum of an allowlist",
    )
    parser.add_argument(
        "--allowlist-sig",
        action="store",
        dest="allowlist_sig",
        default=None,
        help="Specify the GPG signature file of an allowlist",
    )
    parser.add_argument(
        "--allowlist-sig-key",
        action="store",
        dest="allowlist_sig_key",
        default=None,
        help="Specify the GPG public key file used to validate the --allowlist-sig or --allowlist-sig-url",
    )
    parser.add_argument(
        "--allowlist-url",
        action="store",
        dest="allowlist_url",
        default=None,
        help="Specify the URL of a remote allowlist",
    )
    parser.add_argument(
        "--allowlist-sig-url",
        action="store",
        dest="allowlist_sig_url",
        default=None,
        help="Specify the URL of the remote GPG signature file of an allowlist",
    )
    parser.add_argument(
        "--exclude",
        action="store",
        dest="ima_exclude",
        default=None,
        help="Specify the location of an IMA exclude list",
    )
    parser.add_argument(
        "--tpm_policy",
        action="store",
        dest="tpm_policy",
        default=None,
        help='Specify a TPM policy in JSON format. e.g., {"15":"0000000000000000000000000000000000000000"}',
    )
    parser.add_argument(
        "--verify",
        action="store_true",
        default=False,
        help="Block on cryptographically checked key derivation confirmation from the agent once it has been provisioned",
    )
    parser.add_argument("--allowlist-name", help="The name of allowlist to operate with")
    parser.add_argument(
        "--supported-version",
        default=None,
        action="store",
        dest="supported_version",
        help="API version that is supported by the agent. Detected automatically by default",
    )

    args = parser.parse_args(argv[1:])

    # Make sure argument dependencies are enforced
    if args.ima_exclude and not args.allowlist:
        parser.error("--exclude cannot be used without an --allowlist")
    if args.allowlist and args.allowlist_url:
        parser.error("--allowlist and --allowlist-url cannot be specified at the same time")
    if args.allowlist_url and not (args.allowlist_sig or args.allowlist_sig_url or args.allowlist_checksum):
        parser.error(
            "--allowlist-url must have either --allowlist-sig, --allowlist-sig-url or --allowlist-checksum to verifier integrity"
        )
    if args.allowlist_sig and not (args.allowlist_url or args.allowlist):
        parser.error("--allowlist-sig must have either --allowlist or --allowlist-url")
    if args.allowlist_sig_url and not (args.allowlist_url or args.allowlist):
        parser.error("--allowlist-sig-url must have either --allowlist or --allowlist-url")
    if args.allowlist_checksum and not (args.allowlist_url or args.allowlist):
        parser.error("--allowlist-checksum must have either --allowlist or --allowlist-url")
    if args.allowlist_sig and not args.allowlist_sig_key:
        parser.error("--allowlist-sig must also have --allowlist-sig-key")
    if args.allowlist_sig_url and not args.allowlist_sig_key:
        parser.error("--allowlist-sig-url must also have --allowlist-sig-key")
    if args.allowlist_sig_key and not (args.allowlist_sig or args.allowlist_sig_url):
        parser.error("--allowlist-sig-key must have either --allowlist-sig or --allowlist-sig-url")

    mytenant = Tenant()

    if args.agent_uuid is not None:
        mytenant.agent_uuid = args.agent_uuid
        # if the uuid is actually a public key, then hash it
        if mytenant.agent_uuid.startswith("-----BEGIN PUBLIC KEY-----"):
            mytenant.agent_uuid = hashlib.sha256(mytenant.agent_uuid).hexdigest()
        if not validators.valid_agent_id(mytenant.agent_uuid):
            raise UserError("The agent ID set via agent uuid parameter use invalid characters")
    else:
        logger.warning("Using default UUID d432fbb3-d2f1-4a97-9ef7-75bd81c00000")
        mytenant.agent_uuid = "d432fbb3-d2f1-4a97-9ef7-75bd81c00000"

    if args.verifier_id is not None:
        mytenant.verifier_id = args.verifier_id
    if args.verifier_ip is not None:
        mytenant.verifier_ip = args.verifier_ip
    if args.verifier_port is not None:
        mytenant.verifier_port = args.verifier_port

    if args.registrar_ip is not None:
        mytenant.registrar_ip = args.registrar_ip
    if args.registrar_port is not None:
        mytenant.registrar_port = args.registrar_port

    # we only need to fetch remote files if we are adding or updating
    if args.command in ["add", "update", "addallowlist"]:
        delete_tmp_files = logger.level > logging.DEBUG  # delete tmp files unless in DEBUG mode

        if args.allowlist_url:
            logger.info("Downloading Allowlist from %s", args.allowlist_url)
            response = requests.get(args.allowlist_url, timeout=mytenant.request_timeout, allow_redirects=False)
            if response.status_code == 200:
                args.allowlist = write_to_namedtempfile(response.content, delete_tmp_files)
                logger.debug("Allowlist temporarily saved in %s", args.allowlist)
            else:
                raise Exception(
                    f"Downloading allowlist ({args.allowlist_url}) failed with status code {response.status_code}!"
                )

        if args.allowlist_sig_url:
            logger.info("Downloading Allowlist signature from %s", args.allowlist_sig_url)
            response = requests.get(args.allowlist_sig_url, timeout=mytenant.request_timeout, allow_redirects=False)
            if response.status_code == 200:
                args.allowlist_sig = write_to_namedtempfile(response.content, delete_tmp_files)
                logger.debug("Allowlist signature temporarily saved in %s", args.allowlist_sig)
            else:
                raise Exception(
                    f"Downloading allowlist signature ({args.allowlist_sig_url}) failed with status code {response.status_code}!"
                )

        # verify all the local keys for which we have a signature file and a key to verify
        for i, key_file in enumerate(args.ima_sign_verification_keys):
            if len(args.ima_sign_verification_key_sigs) <= i:
                break
            keysig_file = args.ima_sign_verification_key_sigs[i]
            if len(args.ima_sign_verification_key_sig_keys) == 0:
                raise UserError(f"A gpg key is missing for key signature file '{keysig_file}'")

            gpg_key_file = args.ima_sign_verification_key_sig_keys[i]
            signing.verify_signature_from_file(gpg_key_file, key_file, keysig_file, "IMA file signing key")

            logger.info("Signature verification on %s was successful", key_file)

        # verify all the remote keys for which we have a signature URL and key to to verify
        # Append the downloaded key files to args.ima_sign_verification_keys
        for i, key_url in enumerate(args.ima_sign_verification_key_urls):

            logger.info("Downloading key from %s", key_url)
            response = requests.get(key_url, timeout=mytenant.request_timeout, allow_redirects=False)
            if response.status_code == 200:
                key_file = write_to_namedtempfile(response.content, delete_tmp_files)
                args.ima_sign_verification_keys.append(key_file)
                logger.debug("Key temporarily saved in %s", key_file)
            else:
                raise Exception(f"Downloading key ({key_url}) failed with status code {response.status_code}!")

            if len(args.ima_sign_verification_key_sig_urls) <= i:
                continue

            keysig_url = args.ima_sign_verification_key_sig_urls[i]

            if len(args.ima_sign_verification_key_sig_url_keys) == 0:
                raise UserError(f"A gpg key is missing for key signature URL '{keysig_url}'")

            logger.info("Downloading key signature from %s", keysig_url)
            response = requests.get(keysig_url, timeout=mytenant.request_timeout, allow_redirects=False)
            if response.status_code == 200:
                keysig_file = write_to_namedtempfile(response.content, delete_tmp_files)
                logger.debug("Key signature temporarily saved in %s", keysig_file)
            else:
                raise Exception(
                    f"Downloading key signature ({key_url}) failed with status code {response.status_code}!"
                )

            gpg_key_file = args.ima_sign_verification_key_sig_url_keys[i]
            signing.verify_signature_from_file(gpg_key_file, key_file, keysig_file, "IMA file signing key")
            logger.info("Signature verification on %s was successful", key_url)

    if args.command == "add":
        mytenant.init_add(vars(args))
        mytenant.preloop()
        mytenant.do_quote()
        mytenant.do_cv()
        if args.verify:
            mytenant.do_verify()
    elif args.command == "update":
        mytenant.init_add(vars(args))
        mytenant.do_cvdelete(args.verifier_check)
        mytenant.preloop()
        mytenant.do_quote()
        mytenant.do_cv()
        if args.verify:
            mytenant.do_verify()
    elif args.command == "delete":
        mytenant.do_cvdelete(args.verifier_check)
    elif args.command == "status":
        mytenant.do_status()
    elif args.command == "cvstatus":
        mytenant.do_cvstatus()
    elif args.command == "bulkinfo":
        mytenant.do_cvbulkinfo()
    elif args.command == "cvlist":
        mytenant.do_cvlist()
    elif args.command == "reactivate":
        mytenant.do_cvreactivate(args.verifier_check)
    elif args.command == "regstatus":
        mytenant.do_regstatus()
    elif args.command == "reglist":
        mytenant.do_reglist()
    elif args.command == "regdelete":
        mytenant.do_regdelete()
    elif args.command == "addallowlist":
        mytenant.do_add_allowlist(vars(args))
    elif args.command == "showallowlist":
        mytenant.do_show_allowlist(args.allowlist_name)
    elif args.command == "deleteallowlist":
        mytenant.do_delete_allowlist(args.allowlist_name)
    else:
        raise UserError(f"Invalid command specified: {args.command}")
