Ejemplo n.º 1
0
async def invoke_provide_v(agent):
    if agent is None:
        raise Exception("Agent deleted while being processed")
    try:
        if agent['pending_event'] is not None:
            agent['pending_event'] = None
    except KeyError:
        pass
    v_json_message = cloud_verifier_common.prepare_v(agent)
    version = keylime_api_version.current_version()
    res = tornado_requests.request("POST",
                                   "http://%s:%d/%s/keys/vkey" %
                                   (agent['ip'], agent['port'], version),
                                   data=v_json_message)
    response = await res

    if response.status_code != 200:
        if response.status_code == 599:
            asyncio.ensure_future(process_agent(agent, states.PROVIDE_V_RETRY))
        else:
            # catastrophic error, do not continue
            logger.critical(
                "Unexpected Provide V response error for cloud agent %s, Error: %s",
                agent['agent_id'], response.error)
            asyncio.ensure_future(process_agent(agent, states.FAILED))
    else:
        asyncio.ensure_future(process_agent(agent, states.GET_QUOTE))
Ejemplo n.º 2
0
async def invoke_get_quote(agent, need_pubkey):
    if agent is None:
        raise Exception("agent deleted while being processed")
    params = cloud_verifier_common.prepare_get_quote(agent)

    partial_req = "1"
    if need_pubkey:
        partial_req = "0"

    version = keylime_api_version.current_version()
    res = tornado_requests.request(
        "GET",
        "http://%s:%d/v%s/quotes/integrity?nonce=%s&mask=%s&vmask=%s&partial=%s&ima_ml_entry=%d"
        %
        (agent['ip'], agent['port'], version, params["nonce"], params["mask"],
         params['vmask'], partial_req, params['ima_ml_entry']),
        context=None)
    response = await res

    if response.status_code != 200:
        # this is a connection error, retry get quote
        if response.status_code == 599:
            asyncio.ensure_future(process_agent(agent, states.GET_QUOTE_RETRY))
        else:
            # catastrophic error, do not continue
            logger.critical(
                "Unexpected Get Quote response error for cloud agent %s, Error: %s",
                agent['agent_id'], response.status_code)
            asyncio.ensure_future(process_agent(agent, states.FAILED))
    else:
        try:
            json_response = json.loads(response.body)

            # validate the cloud agent response
            if 'provide_V' not in agent:
                agent['provide_V'] = True
            agentAttestState = get_AgentAttestStates().get_by_agent_id(
                agent['agent_id'])
            if cloud_verifier_common.process_quote_response(
                    agent, json_response['results'], agentAttestState):
                if agent['provide_V']:
                    asyncio.ensure_future(
                        process_agent(agent, states.PROVIDE_V))
                else:
                    asyncio.ensure_future(
                        process_agent(agent, states.GET_QUOTE))
            else:
                asyncio.ensure_future(
                    process_agent(agent, states.INVALID_QUOTE))

            # store the attestation state
            store_attestation_state(agentAttestState)

        except Exception as e:
            logger.exception(e)
Ejemplo n.º 3
0
    def test_060_cv_version_get(self):
        """Test CV's GET /version Interface"""
        cv_client = RequestsClient(tenant_templ.verifier_base_url, tls_enabled)
        response = cv_client.get("/version", cert=tenant_templ.cert, verify=False)

        self.assertEqual(response.status_code, 200, "Non-successful CV allowlist Post return code!")
        json_response = response.json()

        # Ensure response is well-formed
        self.assertIn("results", json_response, "Malformed response body!")
        results = json_response["results"]
        self.assertEqual(results["current_version"], api_version.current_version())
        self.assertEqual(results["supported_versions"], api_version.all_versions())
Ejemplo n.º 4
0
    def get(self):
        rest_params = config.get_restful_params(self.request.uri)
        if rest_params is None:
            config.echo_json_response(self, 405, "Not Implemented")
            return

        if "version" not in rest_params:
            config.echo_json_response(self, 400, "URI not supported")
            logger.warning('GET returning 400 response. URI not supported: %s',
                           self.request.path)
            return

        version_info = {
            "current_version": keylime_api_version.current_version(),
            "supported_versions": keylime_api_version.all_versions(),
        }

        config.echo_json_response(self, 200, "Success", version_info)
    def do_GET(self):
        """This method handles the GET requests to the unprotected side of the Registrar Server

        Currently the only supported path is /versions which shows the supported API versions
        """
        rest_params = config.get_restful_params(self.path)
        if rest_params is None:
            config.echo_json_response(
                self, 405, "Not Implemented: Use /version/ interface")
            return

        if "version" not in rest_params:
            config.echo_json_response(self, 400, "URI not supported")
            logger.warning(
                'GET agent returning 400 response. URI not supported: %s',
                self.path)
            return

        version_info = {
            "current_version": keylime_api_version.current_version(),
            "supported_versions": keylime_api_version.all_versions(),
        }

        config.echo_json_response(self, 200, "Success", version_info)
