示例#1
0
def await_notifications(callback, revocation_cert_path):
    # keep old typo "listen_notfications" around for a few versions
    assert config.getboolean(
        "cloud_agent", "listen_notifications",
        fallback=False) or config.getboolean(
            "cloud_agent", "listen_notfications", fallback=False)
    try:
        import zmq  # pylint: disable=import-outside-toplevel
    except ImportError as error:
        raise Exception(
            "install PyZMQ for 'listen_notifications' option") from error

    global cert_key

    if revocation_cert_path is None:
        raise Exception("must specify revocation_cert_path")

    context = zmq.Context()
    mysock = context.socket(zmq.SUB)
    mysock.setsockopt(zmq.SUBSCRIBE, b"")
    mysock.connect(f"tcp://{config.get('general', 'receive_revocation_ip')}:"
                   f"{config.getint('general', 'receive_revocation_port')}")

    logger.info(
        "Waiting for revocation messages on 0mq %s:%s",
        config.get("general", "receive_revocation_ip"),
        config.getint("general", "receive_revocation_port"),
    )

    while True:
        rawbody = mysock.recv()
        body = json.loads(rawbody)

        if cert_key is None:
            # load up the CV signing public key
            if revocation_cert_path is not None and os.path.exists(
                    revocation_cert_path):
                logger.info("Lazy loading the revocation certificate from %s",
                            revocation_cert_path)
                with open(revocation_cert_path, "rb") as f:
                    certpem = f.read()
                cert_key = crypto.x509_import_pubkey(certpem)

        if cert_key is None:
            logger.warning(
                "Unable to check signature of revocation message: %s not available",
                revocation_cert_path)
        elif "signature" not in body or body["signature"] == "none":
            logger.warning("No signature on revocation message from server")
        elif not crypto.rsa_verify(cert_key, body["msg"].encode("utf-8"),
                                   body["signature"].encode("utf-8")):
            logger.error("Invalid revocation message siganture %s", body)
        else:
            message = json.loads(body["msg"])
            logger.debug("Revocation signature validated for revocation: %s",
                         message)
            callback(message)
示例#2
0
def await_notifications(callback, revocation_cert_path):
    # keep old typo "listen_notfications" around for a few versions
    assert config.getboolean(
        "cloud_agent", "listen_notifications",
        fallback=False) or config.getboolean(
            "cloud_agent", "listen_notfications", fallback=False)
    try:
        import zmq  # pylint: disable=import-outside-toplevel
    except ImportError as error:
        raise Exception(
            "install PyZMQ for 'listen_notifications' option") from error

    if revocation_cert_path is None:
        raise Exception("must specify revocation_cert_path")

    context = zmq.Context()
    mysock = context.socket(zmq.SUB)
    mysock.setsockopt(zmq.SUBSCRIBE, b"")
    mysock.connect(f"tcp://{config.get('general', 'receive_revocation_ip')}:"
                   f"{config.getint('general', 'receive_revocation_port')}")

    logger.info(
        "Waiting for revocation messages on 0mq %s:%s",
        config.get("general", "receive_revocation_ip"),
        config.getint("general", "receive_revocation_port"),
    )

    while True:
        rawbody = mysock.recv()
        body = json.loads(rawbody)
        process_revocation(body, callback, revocation_cert_path)
示例#3
0
def process_revocation(revocation, callback, cert_path):
    global cert_key

    if cert_key is None:
        # load up the CV signing public key
        if cert_path is not None and os.path.exists(cert_path):
            logger.info("Lazy loading the revocation certificate from %s",
                        cert_path)
            with open(cert_path, "rb") as f:
                certpem = f.read()
            cert_key = crypto.x509_import_pubkey(certpem)

    if cert_key is None:
        logger.warning(
            "Unable to check signature of revocation message: %s not available",
            cert_path)
    elif "signature" not in revocation or revocation["signature"] == "none":
        logger.warning("No signature on revocation message from server")
    elif not crypto.rsa_verify(cert_key, revocation["msg"].encode("utf-8"),
                               revocation["signature"].encode("utf-8")):
        logger.error("Invalid revocation message siganture %s", revocation)
    else:
        message = json.loads(revocation["msg"])
        logger.debug("Revocation signature validated for revocation: %s",
                     message)
        callback(message)
示例#4
0
    def readPolicy(configval):
        policy = json.loads(configval)

        # compute PCR mask from tpm_policy
        mask = 0
        for key in list(policy.keys()):
            if not key.isdigit() or int(key) > 24:
                raise Exception(f"Invalid tpm policy pcr number: {key}")

            if int(key) == config.TPM_DATA_PCR:
                raise Exception(
                    f"Invalid allowlist PCR number {key}, keylime uses this PCR to bind data."
                )
            if int(key) == config.IMA_PCR:
                raise Exception(
                    f"Invalid allowlist PCR number {key}, this PCR is used for IMA."
                )

            mask = mask | (1 << int(key))

            # wrap it in a list if it is a singleton
            if isinstance(policy[key], str):
                policy[key] = [policy[key]]

            # convert all hash values to lowercase
            policy[key] = [x.lower() for x in policy[key]]

        policy['mask'] = hex(mask)
        return policy
示例#5
0
    async def test_041_agent_keys_verify_get(self):
        """Test agent's GET /keys/verify Interface
        We use async here to allow function await while key processes"""
        self.assertIsNotNone(
            self.K, "Required value not set.  Previous step may have failed?")
        challenge = tpm_abstract.TPM_Utilities.random_password(20)
        encoded = base64.b64encode(self.K).decode("utf-8")

        response = tornado_requests.request(
            "GET",
            f"http://{self.cloudagent_ip}:{self.cloudagent_port}/keys/verify?challenge={challenge}"
        )
        response = await response
        self.assertEqual(response.status, 200,
                         "Non-successful Agent verify return code!")
        json_response = json.loads(response.read().decode())

        # Ensure response is well-formed
        self.assertIn("results", json_response, "Malformed response body!")
        self.assertIn("hmac", json_response["results"],
                      "Malformed response body!")

        # Be sure response is valid
        mac = json_response["results"]["hmac"]
        ex_mac = crypto.do_hmac(encoded, challenge)
        # ex_mac = crypto.do_hmac(self.K, challenge)
        self.assertEqual(mac, ex_mac,
                         "Agent failed to validate challenge code!")
