def gencrl(serials,cert,ca_pk): request = {"certificate": cert, "serialNumber": serials, "issuingKey": ca_pk, "expireTime": "" } secdir = secure_mount.mount() try: # need to temporarily write out the private key with no password # to tmpfs priv_key = os.path.abspath("%s/ca-key.pem"%secdir) with open(priv_key,'w') as f: f.write(ca_pk) cmdline = " -ca-key %s -ca cacert.crt"%(priv_key) start_cfssl(cmdline) body = post_cfssl('api/v1/cfssl/gencrl',request) finally: stop_cfssl() # replace with srm os.remove('%s/ca-key.pem'%secdir) if body['success']: retval = base64.b64decode(body['result']) else: raise Exception("Unable to create crl for cert serials %s. Error: %s"%(serials,body['errors'])) return retval
def worker(): def print_notification(revocation): logger.warning("Received revocation: %s", revocation) keypath = os.path.join(secure_mount.mount(), "unzipped", "RevocationNotifier-cert.crt") await_notifications(print_notification, revocation_cert_path=keypath)
def __init__(self, server_address, RequestHandlerClass, agent_uuid): """Constructor overridden to provide ability to pass configuration arguments to the server""" secdir = secure_mount.mount() keyname = "%s/%s" % (secdir, config.get('cloud_agent', 'rsa_keyname')) # read or generate the key depending on configuration if os.path.isfile(keyname): # read in private key logger.debug("Using existing key in %s" % keyname) f = open(keyname, "rb") rsa_key = crypto.rsa_import_privkey(f.read()) else: logger.debug("key not found, generating a new one") rsa_key = crypto.rsa_generate(2048) with open(keyname, "wb") as f: f.write(crypto.rsa_export_privkey(rsa_key)) self.rsaprivatekey = rsa_key self.rsapublickey_exportable = crypto.rsa_export_pubkey( self.rsaprivatekey) #attempt to get a U value from the TPM NVRAM nvram_u = tpm.read_key_nvram() if nvram_u is not None: logger.info("Existing U loaded from TPM NVRAM") self.add_U(nvram_u) http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass) self.enc_keyname = config.get('cloud_agent', 'enc_keyname') self.agent_uuid = agent_uuid
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 test_010_reg_agent_post(self): """Test registrar's POST /v2/agents/{UUID} Interface""" global keyblob, aik, vtpm, ek # Change CWD for TPM-related operations cwd = os.getcwd() common.ch_dir(common.WORK_DIR, None) secdir = secure_mount.mount() # Initialize the TPM with AIK (ek, ekcert, aik, ek_tpm, aik_name) = tpm.tpm_init(self_activate=False, config_pw=config.get('cloud_agent', 'tpm_ownerpassword')) vtpm = tpm.is_vtpm() # Seed RNG (root only) if common.REQUIRE_ROOT: tpm.init_system_rand() # Handle virtualized and emulated TPMs if ekcert is None: if vtpm: ekcert = 'virtual' elif tpm.is_emulator(): ekcert = 'emulator' # Get back to our original CWD common.ch_dir(cwd, None) data = { 'ek': ek, 'ekcert': ekcert, 'aik': aik, 'aik_name': aik_name, 'ek_tpm': ek_tpm, 'tpm_version': tpm.get_tpm_version(), } v_json_message = json.dumps(data) params = f"/v{self.api_version}/agents/{tenant_templ.agent_uuid}" response = httpclient_requests.request( "POST", "%s" % tenant_templ.registrar_ip, tenant_templ.registrar_boot_port, params=params, data=v_json_message, context=None) self.assertEqual(response.status, 200, "Non-successful Registrar agent Add return code!") json_response = json.loads(response.read().decode()) # 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 worker(): def print_notification(revocation): logger.warning("Received revocation: %s" % revocation) keypath = '%s/unzipped/RevocationNotifier-cert.crt' % ( secure_mount.mount()) await_notifications(print_notification, revocation_cert_path=keypath)
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 test_010_reg_agent_post(self): """Test registrar's POST /v2/agents/{UUID} Interface""" global keyblob, aik, vtpm, ek # Change CWD for TPM-related operations cwd = os.getcwd() config.ch_dir(config.WORK_DIR, None) _ = secure_mount.mount() # Initialize the TPM with AIK (ek, ekcert, aik, ek_tpm, aik_name) = tpm.tpm_init(self_activate=False, config_pw=config.get('cloud_agent', 'tpm_ownerpassword')) vtpm = tpm.is_vtpm() # Seed RNG (root only) if config.REQUIRE_ROOT: tpm.init_system_rand() # Handle virtualized and emulated TPMs if ekcert is None: if vtpm: ekcert = 'virtual' elif tpm.is_emulator(): ekcert = 'emulator' # Get back to our original CWD config.ch_dir(cwd, None) data = { 'ek': ek, 'ekcert': ekcert, 'aik': aik, 'aik_name': aik_name, 'ek_tpm': ek_tpm, 'tpm_version': tpm.VERSION, } 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 test_check_mount_no_secure(self, config_mock, _logger_mock): """Test when mounted outside tmpfs.""" config_mock.MOUNT_SECURE = False with tempfile.TemporaryDirectory() as tmpdirname: config_mock.WORK_DIR = tmpdirname self.assertEqual(secure_mount.mount(), f"{tmpdirname}/tmpfs-dev") # pylint: disable=protected-access self.assertEqual(secure_mount._MOUNTED, [])
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() config.ch_dir(config.WORK_DIR, None) _ = secure_mount.mount() # 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 config.ch_dir(cwd, None) data = { 'ekcert': ekcert, 'aik_tpm': aik_tpm, 'ip': contact_ip, 'port': contact_port } 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 test_check_mount_secure_already_mounted(self, config_mock, check_mounted_mock): """Test when mounting in tmpfs but is already present.""" config_mock.MOUNT_SECURE = True check_mounted_mock.return_value = True with tempfile.TemporaryDirectory() as tmpdirname: config_mock.WORK_DIR = tmpdirname self.assertEqual(secure_mount.mount(), f"{tmpdirname}/secure") # pylint: disable=protected-access self.assertEqual(secure_mount._MOUNTED, [])
def execute(json_revocation): if json_revocation['type'] != 'revocation': return secdir = secure_mount.mount() cert_path = config.get('cloud_agent', 'revocation_cert') if cert_path == "default": cert_path = os.path.join(secdir, "unzipped", "RevocationNotifier-cert.crt") else: # if it is a relative, convert to absolute in work_dir if cert_path[0] != '/': cert_path = os.path.abspath( os.path.join(common.WORK_DIR, cert_path)) if not os.path.exists(cert_path): raise Exception( f"revocation_cert {os.path.abspath(cert_path)} not found") # get the updated CRL dist_path = ca_util.get_crl_distpoint(cert_path) with open(os.path.join(secdir, "unzipped", "cacrl.der"), "rb") as f: oldcrl = f.read() updated = False for _ in range(10): logger.debug("Getting updated CRL from %s", dist_path) response = tornado_requests.request("GET", dist_path, None, None, None) if response.status_code != 200: logger.warning("Unable to get updated CRL from %s. Code %d", dist_path, response.status_code) time.sleep(1) continue if response.body == oldcrl: logger.warning("CRL not yet updated, trying again in 1 second...") time.sleep(1) continue # write out the updated CRL logger.debug("Updating CRL in %s/unzipped/cacrl.der", secdir) with open(os.path.join(secdir, "unzipped", "cacrl.der"), "wb") as f: f.write(response.body) ca_util.convert_crl_to_pem( os.path.join(secdir, "unzipped", "cacrl.der"), os.path.join(secdir, "unzipped", "cacrl.pem")) updated = True break if not updated: logger.error( "Unable to load new CRL from %s after receiving notice of a revocation", dist_path)
def test_check_mount_secure_already_created( self, _cmd_exec_mock, exists_mock, config_mock, check_mounted_mock, _logger_mock ): """Test when mounting in tmpfs but the mount point is present.""" exists_mock.return_value = True config_mock.MOUNT_SECURE = True check_mounted_mock.return_value = False with tempfile.TemporaryDirectory() as tmpdirname: config_mock.WORK_DIR = tmpdirname self.assertEqual(secure_mount.mount(), f"{tmpdirname}/secure") # pylint: disable=protected-access self.assertEqual(secure_mount._MOUNTED, [f"{tmpdirname}/secure"])
def test_010_reg_agent_post(self): """Test registrar's POST /agents/{UUID} Interface""" global keyblob, 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") ) # Handle emulated TPMs if ekcert is None: if 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!")
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 __init__(self, server_address, RequestHandlerClass, agent_uuid): """Constructor overridden to provide ability to pass configuration arguments to the server""" secdir = secure_mount.mount() keyname = os.path.join(secdir, config.get('cloud_agent', 'rsa_keyname')) certname = os.path.join(secdir, config.get('cloud_agent', 'mtls_cert')) # read or generate the key depending on configuration if os.path.isfile(keyname): # read in private key logger.debug("Using existing key in %s", keyname) f = open(keyname, "rb") rsa_key = crypto.rsa_import_privkey(f.read()) else: logger.debug("key not found, generating a new one") rsa_key = crypto.rsa_generate(2048) with open(keyname, "wb") as f: f.write(crypto.rsa_export_privkey(rsa_key)) self.rsakey_path = keyname self.rsaprivatekey = rsa_key self.rsapublickey_exportable = crypto.rsa_export_pubkey( self.rsaprivatekey) if os.path.isfile(certname): logger.debug("Using existing mTLS cert in %s", certname) with open(certname, "rb") as f: mtls_cert = x509.load_pem_x509_certificate(f.read()) else: logger.debug("No mTLS certificate found generating a new one") with open(certname, "wb") as f: # By default generate a TLS certificate valid for 5 years valid_util = datetime.datetime.utcnow() + datetime.timedelta( days=(360 * 5)) mtls_cert = crypto.generate_selfsigned_cert( agent_uuid, rsa_key, valid_util) f.write(mtls_cert.public_bytes(serialization.Encoding.PEM)) self.mtls_cert_path = certname self.mtls_cert = mtls_cert # attempt to get a U value from the TPM NVRAM nvram_u = tpm_instance.read_key_nvram() if nvram_u is not None: logger.info("Existing U loaded from TPM NVRAM") self.add_U(nvram_u) http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass) self.enc_keyname = config.get('cloud_agent', 'enc_keyname') self.agent_uuid = agent_uuid
def activate_identity(self, keyblob): owner_pw = self.get_tpm_metadata('owner_pw') keyhandle = self.get_tpm_metadata('aik_handle') keyblobFile = None secpath = None try: # write out key blob kfd, ktemp = tempfile.mkstemp() keyblobFile = open(ktemp, "wb") keyblobFile.write(base64.b64decode(keyblob)) keyblobFile.close() os.close(kfd) # ok lets write out the key now secdir = secure_mount.mount( ) # confirm that storage is still securely mounted secfd, secpath = tempfile.mkstemp(dir=secdir) command = [ "activateidentity", "-hk", keyhandle, "-pwdo", owner_pw, "-pwdk", self.get_tpm_metadata('aik_pw'), "-if", keyblobFile.name, "-ok", secpath ] retDict = self.__run(command, outputpaths=secpath) fileout = retDict['fileouts'][secpath] logger.info("AIK activated.") key = base64.b64encode(fileout) os.close(secfd) os.remove(secpath) except Exception as e: logger.error("Error decrypting AIK: " + str(e)) logger.exception(e) return None finally: if keyblobFile is not None: os.remove(keyblobFile.name) if secpath is not None and os.path.exists(secpath): os.remove(secpath) return key
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 = [f"revocation_actions.{i}" % i for i in actionlist] # load actions from unzipped secdir = secure_mount.mount() action_list_path = os.path.join(secdir, "unzipped/action_list") if os.path.exists(action_list_path): with open(action_list_path, encoding="utf-8") as f: actionlisttxt = f.read() if actionlisttxt.strip() != "": localactions = actionlisttxt.strip().split(",") for action in localactions: if not action.startswith("local_action_"): logger.warning( "Invalid local action: %s. Must start with local_action_", action) else: actionlist.append(action) uzpath = os.path.join(secdir, "unzipped") 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") loop = asyncio.new_event_loop() loop.run_until_complete(execute(revocation)) except Exception as e: logger.warning( "Exception during execution of revocation action %s: %s", action, e)
def revocation_listener(): """ This configures and starts the revocation listener. It is designed to be started in a separate process. """ if config.has_option("cloud_agent", "listen_notifications"): if not config.getboolean("cloud_agent", "listen_notifications"): return # keep old typo "listen_notfications" around for a few versions if config.has_option("cloud_agent", "listen_notfications"): logger.warning( 'Option typo "listen_notfications" is deprecated. Please use "listen_notifications" instead.' ) if not config.getboolean("cloud_agent", "listen_notfications"): return secdir = secure_mount.mount() cert_path = config.get("cloud_agent", "revocation_cert") if cert_path == "default": cert_path = os.path.join(secdir, "unzipped/RevocationNotifier-cert.crt") elif cert_path[0] != "/": # if it is a relative, convert to absolute in work_dir cert_path = os.path.abspath(os.path.join(config.WORK_DIR, cert_path)) 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, SystemExit): logger.info("Stopping revocation listener...")
def gencrl(serials, cert, ca_pk): request = { "certificate": cert, "serialNumber": serials, "issuingKey": ca_pk, "expireTime": "" } secdir = secure_mount.mount() try: # need to temporarily write out the private key with no password # to tmpfs privkey_path = os.path.abspath(f"{secdir}/ca-key.pem") with open(privkey_path, 'w', encoding="utf-8") as f: f.write(ca_pk) cacert_path = os.path.abspath(f"{secdir}/cacert.crt") with open(cacert_path, 'w', encoding="utf-8") as f: f.write(cert) cmdline = f" -ca-key {privkey_path} -ca {cacert_path}" start_cfssl(cmdline) body = post_cfssl('api/v1/cfssl/gencrl', request) finally: stop_cfssl() # replace with srm os.remove(privkey_path) os.remove(cacert_path) if body['success']: retval = base64.b64decode(body['result']) else: raise Exception(f"Unable to create crl for cert serials {serials}. " f"Error: {body['errors']}") return retval
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 mk_signed_cert(cacert, ca_pk, name, serialnum): del serialnum csr = { "request": { "CN": name, "hosts": [ name, ], "key": { "algo": "rsa", "size": config.getint('ca', 'cert_bits') }, "names": [{ "C": config.get('ca', 'cert_country'), "L": config.get('ca', 'cert_locality'), "O": config.get('ca', 'cert_organization'), "OU": config.get('ca', 'cert_org_unit'), "ST": config.get('ca', 'cert_state') }] } } # check CRL distribution point disturl = config.get('ca', 'cert_crl_dist') if disturl == 'default': disturl = f"http://{socket.getfqdn()}:{config.CRL_PORT}/crl.der" # set up config for cfssl server cfsslconfig = { "signing": { "default": { "usages": [ "client auth", "server auth", "key agreement", "key encipherment", "signing", "digital signature", "data encipherment" ], "expiry": "8760h", "crl_url": disturl, } } } secdir = secure_mount.mount() try: # need to temporarily write out the private key with no password # to tmpfs. with os.fdopen( os.open(f"{secdir}/ca-key.pem", os.O_WRONLY | os.O_CREAT, 0o600), 'wb') as f: f.write( ca_pk.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption(), )) with open(os.path.join(secdir, 'cfsslconfig.yml'), 'w', encoding="utf-8") as f: json.dump(cfsslconfig, f) with open(f"{secdir}/cacert.crt", 'wb') as f: f.write(cacert.public_bytes(serialization.Encoding.PEM)) cmdline = "-config=%s/cfsslconfig.yml" % secdir privkey_path = os.path.abspath(f"{secdir}/ca-key.pem") cacert_path = os.path.abspath(f"{secdir}/cacert.crt") cmdline += f" -ca-key {privkey_path} -ca {cacert_path}" start_cfssl(cmdline) body = post_cfssl('api/v1/cfssl/newcert', csr) finally: stop_cfssl() os.remove('%s/ca-key.pem' % secdir) os.remove('%s/cfsslconfig.yml' % secdir) os.remove('%s/cacert.crt' % secdir) if body['success']: pk = serialization.load_pem_private_key( body['result']['private_key'].encode('utf-8'), password=None, backend=default_backend(), ) cert = x509.load_pem_x509_certificate( data=body['result']['certificate'].encode('utf-8'), backend=default_backend(), ) return cert, pk raise Exception("Unable to create cert for %s" % name)
def revocation_listener(): """ This configures and starts the revocation listener. It is designed to be started in a separate process. """ if config.has_option('cloud_agent', 'listen_notifications'): if not config.getboolean('cloud_agent', 'listen_notifications'): return # keep old typo "listen_notfications" around for a few versions if config.has_option('cloud_agent', 'listen_notfications'): logger.warning( 'Option typo "listen_notfications" is deprecated. Please use "listen_notifications" instead.' ) if not config.getboolean('cloud_agent', 'listen_notfications'): return secdir = secure_mount.mount() cert_path = config.get('cloud_agent', 'revocation_cert') if cert_path == "default": cert_path = os.path.join(secdir, "unzipped/RevocationNotifier-cert.crt") elif cert_path[0] != '/': # if it is a relative, convert to absolute in work_dir cert_path = os.path.abspath(os.path.join(config.WORK_DIR, cert_path)) # Callback function handling the revocations def perform_actions(revocation): actionlist = [] # load the actions from inside the keylime module actionlisttxt = config.get('cloud_agent', 'revocation_actions') if actionlisttxt.strip() != "": actionlist = actionlisttxt.split(',') actionlist = ["revocation_actions.%s" % i for i in actionlist] # load actions from unzipped action_list_path = os.path.join(secdir, "unzipped/action_list") if os.path.exists(action_list_path): with open(action_list_path, encoding="utf-8") as f: actionlisttxt = f.read() if actionlisttxt.strip() != "": localactions = actionlisttxt.strip().split(',') for action in localactions: if not action.startswith('local_action_'): logger.warning( "Invalid local action: %s. Must start with local_action_", action) else: actionlist.append(action) uzpath = "%s/unzipped" % secdir if uzpath not in sys.path: sys.path.append(uzpath) for action in actionlist: logger.info("Executing revocation action %s", action) try: module = importlib.import_module(action) execute = getattr(module, 'execute') asyncio.get_event_loop().run_until_complete( execute(revocation)) except Exception as e: logger.warning( "Exception during execution of revocation action %s: %s", action, e) try: while True: try: revocation_notifier.await_notifications( perform_actions, revocation_cert_path=cert_path) except Exception as e: logger.exception(e) logger.warning( "No connection to revocation server, retrying in 10s...") time.sleep(10) except (KeyboardInterrupt, SystemExit): logger.info("Stopping revocation listener...")
def main(argv=sys.argv): if os.getuid() != 0 and common.REQUIRE_ROOT: logger.critical("This process must be run as root.") return # get params for initialization registrar_ip = config.get('general', 'registrar_ip') registrar_port = config.get('general', 'registrar_port') # initialize the tmpfs partition to store keys if it isn't already available secdir = secure_mount.mount() # change dir to working dir common.ch_dir(common.WORK_DIR, logger) #initialize tpm (ek, ekcert, aik, ek_tpm, aik_name) = 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 = tpm.is_vtpm() # try to get some TPM randomness into the system entropy pool tpm.init_system_rand() if ekcert is None: if virtual_agent: ekcert = 'virtual' elif 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).hexdigest() elif agent_uuid == 'generate' or agent_uuid is None: agent_uuid = str(uuid.uuid4()) if common.DEVELOP_IN_ECLIPSE: agent_uuid = "C432FBB3-D2F1-4A97-9EF7-75BD81C866E9" if common.STUB_VTPM and common.TPM_CANNED_VALUES is not None: # Use canned values for stubbing jsonIn = common.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, tpm_version, ek, ekcert, aik, ek_tpm, aik_name) if keyblob is None: raise Exception("Registration failed") # get the ephemeral registrar key key = tpm.activate_identity(keyblob) if key is None: raise Exception("Activation failed") # tell the registrar server we know the key retval = False if virtual_agent: deepquote = tpm.create_deep_quote( hashlib.sha1(key).hexdigest(), agent_uuid + aik + ek) retval = registrar_client.doActivateVirtualAgent( registrar_ip, registrar_port, agent_uuid, deepquote) else: retval = registrar_client.doActivateAgent(registrar_ip, registrar_port, agent_uuid, key) if not retval: raise Exception("Registration failed on activate") serveraddr = ('', config.getint('general', 'cloudagent_port')) server = CloudAgentHTTPServer(serveraddr, Handler, agent_uuid) serverthread = threading.Thread(target=server.serve_forever) logger.info('Starting Cloud Agent on port %s use <Ctrl-C> to stop' % 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' % (common.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.debug("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.warn( "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.warn( "No connection to revocation server, retrying in 10s..." ) time.sleep(10) except KeyboardInterrupt: logger.info("TERM Signal received, shutting down...") tpm.flush_keys() server.shutdown() else: try: while True: time.sleep(1) except KeyboardInterrupt: logger.info("TERM Signal received, shutting down...") 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") 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_POST(self): """This method services the POST request typically from either the Tenant or the Cloud Verifier. Only tenant and cloudverifier uri's are supported. Both requests require a nonce parameter. The Cloud verifier requires an additional mask parameter. If the uri or parameters are incorrect, a 400 response is returned. """ rest_params = common.get_restful_params(self.path) if rest_params is None: common.echo_json_response(self, 405, "Not Implemented: Use /keys/ interface") return content_length = int(self.headers.get('Content-Length', 0)) if content_length <= 0: logger.warning( 'POST returning 400 response, expected content in message. url: ' + self.path) common.echo_json_response(self, 400, "expected content in message") return post_body = self.rfile.read(content_length) json_body = json.loads(post_body) b64_encrypted_key = json_body['encrypted_key'] decrypted_key = crypto.rsa_decrypt(self.server.rsaprivatekey, base64.b64decode(b64_encrypted_key)) have_derived_key = False if rest_params["keys"] == "ukey": self.server.add_U(decrypted_key) self.server.auth_tag = json_body['auth_tag'] self.server.payload = json_body.get('payload', None) have_derived_key = self.server.attempt_decryption(self) elif rest_params["keys"] == "vkey": self.server.add_V(decrypted_key) have_derived_key = self.server.attempt_decryption(self) else: logger.warning('POST returning response. uri not supported: ' + self.path) common.echo_json_response(self, 400, "uri not supported") return logger.info('POST of %s key returning 200' % (('V', 'U')[rest_params["keys"] == "ukey"])) common.echo_json_response(self, 200, "Success") # no key yet, then we're done if not have_derived_key: return # woo hoo we have a key # ok lets write out the key now secdir = secure_mount.mount( ) # confirm that storage is still securely mounted # clean out the secure dir of any previous info before we extract files if os.path.isdir("%s/unzipped" % secdir): shutil.rmtree("%s/unzipped" % secdir) # write out key file f = open(secdir + "/" + self.server.enc_keyname, 'w') f.write(base64.b64encode(self.server.K).decode()) f.close() #stow the U value for later tpm.write_key_nvram(self.server.final_U) # optionally extend a hash of they key and payload into specified PCR tomeasure = self.server.K # if we have a good key, now attempt to write out the encrypted payload dec_path = "%s/%s" % (secdir, config.get('cloud_agent', "dec_payload_file")) enc_path = "%s/encrypted_payload" % common.WORK_DIR dec_payload = None enc_payload = None if self.server.payload is not None: dec_payload = crypto.decrypt(self.server.payload, bytes(self.server.K)) enc_payload = self.server.payload elif os.path.exists(enc_path): # if no payload provided, try to decrypt one from a previous run stored in encrypted_payload with open(enc_path, 'rb') as f: enc_payload = f.read() try: dec_payload = crypto.decrypt(enc_payload, self.server.K) logger.info("Decrypted previous payload in %s to %s" % (enc_path, dec_path)) except Exception as e: logger.warning( "Unable to decrypt previous payload %s with derived key: %s" % (enc_path, e)) os.remove(enc_path) enc_payload = None # also write out encrypted payload to be decrytped next time if enc_payload is not None: with open(enc_path, 'wb') as f: f.write(self.server.payload.encode('utf-8')) # deal with payload payload_thread = None if dec_payload is not None: tomeasure = tomeasure + dec_payload # see if payload is a zip zfio = io.BytesIO(dec_payload) if config.getboolean( 'cloud_agent', 'extract_payload_zip') and zipfile.is_zipfile(zfio): logger.info("Decrypting and unzipping payload to %s/unzipped" % secdir) with zipfile.ZipFile(zfio, 'r') as f: f.extractall('%s/unzipped' % secdir) # run an included script if one has been provided initscript = config.get('cloud_agent', 'payload_script') if initscript is not "": def initthread(): import subprocess env = os.environ.copy() env['AGENT_UUID'] = self.server.agent_uuid proc = subprocess.Popen(["/bin/bash", initscript], env=env, shell=False, cwd='%s/unzipped' % secdir, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) while True: line = proc.stdout.readline() if line == '' and proc.poll() is not None: break if line: logger.debug("init-output: %s" % line.strip()) # should be a no-op as poll already told us it's done proc.wait() if not os.path.exists("%s/unzipped/%s" % (secdir, initscript)): logger.info( "No payload script %s found in %s/unzipped" % (initscript, secdir)) else: logger.info( "Executing payload script: %s/unzipped/%s" % (secdir, initscript)) payload_thread = threading.Thread(target=initthread) else: logger.info("Decrypting payload to %s" % dec_path) with open(dec_path, 'wb') as f: f.write(dec_payload) zfio.close() # now extend a measurement of the payload and key if there was one pcr = config.getint('cloud_agent', 'measure_payload_pcr') if pcr > 0 and pcr < 24: logger.info("extending measurement of payload into PCR %s" % pcr) measured = tpm.hashdigest(tomeasure) tpm.extendPCR(pcr, measured) if payload_thread is not None: payload_thread.start() return
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 __init__(self, server_address, RequestHandlerClass, agent_uuid, contact_ip, ima_log_file, tpm_log_file_data): """Constructor overridden to provide ability to pass configuration arguments to the server""" # Find the locations for the U/V transport and mTLS key and certificate. # They are either relative to secdir (/var/lib/keylime/secure) or absolute paths. secdir = secure_mount.mount() keyname = config.get("cloud_agent", "rsa_keyname") if not os.path.isabs(keyname): keyname = os.path.join(secdir, keyname) # read or generate the key depending on configuration if os.path.isfile(keyname): # read in private key logger.info("Using existing key in %s", keyname) with open(keyname, "rb") as f: rsa_key = crypto.rsa_import_privkey(f.read()) else: logger.info( "Key for U/V transport and mTLS certificate not found, generating a new one" ) rsa_key = crypto.rsa_generate(2048) with open(keyname, "wb") as f: f.write(crypto.rsa_export_privkey(rsa_key)) self.rsakey_path = keyname self.rsaprivatekey = rsa_key self.rsapublickey_exportable = crypto.rsa_export_pubkey( self.rsaprivatekey) self.mtls_cert_enabled = config.getboolean("cloud_agent", "mtls_cert_enabled", fallback=False) if self.mtls_cert_enabled: certname = config.get("cloud_agent", "mtls_cert") if not os.path.isabs(certname): certname = os.path.join(secdir, certname) if os.path.isfile(certname): logger.info("Using existing mTLS cert in %s", certname) with open(certname, "rb") as f: mtls_cert = x509.load_pem_x509_certificate( f.read(), backend=default_backend()) else: logger.info("No mTLS certificate found, generating a new one") agent_ips = [server_address[0]] if contact_ip is not None: agent_ips.append(contact_ip) with open(certname, "wb") as f: # By default generate a TLS certificate valid for 5 years valid_util = datetime.datetime.utcnow( ) + datetime.timedelta(days=(360 * 5)) mtls_cert = crypto.generate_selfsigned_cert( agent_uuid, rsa_key, valid_util, agent_ips) f.write(mtls_cert.public_bytes(serialization.Encoding.PEM)) self.mtls_cert_path = certname self.mtls_cert = mtls_cert else: self.mtls_cert_path = None self.mtls_cert = None logger.info( "WARNING: mTLS disabled, Tenant and Verifier will reach out to agent via HTTP" ) self.revocation_cert_path = config.get("cloud_agent", "revocation_cert") if self.revocation_cert_path == "default": self.revocation_cert_path = os.path.join( secdir, "unzipped/RevocationNotifier-cert.crt") elif self.revocation_cert_path[0] != "/": # if it is a relative, convert to absolute in work_dir self.revocation_cert_path = os.path.abspath( os.path.join(config.WORK_DIR, self.revocation_cert_path)) # attempt to get a U value from the TPM NVRAM nvram_u = tpm_instance.read_key_nvram() if nvram_u is not None: logger.info("Existing U loaded from TPM NVRAM") self.add_U(nvram_u) http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass) self.enc_keyname = config.get("cloud_agent", "enc_keyname") self.agent_uuid = agent_uuid self.ima_log_file = ima_log_file self.tpm_log_file_data = tpm_log_file_data
def do_POST(self): """This method services the POST request typically from either the Tenant or the Cloud Verifier. Only tenant and cloudverifier uri's are supported. Both requests require a nonce parameter. The Cloud verifier requires an additional mask parameter. If the uri or parameters are incorrect, a 400 response is returned. """ rest_params = web_util.get_restful_params(self.path) if rest_params is None: web_util.echo_json_response( self, 405, "Not Implemented: Use /keys/ or /notifications/ interface") return if not rest_params["api_version"]: web_util.echo_json_response(self, 400, "API Version not supported") return content_length = int(self.headers.get("Content-Length", 0)) if content_length <= 0: logger.warning( "POST returning 400 response, expected content in message. url: %s", self.path) web_util.echo_json_response(self, 400, "expected content in message") return post_body = self.rfile.read(content_length) try: json_body = json.loads(post_body) except Exception as e: logger.warning( "POST returning 400 response, could not parse body data: %s", e) web_util.echo_json_response(self, 400, "content is invalid") return if "notifications" in rest_params: if rest_params["notifications"] == "revocation": revocation_notifier.process_revocation( json_body, perform_actions, cert_path=self.server.revocation_cert_path) web_util.echo_json_response(self, 200, "Success") else: web_util.echo_json_response( self, 400, "Only /notifications/revocation is supported") return if rest_params.get("keys", None) not in ["ukey", "vkey"]: web_util.echo_json_response( self, 400, "Only /keys/ukey or /keys/vkey are supported") return try: b64_encrypted_key = json_body["encrypted_key"] decrypted_key = crypto.rsa_decrypt( self.server.rsaprivatekey, base64.b64decode(b64_encrypted_key)) except (ValueError, KeyError, TypeError) as e: logger.warning( "POST returning 400 response, could not parse body data: %s", e) web_util.echo_json_response(self, 400, "content is invalid") return have_derived_key = False if rest_params["keys"] == "ukey": if "auth_tag" not in json_body: logger.warning( "POST returning 400 response, U key provided without an auth_tag" ) web_util.echo_json_response(self, 400, "auth_tag is missing") return self.server.add_U(decrypted_key) self.server.auth_tag = json_body["auth_tag"] self.server.payload = json_body.get("payload", None) have_derived_key = self.server.attempt_decryption() elif rest_params["keys"] == "vkey": self.server.add_V(decrypted_key) have_derived_key = self.server.attempt_decryption() else: logger.warning("POST returning response. uri not supported: %s", self.path) web_util.echo_json_response(self, 400, "uri not supported") return logger.info("POST of %s key returning 200", ("V", "U")[rest_params["keys"] == "ukey"]) web_util.echo_json_response(self, 200, "Success") # no key yet, then we're done if not have_derived_key: return # woo hoo we have a key # ok lets write out the key now secdir = secure_mount.mount( ) # confirm that storage is still securely mounted # clean out the secure dir of any previous info before we extract files if os.path.isdir(os.path.join(secdir, "unzipped")): shutil.rmtree(os.path.join(secdir, "unzipped")) # write out key file with open(os.path.join(secdir, self.server.enc_keyname), "w", encoding="utf-8") as f: f.write(base64.b64encode(self.server.K).decode()) # stow the U value for later tpm_instance.write_key_nvram(self.server.final_U) # optionally extend a hash of they key and payload into specified PCR tomeasure = self.server.K # if we have a good key, now attempt to write out the encrypted payload dec_path = os.path.join(secdir, config.get("cloud_agent", "dec_payload_file")) enc_path = os.path.join(config.WORK_DIR, "encrypted_payload") dec_payload = None enc_payload = None if self.server.payload is not None: if not self.server.mtls_cert_enabled and not config.getboolean( "cloud_agent", "enable_insecure_payload", fallback=False): logger.warning( 'agent mTLS is disabled, and unless "enable_insecure_payload" is set to "True", payloads cannot be deployed' ) enc_payload = None else: dec_payload = crypto.decrypt(self.server.payload, bytes(self.server.K)) enc_payload = self.server.payload elif os.path.exists(enc_path): # if no payload provided, try to decrypt one from a previous run stored in encrypted_payload with open(enc_path, "rb") as f: enc_payload = f.read() try: dec_payload = crypto.decrypt(enc_payload, self.server.K) logger.info("Decrypted previous payload in %s to %s", enc_path, dec_path) except Exception as e: logger.warning( "Unable to decrypt previous payload %s with derived key: %s", enc_path, e) os.remove(enc_path) enc_payload = None # also write out encrypted payload to be decrytped next time if enc_payload is not None: with open(enc_path, "wb") as f: f.write(self.server.payload.encode("utf-8")) # deal with payload payload_thread = None if dec_payload is not None: tomeasure = tomeasure + dec_payload # see if payload is a zip zfio = io.BytesIO(dec_payload) if config.getboolean( "cloud_agent", "extract_payload_zip") and zipfile.is_zipfile(zfio): logger.info("Decrypting and unzipping payload to %s/unzipped", secdir) with zipfile.ZipFile(zfio, "r") as f: f.extractall(os.path.join(secdir, "unzipped")) # run an included script if one has been provided initscript = config.get("cloud_agent", "payload_script") if initscript != "": def initthread(): env = os.environ.copy() env["AGENT_UUID"] = self.server.agent_uuid with subprocess.Popen( ["/bin/bash", initscript], env=env, shell=False, cwd=os.path.join(secdir, "unzipped"), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) as proc: for line in iter(proc.stdout.readline, b""): logger.debug("init-output: %s", line.strip()) # should be a no-op as poll already told us it's done proc.wait() if not os.path.exists( os.path.join(secdir, "unzipped", initscript)): logger.info( "No payload script %s found in %s/unzipped", initscript, secdir) else: logger.info("Executing payload script: %s/unzipped/%s", secdir, initscript) payload_thread = threading.Thread(target=initthread, daemon=True) else: logger.info("Decrypting payload to %s", dec_path) with open(dec_path, "wb") as f: f.write(dec_payload) zfio.close() # now extend a measurement of the payload and key if there was one pcr = config.getint("cloud_agent", "measure_payload_pcr") if 0 < pcr < 24: logger.info("extending measurement of payload into PCR %s", pcr) measured = tpm_instance.hashdigest(tomeasure) tpm_instance.extendPCR(pcr, measured) if payload_thread is not None: payload_thread.start() return
def mk_signed_cert(cacert,ca_pk,name,serialnum): csr = {"request": { "CN": name, "hosts": [ name, ], "key": { "algo": "rsa", "size": config.getint('ca','cert_bits') }, "names": [ { "C": config.get('ca','cert_country'), "L": config.get('ca','cert_locality'), "O": config.get('ca','cert_organization'), "OU": config.get('ca','cert_org_unit'), "ST": config.get('ca','cert_state') } ] } } # check CRL distribution point disturl = config.get('ca','cert_crl_dist') if disturl == 'default': disturl = "http://%s:%s/crl.der"%(socket.getfqdn(),common.CRL_PORT) # set up config for cfssl server cfsslconfig = { "signing": { "default": { "usages": ["client auth","server auth","key agreement","key encipherment","signing","digital signature","data encipherment"], "expiry": "8760h", "crl_url": disturl, } } } secdir = secure_mount.mount() try: # need to temporarily write out the private key with no password # to tmpfs ca_pk.save_key('%s/ca-key.pem'%secdir, None) with open('%s/cfsslconfig.yml'%secdir,'w') as f: json.dump(cfsslconfig, f) cmdline = "-config=%s/cfsslconfig.yml"%secdir priv_key = os.path.abspath("%s/ca-key.pem"%secdir) cmdline += " -ca-key %s -ca cacert.crt"%(priv_key) start_cfssl(cmdline) body = post_cfssl('api/v1/cfssl/newcert',csr) finally: stop_cfssl() os.remove('%s/ca-key.pem'%secdir) os.remove('%s/cfsslconfig.yml'%secdir) if body['success']: pk = EVP.load_key_string(body['result']['private_key'].encode('utf-8')) cert = X509.load_cert_string(body['result']['certificate'].encode("utf-8")) return cert, pk else: raise Exception("Unable to create cert for %s"%name)