Ejemplo n.º 6
0
 def test_current_version(self):
     self.assertEqual(api_version.current_version(), "1.0", "Current version is 1.0")
Ejemplo n.º 7
0
def main():
    for ML in [config.MEASUREDBOOT_ML, config.IMA_ML]:
        if not os.access(ML, os.F_OK):
            logger.warning(
                'Measurement list path %s not accessible by agent. Any attempt to instruct it to access this path - via "keylime_tenant" CLI - will result in agent process dying',
                ML,
            )

    ima_log_file = None
    if os.path.exists(config.IMA_ML):
        ima_log_file = open(config.IMA_ML, "r", encoding="utf-8")  # pylint: disable=consider-using-with

    tpm_log_file_data = None
    if os.path.exists(config.MEASUREDBOOT_ML):
        with open(config.MEASUREDBOOT_ML, "rb") as tpm_log_file:
            tpm_log_file_data = base64.b64encode(tpm_log_file.read())

    if config.get("cloud_agent", "agent_uuid") == "dmidecode":
        if os.getuid() != 0:
            raise RuntimeError(
                "agent_uuid is configured to use dmidecode, but current process is not running as root."
            )
        cmd = ["which", "dmidecode"]
        ret = cmd_exec.run(cmd, raiseOnError=False)
        if ret["code"] != 0:
            raise RuntimeError(
                "agent_uuid is configured to use dmidecode, but it's is not found on the system."
            )

    # initialize the tmpfs partition to store keys if it isn't already available
    secdir = secure_mount.mount()

    # Now that operations requiring root privileges are done, drop privileges
    # if 'run_as' is available in the configuration.
    if os.getuid() == 0:
        run_as = config.get("cloud_agent", "run_as", fallback="")
        if run_as != "":
            user_utils.chown(secdir, run_as)
            user_utils.change_uidgid(run_as)
            logger.info("Dropped privileges to %s", run_as)
        else:
            logger.warning(
                "Cannot drop privileges since 'run_as' is empty or missing in keylime.conf agent section."
            )

    # Instanitate TPM class

    instance_tpm = tpm()
    # get params for initialization
    registrar_ip = config.get("cloud_agent", "registrar_ip")
    registrar_port = config.get("cloud_agent", "registrar_port")

    # get params for the verifier to contact the agent
    contact_ip = os.getenv("KEYLIME_AGENT_CONTACT_IP", None)
    if contact_ip is None and config.has_option("cloud_agent",
                                                "agent_contact_ip"):
        contact_ip = config.get("cloud_agent", "agent_contact_ip")
    contact_port = os.getenv("KEYLIME_AGENT_CONTACT_PORT", None)
    if contact_port is None and config.has_option("cloud_agent",
                                                  "agent_contact_port"):
        contact_port = config.get("cloud_agent",
                                  "agent_contact_port",
                                  fallback="invalid")

    # change dir to working dir
    fs_util.ch_dir(config.WORK_DIR)

    # set a conservative general umask
    os.umask(0o077)

    # initialize tpm
    (ekcert, ek_tpm, aik_tpm) = instance_tpm.tpm_init(
        self_activate=False,
        config_pw=config.get("cloud_agent", "tpm_ownerpassword")
    )  # this tells initialize not to self activate the AIK

    # Warn if kernel version is <5.10 and another algorithm than SHA1 is used,
    # because otherwise IMA will not work
    kernel_version = tuple(platform.release().split("-")[0].split("."))
    if tuple(map(int, kernel_version)) < (
            5, 10,
            0) and instance_tpm.defaults["hash"] != algorithms.Hash.SHA1:
        logger.warning(
            "IMA attestation only works on kernel versions <5.10 with SHA1 as hash algorithm. "
            'Even if ascii_runtime_measurements shows "%s" as the '
            "algorithm, it might be just padding zeros",
            (instance_tpm.defaults["hash"]),
        )

    if ekcert is None and instance_tpm.is_emulator():
        ekcert = "emulator"

    # now we need the UUID
    try:
        agent_uuid = config.get("cloud_agent", "agent_uuid")
    except configparser.NoOptionError:
        agent_uuid = None
    if agent_uuid == "hash_ek":
        ek_pubkey = pubkey_from_tpm2b_public(base64.b64decode(ek_tpm))
        ek_pubkey_pem = ek_pubkey.public_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PublicFormat.SubjectPublicKeyInfo)
        agent_uuid = hashlib.sha256(ek_pubkey_pem).hexdigest()
    elif agent_uuid == "generate" or agent_uuid is None:
        agent_uuid = str(uuid.uuid4())
    elif agent_uuid == "dmidecode":
        cmd = ["dmidecode", "-s", "system-uuid"]
        ret = cmd_exec.run(cmd)
        sys_uuid = ret["retout"][0].decode("utf-8")
        agent_uuid = sys_uuid.strip()
        try:
            uuid.UUID(agent_uuid)
        except ValueError as e:
            raise RuntimeError(  # pylint: disable=raise-missing-from
                f"The UUID returned from dmidecode is invalid: {str(e)}")
    elif agent_uuid == "hostname":
        agent_uuid = socket.getfqdn()
    elif agent_uuid == "environment":
        agent_uuid = os.getenv("KEYLIME_AGENT_UUID", None)
        if agent_uuid is None:
            raise RuntimeError(
                "Env variable KEYLIME_AGENT_UUID is empty, but agent_uuid is set to 'environment'"
            )
    elif not validators.valid_uuid(agent_uuid):
        raise RuntimeError("The UUID is not valid")

    if not validators.valid_agent_id(agent_uuid):
        raise RuntimeError(
            "The agent ID set via agent uuid parameter use invalid characters")

    logger.info("Agent UUID: %s", agent_uuid)

    serveraddr = (config.get("cloud_agent", "cloudagent_ip"),
                  config.getint("cloud_agent", "cloudagent_port"))

    keylime_ca = config.get("cloud_agent", "keylime_ca")
    if keylime_ca == "default":
        keylime_ca = os.path.join(config.WORK_DIR, "cv_ca", "cacert.crt")

    server = CloudAgentHTTPServer(serveraddr, Handler, agent_uuid, contact_ip,
                                  ima_log_file, tpm_log_file_data)
    if server.mtls_cert_enabled:
        context = web_util.generate_mtls_context(server.mtls_cert_path,
                                                 server.rsakey_path,
                                                 keylime_ca,
                                                 logger=logger)
        server.socket = context.wrap_socket(server.socket, server_side=True)
    else:
        if (not config.getboolean(
                "cloud_agent", "enable_insecure_payload", fallback=False)
                and config.get("cloud_agent", "payload_script") != ""):
            raise RuntimeError(
                "agent mTLS is disabled, while a tenant can instruct the agent to execute code on the node. "
                'In order to allow the running of the agent, "enable_insecure_payload" has to be set to "True"'
            )

    serverthread = threading.Thread(target=server.serve_forever, daemon=True)

    # register it and get back a blob
    mtls_cert = "disabled"
    if server.mtls_cert:
        mtls_cert = server.mtls_cert.public_bytes(serialization.Encoding.PEM)

    keyblob = registrar_client.doRegisterAgent(registrar_ip, registrar_port,
                                               agent_uuid, ek_tpm, ekcert,
                                               aik_tpm, mtls_cert, contact_ip,
                                               contact_port)

    if keyblob is None:
        instance_tpm.flush_keys()
        raise Exception("Registration failed")

    # get the ephemeral registrar key
    key = instance_tpm.activate_identity(keyblob)

    if key is None:
        instance_tpm.flush_keys()
        raise Exception("Activation failed")

    # tell the registrar server we know the key
    retval = registrar_client.doActivateAgent(registrar_ip, registrar_port,
                                              agent_uuid, key)

    if not retval:
        instance_tpm.flush_keys()
        raise Exception("Registration failed on activate")

    # Start revocation listener in a new process to not interfere with tornado
    revocation_process = multiprocessing.Process(target=revocation_listener,
                                                 daemon=True)
    revocation_process.start()

    logger.info(
        "Starting Cloud Agent on %s:%s with API version %s. Use <Ctrl-C> to stop",
        serveraddr[0],
        serveraddr[1],
        keylime_api_version.current_version(),
    )
    serverthread.start()

    def shutdown_handler(*_):
        logger.info("TERM Signal received, shutting down...")
        logger.debug("Stopping revocation notifier...")
        revocation_process.terminate()
        logger.debug("Shutting down HTTP server...")
        server.shutdown()
        server.server_close()
        serverthread.join()
        logger.debug("HTTP server stopped...")
        revocation_process.join()
        logger.debug("Revocation notifier stopped...")
        secure_mount.umount()
        logger.debug("Umounting directories...")
        instance_tpm.flush_keys()
        logger.debug("Flushed keys successfully")
        sys.exit(0)

    signal.signal(signal.SIGTERM, shutdown_handler)
    signal.signal(signal.SIGQUIT, shutdown_handler)
    signal.signal(signal.SIGINT, shutdown_handler)

    # Keep the main thread alive by waiting for the server thread
    serverthread.join()