示例#6
0
def process_get_status(agent):
    allowlist_json = json.loads(agent.ima_policy.ima_policy)
    if isinstance(allowlist_json, dict) and "allowlist" in allowlist_json:
        al_len = len(allowlist_json["allowlist"])
    else:
        al_len = 0

    try:
        mb_refstate = json.loads(agent.mb_refstate)
    except Exception as e:
        logger.warning(
            'Non-fatal problem ocurred while attempting to evaluate agent attribute "mb_refstate" (%s). Will just consider the value of this attribute to be "None"',
            e.args,
        )
        mb_refstate = None
        logger.debug(
            'The contents of the agent attribute "mb_refstate" are %s',
            agent.mb_refstate)

    if isinstance(mb_refstate, dict) and "mb_refstate" in mb_refstate:
        mb_refstate_len = len(mb_refstate["mb_refstate"])
    else:
        mb_refstate_len = 0
    response = {
        "operational_state": agent.operational_state,
        "v": agent.v,
        "ip": agent.ip,
        "port": agent.port,
        "tpm_policy": agent.tpm_policy,
        "meta_data": agent.meta_data,
        "allowlist_len": al_len,
        "mb_refstate_len": mb_refstate_len,
        "accept_tpm_hash_algs": agent.accept_tpm_hash_algs,
        "accept_tpm_encryption_algs": agent.accept_tpm_encryption_algs,
        "accept_tpm_signing_algs": agent.accept_tpm_signing_algs,
        "hash_alg": agent.hash_alg,
        "enc_alg": agent.enc_alg,
        "sign_alg": agent.sign_alg,
        "verifier_id": agent.verifier_id,
        "verifier_ip": agent.verifier_ip,
        "verifier_port": agent.verifier_port,
        "severity_level": agent.severity_level,
        "last_event_id": agent.last_event_id,
        "attestation_count": agent.attestation_count,
    }
    return response
示例#7
0
def process_get_status(agent):
    allowlist = json.loads(agent.allowlist)
    if isinstance(allowlist, dict) and 'allowlist' in allowlist:
        al_len = len(allowlist['allowlist'])
    else:
        al_len = 0

    try:
        mb_refstate = json.loads(agent.mb_refstate)
    except Exception as e:
        logger.warning(
            'Non-fatal problem ocurred while attempting to evaluate agent attribute "mb_refstate" (%s). Will just consider the value of this attribute to be "None"',
            e.args)
        mb_refstate = None
        logger.debug(
            'The contents of the agent attribute "mb_refstate" are %s',
            agent.mb_refstate)

    if isinstance(mb_refstate, dict) and 'mb_refstate' in mb_refstate:
        mb_refstate_len = len(mb_refstate['mb_refstate'])
    else:
        mb_refstate_len = 0
    response = {
        'operational_state': agent.operational_state,
        'v': agent.v,
        'ip': agent.ip,
        'port': agent.port,
        'tpm_policy': agent.tpm_policy,
        'vtpm_policy': agent.vtpm_policy,
        'meta_data': agent.meta_data,
        'allowlist_len': al_len,
        'mb_refstate_len': mb_refstate_len,
        'accept_tpm_hash_algs': agent.accept_tpm_hash_algs,
        'accept_tpm_encryption_algs': agent.accept_tpm_encryption_algs,
        'accept_tpm_signing_algs': agent.accept_tpm_signing_algs,
        'hash_alg': agent.hash_alg,
        'enc_alg': agent.enc_alg,
        'sign_alg': agent.sign_alg,
        'verifier_id': agent.verifier_id,
        'verifier_ip': agent.verifier_ip,
        'verifier_port': agent.verifier_port,
        'severity_level': agent.severity_level,
        'last_event_id': agent.last_event_id
    }
    return response
示例#8
0
        def revoke_callback(revocation):
            json_meta = json.loads(revocation['meta_data'])
            serial = json_meta['cert_serial']
            if revocation.get('type', None) != 'revocation' or serial is None:
                logger.error("Unsupported revocation message: %s", revocation)
                return

            logger.info("Revoking certificate: %s", serial)
            server.setcrl(cmd_revoke(workingdir, None, serial))
def await_notifications(callback, revocation_cert_path):
    global cert_key

    if revocation_cert_path is None:
        raise Exception("must specify revocation_cert_path")

    context = zmq.Context()
    mysock = context.socket(zmq.SUB)
    mysock.setsockopt(zmq.SUBSCRIBE, b'')
    mysock.connect(f"tcp://{config.get('general', 'receive_revocation_ip')}:"
                   f"{config.getint('general', 'receive_revocation_port')}")

    logger.info('Waiting for revocation messages on 0mq %s:%s',
                config.get('general', 'receive_revocation_ip'),
                config.getint('general', 'receive_revocation_port'))

    while True:
        rawbody = mysock.recv()
        body = json.loads(rawbody)

        if cert_key is None:
            # load up the CV signing public key
            if revocation_cert_path is not None and os.path.exists(
                    revocation_cert_path):
                logger.info("Lazy loading the revocation certificate from %s",
                            revocation_cert_path)
                with open(revocation_cert_path, "rb") as f:
                    certpem = f.read()
                cert_key = crypto.x509_import_pubkey(certpem)

        if cert_key is None:
            logger.warning(
                "Unable to check signature of revocation message: %s not available",
                revocation_cert_path)
        elif 'signature' not in body or body['signature'] == 'none':
            logger.warning("No signature on revocation message from server")
        elif not crypto.rsa_verify(cert_key, body['msg'].encode('utf-8'),
                                   body['signature'].encode('utf-8')):
            logger.error("Invalid revocation message siganture %s", body)
        else:
            message = json.loads(body['msg'])
            logger.debug("Revocation signature validated for revocation: %s",
                         message)
            callback(message)
示例#10
0
async def execute(revocation):
    json_meta = json.loads(revocation["meta_data"])
    serial = json_meta["cert_serial"]
    subject = json_meta["subject"]
    if revocation.get(
            "type", None) != "revocation" or serial is None or subject is None:
        logger.error("Unsupported revocation message: %s" % revocation)

    # import the crl into NSS
    secdir = secure_mount.mount()
    logger.info("loading updated CRL from %s/unzipped/cacrl.der into NSS" %
                secdir)
    cmd = ("crlutil", "-I", "-i", "%s/unzipped/cacrl.der" % secdir, "-d",
           "sql:/etc/ipsec.d")
    cmd_exec.run(cmd)

    # need to find any sa's that were established with that cert subject name
    cmd = ("ipsec", "whack", "--trafficstatus")
    output = cmd_exec.run(cmd, raiseOnError=True)["retout"]
    deletelist = set()
    id = ""
    for line in output:
        line = line.strip()
        try:
            idstart = line.index("id='") + 4
            idend = line[idstart:].index("'")

            id = line[idstart:idstart + idend]

            privatestart = line.index("private#") + 8
            privateend = line[privatestart:].index("/")

            ip = line[privatestart:privatestart + privateend]
        except ValueError:
            # weirdly formatted line
            continue

    # kill all the commas
    id = id.replace(",", "")
    cursubj = {}
    for token in id.split():
        cur = token.split("=")
        cursubj[cur[0]] = cur[1]

    cert = {}
    for token in subject[1:].split("/"):
        cur = token.split("=")
        cert[cur[0]] = cur[1]

    if cert == cursubj:
        deletelist.add(ip)

    for todelete in deletelist:
        logger.info("deleting IPsec sa with %s" % todelete)
        cmd = ("ipsec", "whack", "--crash", todelete)
        cmd_exec.run(cmd, raiseOnError=False)
