def convert_crl_to_pem(derfile, pemfile): if config.get('general', 'ca_implementation') == 'openssl': with open(pemfile, 'w', encoding="utf-8") as f: f.write("") else: cmd = ('openssl', 'crl', '-in', derfile, '-inform', 'der', '-out', pemfile) cmd_exec.run(cmd)
def convert_crl_to_pem(derfile, pemfile): if config.get('general', 'ca_implementation') == 'openssl': with open(pemfile, 'w') as f: f.write("") else: cmd_exec.run("openssl crl -in %s -inform der -out %s" % (derfile, pemfile), lock=False)
async def execute(revocation): json_meta = json.loads(revocation['meta_data']) serial = json_meta['cert_serial'] subject = json_meta['subject'] if revocation.get( 'type', None) != 'revocation' or serial is None or subject is None: logger.error("Unsupported revocation message: %s" % revocation) # import the crl into NSS secdir = secure_mount.mount() logger.info("loading updated CRL from %s/unzipped/cacrl.der into NSS" % secdir) cmd_exec.run("crlutil -I -i %s/unzipped/cacrl.der -d sql:/etc/ipsec.d" % secdir, lock=False) # need to find any sa's that were established with that cert subject name output = cmd_exec.run("ipsec whack --trafficstatus", lock=False, raiseOnError=True)['retout'] deletelist = set() id = "" for line in output: line = line.strip() try: idstart = line.index("id='") + 4 idend = line[idstart:].index('\'') id = line[idstart:idstart + idend] privatestart = line.index("private#") + 8 privateend = line[privatestart:].index("/") ip = line[privatestart:privatestart + privateend] except ValueError: # weirdly formatted line continue # kill all the commas id = id.replace(",", "") cursubj = {} for token in id.split(): cur = token.split('=') cursubj[cur[0]] = cur[1] cert = {} for token in subject[1:].split("/"): cur = token.split('=') cert[cur[0]] = cur[1] if cert == cursubj: deletelist.add(ip) for todelete in deletelist: logger.info("deleting IPsec sa with %s" % todelete) cmd_exec.run("ipsec whack --crash %s" % todelete, raiseOnError=False, lock=False)
def __run(self, cmd, expectedcode=tpm_abstract.AbstractTPM.EXIT_SUCESS, raiseOnError=True, lock=True, outputpaths=None): env = _get_cmd_env() # Backwards compat with string input (force all to be dict) if isinstance(outputpaths, str): outputpaths = [outputpaths] # Handle stubbing the TPM out fprt = tpm1.__fingerprint(cmd) if config.STUB_TPM and config.TPM_CANNED_VALUES is not None: stub = _stub_command(fprt, lock, outputpaths) if stub: return stub numtries = 0 while True: if lock: with self.tpmutilLock: retDict = cmd_exec.run( cmd=cmd, expectedcode=expectedcode, raiseOnError=False, outputpaths=outputpaths, env=env) else: retDict = cmd_exec.run( cmd=cmd, expectedcode=expectedcode, raiseOnError=False, outputpaths=outputpaths, env=env) code = retDict['code'] retout = retDict['retout'] # keep trying to communicate with TPM if there was an I/O error if code == tpm_abstract.AbstractTPM.TPM_IO_ERR: numtries += 1 maxr = config.getint('cloud_agent', 'max_retries') if numtries >= maxr: logger.error("TPM appears to be in use by another application. Keylime is incompatible with other TPM TSS applications like trousers/tpm-tools. Please uninstall or disable.") break retry = config.getfloat('cloud_agent', 'retry_interval') logger.info("Failed to call TPM %d/%d times, trying again in %f seconds..." % (numtries, maxr, retry)) time.sleep(retry) continue break # Don't bother continuing if TPM call failed and we're raising on error if code != expectedcode and raiseOnError: raise Exception("Command: %s returned %d, expected %d, output %s" % (cmd, code, expectedcode, retout)) # Metric output if lock or self.tpmutilLock.locked(): _output_metrics(fprt, cmd, retDict, outputpaths) return retDict
def umount(): """Umount all the devices mounted by Keylime.""" # Make sure we leave tmpfs dir empty even if we did not mount it or # if we cannot unmount it. Ignore errors while deleting. The deletion # of the 'secure' directory will result in an error since it's a mount point. # Also, with config.MOUNT_SECURE being False we remove the directory secdir = get_secdir() if not config.MOUNT_SECURE or check_mounted(secdir): shutil.rmtree(secdir, ignore_errors=True) while _MOUNTED: directory = _MOUNTED.pop() logger.info("Unmounting %s", directory) if check_mounted(directory): cmd = ("umount", directory) ret = cmd_exec.run(cmd, raiseOnError=False) if ret["code"] != 0: logger.error( "%s cannot be umounted. A running process can be keeping it bussy: %s", directory, str(ret["reterr"]), ) else: logger.warning("%s already unmounted by another process", directory)
def check_mounted(secdir): whatsmounted = cmd_exec.run("mount")['retout'] whatsmounted_converted = config.convert(whatsmounted) for line in whatsmounted_converted: tokens = line.split() tmpfs = False if len(tokens) < 3: continue if tokens[0] == 'tmpfs': tmpfs = True if tokens[2] == secdir: if not tmpfs: logger.error( "secure storage location %s already mounted " "on wrong file system type: %s. Unmount to" "continue.", secdir, tokens[0]) raise Exception( f"secure storage location {secdir} already mounted on " f"wrong file system type: {tokens[0]}. Unmount to " f"continue.") logger.debug( "secure storage location %s already mounted on tmpfs" % secdir) return True logger.debug("secure storage location %s not mounted " % secdir) return False
def check_expiration(): logger.info("checking CRL for expiration every hour") while True: # pylint: disable=R1702 try: if (os.path.exists('cacrl.der') and os.stat('cacrl.der').st_size): cmd = ('openssl', 'crl', '-inform', 'der', '-in', 'cacrl.der', '-text', '-noout') retout = cmd_exec.run(cmd)['retout'] for line in retout: line = line.strip() if line.startswith(b"Next Update:"): expire = datetime.datetime.strptime( line[13:].decode('utf-8'), "%b %d %H:%M:%S %Y %Z") # check expiration within 6 hours in1hour = datetime.datetime.utcnow() + datetime.timedelta(hours=6) if expire <= in1hour: logger.info( "Certificate to expire soon %s, re-issuing", expire) cmd_regencrl(workingdir) # check a little less than every hour time.sleep(3540) except KeyboardInterrupt: logger.info("TERM Signal received, shutting down...") # server.shutdown() break
def check_expiration(): logger.info("checking CRL for expiration every hour") while True: try: if os.path.exists('cacrl.der'): retout = cmd_exec.run( "openssl crl -inform der -in cacrl.der -text -noout", lock=False)['retout'] for line in retout: line = line.strip() if line.startswith(b"Next Update:"): expire = datetime.datetime.strptime( line[13:].decode('utf-8'), "%b %d %H:%M:%S %Y %Z") # check expiration within 6 hours in1hour = datetime.datetime.utcnow( ) + datetime.timedelta(hours=6) if expire <= in1hour: logger.info( "Certificate to expire soon %s, re-issuing" % expire) cmd_regencrl(workingdir) # check a little less than every hour time.sleep(3540) except KeyboardInterrupt: logger.info("TERM Signal received, shutting down...") #server.shutdown() break
def mount(): secdir = get_secdir() if not config.MOUNT_SECURE: if not os.path.isdir(secdir): os.makedirs(secdir) return secdir if not check_mounted(secdir): # ok now we know it isn't already mounted, go ahead and create and mount if not os.path.exists(secdir): os.makedirs(secdir, 0o700) size = config.get("cloud_agent", "secure_size") logger.info("mounting secure storage location %s on tmpfs", secdir) cmd = ("mount", "-t", "tmpfs", "-o", f"size={size},mode=0700", "tmpfs", secdir) cmd_exec.run(cmd) _MOUNTED.append(secdir) return secdir
def mount(): secdir = os.path.join(config.WORK_DIR, "secure") if not config.MOUNT_SECURE: secdir = os.path.join(config.WORK_DIR, "tmpfs-dev") if not os.path.isdir(secdir): os.makedirs(secdir) return secdir if not check_mounted(secdir): # ok now we know it isn't already mounted, go ahead and create and mount if not os.path.exists(secdir): os.makedirs(secdir, 0o700) size = config.get('cloud_agent', 'secure_size') logger.info("mounting secure storage location %s on tmpfs", secdir) cmd = ('mount', '-t', 'tmpfs', '-o', f'size={size},mode=0700', 'tmpfs', secdir) cmd_exec.run(cmd) _MOUNTED.append(secdir) return secdir
def mount(): secdir = config.WORK_DIR + "/secure" if not config.MOUNT_SECURE: secdir = config.WORK_DIR + "/tmpfs-dev" if not os.path.isdir(secdir): os.makedirs(secdir) return secdir if not check_mounted(secdir): # ok now we know it isn't already mounted, go ahead and create and mount if not os.path.exists(secdir): os.makedirs(secdir, 0o700) config.chownroot(secdir, logger) size = config.get('cloud_agent', 'secure_size') logger.info("mounting secure storage location %s on tmpfs" % secdir) cmd = ('mount', '-t', 'tmpfs', '-o', 'size=%s,mode=0700' % size, 'tmpfs', secdir) cmd_exec.run(cmd, lock=False) return secdir
async def execute(revocation): serial = revocation.get("metadata",{}).get("cert_serial",None) if revocation.get('type',None) != 'revocation' or serial is None: logger.error("Unsupported revocation message: %s"%revocation) # load up the ca cert secdir = secure_mount.mount() ca = X509.load_cert('%s/unzipped/cacert.crt'%secdir) # need to find any sa's that were established with that cert serial output = cmd_exec.run("racoonctl show-sa ipsec",lock=False,raiseOnError=True)['retout'] deletelist=set() for line in output: if not line.startswith(b"\t"): certder = cmd_exec.run("racoonctl get-cert inet %s"%line.strip(),raiseOnError=False,lock=False)['retout'] if len(certder)==0: continue; certobj = X509.load_cert_der_string(b''.join(certder)) # check that CA is the same. the strip indexing bit is to remove the stuff around it 'keyid:THEACTUALKEYID\n' if ca.get_ext('subjectKeyIdentifier').get_value() != certobj.get_ext('authorityKeyIdentifier').get_value().strip()[6:]: continue if certobj.get_serial_number() == serial: deletelist.add(line.strip()) for todelete in deletelist: logger.info("deleting IPsec sa between %s"%todelete) cmd_exec.run("racoonctl delete-sa isakmp inet %s"%todelete,lock=False) tokens = todelete.split() cmd_exec.run("racoonctl delete-sa isakmp inet %s %s"%(tokens[1],tokens[0]),lock=False)
def umount(): """Umount all the devices mounted by Keylime.""" while _MOUNTED: directory = _MOUNTED.pop() logger.info("Unmounting %s", directory) if check_mounted(directory): cmd = ("umount", directory) ret = cmd_exec.run(cmd, raiseOnError=False) if ret["code"] != 0: logger.error( "%s cannot be umounted. " "A running process can be keeping it bussy: %s", directory, str(ret["reterr"])) else: logger.warning("%s already unmounted by another process", directory)
async def execute(revocation): json_meta = json.loads(revocation["meta_data"]) serial = json_meta["cert_serial"] if revocation.get("type", None) != "revocation" or serial is None: logger.error("Unsupported revocation message: %s" % revocation) # load up the ca cert secdir = secure_mount.mount() ca = ca_util.load_cert_by_path(f"{secdir}/unzipped/cacert.crt") # need to find any sa's that were established with that cert serial cmd = ("racoonctl", "show-sa", "ipsec") output = cmd_exec.run(cmd, raiseOnError=True)["retout"] deletelist = set() for line in output: if not line.startswith(b"\t"): cmd = ("racoonctl", "get-cert", "inet", line.strip()) certder = cmd_exec.run(cmd, raiseOnError=False)["retout"] if len(certder) == 0: continue try: certobj = x509.load_der_x509_certificate( data=b"".join(certder), backend=default_backend(), ) # check that CA is the same. ca_keyid = ca.extensions.get_extension_for_oid( x509.oid.ExtensionOID.SUBJECT_KEY_IDENTIFIER ).value.digest cert_authkeyid = certobj.extensions.get_extension_for_oid( x509.oid.ExtensionOID.AUTHORITY_KEY_IDENTIFIER ).value.key_identifier except (ValueError, x509.extensions.ExtensionNotFound): continue if ca_keyid != cert_authkeyid: continue if certobj.serial_number == serial: deletelist.add(line.strip()) for todelete in deletelist: logger.info("deleting IPsec sa between %s" % todelete) cmd = ("racoonctl", "delete-sa", "isakmp", "inet", todelete) cmd_exec.run(cmd) tokens = todelete.split() cmd = ("racoonctl", "delete-sa", "isakmp", "inet", tokens[1], tokens[0]) cmd_exec.run(cmd)
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 __run(self, cmd, expectedcode=tpm_abstract.AbstractTPM.EXIT_SUCESS, raiseOnError=True, lock=True, outputpaths=None): env = os.environ.copy() env['TPM_SERVER_PORT']='9998' env['TPM_SERVER_NAME']='localhost' env['PATH']=env['PATH']+":%s"%common.TPM_TOOLS_PATH # Backwards compat with string input (force all to be dict) if isinstance(outputpaths, str): outputpaths = [outputpaths] # Handle stubbing the TPM out fprt = tpm1.__fingerprint(cmd) if common.STUB_TPM and common.TPM_CANNED_VALUES is not None: # Use canned values for stubbing yamlIn = common.TPM_CANNED_VALUES if fprt in yamlIn: # The value we're looking for has been canned! thisTiming = yamlIn[fprt]['timing'] thisRetout = yamlIn[fprt]['retout'] thisCode = yamlIn[fprt]['code'] thisFileout = yamlIn[fprt]['fileout'] fileoutEncoded = {} # Decode files that are supplied if outputpaths is not None and len(outputpaths) == 1: if thisFileout != '': fileoutEncoded[outputpaths[0]] = zlib.decompress(base64.b64decode(thisFileout)) logger.debug("TPM call '%s' was stubbed out, with a simulated delay of %f sec"%(fprt,thisTiming)) time.sleep(thisTiming) # Package for return returnDict = { 'retout': thisRetout, 'code': thisCode, 'fileouts': fileoutEncoded, 'timing': thisTiming, } return returnDict elif not lock: # non-lock calls don't go to the TPM (just let it pass through) pass else: # Our command hasn't been canned! raise Exception("Command %s not found in canned YAML!"%(fprt)) numtries = 0 while True: if lock: with self.tpmutilLock: retDict = cmd_exec.run(cmd=cmd,expectedcode=expectedcode,raiseOnError=False,lock=lock,outputpaths=outputpaths,env=env) else: retDict = cmd_exec.run(cmd=cmd,expectedcode=expectedcode,raiseOnError=False,lock=lock,outputpaths=outputpaths,env=env) t0 = retDict['timing']['t0'] t1 = retDict['timing']['t1'] code = retDict['code'] retout = retDict['retout'] fileouts = retDict['fileouts'] # keep trying to communicate with TPM if there was an I/O error if code== tpm_abstract.AbstractTPM.TPM_IO_ERR: numtries+=1 maxr = self.config.getint('cloud_agent','max_retries') if numtries >= maxr: logger.error("TPM appears to be in use by another application. Keylime is incompatible with other TPM TSS applications like trousers/tpm-tools. Please uninstall or disable.") break retry = self.config.getfloat('cloud_agent','retry_interval') logger.info("Failed to call TPM %d/%d times, trying again in %f seconds..."%(numtries,maxr,retry)) time.sleep(retry) continue else: break # Don't bother continuing if TPM call failed and we're raising on error if code!=expectedcode and raiseOnError: raise Exception("Command: %s returned %d, expected %d, output %s"%(cmd,code,expectedcode,retout)) # Metric output if lock or self.tpmutilLock.locked(): pad = "" if len(fprt) < 8: pad += "\t" if len(fprt) < 16: pad += "\t" if len(fprt) < 24: pad += "\t" filelen = 0 if fileouts is not None: filelen = len(fileouts) # Print out benchmarking information for TPM (if requested) #print "\033[95mTIMING: %s%s\t:%f\toutlines:%d\tfilelines:%d\t%s\033[0m" % (fprt,pad,t1-t0,len(retout),filelen,cmd) if common.TPM_BENCHMARK_PATH is not None: with open(common.TPM_BENCHMARK_PATH, "ab") as bench: bench.write("TIMING: %s%s\ttime:%f\toutlines:%d\tfilecount:%d\t%s\n" % (fprt,pad,t1-t0,len(retout),filelen,cmd)) # Print out YAML canned values (if requested) # NOTE: resulting file will be missing the surrounding braces! (must add '{' and '}' for reading) if common.TPM_CANNED_VALUES_PATH is not None: with open(common.TPM_CANNED_VALUES_PATH, "ab") as can: fileoutEncoded = "" if outputpaths is not None and len(outputpaths) > 0: if len(fileouts) == 1 and len(outputpaths) == 1: fileoutEncoded = zlib.compress(base64.b64encode(iter(fileouts.values()).next())) else: raise Exception("Command %s is using multiple files unexpectedly!"%(fprt)) # tpm_cexec will need to know the nonce nonce = "" match = re.search("-nonce ([\w]+)", cmd) if match: nonce = match.group(1) yamlObj = {'type':fprt,'retout':retout,'fileout':fileoutEncoded,'cmd':cmd,'timing':t1-t0,'code':code,'nonce':nonce} can.write("\"%s\": %s,\n"%(fprt,yaml.dump(yamlObj,indent=4,sort_keys=True, Dumper=SafeDumper))) return retDict
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 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()