def await_notifications(callback, revocation_cert_path): # keep old typo "listen_notfications" around for a few versions assert config.getboolean( "cloud_agent", "listen_notifications", fallback=False) or config.getboolean( "cloud_agent", "listen_notfications", fallback=False) try: import zmq # pylint: disable=import-outside-toplevel except ImportError as error: raise Exception( "install PyZMQ for 'listen_notifications' option") from error global cert_key if revocation_cert_path is None: raise Exception("must specify revocation_cert_path") context = zmq.Context() mysock = context.socket(zmq.SUB) mysock.setsockopt(zmq.SUBSCRIBE, b"") mysock.connect(f"tcp://{config.get('general', 'receive_revocation_ip')}:" f"{config.getint('general', 'receive_revocation_port')}") logger.info( "Waiting for revocation messages on 0mq %s:%s", config.get("general", "receive_revocation_ip"), config.getint("general", "receive_revocation_port"), ) while True: rawbody = mysock.recv() body = json.loads(rawbody) if cert_key is None: # load up the CV signing public key if revocation_cert_path is not None and os.path.exists( revocation_cert_path): logger.info("Lazy loading the revocation certificate from %s", revocation_cert_path) with open(revocation_cert_path, "rb") as f: certpem = f.read() cert_key = crypto.x509_import_pubkey(certpem) if cert_key is None: logger.warning( "Unable to check signature of revocation message: %s not available", revocation_cert_path) elif "signature" not in body or body["signature"] == "none": logger.warning("No signature on revocation message from server") elif not crypto.rsa_verify(cert_key, body["msg"].encode("utf-8"), body["signature"].encode("utf-8")): logger.error("Invalid revocation message siganture %s", body) else: message = json.loads(body["msg"]) logger.debug("Revocation signature validated for revocation: %s", message) callback(message)
def await_notifications(callback, revocation_cert_path): # keep old typo "listen_notfications" around for a few versions assert config.getboolean( "cloud_agent", "listen_notifications", fallback=False) or config.getboolean( "cloud_agent", "listen_notfications", fallback=False) try: import zmq # pylint: disable=import-outside-toplevel except ImportError as error: raise Exception( "install PyZMQ for 'listen_notifications' option") from error if revocation_cert_path is None: raise Exception("must specify revocation_cert_path") context = zmq.Context() mysock = context.socket(zmq.SUB) mysock.setsockopt(zmq.SUBSCRIBE, b"") mysock.connect(f"tcp://{config.get('general', 'receive_revocation_ip')}:" f"{config.getint('general', 'receive_revocation_port')}") logger.info( "Waiting for revocation messages on 0mq %s:%s", config.get("general", "receive_revocation_ip"), config.getint("general", "receive_revocation_port"), ) while True: rawbody = mysock.recv() body = json.loads(rawbody) process_revocation(body, callback, revocation_cert_path)
def process_revocation(revocation, callback, cert_path): global cert_key if cert_key is None: # load up the CV signing public key if cert_path is not None and os.path.exists(cert_path): logger.info("Lazy loading the revocation certificate from %s", cert_path) with open(cert_path, "rb") as f: certpem = f.read() cert_key = crypto.x509_import_pubkey(certpem) if cert_key is None: logger.warning( "Unable to check signature of revocation message: %s not available", cert_path) elif "signature" not in revocation or revocation["signature"] == "none": logger.warning("No signature on revocation message from server") elif not crypto.rsa_verify(cert_key, revocation["msg"].encode("utf-8"), revocation["signature"].encode("utf-8")): logger.error("Invalid revocation message siganture %s", revocation) else: message = json.loads(revocation["msg"]) logger.debug("Revocation signature validated for revocation: %s", message) callback(message)
def readPolicy(configval): policy = json.loads(configval) # compute PCR mask from tpm_policy mask = 0 for key in list(policy.keys()): if not key.isdigit() or int(key) > 24: raise Exception(f"Invalid tpm policy pcr number: {key}") if int(key) == config.TPM_DATA_PCR: raise Exception( f"Invalid allowlist PCR number {key}, keylime uses this PCR to bind data." ) if int(key) == config.IMA_PCR: raise Exception( f"Invalid allowlist PCR number {key}, this PCR is used for IMA." ) mask = mask | (1 << int(key)) # wrap it in a list if it is a singleton if isinstance(policy[key], str): policy[key] = [policy[key]] # convert all hash values to lowercase policy[key] = [x.lower() for x in policy[key]] policy['mask'] = hex(mask) return policy
async def test_041_agent_keys_verify_get(self): """Test agent's GET /keys/verify Interface We use async here to allow function await while key processes""" self.assertIsNotNone( self.K, "Required value not set. Previous step may have failed?") challenge = tpm_abstract.TPM_Utilities.random_password(20) encoded = base64.b64encode(self.K).decode("utf-8") response = tornado_requests.request( "GET", f"http://{self.cloudagent_ip}:{self.cloudagent_port}/keys/verify?challenge={challenge}" ) response = await response self.assertEqual(response.status, 200, "Non-successful Agent verify return code!") json_response = json.loads(response.read().decode()) # Ensure response is well-formed self.assertIn("results", json_response, "Malformed response body!") self.assertIn("hmac", json_response["results"], "Malformed response body!") # Be sure response is valid mac = json_response["results"]["hmac"] ex_mac = crypto.do_hmac(encoded, challenge) # ex_mac = crypto.do_hmac(self.K, challenge) self.assertEqual(mac, ex_mac, "Agent failed to validate challenge code!")
def process_get_status(agent): allowlist_json = json.loads(agent.ima_policy.ima_policy) if isinstance(allowlist_json, dict) and "allowlist" in allowlist_json: al_len = len(allowlist_json["allowlist"]) else: al_len = 0 try: mb_refstate = json.loads(agent.mb_refstate) except Exception as e: logger.warning( 'Non-fatal problem ocurred while attempting to evaluate agent attribute "mb_refstate" (%s). Will just consider the value of this attribute to be "None"', e.args, ) mb_refstate = None logger.debug( 'The contents of the agent attribute "mb_refstate" are %s', agent.mb_refstate) if isinstance(mb_refstate, dict) and "mb_refstate" in mb_refstate: mb_refstate_len = len(mb_refstate["mb_refstate"]) else: mb_refstate_len = 0 response = { "operational_state": agent.operational_state, "v": agent.v, "ip": agent.ip, "port": agent.port, "tpm_policy": agent.tpm_policy, "meta_data": agent.meta_data, "allowlist_len": al_len, "mb_refstate_len": mb_refstate_len, "accept_tpm_hash_algs": agent.accept_tpm_hash_algs, "accept_tpm_encryption_algs": agent.accept_tpm_encryption_algs, "accept_tpm_signing_algs": agent.accept_tpm_signing_algs, "hash_alg": agent.hash_alg, "enc_alg": agent.enc_alg, "sign_alg": agent.sign_alg, "verifier_id": agent.verifier_id, "verifier_ip": agent.verifier_ip, "verifier_port": agent.verifier_port, "severity_level": agent.severity_level, "last_event_id": agent.last_event_id, "attestation_count": agent.attestation_count, } return response
def process_get_status(agent): allowlist = json.loads(agent.allowlist) if isinstance(allowlist, dict) and 'allowlist' in allowlist: al_len = len(allowlist['allowlist']) else: al_len = 0 try: mb_refstate = json.loads(agent.mb_refstate) except Exception as e: logger.warning( 'Non-fatal problem ocurred while attempting to evaluate agent attribute "mb_refstate" (%s). Will just consider the value of this attribute to be "None"', e.args) mb_refstate = None logger.debug( 'The contents of the agent attribute "mb_refstate" are %s', agent.mb_refstate) if isinstance(mb_refstate, dict) and 'mb_refstate' in mb_refstate: mb_refstate_len = len(mb_refstate['mb_refstate']) else: mb_refstate_len = 0 response = { 'operational_state': agent.operational_state, 'v': agent.v, 'ip': agent.ip, 'port': agent.port, 'tpm_policy': agent.tpm_policy, 'vtpm_policy': agent.vtpm_policy, 'meta_data': agent.meta_data, 'allowlist_len': al_len, 'mb_refstate_len': mb_refstate_len, 'accept_tpm_hash_algs': agent.accept_tpm_hash_algs, 'accept_tpm_encryption_algs': agent.accept_tpm_encryption_algs, 'accept_tpm_signing_algs': agent.accept_tpm_signing_algs, 'hash_alg': agent.hash_alg, 'enc_alg': agent.enc_alg, 'sign_alg': agent.sign_alg, 'verifier_id': agent.verifier_id, 'verifier_ip': agent.verifier_ip, 'verifier_port': agent.verifier_port, 'severity_level': agent.severity_level, 'last_event_id': agent.last_event_id } return response
def revoke_callback(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) return logger.info("Revoking certificate: %s", serial) server.setcrl(cmd_revoke(workingdir, None, serial))
def await_notifications(callback, revocation_cert_path): global cert_key if revocation_cert_path is None: raise Exception("must specify revocation_cert_path") context = zmq.Context() mysock = context.socket(zmq.SUB) mysock.setsockopt(zmq.SUBSCRIBE, b'') mysock.connect(f"tcp://{config.get('general', 'receive_revocation_ip')}:" f"{config.getint('general', 'receive_revocation_port')}") logger.info('Waiting for revocation messages on 0mq %s:%s', config.get('general', 'receive_revocation_ip'), config.getint('general', 'receive_revocation_port')) while True: rawbody = mysock.recv() body = json.loads(rawbody) if cert_key is None: # load up the CV signing public key if revocation_cert_path is not None and os.path.exists( revocation_cert_path): logger.info("Lazy loading the revocation certificate from %s", revocation_cert_path) with open(revocation_cert_path, "rb") as f: certpem = f.read() cert_key = crypto.x509_import_pubkey(certpem) if cert_key is None: logger.warning( "Unable to check signature of revocation message: %s not available", revocation_cert_path) elif 'signature' not in body or body['signature'] == 'none': logger.warning("No signature on revocation message from server") elif not crypto.rsa_verify(cert_key, body['msg'].encode('utf-8'), body['signature'].encode('utf-8')): logger.error("Invalid revocation message siganture %s", body) else: message = json.loads(body['msg']) logger.debug("Revocation signature validated for revocation: %s", message) callback(message)
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 = ("crlutil", "-I", "-i", "%s/unzipped/cacrl.der" % secdir, "-d", "sql:/etc/ipsec.d") cmd_exec.run(cmd) # need to find any sa's that were established with that cert subject name cmd = ("ipsec", "whack", "--trafficstatus") output = cmd_exec.run(cmd, 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 = ("ipsec", "whack", "--crash", todelete) cmd_exec.run(cmd, raiseOnError=False)
def validate_agent_data(agent_data): if agent_data is None: return False, None # validate that the allowlist is proper JSON lists = json.loads(agent_data['allowlist']) # Validate exlude list contains valid regular expressions is_valid, _, err_msg = validators.valid_exclude_list(lists.get('exclude')) if not is_valid: err_msg += " Exclude list regex is misformatted. Please correct the issue and try again." return is_valid, err_msg
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 check_pcrs(self, agentAttestState, tpm_policy, pcrs, data, virtual, ima_measurement_list, allowlist, ima_keyrings, mb_measurement_list, mb_refstate_str, hash_alg) -> Failure: failure = Failure(Component.PCR_VALIDATION) if isinstance(tpm_policy, str): tpm_policy = json.loads(tpm_policy) pcr_allowlist = tpm_policy.copy() if 'mask' in pcr_allowlist: del pcr_allowlist['mask'] # convert all pcr num keys to integers pcr_allowlist = {int(k): v for k, v in list(pcr_allowlist.items())} mb_policy, mb_policy_name, mb_refstate_data = measured_boot.get_policy( mb_refstate_str) mb_pcrs_hashes, boot_aggregates, mb_measurement_data, mb_failure = self.parse_mb_bootlog( mb_measurement_list, hash_alg) failure.merge(mb_failure) pcrs_in_quote = set( ) # PCRs in quote that were already used for some kind of validation pcrs = AbstractTPM.__parse_pcrs(pcrs, virtual) pcr_nums = set(pcrs.keys()) # Validate data PCR if config.TPM_DATA_PCR in pcr_nums and data is not None: expectedval = self.sim_extend(data, hash_alg=hash_alg) if expectedval != pcrs[config.TPM_DATA_PCR]: logger.error( "%sPCR #%s: invalid bind data %s from quote does not match expected value %s", ("", "v")[virtual], config.TPM_DATA_PCR, pcrs[config.TPM_DATA_PCR], expectedval) failure.add_event(f"invalid_pcr_{config.TPM_DATA_PCR}", { "got": pcrs[config.TPM_DATA_PCR], "expected": expectedval }, True) pcrs_in_quote.add(config.TPM_DATA_PCR) else: logger.error( "Binding %sPCR #%s was not included in the quote, but is required", ("", "v")[virtual], config.TPM_DATA_PCR) failure.add_event( f"missing_pcr_{config.TPM_DATA_PCR}", f"Data PCR {config.TPM_DATA_PCR} is missing in quote, but is required", True) # Check for ima PCR if config.IMA_PCR in pcr_nums: if ima_measurement_list is None: logger.error( "IMA PCR in policy, but no measurement list provided") failure.add_event( f"unused_pcr_{config.IMA_PCR}", "IMA PCR in policy, but no measurement list provided", True) else: ima_failure = AbstractTPM.__check_ima(agentAttestState, pcrs[config.IMA_PCR], ima_measurement_list, allowlist, ima_keyrings, boot_aggregates, hash_alg) failure.merge(ima_failure) pcrs_in_quote.add(config.IMA_PCR) # Collect mismatched measured boot PCRs as measured_boot failures mb_pcr_failure = Failure(Component.MEASURED_BOOT) # Handle measured boot PCRs only if the parsing worked if not mb_failure: for pcr_num in set(config.MEASUREDBOOT_PCRS) & pcr_nums: if mb_refstate_data: if not mb_measurement_list: logger.error( "Measured Boot PCR %d in policy, but no measurement list provided", pcr_num) failure.add_event( f"unused_pcr_{pcr_num}", f"Measured Boot PCR {pcr_num} in policy, but no measurement list provided", True) continue val_from_log_int = mb_pcrs_hashes.get(str(pcr_num), 0) val_from_log_hex = hex(val_from_log_int)[2:] val_from_log_hex_stripped = val_from_log_hex.lstrip('0') pcrval_stripped = pcrs[pcr_num].lstrip('0') if val_from_log_hex_stripped != pcrval_stripped: logger.error( "For PCR %d and hash %s the boot event log has value %r but the agent returned %r", str(hash_alg), pcr_num, val_from_log_hex, pcrs[pcr_num]) mb_pcr_failure.add_event( f"invalid_pcr_{pcr_num}", { "context": "SHA256 boot event log PCR value does not match", "got": pcrs[pcr_num], "expected": val_from_log_hex }, True) if pcr_num in pcr_allowlist and pcrs[ pcr_num] not in pcr_allowlist[pcr_num]: logger.error( "%sPCR #%s: %s from quote does not match expected value %s", ("", "v")[virtual], pcr_num, pcrs[pcr_num], pcr_allowlist[pcr_num]) failure.add_event( f"invalid_pcr_{pcr_num}", { "context": "PCR value is not in allowlist", "got": pcrs[pcr_num], "expected": pcr_allowlist[pcr_num] }, True) pcrs_in_quote.add(pcr_num) failure.merge(mb_pcr_failure) # Check the remaining non validated PCRs for pcr_num in pcr_nums - pcrs_in_quote: if pcr_num not in list(pcr_allowlist.keys()): logger.warning( "%sPCR #%s in quote not found in %stpm_policy, skipping.", ("", "v")[virtual], pcr_num, ("", "v")[virtual]) continue if pcrs[pcr_num] not in pcr_allowlist[pcr_num]: logger.error( "%sPCR #%s: %s from quote does not match expected value %s", ("", "v")[virtual], pcr_num, pcrs[pcr_num], pcr_allowlist[pcr_num]) failure.add_event( f"invalid_pcr_{pcr_num}", { "context": "PCR value is not in allowlist", "got": pcrs[pcr_num], "expected": pcr_allowlist[pcr_num] }, True) pcrs_in_quote.add(pcr_num) missing = set(pcr_allowlist.keys()) - pcrs_in_quote if len(missing) > 0: logger.error("%sPCRs specified in policy not in quote: %s", ("", "v")[virtual], missing) failure.add_event("missing_pcrs", { "context": "PCRs are missing in quote", "data": list(missing) }, True) if not mb_failure and mb_refstate_data: mb_policy_failure = measured_boot.evaluate_policy( mb_policy, mb_policy_name, mb_refstate_data, mb_measurement_data, pcrs_in_quote, ("", "v")[virtual], agentAttestState.get_agent_id()) failure.merge(mb_policy_failure) return failure
def do_POST(self): """This method handles the POST requests to add agents to the Registrar Server. Currently, only agents resources are available for POSTing, i.e. /agents. All other POST uri's will return errors. POST requests require an an agent_id identifying the agent to add, and json block sent in the body with 2 entries: ek and aik. """ session = SessionManager().make_session(engine) rest_params = web_util.get_restful_params(self.path) if rest_params is None: web_util.echo_json_response( self, 405, "Not Implemented: Use /agents/ interface") return if not rest_params["api_version"]: web_util.echo_json_response(self, 400, "API Version not supported") return if "agents" not in rest_params: web_util.echo_json_response(self, 400, "uri not supported") logger.warning( 'POST agent returning 400 response. uri not supported: %s', self.path) return agent_id = rest_params["agents"] if agent_id is None: web_util.echo_json_response(self, 400, "agent id not found in uri") logger.warning( 'POST agent returning 400 response. agent id not found in uri %s', self.path) return # If the agent ID is not valid (wrong set of characters), just # do nothing. if not validators.valid_agent_id(agent_id): web_util.echo_json_response(self, 400, "agent id not valid") logger.error("POST received an invalid agent ID: %s", agent_id) return try: content_length = int(self.headers.get('Content-Length', 0)) if content_length == 0: web_util.echo_json_response( self, 400, "Expected non zero content length") logger.warning( 'POST for %s returning 400 response. Expected non zero content length.', agent_id) return post_body = self.rfile.read(content_length) json_body = json.loads(post_body) ekcert = json_body['ekcert'] aik_tpm = json_body['aik_tpm'] initialize_tpm = tpm() if ekcert is None or ekcert == 'emulator': logger.warning('Agent %s did not submit an ekcert', agent_id) ek_tpm = json_body['ek_tpm'] else: if 'ek_tpm' in json_body: # This would mean the agent submitted both a non-None ekcert, *and* # an ek_tpm... We can deal with it by just ignoring the ek_tpm they sent logger.warning( 'Overriding ek_tpm for agent %s from ekcert', agent_id) # If there's an EKCert, we just overwrite their ek_tpm # Note, we don't validate the EKCert here, other than the implicit # "is it a valid x509 cert" check. So it's still untrusted. # This will be validated by the tenant. ek509 = load_der_x509_certificate( base64.b64decode(ekcert), backend=default_backend(), ) ek_tpm = base64.b64encode( tpm2_objects.ek_low_tpm2b_public_from_pubkey( ek509.public_key(), )).decode() aik_attrs = tpm2_objects.get_tpm2b_public_object_attributes( base64.b64decode(aik_tpm), ) if aik_attrs != tpm2_objects.AK_EXPECTED_ATTRS: web_util.echo_json_response(self, 400, "Invalid AK attributes") logger.warning( "Agent %s submitted AIK with invalid attributes! %s (provided) != %s (expected)", agent_id, tpm2_objects.object_attributes_description(aik_attrs), tpm2_objects.object_attributes_description( tpm2_objects.AK_EXPECTED_ATTRS), ) return # try to encrypt the AIK (blob, key) = initialize_tpm.encryptAIK( agent_id, base64.b64decode(ek_tpm), base64.b64decode(aik_tpm), ) # special behavior if we've registered this uuid before regcount = 1 try: agent = session.query(RegistrarMain).filter_by( agent_id=agent_id).first() except NoResultFound: agent = None except SQLAlchemyError as e: logger.error('SQLAlchemy Error: %s', e) raise if agent is not None: # keep track of how many ek-ekcerts have registered on this uuid regcount = agent.regcount if agent.ek_tpm != ek_tpm or agent.ekcert != ekcert: logger.warning( 'WARNING: Overwriting previous registration for this UUID with new ek-ekcert pair!' ) regcount += 1 # force overwrite logger.info('Overwriting previous registration for this UUID.') try: session.query(RegistrarMain).filter_by( agent_id=agent_id).delete() session.commit() except SQLAlchemyError as e: logger.error('SQLAlchemy Error: %s', e) raise # Check for ip and port contact_ip = json_body.get('ip', None) contact_port = json_body.get('port', None) # Validate ip and port if contact_ip is not None: try: # Use parser from the standard library instead of implementing our own ipaddress.ip_address(contact_ip) except ValueError: logger.warning( "Contact ip for agent %s is not a valid ip got: %s.", agent_id, contact_ip) contact_ip = None if contact_port is not None: try: contact_port = int(contact_port) if contact_port < 1 or contact_port > 65535: logger.warning( "Contact port for agent %s is not a number between 1 and got: %s.", agent_id, contact_port) contact_port = None except ValueError: logger.warning( "Contact port for agent %s is not a valid number got: %s.", agent_id, contact_port) contact_port = None # Check for mTLS cert mtls_cert = json_body.get('mtls_cert', None) if mtls_cert is None: logger.warning( "Agent %s did not send a mTLS certificate. Most operations will not work!", agent_id) # Add values to database d = {} d['agent_id'] = agent_id d['ek_tpm'] = ek_tpm d['aik_tpm'] = aik_tpm d['ekcert'] = ekcert d['ip'] = contact_ip d['mtls_cert'] = mtls_cert d['port'] = contact_port d['virtual'] = int(ekcert == 'virtual') d['active'] = int(False) d['key'] = key d['provider_keys'] = {} d['regcount'] = regcount try: session.add(RegistrarMain(**d)) session.commit() except SQLAlchemyError as e: logger.error('SQLAlchemy Error: %s', e) raise response = { 'blob': blob, } web_util.echo_json_response(self, 200, "Success", response) logger.info('POST returning key blob for agent_id: %s', agent_id) except Exception as e: web_util.echo_json_response(self, 400, "Error: %s" % e) logger.warning("POST for %s returning 400 response. Error: %s", agent_id, e) logger.exception(e)
def post(self): """This method handles the POST requests to add agents to the Agent Monitor. Currently, only agents resources are available for POSTing, i.e. /agents. All other POST uri's will return errors. agents requests require a json block sent in the body """ logger.info('Agent Monitor POST') try: rest_params = keylime.web_util.get_restful_params( self.request.path) if "agents" not in rest_params: keylime.web_util.echo_json_response(self, 400, "uri not supported") logger.warning( 'POST returning 400 response. uri not supported: ' + self.request.path) return agent_id = rest_params["agents"] if agent_id is not None: # we have to know who phoned home content_length = len(self.request.body) if content_length == 0: keylime.web_util.echo_json_response( self, 400, "Expected non zero content length") logger.warning( 'POST returning 400 response. Expected non zero content length.' ) else: json_body = json.loads(self.request.body) # VERIFY CLIENT CERT ID MATCHES AGENT ID (agent_id) client_cert = self.request.get_ssl_certificate() ssl.match_hostname(client_cert, agent_id) # Execute specified script if all is well global initscript if initscript is not None and initscript != "": def initthread(): import subprocess logger.debug("Executing specified script: %s" % initscript) env = os.environ.copy() env['AGENT_UUID'] = agent_id proc = subprocess.Popen(["/bin/sh", initscript], env=env, shell=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) proc.wait() while True: line = proc.stdout.readline() if line == "": break logger.debug("init-output: %s" % line.strip()) t = threading.Thread(target=initthread) t.start() keylime.web_util.echo_json_response( self, 200, "Success", json_body) logger.info( 'POST returning 200 response for Agent Monitor connection as ' + agent_id) else: keylime.web_util.echo_json_response(self, 400, "uri not supported") logger.warning( "POST returning 400 response. uri not supported") except Exception as e: keylime.web_util.echo_json_response(self, 400, "Exception error: %s" % e) logger.warning("POST returning 400 response. Exception error: %s" % e) logger.exception(e)
def do_PUT(self): """This method handles the PUT requests to add agents to the Registrar Server. Currently, only agents resources are available for PUTing, i.e. /agents. All other PUT uri's will return errors. """ session = SessionManager().make_session(engine) rest_params = web_util.get_restful_params(self.path) if rest_params is None: web_util.echo_json_response( self, 405, "Not Implemented: Use /agents/ interface") return if not rest_params["api_version"]: web_util.echo_json_response(self, 400, "API Version not supported") return if "agents" not in rest_params: web_util.echo_json_response(self, 400, "uri not supported") logger.warning( 'PUT agent returning 400 response. uri not supported: %s', self.path) return agent_id = rest_params["agents"] if agent_id is None: web_util.echo_json_response(self, 400, "agent id not found in uri") logger.warning( 'PUT agent returning 400 response. agent id not found in uri %s', self.path) return # If the agent ID is not valid (wrong set of characters), just # do nothing. if not validators.valid_agent_id(agent_id): web_util.echo_json_response(self, 400, "agent_id not not valid") logger.error("PUT received an invalid agent ID: %s", agent_id) return try: content_length = int(self.headers.get('Content-Length', 0)) if content_length == 0: web_util.echo_json_response( self, 400, "Expected non zero content length") logger.warning( 'PUT for %s returning 400 response. Expected non zero content length.', agent_id) return post_body = self.rfile.read(content_length) json_body = json.loads(post_body) auth_tag = json_body['auth_tag'] try: agent = session.query(RegistrarMain).filter_by( agent_id=agent_id).first() except NoResultFound as e: raise Exception( "attempting to activate agent before requesting " "registrar for %s" % agent_id) from e except SQLAlchemyError as e: logger.error('SQLAlchemy Error: %s', e) raise if config.STUB_TPM: try: session.query(RegistrarMain).filter( RegistrarMain.agent_id == agent_id).update( {'active': int(True)}) session.commit() except SQLAlchemyError as e: logger.error('SQLAlchemy Error: %s', e) raise else: ex_mac = crypto.do_hmac(agent.key.encode(), agent_id) if ex_mac == auth_tag: try: session.query(RegistrarMain).filter( RegistrarMain.agent_id == agent_id).update( {'active': int(True)}) session.commit() except SQLAlchemyError as e: logger.error('SQLAlchemy Error: %s', e) raise else: raise Exception( f"Auth tag {auth_tag} does not match expected value {ex_mac}" ) web_util.echo_json_response(self, 200, "Success") logger.info('PUT activated: %s', agent_id) except Exception as e: web_util.echo_json_response(self, 400, "Error: %s" % e) logger.warning("PUT for %s returning 400 response. Error: %s", agent_id, e) logger.exception(e) return
async def execute(revocation): print(json.loads(revocation['meta_data']))
REQUIRE_ROOT = False DISABLE_EK_CERT_CHECK_EMULATOR = True # whether to use tpmfs or not MOUNT_SECURE = True # load in JSON canned values if we're in stub mode (and JSON file given) TPM_CANNED_VALUES = None if STUB_TPM and TPM_CANNED_VALUES_PATH is not None: with open(TPM_CANNED_VALUES_PATH, "rb") as can: print("WARNING: using canned values in stub mode from file '%s'" % (TPM_CANNED_VALUES_PATH)) # Read in JSON and strip trailing extraneous commas jsonInTxt = can.read().rstrip(',\r\n') # Saved JSON is missing surrounding braces, so add them here TPM_CANNED_VALUES = json.loads('{' + jsonInTxt + '}') elif STUB_TPM: raise Exception( 'STUB_TPM=True but required TPM_CANNED_VALUES_PATH not provided!') if not REQUIRE_ROOT: MOUNT_SECURE = False if not REQUIRE_ROOT: print("WARNING: running without root access") # Config files can be merged together, reading from the system to the # user. CONFIG_FILES = [ "/usr/etc/keylime.conf", "/etc/keylime.conf", os.path.expanduser("~/.config/keylime.conf")
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 get(self): """This method handles the GET requests to retrieve status on agents for all agents in a Web-based GUI. Currently, only the web app is available for GETing, i.e. /webapp. All other GET uri's will return errors. """ # Get default policies for TPM/vTPM from config as suggestions to user tpm_policy = json.dumps(json.loads(config.get('tenant', 'tpm_policy')), indent=2) vtpm_policy = json.dumps(json.loads(config.get('tenant', 'vtpm_policy')), indent=2) # Get default intervals for populating angents, updating agents and updating terminal populate_agents_interval = json.dumps(json.loads( config.get('webapp', 'populate_agents_interval')), indent=2) update_agents_interval = json.dumps(json.loads( config.get('webapp', 'update_agents_interval')), indent=2) update_terminal_interval = json.dumps(json.loads( config.get('webapp', 'update_terminal_interval')), indent=2) self.set_status(200) self.set_header('Content-Type', 'text/html') self.write(""" <!DOCTYPE html> <html> <head> <meta charset='UTF-8'> <title>Advanced Tenant Management System</title> <script type='text/javascript' src='/static/js/webapp.js'></script> <script type='text/javascript'> window.onload = function(e) {{ let droppable = document.getElementsByClassName("file_drop"); for (let i = 0; i < droppable.length; i++) {{ droppable[i].addEventListener('dragover', dragoverCallback, false); droppable[i].addEventListener('drop', fileUploadCallback, false); }} populateAgents(); setInterval(populateAgents, {0}); setInterval(updateAgentsInfo, {1}); setInterval(updateTerminal, {2}); }} </script> <link href='/static/css/webapp.css' rel='stylesheet' type='text/css'/> </head> <body> <div id='modal_box' onclick="if (event.target == this) {{toggleVisibility(this.id);resetAddAgentForm();return false;}}"> """.format(populate_agents_interval, update_agents_interval, update_terminal_interval)) self.write(""" <div id='modal_body'> <center> <h3>Add Agent</h3> <h4 id='uuid_str'></h4> </center> <form id='add_agent' name='add_agent' onsubmit='submitAddAgentForm(this); return false;'> <div class="form_block"> <label for='agent_ip'>Agent IP: </label> <input type='text' id='agent_ip' name='agent_ip' value='127.0.0.1' required onfocus='this.select()'> <br> </div> <div id='imalist_toggle' onclick="toggleVisibility('imalist_block');" title='IMA Configuration'> IMA Configuration </div> <div id="imalist_block"> <div class="form_block"> <label for='a_list'>Allow-List: </label> <div id='a_list' name='a_list' class='file_drop'> <i>Drag payload here …</i> </div> <input type='hidden' name='a_list_data' id='a_list_data' value=''> <input type='hidden' name='a_list_name' id='a_list_name' value=''> <br> </div> <div class="form_block"> <label for='e_list'>Exclude: </label> <div id='e_list' name='e_list' class='file_drop'> <i>Drag payload here …</i> </div> <input type='hidden' name='e_list_data' id='e_list_data' value=''> <input type='hidden' name='e_list_name' id='e_list_name' value=''> <br> </div> </div> <br> <div id='policy_toggle' onclick="toggleVisibility('policy_block');" title='TPM & vTPM Policy Configuration'> TPM & vTPM Policy Configuration </div> <div id="policy_block"> <div class="form_block"> <label for='tpm_policy'>TPM Policy: </label><br> <textarea class='json_input' id='tpm_policy' name='tpm_policy'>{}</textarea> <br> </div> <div class="form_block"> <label for='vtpm_policy'>vTPM Policy: </label><br> <textarea class='json_input' id='vtpm_policy' name='vtpm_policy'>{}</textarea> <br> </div> </div> <br> """.format(tpm_policy, vtpm_policy)) self.write(""" <div id="payload_block"> <div class="form_block"> <label for='ptype'>Payload type: </label> <label><input type='radio' name='ptype' value='{}' checked="checked" onclick='toggleTabs(this.value)'> File </label> <label><input type='radio' name='ptype' value='{}' onclick='toggleTabs(this.value)'> Keyfile </label> <label><input type='radio' name='ptype' value='{}' onclick='toggleTabs(this.value)'> CA Dir </label> <br> </div> """.format(Agent_Init_Types.FILE, Agent_Init_Types.KEYFILE, Agent_Init_Types.CA_DIR)) self.write(""" <div id='keyfile_container' class="form_block" style="display:none;"> <label for='file'>Keyfile: </label> <div id='keyfile' name='keyfile' class='file_drop'> <i>Drag key file here …</i> </div> <input type='hidden' name='keyfile_data' id='keyfile_data' value=''> <input type='hidden' name='keyfile_name' id='keyfile_name' value=''> <br> </div> <div id='file_container' class="form_block"> <label for='file'>Payload: </label> <div id='file' name='file' class='file_drop'> <i>Drag payload here …</i> </div> <input type='hidden' name='file_data' id='file_data' value=''> <input type='hidden' name='file_name' id='file_name' value=''> <br> </div> <div id='ca_dir_container' style="display:none;"> <div class="form_block"> <label for='ca_dir'>CA Dir: </label> <input type='text' id='ca_dir' name='ca_dir' placeholder='e.g., default'> <br> </div> <div class="form_block"> <label for='ca_dir_pw'>CA Password: </label> <input type='password' id='ca_dir_pw' name='ca_dir_pw' placeholder='e.g., default'> <br> </div> <div class="form_block"> <label for='include_dir'>Include dir: </label> <div id='include_dir' name='include_dir' class='file_drop multi_file'> <i>Drag files here …</i> </div> <input type='hidden' name='include_dir_data' id='include_dir_data' value=''> <input type='hidden' name='include_dir_name' id='include_dir_name' value=''> <br> </div> </div> </div> <br> <input type='hidden' name='uuid' id='uuid' value=''> <center><button type="submit" value="Add Agent">Add Agent</button></center> <br> </form> </div> </div> <div id="header"> <div class="logo" title="Keylime"> </div> <div id="header_banner"> <h1>Keylime Advanced Tenant Management System</h1> </div> <div class="logo" style="float:right;" title="Keylime"> </div> <br style="clear:both;"> </div> <div id="agent_body"> <h2>Agents</h2> <div class='table_header'> <div class='table_control'> </div> <div class='table_col'>UUID</div> <div class='table_col'>address</div> <div class='table_col'>status</div> <br style='clear:both;' /> </div> <div id='agent_template' style='display:none;'> <li class='agent'> <div style='display:block;cursor:help;width:800px;'></div> <div style='display:none;'></div> </li> </div> <ol id='agent_container'></ol> <div style="color:#888;margin-left:15px;padding:10px;"> <i>End of results</i> </div> <div id="terminal-frame"> <div id="terminal-header" onmousedown="toggleVisibility('terminal')">Tenant Logs</div> <div id="terminal"></div> </div> </div> </body> </html> """)