def put(self): """This method handles the PUT requests to add agents to the Cloud Verifier. Currently, only agents resources are available for PUTing, i.e. /agents. All other PUT uri's will return errors. """ rest_params = web_util.get_restful_params(self.request.uri) if rest_params is None: web_util.echo_json_response( self, 405, "Not Implemented: Use /agents/ interface") return if "agents" not in rest_params: web_util.echo_json_response(self, 400, "uri not supported") logger.warning('PUT returning 400 response. uri not supported: %s', self.request.path) return agent_id = rest_params["agents"] # 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 # let Tenant do dirty work of reactivating agent mytenant = tenant.Tenant() mytenant.agent_uuid = agent_id mytenant.do_cvreactivate() web_util.echo_json_response(self, 200, "Success")
def do_DELETE(self): """This method handles the DELETE requests to remove agents from the Registrar Server. Currently, only agents resources are available for DELETEing, i.e. /agents. All other DELETE uri's will return errors. agents requests require a single agent_id parameter which identifies the agent to be deleted. """ 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( 'DELETE agent returning 400 response. uri not supported: %s', self.path) return agent_id = rest_params["agents"] if agent_id is not None: # 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("DELETE received an invalid agent ID: %s", agent_id) return if session.query(RegistrarMain).filter_by( agent_id=agent_id).delete(): # send response try: session.commit() except SQLAlchemyError as e: logger.error('SQLAlchemy Error: %s', e) web_util.echo_json_response(self, 200, "Success") return # send response web_util.echo_json_response(self, 404) return web_util.echo_json_response(self, 404)
def test_invalid(self): """Check an invalid user ID with non valid characters.""" self.assertFalse(validators.valid_agent_id("rm -fr *"))
def test_valid_hostname(self): """Check a valid hostname that mix upper and lower case.""" self.assertTrue(validators.valid_agent_id("my-Hostname.example.com"))
def test_valid_uuid(self): """Check a valid UUID that mix upper and lower case.""" self.assertTrue(validators.valid_agent_id("74a93e15-da24-4ff1-ABC0-55beed02a16a"))
def test_empty(self): """Check that the empty string is not valid.""" self.assertFalse(validators.valid_agent_id(""))
def test_none(self): """Check that None is not valid.""" self.assertFalse(validators.valid_agent_id(None))
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()
def post(self): """This method handles the POST requests to add agents to the Cloud Verifier. Currently, only agents resources are available for POSTing, i.e. /agents. All other POST uri's will return errors. agents requests require a yaml block sent in the body """ rest_params = web_util.get_restful_params(self.request.uri) if rest_params is None: web_util.echo_json_response( self, 405, "Not Implemented: Use /agents/ interface") return if "agents" not in rest_params: web_util.echo_json_response(self, 400, "uri not supported") logger.warning( 'POST returning 400 response. uri not supported: %s', self.request.path) return agent_id = rest_params["agents"] # 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("POST received an invalid agent ID: %s", agent_id) return # Parse payload files (base64 data-uri) if self.get_argument("ptype", Agent_Init_Types.FILE, True) == Agent_Init_Types.FILE: keyfile = None payload = None data = { 'data': parse_data_uri(self.get_argument("file_data", None, True)) } ca_dir = None incl_dir = None ca_dir_pw = None elif self.get_argument("ptype", Agent_Init_Types.FILE, True) == Agent_Init_Types.KEYFILE: keyfile = { 'data': parse_data_uri(self.get_argument("keyfile_data", None, True)), } payload = { 'data': parse_data_uri(self.get_argument("file_data", None, True)) } data = None ca_dir = None incl_dir = None ca_dir_pw = None elif self.get_argument("ptype", Agent_Init_Types.FILE, True) == Agent_Init_Types.CA_DIR: keyfile = None payload = None data = None incl_dir = { 'data': parse_data_uri( self.get_argument("include_dir_data", None, True)), 'name': self.get_argument("include_dir_name", "", True).splitlines() } ca_dir = self.get_argument("ca_dir", 'default', True) if ca_dir == "": ca_dir = 'default' ca_dir_pw = self.get_argument("ca_dir_pw", 'default', True) if ca_dir_pw == "": ca_dir_pw = 'default' else: web_util.echo_json_response(self, 400, "invalid payload type chosen") logger.warning('POST returning 400 response. malformed query') return # Pull in user-defined v/TPM policies tpm_policy = self.get_argument("tpm_policy", "", True) if tpm_policy == "": tpm_policy = None vtpm_policy = self.get_argument("vtpm_policy", "", True) if vtpm_policy == "": vtpm_policy = None # Pull in allowlist allowlist = None a_list_data = self.get_argument("a_list_data", None, True) if a_list_data != "": allowlist_str = parse_data_uri(a_list_data) if allowlist_str is not None: allowlist = allowlist_str[0].splitlines() # Pull in IMA exclude list ima_exclude = None e_list_data = self.get_argument("e_list_data", None, True) if e_list_data != "": ima_exclude_str = parse_data_uri(e_list_data) if ima_exclude_str is not None: ima_exclude = ima_exclude_str[0].splitlines() # Build args to give to Tenant's init_add method args = { 'agent_ip': self.get_argument("agent_ip", None, True), 'file': data, 'keyfile': keyfile, 'payload': payload, 'ca_dir': ca_dir, 'incl_dir': incl_dir, 'ca_dir_pw': ca_dir_pw, 'tpm_policy': tpm_policy, 'vtpm_policy': vtpm_policy, 'allowlist': allowlist, 'ima_exclude': ima_exclude, } # let Tenant do dirty work of adding agent try: mytenant = tenant.Tenant() mytenant.agent_uuid = agent_id mytenant.init_add(args) mytenant.preloop() mytenant.do_cv() mytenant.do_quote() except Exception as e: logger.exception(e) logger.warning('POST returning 500 response. Tenant error: %s', e) web_util.echo_json_response(self, 500, "Request failure", str(e)) return web_util.echo_json_response(self, 200, "Success")
async def get(self): """This method handles the GET requests to retrieve status on agents from the WebApp. Currently, only the web app is available for GETing, i.e. /agents. All other GET uri's will return errors. """ rest_params = web_util.get_restful_params(self.request.uri) if rest_params is None: web_util.echo_json_response( self, 405, "Not Implemented: Use /agents/ or /logs/ interface") return if "logs" in rest_params and rest_params["logs"] == "tenant": offset = 0 if "pos" in rest_params and rest_params[ "pos"] is not None and rest_params["pos"].isdigit(): offset = int(rest_params["pos"]) # intercept requests for logs with open(keylime_logging.LOGSTREAM, encoding="utf-8") as f: logValue = f.readlines() web_util.echo_json_response(self, 200, "Success", {'log': logValue[offset:]}) return if "agents" not in rest_params: # otherwise they must be looking for agent info web_util.echo_json_response(self, 400, "uri not supported") logger.warning('GET returning 400 response. uri not supported: %s', self.request.path) return agent_id = rest_params["agents"] if agent_id is not None: # 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("GET received an invalid agent ID: %s", agent_id) return # Handle request for specific agent data separately agents = await self.get_agent_state(agent_id) agents["id"] = agent_id web_util.echo_json_response(self, 200, "Success", agents) return # If no agent ID, get list of all agents from Registrar try: get_agents = RequestsClient(registrar_base_tls_url, tls_enabled) response = get_agents.get((f'/v{api_version}/agents/'), cert=cert, verify=False) except Exception as e: logger.error( "Status command response: %s:%s Unexpected response from Registrar.", tenant_templ.registrar_ip, tenant_templ.registrar_port) logger.exception(e) web_util.echo_json_response(self, 500, "Unexpected response from Registrar", str(e)) return response_body = response.json() if response.status_code != 200: logger.error( "Status command response: %d Unexpected response from Registrar.", response.status_code) keylime_logging.log_http_response(logger, logging.ERROR, response_body) return None if ("results" not in response_body) or ("uuids" not in response_body["results"]): logger.critical( "Error: unexpected http response body from Registrar: %s", response.status_code) return None agent_list = response_body["results"]["uuids"] web_util.echo_json_response(self, 200, "Success", {'uuids': agent_list})
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()
def do_GET(self): """This method handles the GET requests to retrieve status on agents from the Registrar Server. Currently, only agents resources are available for GETing, i.e. /agents. All other GET uri's will return errors. agents requests require a single agent_id parameter which identifies the agent to be returned. If the agent_id is not found, a 404 response is returned. """ 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('GET returning 400 response. uri not supported: %s', self.path) return agent_id = rest_params["agents"] if agent_id is not None: # 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("GET received an invalid agent ID: %s", agent_id) return try: agent = session.query(RegistrarMain).filter_by( agent_id=agent_id).first() except SQLAlchemyError as e: logger.error('SQLAlchemy Error: %s', e) if agent is None: web_util.echo_json_response(self, 404, "agent_id not found") logger.warning( 'GET returning 404 response. agent_id %s not found.', agent_id) return if not bool(agent.active): web_util.echo_json_response(self, 404, "agent_id not yet active") logger.warning( 'GET returning 404 response. agent_id %s not yet active.', agent_id) return response = { 'aik_tpm': agent.aik_tpm, 'ek_tpm': agent.ek_tpm, 'ekcert': agent.ekcert, 'mtls_cert': agent.mtls_cert, 'ip': agent.ip, 'port': agent.port, 'regcount': agent.regcount, } if agent.virtual: response['provider_keys'] = agent.provider_keys web_util.echo_json_response(self, 200, "Success", response) logger.info('GET returning 200 response for agent_id: %s', agent_id) else: # return the available registered uuids from the DB json_response = session.query(RegistrarMain.agent_id).all() return_response = [item[0] for item in json_response] web_util.echo_json_response(self, 200, "Success", {'uuids': return_response}) logger.info('GET returning 200 response for agent_id list') return
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
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)