Ejemplo n.º 8
0
    def do_GET(self):
        """This method services the GET 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 paramter.  If the uri or parameters are incorrect, a 400 response is returned.
        """

        logger.info("GET invoked from %s with uri: %s", self.client_address,
                    self.path)
        rest_params = web_util.get_restful_params(self.path)
        if rest_params is None:
            web_util.echo_json_response(
                self, 405,
                "Not Implemented: Use /version, /keys/ or /quotes/ interfaces")
            return

        if "version" in rest_params:
            version_info = {
                "supported_version": keylime_api_version.current_version()
            }
            web_util.echo_json_response(self, 200, "Success", version_info)
            return

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

        if "keys" in rest_params and rest_params["keys"] == "verify":
            if self.server.K is None:
                logger.info(
                    "GET key challenge returning 400 response. bootstrap key not available"
                )
                web_util.echo_json_response(
                    self, 400, "Bootstrap key not yet available.")
                return
            if "challenge" not in rest_params:
                logger.info(
                    "GET key challenge returning 400 response. No challenge provided"
                )
                web_util.echo_json_response(self, 400,
                                            "No challenge provided.")
                return

            challenge = rest_params["challenge"]
            response = {}
            response["hmac"] = crypto.do_hmac(self.server.K, challenge)
            web_util.echo_json_response(self, 200, "Success", response)
            logger.info("GET key challenge returning 200 response.")

        # If agent pubkey requested
        elif "keys" in rest_params and rest_params["keys"] == "pubkey":
            response = {}
            response["pubkey"] = self.server.rsapublickey_exportable

            web_util.echo_json_response(self, 200, "Success", response)
            logger.info("GET pubkey returning 200 response.")
            return

        elif "quotes" in rest_params:
            nonce = rest_params.get("nonce", None)
            pcrmask = rest_params.get("mask", None)
            ima_ml_entry = rest_params.get("ima_ml_entry", "0")

            # if the query is not messed up
            if nonce is None:
                logger.warning(
                    "GET quote returning 400 response. nonce not provided as an HTTP parameter in request"
                )
                web_util.echo_json_response(
                    self, 400,
                    "nonce not provided as an HTTP parameter in request")
                return

            # Sanitization assurance (for tpm.run() tasks below)
            if not (nonce.isalnum() and
                    (pcrmask is None or validators.valid_hex(pcrmask))
                    and ima_ml_entry.isalnum()):
                logger.warning(
                    "GET quote returning 400 response. parameters should be strictly alphanumeric"
                )
                web_util.echo_json_response(
                    self, 400, "parameters should be strictly alphanumeric")
                return

            if len(nonce) > tpm_instance.MAX_NONCE_SIZE:
                logger.warning(
                    "GET quote returning 400 response. Nonce is too long (max size %i): %i",
                    tpm_instance.MAX_NONCE_SIZE,
                    len(nonce),
                )
                web_util.echo_json_response(
                    self, 400,
                    f"Nonce is too long (max size {tpm_instance.MAX_NONCE_SIZE}): {len(nonce)}"
                )
                return

            hash_alg = tpm_instance.defaults["hash"]
            quote = tpm_instance.create_quote(
                nonce, self.server.rsapublickey_exportable, pcrmask, hash_alg)
            imaMask = pcrmask

            # Allow for a partial quote response (without pubkey)
            enc_alg = tpm_instance.defaults["encrypt"]
            sign_alg = tpm_instance.defaults["sign"]

            if "partial" in rest_params and (rest_params["partial"] is None
                                             or rest_params["partial"] == "1"):
                response = {
                    "quote": quote,
                    "hash_alg": hash_alg,
                    "enc_alg": enc_alg,
                    "sign_alg": sign_alg,
                }
            else:
                response = {
                    "quote": quote,
                    "hash_alg": hash_alg,
                    "enc_alg": enc_alg,
                    "sign_alg": sign_alg,
                    "pubkey": self.server.rsapublickey_exportable,
                }

            response["boottime"] = self.server.boottime

            # return a measurement list if available
            if TPM_Utilities.check_mask(imaMask, config.IMA_PCR):
                ima_ml_entry = int(ima_ml_entry)
                if ima_ml_entry > self.server.next_ima_ml_entry:
                    ima_ml_entry = 0
                ml, nth_entry, num_entries = ima.read_measurement_list(
                    self.server.ima_log_file, ima_ml_entry)
                if num_entries > 0:
                    response["ima_measurement_list"] = ml
                    response["ima_measurement_list_entry"] = nth_entry
                    self.server.next_ima_ml_entry = num_entries

            # similar to how IMA log retrievals are triggered by IMA_PCR, we trigger boot logs with MEASUREDBOOT_PCRs
            # other possibilities would include adding additional data to rest_params to trigger boot log retrievals
            # generally speaking, retrieving the 15Kbytes of a boot log does not seem significant compared to the
            # potential Mbytes of an IMA measurement list.
            if TPM_Utilities.check_mask(imaMask, config.MEASUREDBOOT_PCRS[0]):
                if not self.server.tpm_log_file_data:
                    logger.warning("TPM2 event log not available: %s",
                                   config.MEASUREDBOOT_ML)
                else:
                    response[
                        "mb_measurement_list"] = self.server.tpm_log_file_data

            web_util.echo_json_response(self, 200, "Success", response)
            logger.info("GET %s quote returning 200 response.",
                        rest_params["quotes"])
            return

        else:
            logger.warning("GET returning 400 response. uri not supported: %s",
                           self.path)
            web_util.echo_json_response(self, 400, "uri not supported")
            return