示例#11
0
def validate_agent_data(agent_data):
    if agent_data is None:
        return False, None

    # validate that the allowlist is proper JSON
    lists = json.loads(agent_data['allowlist'])

    # Validate exlude list contains valid regular expressions
    is_valid, _, err_msg = validators.valid_exclude_list(lists.get('exclude'))
    if not is_valid:
        err_msg += " Exclude list regex is misformatted. Please correct the issue and try again."

    return is_valid, err_msg
示例#12
0
async def execute(revocation):
    json_meta = json.loads(revocation["meta_data"])
    serial = json_meta["cert_serial"]
    if revocation.get("type", None) != "revocation" or serial is None:
        logger.error("Unsupported revocation message: %s" % revocation)

    # load up the ca cert
    secdir = secure_mount.mount()
    ca = ca_util.load_cert_by_path(f"{secdir}/unzipped/cacert.crt")

    # need to find any sa's that were established with that cert serial
    cmd = ("racoonctl", "show-sa", "ipsec")
    output = cmd_exec.run(cmd, raiseOnError=True)["retout"]
    deletelist = set()
    for line in output:
        if not line.startswith(b"\t"):
            cmd = ("racoonctl", "get-cert", "inet", line.strip())
            certder = cmd_exec.run(cmd, raiseOnError=False)["retout"]
            if len(certder) == 0:
                continue

            try:
                certobj = x509.load_der_x509_certificate(
                    data=b"".join(certder),
                    backend=default_backend(),
                )

                # check that CA is the same.
                ca_keyid = ca.extensions.get_extension_for_oid(
                    x509.oid.ExtensionOID.SUBJECT_KEY_IDENTIFIER
                ).value.digest
                cert_authkeyid = certobj.extensions.get_extension_for_oid(
                    x509.oid.ExtensionOID.AUTHORITY_KEY_IDENTIFIER
                ).value.key_identifier
            except (ValueError, x509.extensions.ExtensionNotFound):
                continue

            if ca_keyid != cert_authkeyid:
                continue

            if certobj.serial_number == serial:
                deletelist.add(line.strip())

    for todelete in deletelist:
        logger.info("deleting IPsec sa between %s" % todelete)
        cmd = ("racoonctl", "delete-sa", "isakmp", "inet", todelete)
        cmd_exec.run(cmd)
        tokens = todelete.split()
        cmd = ("racoonctl", "delete-sa", "isakmp", "inet", tokens[1], tokens[0])
        cmd_exec.run(cmd)
示例#13
0
    def check_pcrs(self, agentAttestState, tpm_policy, pcrs, data, virtual,
                   ima_measurement_list, allowlist, ima_keyrings,
                   mb_measurement_list, mb_refstate_str, hash_alg) -> Failure:
        failure = Failure(Component.PCR_VALIDATION)
        if isinstance(tpm_policy, str):
            tpm_policy = json.loads(tpm_policy)

        pcr_allowlist = tpm_policy.copy()

        if 'mask' in pcr_allowlist:
            del pcr_allowlist['mask']
        # convert all pcr num keys to integers
        pcr_allowlist = {int(k): v for k, v in list(pcr_allowlist.items())}

        mb_policy, mb_policy_name, mb_refstate_data = measured_boot.get_policy(
            mb_refstate_str)
        mb_pcrs_hashes, boot_aggregates, mb_measurement_data, mb_failure = self.parse_mb_bootlog(
            mb_measurement_list, hash_alg)
        failure.merge(mb_failure)

        pcrs_in_quote = set(
        )  # PCRs in quote that were already used for some kind of validation

        pcrs = AbstractTPM.__parse_pcrs(pcrs, virtual)
        pcr_nums = set(pcrs.keys())

        # Validate data PCR
        if config.TPM_DATA_PCR in pcr_nums and data is not None:
            expectedval = self.sim_extend(data, hash_alg=hash_alg)
            if expectedval != pcrs[config.TPM_DATA_PCR]:
                logger.error(
                    "%sPCR #%s: invalid bind data %s from quote does not match expected value %s",
                    ("", "v")[virtual], config.TPM_DATA_PCR,
                    pcrs[config.TPM_DATA_PCR], expectedval)
                failure.add_event(f"invalid_pcr_{config.TPM_DATA_PCR}", {
                    "got": pcrs[config.TPM_DATA_PCR],
                    "expected": expectedval
                }, True)
            pcrs_in_quote.add(config.TPM_DATA_PCR)
        else:
            logger.error(
                "Binding %sPCR #%s was not included in the quote, but is required",
                ("", "v")[virtual], config.TPM_DATA_PCR)
            failure.add_event(
                f"missing_pcr_{config.TPM_DATA_PCR}",
                f"Data PCR {config.TPM_DATA_PCR} is missing in quote, but is required",
                True)
        # Check for ima PCR
        if config.IMA_PCR in pcr_nums:
            if ima_measurement_list is None:
                logger.error(
                    "IMA PCR in policy, but no measurement list provided")
                failure.add_event(
                    f"unused_pcr_{config.IMA_PCR}",
                    "IMA PCR in policy, but no measurement list provided",
                    True)
            else:
                ima_failure = AbstractTPM.__check_ima(agentAttestState,
                                                      pcrs[config.IMA_PCR],
                                                      ima_measurement_list,
                                                      allowlist, ima_keyrings,
                                                      boot_aggregates,
                                                      hash_alg)
                failure.merge(ima_failure)

            pcrs_in_quote.add(config.IMA_PCR)

        # Collect mismatched measured boot PCRs as measured_boot failures
        mb_pcr_failure = Failure(Component.MEASURED_BOOT)
        # Handle measured boot PCRs only if the parsing worked
        if not mb_failure:
            for pcr_num in set(config.MEASUREDBOOT_PCRS) & pcr_nums:
                if mb_refstate_data:
                    if not mb_measurement_list:
                        logger.error(
                            "Measured Boot PCR %d in policy, but no measurement list provided",
                            pcr_num)
                        failure.add_event(
                            f"unused_pcr_{pcr_num}",
                            f"Measured Boot PCR {pcr_num} in policy, but no measurement list provided",
                            True)
                        continue

                    val_from_log_int = mb_pcrs_hashes.get(str(pcr_num), 0)
                    val_from_log_hex = hex(val_from_log_int)[2:]
                    val_from_log_hex_stripped = val_from_log_hex.lstrip('0')
                    pcrval_stripped = pcrs[pcr_num].lstrip('0')
                    if val_from_log_hex_stripped != pcrval_stripped:
                        logger.error(
                            "For PCR %d and hash %s the boot event log has value %r but the agent returned %r",
                            str(hash_alg), pcr_num, val_from_log_hex,
                            pcrs[pcr_num])
                        mb_pcr_failure.add_event(
                            f"invalid_pcr_{pcr_num}", {
                                "context":
                                "SHA256 boot event log PCR value does not match",
                                "got": pcrs[pcr_num],
                                "expected": val_from_log_hex
                            }, True)

                    if pcr_num in pcr_allowlist and pcrs[
                            pcr_num] not in pcr_allowlist[pcr_num]:
                        logger.error(
                            "%sPCR #%s: %s from quote does not match expected value %s",
                            ("", "v")[virtual], pcr_num, pcrs[pcr_num],
                            pcr_allowlist[pcr_num])
                        failure.add_event(
                            f"invalid_pcr_{pcr_num}", {
                                "context": "PCR value is not in allowlist",
                                "got": pcrs[pcr_num],
                                "expected": pcr_allowlist[pcr_num]
                            }, True)
                    pcrs_in_quote.add(pcr_num)
        failure.merge(mb_pcr_failure)

        # Check the remaining non validated PCRs
        for pcr_num in pcr_nums - pcrs_in_quote:
            if pcr_num not in list(pcr_allowlist.keys()):
                logger.warning(
                    "%sPCR #%s in quote not found in %stpm_policy, skipping.",
                    ("", "v")[virtual], pcr_num, ("", "v")[virtual])
                continue
            if pcrs[pcr_num] not in pcr_allowlist[pcr_num]:
                logger.error(
                    "%sPCR #%s: %s from quote does not match expected value %s",
                    ("", "v")[virtual], pcr_num, pcrs[pcr_num],
                    pcr_allowlist[pcr_num])
                failure.add_event(
                    f"invalid_pcr_{pcr_num}", {
                        "context": "PCR value is not in allowlist",
                        "got": pcrs[pcr_num],
                        "expected": pcr_allowlist[pcr_num]
                    }, True)

            pcrs_in_quote.add(pcr_num)

        missing = set(pcr_allowlist.keys()) - pcrs_in_quote
        if len(missing) > 0:
            logger.error("%sPCRs specified in policy not in quote: %s",
                         ("", "v")[virtual], missing)
            failure.add_event("missing_pcrs", {
                "context": "PCRs are missing in quote",
                "data": list(missing)
            }, True)

        if not mb_failure and mb_refstate_data:
            mb_policy_failure = measured_boot.evaluate_policy(
                mb_policy, mb_policy_name, mb_refstate_data,
                mb_measurement_data, pcrs_in_quote, ("", "v")[virtual],
                agentAttestState.get_agent_id())
            failure.merge(mb_policy_failure)

        return failure
