def invalid(self) -> Failure: failure = Failure(Component.IMA, ["validation"]) if self.pcr != str(config.IMA_PCR): logger.warning("IMA entry PCR does not match %s. It was: %s", config.IMA_PCR, self.pcr) failure.add_event("ima_pcr", {"message": "IMA PCR is not the configured one", "expected": str(config.IMA_PCR), "got": self.pcr}, True) # Ignore template hash for ToMToU errors if self.ima_template_hash == get_FF_HASH(self._ima_hash_alg): logger.warning("Skipped template_hash validation entry with FF_HASH") # By default ToMToU errors are not treated as a failure if config.getboolean("cloud_verifier", "tomtou_errors", fallback=False): failure.add_event("tomtou", "hash validation was skipped", True) return failure if self.ima_template_hash != self._ima_hash_alg.hash(self._bytes): failure.add_event("ima_hash", {"message": "IMA template hash does not match the calculated hash.", "expected": str(self.ima_template_hash), "got": str(self.mode.bytes())}, True) return failure if self._validator is None: failure.add_event("no_validator", "No validator specified", True) return failure failure.merge(self.mode.is_data_valid(self._validator)) return failure
def __check_ima(agentAttestState, pcrval, ima_measurement_list, allowlist, ima_keyrings, boot_aggregates, hash_alg): failure = Failure(Component.IMA) logger.info("Checking IMA measurement list on agent: %s", agentAttestState.get_agent_id()) _, ima_failure = ima.process_measurement_list( agentAttestState, ima_measurement_list.split('\n'), allowlist, pcrval=pcrval, ima_keyrings=ima_keyrings, boot_aggregates=boot_aggregates, hash_alg=hash_alg) failure.merge(ima_failure) if not failure: logger.debug("IMA measurement list of agent %s validated", agentAttestState.get_agent_id()) return failure
def _process_measurement_list(agentAttestState, lines, hash_alg, lists=None, m2w=None, pcrval=None, ima_keyrings=None, boot_aggregates=None): failure = Failure(Component.IMA) running_hash = agentAttestState.get_pcr_state(config.IMA_PCR, hash_alg) found_pcr = pcrval is None errors = {} pcrval_bytes = b"" if pcrval is not None: pcrval_bytes = codecs.decode(pcrval.encode("utf-8"), "hex") if lists is not None: if isinstance(lists, str): lists = json.loads(lists) allow_list = lists["allowlist"] exclude_list = lists["exclude"] else: allow_list = None exclude_list = None ima_log_hash_alg = algorithms.Hash.SHA1 if allow_list is not None: try: ima_log_hash_alg = algorithms.Hash( allow_list["ima"]["log_hash_alg"]) except ValueError: logger.warning( "Specified IMA log hash algorithm %s is not a valid algorithm! Defaulting to SHA1.", allow_list["ima"]["log_hash_alg"], ) if boot_aggregates and allow_list: if "boot_aggregate" not in allow_list["hashes"]: allow_list["hashes"]["boot_aggregate"] = [] for alg in boot_aggregates.keys(): for val in boot_aggregates[alg]: if val not in allow_list["hashes"]["boot_aggregate"]: allow_list["hashes"]["boot_aggregate"].append(val) is_valid, compiled_regex, err_msg = validators.valid_exclude_list( exclude_list) if not is_valid: # This should not happen as the exclude list has already been validated # by the verifier before acceping it. This is a safety net just in case. err_msg += " Exclude list will be ignored." logger.error(err_msg) # Setup device mapper validation dm_validator = None if allow_list is not None: dm_policy = allow_list["ima"]["dm_policy"] if dm_policy is not None: dm_validator = ima_dm.DmIMAValidator(dm_policy) dm_state = agentAttestState.get_ima_dm_state() # Only load state when using incremental attestation if agentAttestState.get_next_ima_ml_entry() != 0: dm_validator.state_load(dm_state) ima_validator = ast.Validator({ ast.ImaSig: functools.partial(_validate_ima_sig, compiled_regex, ima_keyrings, allow_list), ast.ImaNg: functools.partial(_validate_ima_ng, compiled_regex, allow_list), ast.Ima: functools.partial(_validate_ima_ng, compiled_regex, allow_list), ast.ImaBuf: functools.partial(_validate_ima_buf, compiled_regex, allow_list, ima_keyrings, dm_validator), }) # Iterative attestation may send us no log [len(lines) == 1]; compare last know PCR 10 state # against current PCR state. # Since IMA log append and PCR extend is not atomic, we may get a quote that does not yet take # into account the next appended measurement's [len(lines) == 2] PCR extension. if not found_pcr and len(lines) <= 2: found_pcr = running_hash == pcrval_bytes for linenum, line in enumerate(lines): # remove only the newline character, as there can be the space # as the delimiter character followed by an empty field at the # end line = line.strip("\n") if line == "": continue try: entry = ast.Entry(line, ima_validator, ima_hash_alg=ima_log_hash_alg, pcr_hash_alg=hash_alg) # update hash running_hash = hash_alg.hash(running_hash + entry.pcr_template_hash) validation_failure = entry.invalid() if validation_failure: failure.merge(validation_failure) errors[type(entry.mode)] = errors.get(type(entry.mode), 0) + 1 if not found_pcr: # End of list should equal pcr value found_pcr = running_hash == pcrval_bytes if found_pcr: logger.debug("Found match at linenum %s", linenum + 1) # We always want to have the very last line for the attestation, so # we keep the previous runninghash, which is not the last one! agentAttestState.update_ima_attestation( int(entry.pcr), running_hash, linenum + 1) if dm_validator: agentAttestState.set_ima_dm_state( dm_validator.state_dump()) # Keep old functionality for writing the parsed files with hashes into a file if m2w is not None and (type(entry.mode) in [ast.Ima, ast.ImaNg, ast.ImaSig]): hash_value = codecs.encode(entry.mode.digest.bytes, "hex") path = entry.mode.path.name m2w.write(f"{hash_value} {path}\n") except ast.ParserError: failure.add_event( "entry", f"Line was not parsable into a valid IMA entry: {line}", True, ["parser"]) logger.error("Line was not parsable into a valid IMA entry: %s", line) # check PCR value has been found if not found_pcr: logger.error("IMA measurement list does not match TPM PCR %s", pcrval) failure.add_event( "pcr_mismatch", f"IMA measurement list does not match TPM PCR {pcrval}", True) # Check if any validators failed if sum(errors.values()) > 0: error_msg = "IMA ERRORS: Some entries couldn't be validated. Number of failures in modes: " error_msg += ", ".join( [f"{k.__name__ } {v}" for k, v in errors.items()]) logger.error("%s.", error_msg) return codecs.encode(running_hash, "hex").decode("utf-8"), failure
def process_quote_response(agent, json_response, agentAttestState) -> Failure: """Validates the response from the Cloud agent. This method invokes an Registrar Server call to register, and then check the quote. """ failure = Failure(Component.QUOTE_VALIDATION) received_public_key = None quote = None # in case of failure in response content do not continue try: received_public_key = json_response.get("pubkey", None) quote = json_response["quote"] ima_measurement_list = json_response.get("ima_measurement_list", None) ima_measurement_list_entry = json_response.get( "ima_measurement_list_entry", 0) mb_measurement_list = json_response.get("mb_measurement_list", None) boottime = json_response.get("boottime", 0) logger.debug("received quote: %s", quote) logger.debug("for nonce: %s", agent['nonce']) logger.debug("received public key: %s", received_public_key) logger.debug("received ima_measurement_list %s", (ima_measurement_list is not None)) logger.debug("received ima_measurement_list_entry: %d", ima_measurement_list_entry) logger.debug("received boottime: %s", boottime) logger.debug("received boot log %s", (mb_measurement_list is not None)) except Exception as e: failure.add_event("invalid_data", { "message": "parsing agents get quote respone failed", "data": e }, False) return failure # TODO: Are those separate failures? if not isinstance(ima_measurement_list_entry, int): raise Exception( "ima_measurement_list_entry parameter must be an integer") if not isinstance(boottime, int): raise Exception("boottime parameter must be an integer") # if no public key provided, then ensure we have cached it if received_public_key is None: if agent.get('public_key', "") == "" or agent.get( 'b64_encrypted_V', "") == "": logger.error( "agent did not provide public key and no key or encrypted_v was cached at CV" ) failure.add_event( "no_pubkey", "agent did not provide public key and no key or encrypted_v was cached at CV", False) return failure agent['provide_V'] = False received_public_key = agent['public_key'] hash_alg = json_response.get('hash_alg') enc_alg = json_response.get('enc_alg') sign_alg = json_response.get('sign_alg') # Update chosen tpm and algorithms agent['hash_alg'] = hash_alg agent['enc_alg'] = enc_alg agent['sign_alg'] = sign_alg # Ensure hash_alg is in accept_tpm_hash_alg list if not algorithms.is_accepted(hash_alg, agent['accept_tpm_hash_algs'])\ or not algorithms.Hash.is_recognized(hash_alg): logger.error("TPM Quote is using an unaccepted hash algorithm: %s", hash_alg) failure.add_event( "invalid_hash_alg", { "message": f"TPM Quote is using an unaccepted hash algorithm: {hash_alg}", "data": hash_alg }, False) return failure # Ensure enc_alg is in accept_tpm_encryption_algs list if not algorithms.is_accepted(enc_alg, agent['accept_tpm_encryption_algs']): logger.error( "TPM Quote is using an unaccepted encryption algorithm: %s", enc_alg) failure.add_event( "invalid_enc_alg", { "message": f"TPM Quote is using an unaccepted encryption algorithm: {enc_alg}", "data": enc_alg }, False) return failure # Ensure sign_alg is in accept_tpm_encryption_algs list if not algorithms.is_accepted(sign_alg, agent['accept_tpm_signing_algs']): logger.error("TPM Quote is using an unaccepted signing algorithm: %s", sign_alg) failure.add_event( "invalid_sign_alg", { "message": f"TPM Quote is using an unaccepted signing algorithm: {sign_alg}", "data": {sign_alg} }, False) return failure if ima_measurement_list_entry == 0: agentAttestState.reset_ima_attestation() elif ima_measurement_list_entry != agentAttestState.get_next_ima_ml_entry( ): # If we requested a particular entry number then the agent must return either # starting at 0 (handled above) or with the requested number. logger.error( "Agent did not respond with requested next IMA measurement list entry %s but started at %s", agentAttestState.get_next_ima_ml_entry(), ima_measurement_list_entry) failure.add_event( "invalid_ima_entry_nb", { "message": "Agent did not respond with requested next IMA measurement list entry", "got": ima_measurement_list_entry, "expected": agentAttestState.get_next_ima_ml_entry() }, False) elif not agentAttestState.is_expected_boottime(boottime): # agent sent a list not starting at 0 and provided a boottime that doesn't # match the expected boottime, so it must have been rebooted; we would fail # attestation this time so we retry with a full attestation next time. agentAttestState.reset_ima_attestation() return failure agentAttestState.set_boottime(boottime) ima_keyrings = agentAttestState.get_ima_keyrings() tenant_keyring = file_signatures.ImaKeyring.from_string( agent['ima_sign_verification_keys']) ima_keyrings.set_tenant_keyring(tenant_keyring) quote_validation_failure = get_tpm_instance().check_quote( agentAttestState, agent['nonce'], received_public_key, quote, agent['ak_tpm'], agent['tpm_policy'], ima_measurement_list, agent['allowlist'], algorithms.Hash(hash_alg), ima_keyrings, mb_measurement_list, agent['mb_refstate'], compressed=(agent['supported_version'] == "1.0") ) # TODO: change this to always False after initial update failure.merge(quote_validation_failure) if not failure: # set a flag so that we know that the agent was verified once. # we only issue notifications for agents that were at some point good agent['first_verified'] = True # has public key changed? if so, clear out b64_encrypted_V, it is no longer valid if received_public_key != agent.get('public_key', ""): agent['public_key'] = received_public_key agent['b64_encrypted_V'] = "" agent['provide_V'] = True # ok we're done return failure
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 validate(self, digest: ast.Digest, path: ast.Name, data: ast.Buffer) -> Failure: """Validate a single entry.""" failure = Failure(Component.IMA, ["validation", "dm"]) try: event = parse(data.data.decode("utf-8"), path.name) hash_alg = Hash(digest.algorithm) if digest.hash != hash_alg.hash(data.data): failure.add_event( "invalid_data", "hash in IMA log and of the actual data mismatch", True) match_key = self.policies["match_on"] if path.name == "dm_table_load": failure.merge( self.validate_table_load(event, match_key, digest)) elif path.name == "dm_device_resume": failure.merge(self.validate_device_resume(event, match_key)) elif path.name == "dm_device_remove": failure.merge(self.validate_device_remove(event, match_key)) elif path.name == "dm_device_rename": failure.merge(self.validate_device_rename(event, match_key)) elif path.name == "dm_table_clear": failure.merge(self.validate_table_clear(event, match_key)) elif path.name == "dm_target_update": failure.merge(self.validate_target_update(event, match_key)) else: failure.add_event("invalid_event_type", {"got": path.name}, True) except lark.exceptions.LarkError as e: failure.add_event("parsing_failed", f"Could not construct valid entry: {e}", True) return failure