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( f"Contact ip for agent {agent_id} is not a valid ip got: {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( f"Contact port for agent {agent_id} is not a number between 1 and got: {contact_port}." ) contact_port = None except ValueError: logger.warning( f"Contact port for agent {agent_id} is not a valid number got: {contact_port}." ) contact_port = None # Check for mTLS cert mtls_cert = json_body.get('mtls_cert', None) if mtls_cert is None: logger.warning( f"Agent {agent_id} did not sent a mTLS certificate. Most operations will not work!" ) # 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)
def setUp(self): self.tpm = tpm()
from keylime import crypto from keylime import openstack from keylime import revocation_notifier from keylime import registrar_client from keylime import secure_mount from keylime.tpm.tpm_main import tpm from keylime.tpm.tpm_abstract import TPM_Utilities # Configure logger logger = keylime_logging.init_logging('cloudagent') # lock required for multithreaded operation uvLock = threading.Lock() # Instaniate tpm tpm_instance = tpm(need_hw_tpm=True) class Handler(BaseHTTPRequestHandler): parsed_path = '' def do_HEAD(self): """Not supported""" config.echo_json_response(self, 405, "HEAD not supported") 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. """
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') # 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() # try to get some TPM randomness into the system entropy pool instance_tpm.init_system_rand() 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'].decode('utf-8') agent_uuid = sys_uuid.strip() 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) 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 use <Ctrl-C> to stop", serveraddr[0], serveraddr[1]) 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 = '%s/unzipped/RevocationNotifier-cert.crt' % (secdir) elif cert_path[0] != '/': # if it is a relative, convert to absolute in work_dir cert_path = os.path.abspath('%s/%s' % (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 if os.path.exists("%s/unzipped/action_list" % secdir): with open("%s/unzipped/action_list" % secdir, 'r') 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()
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()
class Tenant(): """Simple command processor example.""" config = None cloudverifier_ip = None cloudverifier_port = None cloudagent_ip = None cv_cloudagent_ip = None cloudagent_port = None registrar_ip = None registrar_port = None webapp_ip = None webapp_port = None uuid_service_generate_locally = None agent_uuid = None K = None V = None U = None auth_tag = None tpm_policy = None vtpm_policy = {} metadata = {} allowlist = {} ima_sign_verification_keys = [] revocation_key = "" accept_tpm_hash_algs = [] accept_tpm_encryption_algs = [] accept_tpm_signing_algs = [] payload = None tpm_instance = tpm() def __init__(self): """ Set up required values and TLS """ self.nonce = None self.agent_ip = None self.agent_port = config.get('cloud_agent', 'cloudagent_port') self.verifier_ip = config.get('tenant', 'cloudverifier_ip') self.verifier_port = config.get('tenant', 'cloudverifier_port') self.registrar_ip = config.get('tenant', 'registrar_ip') self.registrar_port = config.get('tenant', 'registrar_port') self.webapp_port = config.getint('webapp', 'webapp_port') if not config.REQUIRE_ROOT and self.webapp_port < 1024: self.webapp_port += 2000 self.webapp_ip = config.get('webapp', 'webapp_ip') self.my_cert, self.my_priv_key = self.get_tls_context() self.cert = (self.my_cert, self.my_priv_key) if config.getboolean('general', "enable_tls"): self.tls_enabled = True else: self.tls_enabled = False self.cert = "" logger.warning( "Warning: TLS is currently disabled, keys will be sent in the clear! This should only be used for testing." ) @property def verifier_base_url(self): return f'{self.verifier_ip}:{self.verifier_port}' def get_tls_context(self): """Generate certifcate naming and path Returns: string -- my_cert (client_cert), my_priv_key (client private key) """ my_cert = config.get('tenant', 'my_cert') my_priv_key = config.get('tenant', 'private_key') tls_dir = config.get('tenant', 'tls_dir') if tls_dir == 'default': my_cert = 'client-cert.crt' my_priv_key = 'client-private.pem' tls_dir = 'cv_ca' if tls_dir[0] != '/': tls_dir = os.path.abspath('%s/%s' % (config.WORK_DIR, tls_dir)) logger.info(f"Setting up client TLS in {tls_dir}") my_cert = "%s/%s" % (tls_dir, my_cert) my_priv_key = "%s/%s" % (tls_dir, my_priv_key) return my_cert, my_priv_key def init_add(self, args): """ Set up required values. Command line options can overwrite these config values Arguments: args {[string]} -- agent_ip|agent_port|cv_agent_ip """ if "agent_ip" in args: self.agent_ip = args["agent_ip"] if 'agent_port' in args and args['agent_port'] is not None: self.agent_port = args['agent_port'] if 'cv_agent_ip' in args and args['cv_agent_ip'] is not None: self.cv_cloudagent_ip = args['cv_agent_ip'] else: self.cv_cloudagent_ip = self.agent_ip # Make sure all keys exist in dictionary if "file" not in args: args["file"] = None if "keyfile" not in args: args["keyfile"] = None if "payload" not in args: args["payload"] = None if "ca_dir" not in args: args["ca_dir"] = None if "incl_dir" not in args: args["incl_dir"] = None if "ca_dir_pw" not in args: args["ca_dir_pw"] = None # Set up accepted algorithms self.accept_tpm_hash_algs = config.get( 'tenant', 'accept_tpm_hash_algs').split(',') self.accept_tpm_encryption_algs = config.get( 'tenant', 'accept_tpm_encryption_algs').split(',') self.accept_tpm_signing_algs = config.get( 'tenant', 'accept_tpm_signing_algs').split(',') # Set up PCR values tpm_policy = config.get('tenant', 'tpm_policy') if "tpm_policy" in args and args["tpm_policy"] is not None: tpm_policy = args["tpm_policy"] self.tpm_policy = TPM_Utilities.readPolicy(tpm_policy) logger.info(f"TPM PCR Mask from policy is {self.tpm_policy['mask']}") vtpm_policy = config.get('tenant', 'vtpm_policy') if "vtpm_policy" in args and args["vtpm_policy"] is not None: vtpm_policy = args["vtpm_policy"] self.vtpm_policy = TPM_Utilities.readPolicy(vtpm_policy) logger.info(f"TPM PCR Mask from policy is {self.vtpm_policy['mask']}") if args.get("ima_sign_verification_keys") is not None: # Auto-enable IMA (or-bit mask) self.tpm_policy['mask'] = "0x%X" % (int(self.tpm_policy['mask'], 0) | (1 << config.IMA_PCR)) # Add all IMA file signing verification keys to a keyring ima_keyring = ima_file_signatures.ImaKeyring() for filename in args["ima_sign_verification_keys"]: pubkey = ima_file_signatures.get_pubkey_from_file(filename) if not pubkey: raise UserError("File '%s' is not a file with a key" % filename) ima_keyring.add_pubkey(pubkey) self.ima_sign_verification_keys = ima_keyring.to_string() # Read command-line path string allowlist al_data = None if "allowlist" in args and args["allowlist"] is not None: self.enforce_pcrs(list(self.tpm_policy.keys()), [config.IMA_PCR], "IMA") # Auto-enable IMA (or-bit mask) self.tpm_policy['mask'] = "0x%X" % (int(self.tpm_policy['mask'], 0) | (1 << config.IMA_PCR)) if isinstance(args["allowlist"], str): if args["allowlist"] == "default": args["allowlist"] = config.get('tenant', 'allowlist') al_data = ima.read_allowlist(args["allowlist"]) elif isinstance(args["allowlist"], list): al_data = args["allowlist"] else: raise UserError("Invalid allowlist provided") # Read command-line path string IMA exclude list excl_data = None if "ima_exclude" in args and args["ima_exclude"] is not None: if isinstance(args["ima_exclude"], str): if args["ima_exclude"] == "default": args["ima_exclude"] = config.get('tenant', 'ima_excludelist') excl_data = ima.read_excllist(args["ima_exclude"]) elif isinstance(args["ima_exclude"], list): excl_data = args["ima_exclude"] else: raise UserError("Invalid exclude list provided") # Set up IMA if TPM_Utilities.check_mask(self.tpm_policy['mask'], config.IMA_PCR) or \ TPM_Utilities.check_mask(self.vtpm_policy['mask'], config.IMA_PCR): # Process allowlists self.allowlist = ima.process_allowlists(al_data, excl_data) # Read command-line path string TPM2 event log template if "mb_refstate" in args and args["mb_refstate"] is not None: self.enforce_pcrs(list(self.tpm_policy.keys()), config.MEASUREDBOOT_PCRS, "measured boot") # Auto-enable TPM2 event log (or-bit mask) for _pcr in config.MEASUREDBOOT_PCRS: self.tpm_policy['mask'] = "0x%X" % (int( self.tpm_policy['mask'], 0) | (1 << _pcr)) logger.info( f"TPM PCR Mask automatically modified is {self.tpm_policy['mask']} to include IMA/Event log PCRs" ) if isinstance(args["mb_refstate"], str): if args["mb_refstate"] == "default": args["mb_refstate"] = config.get('tenant', 'mb_refstate') al_data = 'test' elif isinstance(args["mb_refstate"], list): al_data = args["mb_refstate"] else: raise UserError( "Invalid measured boot reference state (intended state) provided" ) # if none if (args["file"] is None and args["keyfile"] is None and args["ca_dir"] is None): raise UserError( "You must specify one of -k, -f, or --cert to specify the key/contents to be securely delivered to the agent" ) if args["keyfile"] is not None: if args["file"] is not None or args["ca_dir"] is not None: raise UserError( "You must specify one of -k, -f, or --cert to specify the key/contents to be securely delivered to the agent" ) # read the keys in if isinstance(args["keyfile"], dict) and "data" in args["keyfile"]: if isinstance(args["keyfile"]["data"], list) and len( args["keyfile"]["data"]) == 1: keyfile = args["keyfile"]["data"][0] if keyfile is None: raise UserError("Invalid key file contents") f = io.StringIO(keyfile) else: raise UserError("Invalid key file provided") else: f = open(args["keyfile"], 'r') self.K = base64.b64decode(f.readline()) self.U = base64.b64decode(f.readline()) self.V = base64.b64decode(f.readline()) f.close() # read the payload in (opt.) if isinstance(args["payload"], dict) and "data" in args["payload"]: if isinstance(args["payload"]["data"], list) and len(args["payload"]["data"]) > 0: self.payload = args["payload"]["data"][0] else: if args["payload"] is not None: f = open(args["payload"], 'r') self.payload = f.read() f.close() if args["file"] is not None: if args["keyfile"] is not None or args["ca_dir"] is not None: raise UserError( "You must specify one of -k, -f, or --cert to specify the key/contents to be securely delivered to the agent" ) if isinstance(args["file"], dict) and "data" in args["file"]: if isinstance(args["file"]["data"], list) and len(args["file"]["data"]) > 0: contents = args["file"]["data"][0] if contents is None: raise UserError("Invalid file payload contents") else: raise UserError("Invalid file payload provided") else: with open(args["file"], 'r') as f: contents = f.read() ret = user_data_encrypt.encrypt(contents) self.K = ret['k'] self.U = ret['u'] self.V = ret['v'] self.payload = ret['ciphertext'] if args["ca_dir"] is None and args["incl_dir"] is not None: raise UserError( "--include option is only valid when used with --cert") if args["ca_dir"] is not None: if args["file"] is not None or args["keyfile"] is not None: raise UserError( "You must specify one of -k, -f, or --cert to specify the key/contents to be securely delivered to the agent" ) if args["ca_dir"] == 'default': args["ca_dir"] = config.CA_WORK_DIR if "ca_dir_pw" in args and args["ca_dir_pw"] is not None: ca_util.setpassword(args["ca_dir_pw"]) if not os.path.exists(args["ca_dir"]) or not os.path.exists( "%s/cacert.crt" % args["ca_dir"]): logger.warning(" CA directory does not exist. Creating...") ca_util.cmd_init(args["ca_dir"]) if not os.path.exists("%s/%s-private.pem" % (args["ca_dir"], self.agent_uuid)): ca_util.cmd_mkcert(args["ca_dir"], self.agent_uuid) cert_pkg, serial, subject = ca_util.cmd_certpkg( args["ca_dir"], self.agent_uuid) # support revocation if not os.path.exists( "%s/RevocationNotifier-private.pem" % args["ca_dir"]): ca_util.cmd_mkcert(args["ca_dir"], "RevocationNotifier") rev_package, _, _ = ca_util.cmd_certpkg(args["ca_dir"], "RevocationNotifier") # extract public and private keys from package sf = io.BytesIO(rev_package) with zipfile.ZipFile(sf) as zf: privkey = zf.read("RevocationNotifier-private.pem") cert = zf.read("RevocationNotifier-cert.crt") # put the cert of the revoker into the cert package sf = io.BytesIO(cert_pkg) with zipfile.ZipFile(sf, 'a', compression=zipfile.ZIP_STORED) as zf: zf.writestr('RevocationNotifier-cert.crt', cert) # add additional files to zip if args["incl_dir"] is not None: if isinstance(args["incl_dir"], dict) and "data" in args[ "incl_dir"] and "name" in args["incl_dir"]: if isinstance(args["incl_dir"]["data"], list) and isinstance( args["incl_dir"]["name"], list): if len(args["incl_dir"]["data"]) != len( args["incl_dir"]["name"]): raise UserError("Invalid incl_dir provided") for i in range(len(args["incl_dir"]["data"])): zf.writestr( os.path.basename( args["incl_dir"]["name"][i]), args["incl_dir"]["data"][i]) else: if os.path.exists(args["incl_dir"]): files = next(os.walk(args["incl_dir"]))[2] for filename in files: with open( "%s/%s" % (args["incl_dir"], filename), 'rb') as f: zf.writestr(os.path.basename(f.name), f.read()) else: logger.warning( f'Specified include directory {args["incl_dir"]} does not exist. Skipping...' ) cert_pkg = sf.getvalue() # put the private key into the data to be send to the CV self.revocation_key = privkey # encrypt up the cert package ret = user_data_encrypt.encrypt(cert_pkg) self.K = ret['k'] self.U = ret['u'] self.V = ret['v'] self.metadata = {'cert_serial': serial, 'subject': subject} self.payload = ret['ciphertext'] if self.payload is not None and len(self.payload) > config.getint( 'tenant', 'max_payload_size'): raise UserError("Payload size %s exceeds max size %d" % (len( self.payload), config.getint('tenant', 'max_payload_size'))) def enforce_pcrs(self, policy_pcrs, protected_pcrs, pcr_use): policy_pcrs = list(self.tpm_policy.keys()) policy_pcrs.remove('mask') for _pcr in policy_pcrs: if int(_pcr) in protected_pcrs: logger.error( f"WARNING: PCR {_pcr} is specified in \"tpm_policy\", but will in fact be used by {pcr_use}. Please remove it from policy" ) sys.exit(1) def preloop(self): """ encrypt the agent UUID as a check for delivering the correct key """ self.auth_tag = crypto.do_hmac(self.K, self.agent_uuid) # be very careful printing K, U, or V as they leak in logs stored on unprotected disks if config.INSECURE_DEBUG: logger.debug(F"K: {base64.b64encode(self.K)}") logger.debug(F"V: {base64.b64encode(self.V)}") logger.debug(F"U: {base64.b64encode(self.U)}") logger.debug(F"Auth Tag: {self.auth_tag}") def check_ek(self, ekcert): """ Check the Entity Key Arguments: ekcert {str} -- The endorsement key, either None, "emulator", or base64 encoded der cert Returns: [type] -- [description] """ if config.getboolean('tenant', 'require_ek_cert'): if config.STUB_TPM: logger.debug("not checking ekcert due to STUB_TPM mode") elif ekcert == 'emulator' and config.DISABLE_EK_CERT_CHECK_EMULATOR: logger.info("not checking ekcert of TPM emulator") elif ekcert is None: logger.warning( "No EK cert provided, require_ek_cert option in config set to True" ) return False elif not self.tpm_instance.verify_ek(base64.b64decode(ekcert)): logger.warning("Invalid EK certificate") return False return True def validate_tpm_quote(self, public_key, quote, hash_alg): """ Validate TPM Quote received from the Agent Arguments: public_key {[type]} -- [description] quote {[type]} -- [description] hash_alg {bool} -- [description] Raises: UserError: [description] Returns: [type] -- [description] """ registrar_client.init_client_tls('tenant') reg_keys = registrar_client.getKeys(self.registrar_ip, self.registrar_port, self.agent_uuid) if reg_keys is None: logger.warning("AIK not found in registrar, quote not validated") return False if not self.tpm_instance.check_quote(self.agent_uuid, self.nonce, public_key, quote, reg_keys['aik_tpm'], hash_alg=hash_alg): if reg_keys['regcount'] > 1: logger.error( "WARNING: This UUID had more than one ek-ekcert registered to it! This might indicate that your system is misconfigured or a malicious host is present. Run 'regdelete' for this agent and restart" ) sys.exit() return False if reg_keys['regcount'] > 1: logger.warning( "WARNING: This UUID had more than one ek-ekcert registered to it! This might indicate that your system is misconfigured. Run 'regdelete' for this agent and restart" ) if not config.STUB_TPM and ( not config.getboolean('tenant', 'require_ek_cert') and config.get('tenant', 'ek_check_script') == ""): logger.warning( "DANGER: EK cert checking is disabled and no additional checks on EKs have been specified with ek_check_script option. Keylime is not secure!!" ) # check EK cert and make sure it matches EK if not self.check_ek(reg_keys['ekcert']): return False # if agent is virtual, check phyisical EK cert and make sure it matches phyiscal EK if 'provider_keys' in reg_keys: if not self.check_ek(reg_keys['provider_keys']['ekcert']): return False # check all EKs with optional script: script = config.get('tenant', 'ek_check_script') if not script: return True if script[0] != '/': script = "%s/%s" % (config.WORK_DIR, script) logger.info(f"Checking EK with script {script}") # now we need to exec the script with the ek and ek cert in vars env = os.environ.copy() env['AGENT_UUID'] = self.agent_uuid env['EK'] = tpm2_objects.pubkey_from_tpm2b_public( reg_keys['ek_tpm'], ).public_bytes( crypto_serialization.Encoding.PEM, crypto_serialization.PublicFormat.SubjectPublicKeyInfo, ) env['EK_TPM'] = reg_keys['ek_tpm'] if reg_keys['ekcert'] is not None: env['EK_CERT'] = reg_keys['ekcert'] else: env['EK_CERT'] = "" env['PROVKEYS'] = json.dumps(reg_keys.get('provider_keys', {})) proc = subprocess.Popen(script, env=env, shell=True, cwd=config.WORK_DIR, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) retval = proc.wait() if retval != 0: raise UserError("External check script failed to validate EK") logger.debug("External check script successfully to validated EK") while True: line = proc.stdout.readline().decode() if line == "": break logger.debug(f"ek_check output: {line.strip()}") return True def do_cv(self): """ Initiaite v, agent_id and ip and initiate the cloudinit sequence """ b64_v = base64.b64encode(self.V).decode('utf-8') logger.debug("b64_v:" + b64_v) data = { 'v': b64_v, 'cloudagent_ip': self.cv_cloudagent_ip, 'cloudagent_port': self.agent_port, 'tpm_policy': json.dumps(self.tpm_policy), 'vtpm_policy': json.dumps(self.vtpm_policy), 'allowlist': json.dumps(self.allowlist), 'ima_sign_verification_keys': json.dumps(self.ima_sign_verification_keys), 'metadata': json.dumps(self.metadata), 'revocation_key': self.revocation_key, 'accept_tpm_hash_algs': self.accept_tpm_hash_algs, 'accept_tpm_encryption_algs': self.accept_tpm_encryption_algs, 'accept_tpm_signing_algs': self.accept_tpm_signing_algs, } json_message = json.dumps(data) do_cv = RequestsClient(self.verifier_base_url, self.tls_enabled) response = do_cv.post((f'/agents/{self.agent_uuid}'), data=json_message, cert=self.cert, verify=False) if response.status_code == 503: logger.error( f"Cannot connect to Verifier at {self.verifier_ip} with Port {self.verifier_port}. Connection refused." ) sys.exit() elif response.status_code == 504: logger.error( f"Verifier at {self.verifier_ip} with Port {self.verifier_port} timed out." ) sys.exit() if response.status_code == 409: # this is a conflict, need to update or delete it logger.error( f"Agent {self.agent_uuid} already existed at CV. Please use delete or update." ) sys.exit() elif response.status_code != 200: keylime_logging.log_http_response(logger, logging.ERROR, response.json()) logger.error( f"POST command response: {response.status_code} Unexpected response from Cloud Verifier: {response.text}" ) sys.exit() def do_cvstatus(self, listing=False): """ Perform opertional state look up for agent Keyword Arguments: listing {bool} -- If True, list all agent statues (default: {False}) """ agent_uuid = "" if not listing: agent_uuid = self.agent_uuid do_cvstatus = RequestsClient(self.verifier_base_url, self.tls_enabled) response = do_cvstatus.get((f'/agents/{agent_uuid}'), cert=self.cert, verify=False) if response.status_code == 503: logger.error( f"Cannot connect to Verifier at {self.verifier_ip} with Port {self.verifier_port}. Connection refused." ) sys.exit() elif response == 504: logger.error( f"Verifier at {self.verifier_ip} with Port {self.verifier_port} timed out." ) sys.exit() if response.status_code == 404: logger.error( f"Agent {agent_uuid} does not exist on the verifier. Please try to add or update agent" ) sys.exit() if response.status_code != 200: logger.error( f"Status command response: {response.status_code}. Unexpected response from Cloud Verifier." ) sys.exit() else: response_json = response.json() if not listing: operational_state = response_json["results"][ "operational_state"] logger.info( f'Agent Status: "{states.state_to_str(operational_state)}"' ) else: agent_array = response_json["results"]["uuids"] logger.info(f'Agents: "{agent_array}"') def do_cvdelete(self): """Delete agent from Verifier """ do_cvdelete = RequestsClient(self.verifier_base_url, self.tls_enabled) response = do_cvdelete.delete((f'/agents/{self.agent_uuid}'), cert=self.cert, verify=False) if response.status_code == 503: logger.error( f"Cannot connect to Verifier at {self.verifier_ip} with Port {self.verifier_port}. Connection refused." ) sys.exit() elif response.status_code == 504: logger.error( f"Verifier at {self.verifier_ip} with Port {self.verifier_port} timed out." ) sys.exit() if response.status_code == 202: deleted = False for _ in range(12): get_cvdelete = RequestsClient(self.verifier_base_url, self.tls_enabled) response = get_cvdelete.get((f'/agents/{self.agent_uuid}'), cert=self.cert, verify=False) if response.status_code in (200, 404): deleted = True break time.sleep(.4) if deleted: logger.info( f"CV completed deletion of agent {self.agent_uuid}") else: logger.error( f"Timed out waiting for delete of agent {self.agent_uuid} to complete at CV" ) sys.exit() elif response.status_code == 200: logger.info(f"Agent {self.agent_uuid} deleted from the CV") else: response_body = response.json() keylime_logging.log_http_response(logger, logging.ERROR, response_body) def do_reglist(self): """List agents from Registrar """ registrar_client.init_client_tls('tenant') response = registrar_client.doRegistrarList(self.registrar_ip, self.registrar_port) print(response) def do_regdelete(self): """ Delete agent from Registrar """ registrar_client.init_client_tls('tenant') registrar_client.doRegistrarDelete(self.registrar_ip, self.registrar_port, self.agent_uuid) def do_cvreactivate(self): """ Reactive Agent """ do_cvreactivate = RequestsClient(self.verifier_base_url, self.tls_enabled) response = do_cvreactivate.put( (f'/agents/{self.agent_uuid}/reactivate'), data=b'', cert=self.cert, verify=False) if response.status_code == 503: logger.error( f"Cannot connect to Verifier at {self.verifier_ip} with Port {self.verifier_port}. Connection refused." ) sys.exit() elif response.status_code == 504: logger.error( f"Verifier at {self.verifier_ip} with Port {self.verifier_port} timed out." ) sys.exit() response_body = response.json() if response.status_code != 200: keylime_logging.log_http_response(logger, logging.ERROR, response_body) logger.error( f"Update command response: {response.status_code} Unexpected response from Cloud Verifier." ) else: logger.info(f"Agent {self.agent_uuid} re-activated") def do_cvstop(self): """ Stop declared active agent """ params = f'/agents/{self.agent_uuid}/stop' do_cvstop = RequestsClient(self.verifier_base_url, self.tls_enabled) response = do_cvstop.put(params, cert=self.cert, data=b'', verify=False) if response.status_code == 503: logger.error( f"Cannot connect to Verifier at {self.verifier_ip} with Port {self.verifier_port}. Connection refused." ) sys.exit() elif response.status_code == 504: logger.error( f"Verifier at {self.verifier_ip} with Port {self.verifier_port} timed out." ) sys.exit() response_body = response.json() if response.status_code != 200: keylime_logging.log_http_response(logger, logging.ERROR, response_body) else: logger.info(f"Agent {self.agent_uuid} stopped") def do_quote(self): """ Perform TPM quote by GET towards Agent Raises: UserError: Connection handler """ self.nonce = TPM_Utilities.random_password(20) numtries = 0 response = None # Note: We need a specific retry handler (perhaps in common), no point having localised unless we have too. while True: try: params = '/quotes/identity?nonce=%s' % (self.nonce) cloudagent_base_url = f'{self.agent_ip}:{self.agent_port}' do_quote = RequestsClient(cloudagent_base_url, tls_enabled=False) response = do_quote.get(params, cert=self.cert) response_body = response.json() except Exception as e: if response.status_code in (503, 504): numtries += 1 maxr = config.getint('tenant', 'max_retries') if numtries >= maxr: logger.error( f"tenant cannot establish connection to agent on {self.agent_ip} with port {self.agent_port}" ) sys.exit() retry = config.getfloat('tenant', 'retry_interval') logger.info( f"tenant connection to agent at {self.agent_ip} refused {numtries}/{maxr} times, trying again in {retry} seconds..." ) time.sleep(retry) continue raise e break try: if response is not None and response.status_code != 200: raise UserError( "Status command response: %d Unexpected response from Cloud Agent." % response.status) if "results" not in response_body: raise UserError( "Error: unexpected http response body from Cloud Agent: %s" % str(response.status)) quote = response_body["results"]["quote"] logger.debug(f"agent_quote received quote: {quote}") public_key = response_body["results"]["pubkey"] logger.debug(f"agent_quote received public key: {public_key}") # Ensure hash_alg is in accept_tpm_hash_algs list hash_alg = response_body["results"]["hash_alg"] logger.debug(f"agent_quote received hash algorithm: {hash_alg}") if not algorithms.is_accepted( hash_alg, config.get('tenant', 'accept_tpm_hash_algs').split(',')): raise UserError( "TPM Quote is using an unaccepted hash algorithm: %s" % hash_alg) # Ensure enc_alg is in accept_tpm_encryption_algs list enc_alg = response_body["results"]["enc_alg"] logger.debug( f"agent_quote received encryption algorithm: {enc_alg}") if not algorithms.is_accepted( enc_alg, config.get('tenant', 'accept_tpm_encryption_algs').split(',')): raise UserError( "TPM Quote is using an unaccepted encryption algorithm: %s" % enc_alg) # Ensure sign_alg is in accept_tpm_encryption_algs list sign_alg = response_body["results"]["sign_alg"] logger.debug(f"agent_quote received signing algorithm: {sign_alg}") if not algorithms.is_accepted( sign_alg, config.get('tenant', 'accept_tpm_signing_algs').split(',')): raise UserError( "TPM Quote is using an unaccepted signing algorithm: %s" % sign_alg) if not self.validate_tpm_quote(public_key, quote, hash_alg): raise UserError( "TPM Quote from cloud agent is invalid for nonce: %s" % self.nonce) logger.info(f"Quote from {self.agent_ip} validated") # encrypt U with the public key encrypted_U = crypto.rsa_encrypt( crypto.rsa_import_pubkey(public_key), self.U) b64_encrypted_u = base64.b64encode(encrypted_U) logger.debug("b64_encrypted_u: " + b64_encrypted_u.decode('utf-8')) data = { 'encrypted_key': b64_encrypted_u, 'auth_tag': self.auth_tag } if self.payload is not None: data['payload'] = self.payload u_json_message = json.dumps(data) # post encrypted U back to CloudAgent params = '/keys/ukey' cloudagent_base_url = (f'{self.agent_ip}:{self.agent_port}') post_ukey = RequestsClient(cloudagent_base_url, tls_enabled=False) response = post_ukey.post(params, data=u_json_message) if response.status_code == 503: logger.error( f"Cannot connect to Agent at {self.agent_ip} with Port {self.agent_port}. Connection refused." ) sys.exit() elif response.status_code == 504: logger.error( f"Verifier at {self.verifier_ip} with Port {self.verifier_port} timed out." ) sys.exit() if response.status_code != 200: keylime_logging.log_http_response(logger, logging.ERROR, response_body) raise UserError( "Posting of Encrypted U to the Cloud Agent failed with response code %d" % response.status) except Exception as e: self.do_cvstop() raise e def do_verify(self): """ Perform verify using a random generated challenge """ challenge = TPM_Utilities.random_password(20) numtries = 0 while True: try: cloudagent_base_url = (f'{self.agent_ip}:{self.agent_port}') do_verify = RequestsClient(cloudagent_base_url, tls_enabled=False) response = do_verify.get( (f'/keys/verify?challenge={challenge}'), cert=self.cert, verify=False) except Exception as e: if response.status_code in (503, 504): numtries += 1 maxr = config.getint('tenant', 'max_retries') if numtries >= maxr: logger.error( f"Cannot establish connection to agent on {self.agent_ip} with port {self.agent_port}" ) sys.exit() retry = config.getfloat('tenant', 'retry_interval') logger.info( f"Verifier connection to agent at {self.agent_ip} refused {numtries}/{maxr} times, trying again in {retry} seconds..." ) time.sleep(retry) continue raise e response_body = response.json() if response.status_code == 200: if "results" not in response_body or 'hmac' not in response_body[ 'results']: logger.critical( f"Error: unexpected http response body from Cloud Agent: {response.status_code}" ) break mac = response_body['results']['hmac'] ex_mac = crypto.do_hmac(self.K, challenge) if mac == ex_mac: logger.info("Key derivation successful") else: logger.error("Key derivation failed") else: keylime_logging.log_http_response(logger, logging.ERROR, response_body) retry = config.getfloat('tenant', 'retry_interval') logger.warning( f"Key derivation not yet complete...trying again in {retry} seconds...Ctrl-C to stop" ) time.sleep(retry) continue break
def get_tpm_instance(): global GLOBAL_TPM_INSTANCE if GLOBAL_TPM_INSTANCE is None: GLOBAL_TPM_INSTANCE = tpm() return GLOBAL_TPM_INSTANCE
def test_010_reg_agent_post(self): """Test registrar's POST /agents/{UUID} Interface""" global keyblob, vtpm, tpm_instance, ek_tpm, aik_tpm contact_ip = "127.0.0.1" contact_port = 9002 tpm_instance = tpm_main.tpm() # Change CWD for TPM-related operations cwd = os.getcwd() fs_util.ch_dir(config.WORK_DIR) _ = secure_mount.mount() # Create a mTLS cert for testing global mtls_cert rsa_key = crypto.rsa_generate(2048) valid_util = datetime.datetime.utcnow() + datetime.timedelta( days=(360 * 5)) mtls_cert = crypto.generate_selfsigned_cert( "TEST_CERT", rsa_key, valid_util).public_bytes(serialization.Encoding.PEM) # Initialize the TPM with AIK (ekcert, ek_tpm, aik_tpm) = tpm_instance.tpm_init( self_activate=False, config_pw=config.get('cloud_agent', 'tpm_ownerpassword')) vtpm = tpm_instance.is_vtpm() # Handle virtualized and emulated TPMs if ekcert is None: if vtpm: ekcert = 'virtual' elif tpm_instance.is_emulator(): ekcert = 'emulator' # Get back to our original CWD fs_util.ch_dir(cwd) data = { 'ekcert': ekcert, 'aik_tpm': aik_tpm, 'ip': contact_ip, 'port': contact_port, 'mtls_cert': mtls_cert } if ekcert is None or ekcert == 'emulator': data['ek_tpm'] = ek_tpm test_010_reg_agent_post = RequestsClient( tenant_templ.registrar_base_url, tls_enabled=False) response = test_010_reg_agent_post.post( f'/v{self.api_version}/agents/{tenant_templ.agent_uuid}', data=json.dumps(data), cert="", verify=False) self.assertEqual(response.status_code, 200, "Non-successful Registrar agent Add return code!") json_response = response.json() # Ensure response is well-formed self.assertIn("results", json_response, "Malformed response body!") self.assertIn("blob", json_response["results"], "Malformed response body!") keyblob = json_response["results"]["blob"] self.assertIsNotNone(keyblob, "Malformed response body!")
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 secure_mount.mount() # 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) 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()