示例#14
0
    def do_POST(self):
        """This method handles the POST requests to add agents to the Registrar Server.

        Currently, only agents resources are available for POSTing, i.e. /agents. All other POST uri's
        will return errors. POST requests require an an agent_id identifying the agent to add, and json
        block sent in the body with 2 entries: ek and aik.
        """
        session = SessionManager().make_session(engine)
        rest_params = web_util.get_restful_params(self.path)
        if rest_params is None:
            web_util.echo_json_response(
                self, 405, "Not Implemented: Use /agents/ interface")
            return

        if not rest_params["api_version"]:
            web_util.echo_json_response(self, 400, "API Version not supported")
            return

        if "agents" not in rest_params:
            web_util.echo_json_response(self, 400, "uri not supported")
            logger.warning(
                'POST agent returning 400 response. uri not supported: %s',
                self.path)
            return

        agent_id = rest_params["agents"]

        if agent_id is None:
            web_util.echo_json_response(self, 400, "agent id not found in uri")
            logger.warning(
                'POST agent returning 400 response. agent id not found in uri %s',
                self.path)
            return

        # If the agent ID is not valid (wrong set of characters), just
        # do nothing.
        if not validators.valid_agent_id(agent_id):
            web_util.echo_json_response(self, 400, "agent id not valid")
            logger.error("POST received an invalid agent ID: %s", agent_id)
            return

        try:
            content_length = int(self.headers.get('Content-Length', 0))
            if content_length == 0:
                web_util.echo_json_response(
                    self, 400, "Expected non zero content length")
                logger.warning(
                    'POST for %s returning 400 response. Expected non zero content length.',
                    agent_id)
                return

            post_body = self.rfile.read(content_length)
            json_body = json.loads(post_body)

            ekcert = json_body['ekcert']
            aik_tpm = json_body['aik_tpm']

            initialize_tpm = tpm()

            if ekcert is None or ekcert == 'emulator':
                logger.warning('Agent %s did not submit an ekcert', agent_id)
                ek_tpm = json_body['ek_tpm']
            else:
                if 'ek_tpm' in json_body:
                    # This would mean the agent submitted both a non-None ekcert, *and*
                    #  an ek_tpm... We can deal with it by just ignoring the ek_tpm they sent
                    logger.warning(
                        'Overriding ek_tpm for agent %s from ekcert', agent_id)
                # If there's an EKCert, we just overwrite their ek_tpm
                # Note, we don't validate the EKCert here, other than the implicit
                #  "is it a valid x509 cert" check. So it's still untrusted.
                # This will be validated by the tenant.
                ek509 = load_der_x509_certificate(
                    base64.b64decode(ekcert),
                    backend=default_backend(),
                )
                ek_tpm = base64.b64encode(
                    tpm2_objects.ek_low_tpm2b_public_from_pubkey(
                        ek509.public_key(), )).decode()

            aik_attrs = tpm2_objects.get_tpm2b_public_object_attributes(
                base64.b64decode(aik_tpm), )
            if aik_attrs != tpm2_objects.AK_EXPECTED_ATTRS:
                web_util.echo_json_response(self, 400, "Invalid AK attributes")
                logger.warning(
                    "Agent %s submitted AIK with invalid attributes! %s (provided) != %s (expected)",
                    agent_id,
                    tpm2_objects.object_attributes_description(aik_attrs),
                    tpm2_objects.object_attributes_description(
                        tpm2_objects.AK_EXPECTED_ATTRS),
                )
                return

            # try to encrypt the AIK
            (blob, key) = initialize_tpm.encryptAIK(
                agent_id,
                base64.b64decode(ek_tpm),
                base64.b64decode(aik_tpm),
            )

            # special behavior if we've registered this uuid before
            regcount = 1
            try:
                agent = session.query(RegistrarMain).filter_by(
                    agent_id=agent_id).first()
            except NoResultFound:
                agent = None
            except SQLAlchemyError as e:
                logger.error('SQLAlchemy Error: %s', e)
                raise

            if agent is not None:

                # keep track of how many ek-ekcerts have registered on this uuid
                regcount = agent.regcount
                if agent.ek_tpm != ek_tpm or agent.ekcert != ekcert:
                    logger.warning(
                        'WARNING: Overwriting previous registration for this UUID with new ek-ekcert pair!'
                    )
                    regcount += 1

                # force overwrite
                logger.info('Overwriting previous registration for this UUID.')
                try:
                    session.query(RegistrarMain).filter_by(
                        agent_id=agent_id).delete()
                    session.commit()
                except SQLAlchemyError as e:
                    logger.error('SQLAlchemy Error: %s', e)
                    raise
            # Check for ip and port
            contact_ip = json_body.get('ip', None)
            contact_port = json_body.get('port', None)

            # Validate ip and port
            if contact_ip is not None:
                try:
                    # Use parser from the standard library instead of implementing our own
                    ipaddress.ip_address(contact_ip)
                except ValueError:
                    logger.warning(
                        "Contact ip for agent %s is not a valid ip got: %s.",
                        agent_id, contact_ip)
                    contact_ip = None
            if contact_port is not None:
                try:
                    contact_port = int(contact_port)
                    if contact_port < 1 or contact_port > 65535:
                        logger.warning(
                            "Contact port for agent %s is not a number between 1 and got: %s.",
                            agent_id, contact_port)
                        contact_port = None
                except ValueError:
                    logger.warning(
                        "Contact port for agent %s is not a valid number got: %s.",
                        agent_id, contact_port)
                    contact_port = None

            # Check for mTLS cert
            mtls_cert = json_body.get('mtls_cert', None)
            if mtls_cert is None:
                logger.warning(
                    "Agent %s did not send a mTLS certificate. Most operations will not work!",
                    agent_id)

            # Add values to database
            d = {}
            d['agent_id'] = agent_id
            d['ek_tpm'] = ek_tpm
            d['aik_tpm'] = aik_tpm
            d['ekcert'] = ekcert
            d['ip'] = contact_ip
            d['mtls_cert'] = mtls_cert
            d['port'] = contact_port
            d['virtual'] = int(ekcert == 'virtual')
            d['active'] = int(False)
            d['key'] = key
            d['provider_keys'] = {}
            d['regcount'] = regcount

            try:
                session.add(RegistrarMain(**d))
                session.commit()
            except SQLAlchemyError as e:
                logger.error('SQLAlchemy Error: %s', e)
                raise

            response = {
                'blob': blob,
            }
            web_util.echo_json_response(self, 200, "Success", response)

            logger.info('POST returning key blob for agent_id: %s', agent_id)
        except Exception as e:
            web_util.echo_json_response(self, 400, "Error: %s" % e)
            logger.warning("POST for %s returning 400 response. Error: %s",
                           agent_id, e)
            logger.exception(e)