Ejemplo n.º 9
0
else:
    tls_enabled = False
    cert = ""
    logger.warning(
        "Warning: TLS is currently disabled, keys will be sent in the clear! This should only be used for testing."
    )

verifier_ip = config.get('cloud_verifier', 'cloudverifier_ip')
verifier_port = config.get('cloud_verifier', 'cloudverifier_port')
verifier_base_url = f'{verifier_ip}:{verifier_port}'

registrar_ip = config.get('registrar', 'registrar_ip')
registrar_tls_port = config.get('registrar', 'registrar_tls_port')
registrar_base_tls_url = f'{registrar_ip}:{registrar_tls_port}'

api_version = keylime_api_version.current_version()


class Agent_Init_Types:
    FILE = '0'
    KEYFILE = '1'
    CA_DIR = '2'


class BaseHandler(tornado.web.RequestHandler):
    def write_error(self, status_code, **kwargs):

        if self.settings.get("serve_traceback") and "exc_info" in kwargs:
            # in debug mode, try to send a traceback
            lines = []
            for line in traceback.format_exception(*kwargs["exc_info"]):
Ejemplo n.º 10
0
def main():
    for ML in [config.MEASUREDBOOT_ML, config.IMA_ML]:
        if not os.access(ML, os.F_OK):
            logger.warning(
                "Measurement list path %s not accessible by agent. Any attempt to instruct it to access this path - via \"keylime_tenant\" CLI - will result in agent process dying",
                ML)

    ima_log_file = None
    if os.path.exists(config.IMA_ML):
        ima_log_file = open(config.IMA_ML, 'r', encoding="utf-8")

    tpm_log_file_data = None
    if os.path.exists(config.MEASUREDBOOT_ML):
        with open(config.MEASUREDBOOT_ML, 'rb') as tpm_log_file:
            tpm_log_file_data = base64.b64encode(tpm_log_file.read())

    if config.get('cloud_agent', 'agent_uuid') == 'dmidecode':
        if os.getuid() != 0:
            raise RuntimeError('agent_uuid is configured to use dmidecode, '
                               'but current process is not running as root.')
        cmd = ['which', 'dmidecode']
        ret = cmd_exec.run(cmd, raiseOnError=False)
        if ret['code'] != 0:
            raise RuntimeError('agent_uuid is configured to use dmidecode, '
                               'but it\'s is not found on the system.')

    # initialize the tmpfs partition to store keys if it isn't already available
    secdir = secure_mount.mount()

    # Now that operations requiring root privileges are done, drop privileges
    # if 'run_as' is available in the configuration.
    if os.getuid() == 0:
        run_as = config.get('cloud_agent', 'run_as', fallback='')
        if run_as != '':
            user_utils.chown(secdir, run_as)
            user_utils.change_uidgid(run_as)
            logger.info(f"Dropped privileges to {run_as}")
        else:
            logger.warning(
                "Cannot drop privileges since 'run_as' is empty or missing in keylime.conf agent section."
            )

    # Instanitate TPM class

    instance_tpm = tpm()
    # get params for initialization
    registrar_ip = config.get('cloud_agent', 'registrar_ip')
    registrar_port = config.get('cloud_agent', 'registrar_port')

    # get params for the verifier to contact the agent
    contact_ip = os.getenv("KEYLIME_AGENT_CONTACT_IP", None)
    if contact_ip is None and config.has_option('cloud_agent',
                                                'agent_contact_ip'):
        contact_ip = config.get('cloud_agent', 'agent_contact_ip')
    contact_port = os.getenv("KEYLIME_AGENT_CONTACT_PORT", None)
    if contact_port is None and config.has_option('cloud_agent',
                                                  'agent_contact_port'):
        contact_port = config.get('cloud_agent',
                                  'agent_contact_port',
                                  fallback="invalid")

    # change dir to working dir
    fs_util.ch_dir(config.WORK_DIR)

    # set a conservative general umask
    os.umask(0o077)

    # initialize tpm
    (ekcert, ek_tpm, aik_tpm) = instance_tpm.tpm_init(
        self_activate=False,
        config_pw=config.get('cloud_agent', 'tpm_ownerpassword')
    )  # this tells initialize not to self activate the AIK
    virtual_agent = instance_tpm.is_vtpm()

    # Warn if kernel version is <5.10 and another algorithm than SHA1 is used,
    # because otherwise IMA will not work
    kernel_version = tuple(platform.release().split("-")[0].split("."))
    if tuple(map(int, kernel_version)) < (
            5, 10,
            0) and instance_tpm.defaults["hash"] != algorithms.Hash.SHA1:
        logger.warning(
            "IMA attestation only works on kernel versions <5.10 with SHA1 as hash algorithm. "
            "Even if ascii_runtime_measurements shows \"%s\" as the "
            "algorithm, it might be just padding zeros",
            (instance_tpm.defaults["hash"]))

    if ekcert is None:
        if virtual_agent:
            ekcert = 'virtual'
        elif instance_tpm.is_emulator():
            ekcert = 'emulator'

    # now we need the UUID
    try:
        agent_uuid = config.get('cloud_agent', 'agent_uuid')
    except configparser.NoOptionError:
        agent_uuid = None
    if agent_uuid == 'hash_ek':
        ek_pubkey = pubkey_from_tpm2b_public(base64.b64decode(ek_tpm))
        ek_pubkey_pem = ek_pubkey.public_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PublicFormat.SubjectPublicKeyInfo)
        agent_uuid = hashlib.sha256(ek_pubkey_pem).hexdigest()
    elif agent_uuid == 'generate' or agent_uuid is None:
        agent_uuid = str(uuid.uuid4())
    elif agent_uuid == 'dmidecode':
        cmd = ['dmidecode', '-s', 'system-uuid']
        ret = cmd_exec.run(cmd)
        sys_uuid = ret['retout'][0].decode('utf-8')
        agent_uuid = sys_uuid.strip()
        try:
            uuid.UUID(agent_uuid)
        except ValueError as e:
            raise RuntimeError(
                "The UUID returned from dmidecode is invalid: %s" % e)  # pylint: disable=raise-missing-from
    elif agent_uuid == 'hostname':
        agent_uuid = socket.getfqdn()
    elif agent_uuid == 'environment':
        agent_uuid = os.getenv("KEYLIME_AGENT_UUID", None)
        if agent_uuid is None:
            raise RuntimeError(
                "Env variable KEYLIME_AGENT_UUID is empty, but agent_uuid is set to 'environment'"
            )
    elif not validators.valid_uuid(agent_uuid):
        raise RuntimeError("The UUID is not valid")

    if not validators.valid_agent_id(agent_uuid):
        raise RuntimeError(
            "The agent ID set via agent uuid parameter use invalid characters")

    if config.STUB_VTPM and config.TPM_CANNED_VALUES is not None:
        # Use canned values for stubbing
        jsonIn = config.TPM_CANNED_VALUES
        if "add_vtpm_to_group" in jsonIn:
            # The value we're looking for has been canned!
            agent_uuid = jsonIn['add_vtpm_to_group']['retout']
        else:
            # Our command hasn't been canned!
            raise Exception("Command %s not found in canned json!" %
                            ("add_vtpm_to_group"))

    logger.info("Agent UUID: %s", agent_uuid)

    serveraddr = (config.get('cloud_agent', 'cloudagent_ip'),
                  config.getint('cloud_agent', 'cloudagent_port'))

    keylime_ca = config.get('cloud_agent', 'keylime_ca')
    if keylime_ca == "default":
        keylime_ca = os.path.join(config.WORK_DIR, 'cv_ca', 'cacert.crt')

    server = CloudAgentHTTPServer(serveraddr, Handler, agent_uuid, contact_ip,
                                  ima_log_file, tpm_log_file_data)
    context = web_util.generate_mtls_context(server.mtls_cert_path,
                                             server.rsakey_path,
                                             keylime_ca,
                                             logger=logger)
    server.socket = context.wrap_socket(server.socket, server_side=True)
    serverthread = threading.Thread(target=server.serve_forever, daemon=True)

    # register it and get back a blob
    mtls_cert = server.mtls_cert.public_bytes(serialization.Encoding.PEM)
    keyblob = registrar_client.doRegisterAgent(registrar_ip, registrar_port,
                                               agent_uuid, ek_tpm, ekcert,
                                               aik_tpm, mtls_cert, contact_ip,
                                               contact_port)

    if keyblob is None:
        instance_tpm.flush_keys()
        raise Exception("Registration failed")

    # get the ephemeral registrar key
    key = instance_tpm.activate_identity(keyblob)

    if key is None:
        instance_tpm.flush_keys()
        raise Exception("Activation failed")

    # tell the registrar server we know the key
    retval = registrar_client.doActivateAgent(registrar_ip, registrar_port,
                                              agent_uuid, key)

    if not retval:
        instance_tpm.flush_keys()
        raise Exception("Registration failed on activate")

    # Start revocation listener in a new process to not interfere with tornado
    revocation_process = multiprocessing.Process(target=revocation_listener,
                                                 daemon=True)
    revocation_process.start()

    logger.info(
        "Starting Cloud Agent on %s:%s with API version %s. Use <Ctrl-C> to stop",
        serveraddr[0], serveraddr[1], keylime_api_version.current_version())
    serverthread.start()

    def shutdown_handler(*_):
        logger.info("TERM Signal received, shutting down...")
        logger.debug("Stopping revocation notifier...")
        revocation_process.terminate()
        logger.debug("Shutting down HTTP server...")
        server.shutdown()
        server.server_close()
        serverthread.join()
        logger.debug("HTTP server stopped...")
        revocation_process.join()
        logger.debug("Revocation notifier stopped...")
        secure_mount.umount()
        logger.debug("Umounting directories...")
        instance_tpm.flush_keys()
        logger.debug("Flushed keys successfully")
        sys.exit(0)

    signal.signal(signal.SIGTERM, shutdown_handler)
    signal.signal(signal.SIGQUIT, shutdown_handler)
    signal.signal(signal.SIGINT, shutdown_handler)

    # Keep the main thread alive by waiting for the server thread
    serverthread.join()