示例#15
0
    def post(self):
        """This method handles the POST requests to add agents to the Agent Monitor.

        Currently, only agents resources are available for POSTing, i.e. /agents. All other POST uri's will return errors.
        agents requests require a json block sent in the body
        """
        logger.info('Agent Monitor POST')
        try:
            rest_params = keylime.web_util.get_restful_params(
                self.request.path)

            if "agents" not in rest_params:
                keylime.web_util.echo_json_response(self, 400,
                                                    "uri not supported")
                logger.warning(
                    'POST returning 400 response. uri not supported: ' +
                    self.request.path)
                return

            agent_id = rest_params["agents"]

            if agent_id is not None:  # we have to know who phoned home
                content_length = len(self.request.body)
                if content_length == 0:
                    keylime.web_util.echo_json_response(
                        self, 400, "Expected non zero content length")
                    logger.warning(
                        'POST returning 400 response. Expected non zero content length.'
                    )
                else:
                    json_body = json.loads(self.request.body)

                    # VERIFY CLIENT CERT ID MATCHES AGENT ID (agent_id)
                    client_cert = self.request.get_ssl_certificate()
                    ssl.match_hostname(client_cert, agent_id)

                    # Execute specified script if all is well
                    global initscript
                    if initscript is not None and initscript != "":

                        def initthread():
                            import subprocess
                            logger.debug("Executing specified script: %s" %
                                         initscript)
                            env = os.environ.copy()
                            env['AGENT_UUID'] = agent_id
                            proc = subprocess.Popen(["/bin/sh", initscript],
                                                    env=env,
                                                    shell=False,
                                                    stdout=subprocess.PIPE,
                                                    stderr=subprocess.STDOUT)
                            proc.wait()
                            while True:
                                line = proc.stdout.readline()
                                if line == "":
                                    break
                                logger.debug("init-output: %s" % line.strip())

                        t = threading.Thread(target=initthread)
                        t.start()

                    keylime.web_util.echo_json_response(
                        self, 200, "Success", json_body)
                    logger.info(
                        'POST returning 200 response for Agent Monitor connection as '
                        + agent_id)
            else:
                keylime.web_util.echo_json_response(self, 400,
                                                    "uri not supported")
                logger.warning(
                    "POST returning 400 response. uri not supported")
        except Exception as e:
            keylime.web_util.echo_json_response(self, 400,
                                                "Exception error: %s" % e)
            logger.warning("POST returning 400 response. Exception error: %s" %
                           e)
            logger.exception(e)
示例#16
0
    def do_PUT(self):
        """This method handles the PUT requests to add agents to the Registrar Server.

        Currently, only agents resources are available for PUTing, i.e. /agents. All other PUT uri's
        will return errors.
        """
        session = SessionManager().make_session(engine)
        rest_params = web_util.get_restful_params(self.path)
        if rest_params is None:
            web_util.echo_json_response(
                self, 405, "Not Implemented: Use /agents/ interface")
            return

        if not rest_params["api_version"]:
            web_util.echo_json_response(self, 400, "API Version not supported")
            return

        if "agents" not in rest_params:
            web_util.echo_json_response(self, 400, "uri not supported")
            logger.warning(
                'PUT agent returning 400 response. uri not supported: %s',
                self.path)
            return

        agent_id = rest_params["agents"]

        if agent_id is None:
            web_util.echo_json_response(self, 400, "agent id not found in uri")
            logger.warning(
                'PUT agent returning 400 response. agent id not found in uri %s',
                self.path)
            return

        # If the agent ID is not valid (wrong set of characters), just
        # do nothing.
        if not validators.valid_agent_id(agent_id):
            web_util.echo_json_response(self, 400, "agent_id not not valid")
            logger.error("PUT received an invalid agent ID: %s", agent_id)
            return

        try:
            content_length = int(self.headers.get('Content-Length', 0))
            if content_length == 0:
                web_util.echo_json_response(
                    self, 400, "Expected non zero content length")
                logger.warning(
                    'PUT for %s returning 400 response. Expected non zero content length.',
                    agent_id)
                return

            post_body = self.rfile.read(content_length)
            json_body = json.loads(post_body)

            auth_tag = json_body['auth_tag']
            try:
                agent = session.query(RegistrarMain).filter_by(
                    agent_id=agent_id).first()
            except NoResultFound as e:
                raise Exception(
                    "attempting to activate agent before requesting "
                    "registrar for %s" % agent_id) from e
            except SQLAlchemyError as e:
                logger.error('SQLAlchemy Error: %s', e)
                raise

            if config.STUB_TPM:
                try:
                    session.query(RegistrarMain).filter(
                        RegistrarMain.agent_id == agent_id).update(
                            {'active': int(True)})
                    session.commit()
                except SQLAlchemyError as e:
                    logger.error('SQLAlchemy Error: %s', e)
                    raise
            else:
                ex_mac = crypto.do_hmac(agent.key.encode(), agent_id)
                if ex_mac == auth_tag:
                    try:
                        session.query(RegistrarMain).filter(
                            RegistrarMain.agent_id == agent_id).update(
                                {'active': int(True)})
                        session.commit()
                    except SQLAlchemyError as e:
                        logger.error('SQLAlchemy Error: %s', e)
                        raise
                else:
                    raise Exception(
                        f"Auth tag {auth_tag} does not match expected value {ex_mac}"
                    )

            web_util.echo_json_response(self, 200, "Success")
            logger.info('PUT activated: %s', agent_id)
        except Exception as e:
            web_util.echo_json_response(self, 400, "Error: %s" % e)
            logger.warning("PUT for %s returning 400 response. Error: %s",
                           agent_id, e)
            logger.exception(e)
            return
示例#17
0
async def execute(revocation):
    print(json.loads(revocation['meta_data']))
示例#18
0
    REQUIRE_ROOT = False
    DISABLE_EK_CERT_CHECK_EMULATOR = True

# whether to use tpmfs or not
MOUNT_SECURE = True

# load in JSON canned values if we're in stub mode (and JSON file given)
TPM_CANNED_VALUES = None
if STUB_TPM and TPM_CANNED_VALUES_PATH is not None:
    with open(TPM_CANNED_VALUES_PATH, "rb") as can:
        print("WARNING: using canned values in stub mode from file '%s'" %
              (TPM_CANNED_VALUES_PATH))
        # Read in JSON and strip trailing extraneous commas
        jsonInTxt = can.read().rstrip(',\r\n')
        # Saved JSON is missing surrounding braces, so add them here
        TPM_CANNED_VALUES = json.loads('{' + jsonInTxt + '}')
elif STUB_TPM:
    raise Exception(
        'STUB_TPM=True but required TPM_CANNED_VALUES_PATH not provided!')

if not REQUIRE_ROOT:
    MOUNT_SECURE = False

if not REQUIRE_ROOT:
    print("WARNING: running without root access")