Ejemplo n.º 11
0
 def test_current_version(self):
     self.assertEqual(api_version.current_version(), "2.1", "Current version is 2.1")
Ejemplo n.º 12
0
def main():
    for ML in [ config.MEASUREDBOOT_ML, config.IMA_ML ] :
        if not os.access(ML, os.F_OK) :
            logger.warning("Measurement list path %s not accessible by agent. Any attempt to instruct it to access this path - via \"keylime_tenant\" CLI - will result in agent process dying", ML)

    if config.get('cloud_agent', 'agent_uuid') == 'dmidecode':
        if os.getuid() != 0:
            raise RuntimeError('agent_uuid is configured to use dmidecode, '
                               'but current process is not running as root.')
        cmd = ['which', 'dmidecode']
        ret = cmd_exec.run(cmd, raiseOnError=False)
        if ret['code'] != 0:
            raise RuntimeError('agent_uuid is configured to use dmidecode, '
                               'but it\'s is not found on the system.')

    # Instanitate TPM class

    instance_tpm = tpm()
    # get params for initialization
    registrar_ip = config.get('cloud_agent', 'registrar_ip')
    registrar_port = config.get('cloud_agent', 'registrar_port')

    # get params for the verifier to contact the agent
    contact_ip = os.getenv("KEYLIME_AGENT_CONTACT_IP", None)
    if contact_ip is None and config.has_option('cloud_agent', 'agent_contact_ip'):
        contact_ip = config.get('cloud_agent', 'agent_contact_ip')
    contact_port = os.getenv("KEYLIME_AGENT_CONTACT_PORT", None)
    if contact_port is None and config.has_option('cloud_agent', 'agent_contact_port'):
        contact_port = config.get('cloud_agent', 'agent_contact_port', fallback="invalid")

    # initialize the tmpfs partition to store keys if it isn't already available
    secdir = secure_mount.mount()

    # change dir to working dir
    config.ch_dir(config.WORK_DIR, logger)

    # initialize tpm
    (ekcert, ek_tpm, aik_tpm) = instance_tpm.tpm_init(self_activate=False, config_pw=config.get(
        'cloud_agent', 'tpm_ownerpassword'))  # this tells initialize not to self activate the AIK
    virtual_agent = instance_tpm.is_vtpm()

    if ekcert is None:
        if virtual_agent:
            ekcert = 'virtual'
        elif instance_tpm.is_emulator():
            ekcert = 'emulator'

    # now we need the UUID
    try:
        agent_uuid = config.get('cloud_agent', 'agent_uuid')
    except configparser.NoOptionError:
        agent_uuid = None
    if agent_uuid == 'openstack':
        agent_uuid = openstack.get_openstack_uuid()
    elif agent_uuid == 'hash_ek':
        agent_uuid = hashlib.sha256(ek_tpm).hexdigest()
    elif agent_uuid == 'generate' or agent_uuid is None:
        agent_uuid = str(uuid.uuid4())
    elif agent_uuid == 'dmidecode':
        cmd = ['dmidecode', '-s', 'system-uuid']
        ret = cmd_exec.run(cmd)
        sys_uuid = ret['retout'][0].decode('utf-8')
        agent_uuid = sys_uuid.strip()
        try:
            uuid.UUID(agent_uuid)
        except ValueError as e:
            raise RuntimeError("The UUID returned from dmidecode is invalid: %s" % e)  # pylint: disable=raise-missing-from
    elif agent_uuid == 'hostname':
        agent_uuid = socket.getfqdn()
    if config.STUB_VTPM and config.TPM_CANNED_VALUES is not None:
        # Use canned values for stubbing
        jsonIn = config.TPM_CANNED_VALUES
        if "add_vtpm_to_group" in jsonIn:
            # The value we're looking for has been canned!
            agent_uuid = jsonIn['add_vtpm_to_group']['retout']
        else:
            # Our command hasn't been canned!
            raise Exception("Command %s not found in canned json!" %
                            ("add_vtpm_to_group"))

    logger.info("Agent UUID: %s", agent_uuid)

    # register it and get back a blob
    keyblob = registrar_client.doRegisterAgent(
        registrar_ip, registrar_port, agent_uuid, ek_tpm, ekcert, aik_tpm, contact_ip, contact_port)

    if keyblob is None:
        instance_tpm.flush_keys()
        raise Exception("Registration failed")

    # get the ephemeral registrar key
    key = instance_tpm.activate_identity(keyblob)

    if key is None:
        instance_tpm.flush_keys()
        raise Exception("Activation failed")

    # tell the registrar server we know the key
    retval = False
    retval = registrar_client.doActivateAgent(
        registrar_ip, registrar_port, agent_uuid, key)

    if not retval:
        instance_tpm.flush_keys()
        raise Exception("Registration failed on activate")

    serveraddr = (config.get('cloud_agent', 'cloudagent_ip'),
                  config.getint('cloud_agent', 'cloudagent_port'))
    server = CloudAgentHTTPServer(serveraddr, Handler, agent_uuid)
    serverthread = threading.Thread(target=server.serve_forever)

    logger.info("Starting Cloud Agent on %s:%s with API version %s. Use <Ctrl-C> to stop", serveraddr[0], serveraddr[1], keylime_api_version.current_version())
    serverthread.start()

    # want to listen for revocations?
    if config.getboolean('cloud_agent', 'listen_notfications'):
        cert_path = config.get('cloud_agent', 'revocation_cert')
        if cert_path == "default":
            cert_path = os.path.join(secdir,
                                      "unzipped/RevocationNotifier-cert.crt")
        elif cert_path[0] != '/':
            # if it is a relative, convert to absolute in work_dir
            cert_path = os.path.abspath(
                os.path.join(config.WORK_DIR, cert_path))

        def perform_actions(revocation):
            actionlist = []

            # load the actions from inside the keylime module
            actionlisttxt = config.get('cloud_agent', 'revocation_actions')
            if actionlisttxt.strip() != "":
                actionlist = actionlisttxt.split(',')
                actionlist = ["revocation_actions.%s" % i for i in actionlist]

            # load actions from unzipped
            action_list_path = os.path.join(secdir, "unzipped/action_list")
            if os.path.exists(action_list_path):
                with open(action_list_path, encoding="utf-8") as f:
                    actionlisttxt = f.read()
                if actionlisttxt.strip() != "":
                    localactions = actionlisttxt.strip().split(',')
                    for action in localactions:
                        if not action.startswith('local_action_'):
                            logger.warning("Invalid local action: %s. Must start with local_action_", action)
                        else:
                            actionlist.append(action)

                    uzpath = "%s/unzipped" % secdir
                    if uzpath not in sys.path:
                        sys.path.append(uzpath)

            for action in actionlist:
                logger.info("Executing revocation action %s", action)
                try:
                    module = importlib.import_module(action)
                    execute = getattr(module, 'execute')
                    asyncio.get_event_loop().run_until_complete(execute(revocation))
                except Exception as e:
                    logger.warning("Exception during execution of revocation action %s: %s", action, e)
        try:
            while True:
                try:
                    revocation_notifier.await_notifications(
                        perform_actions, revocation_cert_path=cert_path)
                except Exception as e:
                    logger.exception(e)
                    logger.warning("No connection to revocation server, retrying in 10s...")
                    time.sleep(10)
        except KeyboardInterrupt:
            logger.info("TERM Signal received, shutting down...")
            instance_tpm.flush_keys()
            server.shutdown()
    else:
        try:
            while True:
                time.sleep(1)
        except KeyboardInterrupt:
            logger.info("TERM Signal received, shutting down...")
            instance_tpm.flush_keys()
            server.shutdown()