# Config files can be merged together, reading from the system to the
# user.
CONFIG_FILES = [
    "/usr/etc/keylime.conf", "/etc/keylime.conf",
    os.path.expanduser("~/.config/keylime.conf")
示例#19
0
    def do_POST(self):
        """This method services the POST request typically from either the Tenant or the Cloud Verifier.

        Only tenant and cloudverifier uri's are supported. Both requests require a nonce parameter.
        The Cloud verifier requires an additional mask parameter.  If the uri or parameters are incorrect, a 400 response is returned.
        """
        rest_params = web_util.get_restful_params(self.path)

        if rest_params is None:
            web_util.echo_json_response(
                self, 405,
                "Not Implemented: Use /keys/ or /notifications/ interface")
            return

        if not rest_params["api_version"]:
            web_util.echo_json_response(self, 400, "API Version not supported")
            return

        content_length = int(self.headers.get("Content-Length", 0))
        if content_length <= 0:
            logger.warning(
                "POST returning 400 response, expected content in message. url: %s",
                self.path)
            web_util.echo_json_response(self, 400,
                                        "expected content in message")
            return

        post_body = self.rfile.read(content_length)
        try:
            json_body = json.loads(post_body)
        except Exception as e:
            logger.warning(
                "POST returning 400 response, could not parse body data: %s",
                e)
            web_util.echo_json_response(self, 400, "content is invalid")
            return

        if "notifications" in rest_params:
            if rest_params["notifications"] == "revocation":
                revocation_notifier.process_revocation(
                    json_body,
                    perform_actions,
                    cert_path=self.server.revocation_cert_path)
                web_util.echo_json_response(self, 200, "Success")
            else:
                web_util.echo_json_response(
                    self, 400, "Only /notifications/revocation is supported")
            return

        if rest_params.get("keys", None) not in ["ukey", "vkey"]:
            web_util.echo_json_response(
                self, 400, "Only /keys/ukey or /keys/vkey are supported")
            return

        try:
            b64_encrypted_key = json_body["encrypted_key"]
            decrypted_key = crypto.rsa_decrypt(
                self.server.rsaprivatekey, base64.b64decode(b64_encrypted_key))
        except (ValueError, KeyError, TypeError) as e:
            logger.warning(
                "POST returning 400 response, could not parse body data: %s",
                e)
            web_util.echo_json_response(self, 400, "content is invalid")
            return

        have_derived_key = False

        if rest_params["keys"] == "ukey":
            if "auth_tag" not in json_body:
                logger.warning(
                    "POST returning 400 response, U key provided without an auth_tag"
                )
                web_util.echo_json_response(self, 400, "auth_tag is missing")
                return
            self.server.add_U(decrypted_key)
            self.server.auth_tag = json_body["auth_tag"]
            self.server.payload = json_body.get("payload", None)
            have_derived_key = self.server.attempt_decryption()
        elif rest_params["keys"] == "vkey":
            self.server.add_V(decrypted_key)
            have_derived_key = self.server.attempt_decryption()
        else:
            logger.warning("POST returning  response. uri not supported: %s",
                           self.path)
            web_util.echo_json_response(self, 400, "uri not supported")
            return
        logger.info("POST of %s key returning 200",
                    ("V", "U")[rest_params["keys"] == "ukey"])
        web_util.echo_json_response(self, 200, "Success")

        # no key yet, then we're done
        if not have_derived_key:
            return

        # woo hoo we have a key
        # ok lets write out the key now
        secdir = secure_mount.mount(
        )  # confirm that storage is still securely mounted

        # clean out the secure dir of any previous info before we extract files
        if os.path.isdir(os.path.join(secdir, "unzipped")):
            shutil.rmtree(os.path.join(secdir, "unzipped"))

        # write out key file
        with open(os.path.join(secdir, self.server.enc_keyname),
                  "w",
                  encoding="utf-8") as f:
            f.write(base64.b64encode(self.server.K).decode())

        # stow the U value for later
        tpm_instance.write_key_nvram(self.server.final_U)

        # optionally extend a hash of they key and payload into specified PCR
        tomeasure = self.server.K

        # if we have a good key, now attempt to write out the encrypted payload
        dec_path = os.path.join(secdir,
                                config.get("cloud_agent", "dec_payload_file"))
        enc_path = os.path.join(config.WORK_DIR, "encrypted_payload")

        dec_payload = None
        enc_payload = None
        if self.server.payload is not None:
            if not self.server.mtls_cert_enabled and not config.getboolean(
                    "cloud_agent", "enable_insecure_payload", fallback=False):
                logger.warning(
                    'agent mTLS is disabled, and unless "enable_insecure_payload" is set to "True", payloads cannot be deployed'
                )
                enc_payload = None
            else:
                dec_payload = crypto.decrypt(self.server.payload,
                                             bytes(self.server.K))
                enc_payload = self.server.payload

        elif os.path.exists(enc_path):
            # if no payload provided, try to decrypt one from a previous run stored in encrypted_payload
            with open(enc_path, "rb") as f:
                enc_payload = f.read()
            try:
                dec_payload = crypto.decrypt(enc_payload, self.server.K)
                logger.info("Decrypted previous payload in %s to %s", enc_path,
                            dec_path)
            except Exception as e:
                logger.warning(
                    "Unable to decrypt previous payload %s with derived key: %s",
                    enc_path, e)
                os.remove(enc_path)
                enc_payload = None

        # also write out encrypted payload to be decrytped next time
        if enc_payload is not None:
            with open(enc_path, "wb") as f:
                f.write(self.server.payload.encode("utf-8"))

        # deal with payload
        payload_thread = None
        if dec_payload is not None:
            tomeasure = tomeasure + dec_payload
            # see if payload is a zip
            zfio = io.BytesIO(dec_payload)
            if config.getboolean(
                    "cloud_agent",
                    "extract_payload_zip") and zipfile.is_zipfile(zfio):
                logger.info("Decrypting and unzipping payload to %s/unzipped",
                            secdir)
                with zipfile.ZipFile(zfio, "r") as f:
                    f.extractall(os.path.join(secdir, "unzipped"))

                # run an included script if one has been provided
                initscript = config.get("cloud_agent", "payload_script")
                if initscript != "":

                    def initthread():
                        env = os.environ.copy()
                        env["AGENT_UUID"] = self.server.agent_uuid
                        with subprocess.Popen(
                            ["/bin/bash", initscript],
                                env=env,
                                shell=False,
                                cwd=os.path.join(secdir, "unzipped"),
                                stdout=subprocess.PIPE,
                                stderr=subprocess.STDOUT,
                        ) as proc:
                            for line in iter(proc.stdout.readline, b""):
                                logger.debug("init-output: %s", line.strip())
                            # should be a no-op as poll already told us it's done
                            proc.wait()

                    if not os.path.exists(
                            os.path.join(secdir, "unzipped", initscript)):
                        logger.info(
                            "No payload script %s found in %s/unzipped",
                            initscript, secdir)
                    else:
                        logger.info("Executing payload script: %s/unzipped/%s",
                                    secdir, initscript)
                        payload_thread = threading.Thread(target=initthread,
                                                          daemon=True)
            else:
                logger.info("Decrypting payload to %s", dec_path)
                with open(dec_path, "wb") as f:
                    f.write(dec_payload)
            zfio.close()

        # now extend a measurement of the payload and key if there was one
        pcr = config.getint("cloud_agent", "measure_payload_pcr")
        if 0 < pcr < 24:
            logger.info("extending measurement of payload into PCR %s", pcr)
            measured = tpm_instance.hashdigest(tomeasure)
            tpm_instance.extendPCR(pcr, measured)

        if payload_thread is not None:
            payload_thread.start()

        return
示例#20
0
    def get(self):
        """This method handles the GET requests to retrieve status on agents for all agents in a Web-based GUI.

        Currently, only the web app is available for GETing, i.e. /webapp. All other GET uri's
        will return errors.
        """

        # Get default policies for TPM/vTPM from config as suggestions to user
        tpm_policy = json.dumps(json.loads(config.get('tenant', 'tpm_policy')),
                                indent=2)
        vtpm_policy = json.dumps(json.loads(config.get('tenant',
                                                       'vtpm_policy')),
                                 indent=2)

        # Get default intervals for populating angents, updating agents and updating terminal
        populate_agents_interval = json.dumps(json.loads(
            config.get('webapp', 'populate_agents_interval')),
                                              indent=2)
        update_agents_interval = json.dumps(json.loads(
            config.get('webapp', 'update_agents_interval')),
                                            indent=2)
        update_terminal_interval = json.dumps(json.loads(
            config.get('webapp', 'update_terminal_interval')),
                                              indent=2)

        self.set_status(200)
        self.set_header('Content-Type', 'text/html')
        self.write("""
            <!DOCTYPE html>
            <html>
                <head>
                    <meta charset='UTF-8'>
                    <title>Advanced Tenant Management System</title>
                    <script type='text/javascript' src='/static/js/webapp.js'></script>
                    <script type='text/javascript'>
                        window.onload = function(e) {{
                            let droppable = document.getElementsByClassName("file_drop");
                            for (let i = 0; i < droppable.length; i++) {{
                                droppable[i].addEventListener('dragover', dragoverCallback, false);
                                droppable[i].addEventListener('drop', fileUploadCallback, false);
                            }}
                            populateAgents();
                            setInterval(populateAgents, {0});
                            setInterval(updateAgentsInfo, {1});
                            setInterval(updateTerminal, {2});
                        }}
                    </script>
                    <link href='/static/css/webapp.css' rel='stylesheet' type='text/css'/>
                </head>
                <body>
                    <div id='modal_box' onclick="if (event.target == this) {{toggleVisibility(this.id);resetAddAgentForm();return false;}}">

            """.format(populate_agents_interval, update_agents_interval,
                       update_terminal_interval))

        self.write("""
                        <div id='modal_body'>
                            <center>
                                <h3>Add Agent</h3>
                                <h4 id='uuid_str'></h4>
                            </center>
                            <form id='add_agent' name='add_agent' onsubmit='submitAddAgentForm(this); return false;'>
                                <div class="form_block">
                                    <label for='agent_ip'>Agent IP: </label>
                                    <input type='text' id='agent_ip' name='agent_ip' value='127.0.0.1' required onfocus='this.select()'>
                                    <br>
                                </div>

                                <div id='imalist_toggle' onclick="toggleVisibility('imalist_block');" title='IMA Configuration'>
                                    IMA Configuration
                                </div>
                                <div id="imalist_block">
                                    <div class="form_block">
                                        <label for='a_list'>Allow-List: </label>
                                        <div id='a_list' name='a_list' class='file_drop'>
                                            <i>Drag payload here &hellip;</i>
                                        </div>
                                        <input type='hidden' name='a_list_data' id='a_list_data' value=''>
                                        <input type='hidden' name='a_list_name' id='a_list_name' value=''>
                                        <br>
                                    </div>

                                    <div class="form_block">
                                        <label for='e_list'>Exclude: </label>
                                        <div id='e_list' name='e_list' class='file_drop'>
                                            <i>Drag payload here &hellip;</i>
                                        </div>
                                        <input type='hidden' name='e_list_data' id='e_list_data' value=''>
                                        <input type='hidden' name='e_list_name' id='e_list_name' value=''>
                                        <br>
                                    </div>
                                </div>
                                <br>

                                <div id='policy_toggle' onclick="toggleVisibility('policy_block');" title='TPM &amp; vTPM Policy Configuration'>
                                    TPM &amp; vTPM Policy Configuration
                                </div>
                                <div id="policy_block">
                                    <div class="form_block">
                                        <label for='tpm_policy'>TPM Policy: </label><br>
                                        <textarea class='json_input' id='tpm_policy' name='tpm_policy'>{}</textarea>
                                        <br>
                                    </div>

                                    <div class="form_block">
                                        <label for='vtpm_policy'>vTPM Policy: </label><br>
                                        <textarea class='json_input' id='vtpm_policy' name='vtpm_policy'>{}</textarea>
                                        <br>
                                    </div>
                                </div>
                                <br>
            """.format(tpm_policy, vtpm_policy))

        self.write("""
                                <div id="payload_block">
                                    <div class="form_block">
                                        <label for='ptype'>Payload type: </label>
                                        <label><input type='radio' name='ptype' value='{}' checked="checked" onclick='toggleTabs(this.value)'> File </label>&nbsp;
                                        <label><input type='radio' name='ptype' value='{}' onclick='toggleTabs(this.value)'> Keyfile </label>&nbsp;
                                        <label><input type='radio' name='ptype' value='{}' onclick='toggleTabs(this.value)'> CA Dir </label>&nbsp;
                                        <br>
                                    </div>
            """.format(Agent_Init_Types.FILE, Agent_Init_Types.KEYFILE,
                       Agent_Init_Types.CA_DIR))

        self.write("""
                                    <div id='keyfile_container' class="form_block" style="display:none;">
                                        <label for='file'>Keyfile: </label>
                                        <div id='keyfile' name='keyfile' class='file_drop'>
                                            <i>Drag key file here &hellip;</i>
                                        </div>
                                        <input type='hidden' name='keyfile_data' id='keyfile_data' value=''>
                                        <input type='hidden' name='keyfile_name' id='keyfile_name' value=''>
                                        <br>
                                    </div>

                                    <div id='file_container' class="form_block">
                                        <label for='file'>Payload: </label>
                                        <div id='file' name='file' class='file_drop'>
                                            <i>Drag payload here &hellip;</i>
                                        </div>
                                        <input type='hidden' name='file_data' id='file_data' value=''>
                                        <input type='hidden' name='file_name' id='file_name' value=''>
                                        <br>
                                    </div>

                                    <div id='ca_dir_container' style="display:none;">
                                        <div class="form_block">
                                            <label for='ca_dir'>CA Dir: </label>
                                            <input type='text' id='ca_dir' name='ca_dir' placeholder='e.g., default'>
                                            <br>
                                        </div>

                                        <div class="form_block">
                                            <label for='ca_dir_pw'>CA Password: </label>
                                            <input type='password' id='ca_dir_pw' name='ca_dir_pw' placeholder='e.g., default'>
                                            <br>
                                        </div>

                                        <div class="form_block">
                                            <label for='include_dir'>Include dir: </label>
                                            <div id='include_dir' name='include_dir' class='file_drop multi_file'>
                                                <i>Drag files here &hellip;</i>
                                            </div>
                                            <input type='hidden' name='include_dir_data' id='include_dir_data' value=''>
                                            <input type='hidden' name='include_dir_name' id='include_dir_name' value=''>
                                            <br>
                                        </div>
                                    </div>
                                </div>
                                <br>

                                <input type='hidden' name='uuid' id='uuid' value=''>
                                <center><button type="submit" value="Add Agent">Add Agent</button></center>
                                <br>
                            </form>
                        </div>
                    </div>

                    <div id="header">
                        <div class="logo" title="Keylime">&nbsp;</div>
                        <div id="header_banner">
                            <h1>Keylime Advanced Tenant Management System</h1>
                        </div>
                        <div class="logo" style="float:right;" title="Keylime">&nbsp;</div>
                       <br style="clear:both;">
                    </div>

                    <div id="agent_body">
                        <h2>Agents</h2>
                        <div class='table_header'>
                            <div class='table_control'>&nbsp;</div>
                            <div class='table_col'>UUID</div>
                            <div class='table_col'>address</div>
                            <div class='table_col'>status</div>
                            <br style='clear:both;' />
                        </div>
                        <div id='agent_template' style='display:none;'>
                            <li class='agent'>
                                <div style='display:block;cursor:help;width:800px;'></div>
                                <div style='display:none;'></div>
                            </li>
                        </div>
                        <ol id='agent_container'></ol>
                        <div style="color:#888;margin-left:15px;padding:10px;">
                            <i>End of results</i>
                        </div>
                        <div id="terminal-frame">
                            <div id="terminal-header" onmousedown="toggleVisibility('terminal')">Tenant Logs</div>
                            <div id="terminal"></div>
                        </div>
                    </div>
                </body>
            </html>
            """)