def get_validator(self, class_type: typing.Type['Mode']) -> Callable[..., Failure]: validator = self.functions.get(class_type, None) if validator is None: logger.warning("No validator was implemented for: %s. Using always false validator!", class_type) failure = Failure(Component.IMA, ["validation"]) failure.add_event("no_validator", f"No validator was implemented for: {class_type} . Using always false validator!", True) return lambda *_: failure return validator
def process_measurement_list( agentAttestState, lines, lists=None, m2w=None, pcrval=None, ima_keyrings=None, boot_aggregates=None, hash_alg=algorithms.Hash.SHA1, ): failure = Failure(Component.IMA) try: running_hash, failure = _process_measurement_list( agentAttestState, lines, hash_alg, lists=lists, m2w=m2w, pcrval=pcrval, ima_keyrings=ima_keyrings, boot_aggregates=boot_aggregates, ) except: # pylint: disable=try-except-raise raise finally: if failure: # TODO currently reset on any failure which might be an issue agentAttestState.reset_ima_attestation() return running_hash, failure
def _validate_ima_buf( exclude_regex, allowlist, ima_keyrings: file_signatures.ImaKeyrings, dm_validator: Optional[ima_dm.DmIMAValidator], digest: ast.Digest, path: ast.Name, data: ast.Buffer, ): failure = Failure(Component.IMA) # Is data.data a key? pubkey, keyidv2 = file_signatures.get_pubkey(data.data) if pubkey: ignored_keyrings = allowlist["ima"]["ignored_keyrings"] if "*" not in ignored_keyrings and path.name not in ignored_keyrings: failure = _validate_ima_ng(exclude_regex, allowlist, digest, path, hash_types="keyrings") if not failure: # Add the key only now that it's validated (no failure) ima_keyrings.add_pubkey_to_keyring(pubkey, path.name, keyidv2=keyidv2) # Check if this is a device mapper entry only if we have a validator for that elif dm_validator is not None and path.name in dm_validator.valid_names: failure = dm_validator.validate(digest, path, data) else: # handling of generic ima-buf entries that for example carry a hash in the buf field failure = _validate_ima_ng(exclude_regex, allowlist, digest, path, hash_types="ima-buf") # Anything else evaluates to true for now return failure
def _validate_ima_buf(exclude_regex, allowlist, ima_keyrings: ima_file_signatures.ImaKeyrings, digest: ima_ast.Digest, path: ima_ast.Name, data: ima_ast.Buffer): failure = Failure(Component.IMA) # Is data.data a key? pubkey, keyidv2 = ima_file_signatures.get_pubkey(data.data) if pubkey: ignored_keyrings = allowlist['ima']['ignored_keyrings'] if '*' not in ignored_keyrings and path.name not in ignored_keyrings: failure = _validate_ima_ng(exclude_regex, allowlist, digest, path, hash_types='keyrings') if not failure: # Add the key only now that it's validated (no failure) ima_keyrings.add_pubkey_to_keyring(pubkey, path.name, keyidv2=keyidv2) else: # handling of generic ima-buf entries that for example carry a hash in the buf field failure = _validate_ima_ng(exclude_regex, allowlist, digest, path, hash_types='ima-buf') # Anything else evaluates to true for now 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 validate_target_update(self, event: "UpdateEvent", match_key) -> Failure: failure = Failure(Component.IMA, ["validation", "dm", "dm_target_update"]) device_key = getattr(event.device_metadata, match_key) device_state = self.devices.get(device_key, None) if device_state is None: failure.add_event("update_before_table_load", "Update event before table was loaded", True) return failure policy = self.policies["rules"][device_state.policy_name] target_data = policy["table_load"]["targets"][ event.target.target_index] # Validate the target data again self.validate_target_table(event.target, target_data, failure) return failure
def _validate_ima_sig(exclude_regex, ima_keyrings, allowlist, digest: ima_ast.Digest, path: ima_ast.Name, signature: ima_ast.Signature) -> Failure: failure = Failure(Component.IMA, ["validator", "ima-sig"]) valid_signature = False if ima_keyrings and signature: if exclude_regex is not None and exclude_regex.match(path.name): logger.debug(f"IMA: ignoring excluded path {path.name}") return failure if not ima_keyrings.integrity_digsig_verify(signature.data, digest.hash, digest.algorithm): logger.warning(f"signature for file {path.name} is not valid") failure.add_event("invalid_signature", f"signature for file {path.name} is not valid", True) return failure valid_signature = True logger.debug("signature for file %s is good" % path) # If there is also an allowlist verify the file against that but only do this if: # - we did not evaluate the signature (valid_siganture = False) # - the signature is valid and the file is also in the allowlist if allowlist is not None and allowlist.get('hashes') is not None and \ ((allowlist['hashes'].get(path.name, None) is not None and valid_signature) or not valid_signature): # We use the normal ima_ng validator to validate hash return _validate_ima_ng(exclude_regex, allowlist, digest, path) # If we don't have a allowlist and don't have a keyring we just ignore the validation. if ima_keyrings is None: return failure if not valid_signature: failure.add_event("invalid_signature", f"signature for file {path.name} could not be validated", True) return failure
def evaluate_policy(mb_policy, mb_policy_name, mb_refstate_data, mb_measurement_data, pcrsInQuote, pcrPrefix, agent_id) -> Failure: failure = Failure(Component.MEASURED_BOOT) missing = list(set(config.MEASUREDBOOT_PCRS).difference(pcrsInQuote)) if len(missing) > 0: logger.error("%sPCRs specified for measured boot not in quote: %s", pcrPrefix, missing) failure.add_event("missing_pcrs", { "context": "PCRs are missing in quote", "data": missing }, True) try: reason = mb_policy.evaluate(mb_refstate_data, mb_measurement_data) except Exception as exn: reason = f"policy evaluation failed: {str(exn)}" if reason: logger.error( "Boot attestation failed for agent %s, policy %s, refstate=%s, reason=%s", agent_id, mb_policy_name, json.dumps(mb_refstate_data), reason, ) failure.add_event( f"policy_{mb_policy_name}", { "context": "Boot attestation failed", "policy": mb_policy_name, "refstate": mb_refstate_data, "reason": reason, }, True, ) return failure
def _validate_ima_ng(exclude_regex, allowlist, digest: ast.Digest, path: ast.Name, hash_types="hashes") -> Failure: failure = Failure(Component.IMA, ["validation", "ima-ng"]) if allowlist is not None: if exclude_regex is not None and exclude_regex.match(path.name): logger.debug("IMA: ignoring excluded path %s", path) return failure accept_list = allowlist[hash_types].get(path.name, None) if accept_list is None: logger.warning("File not found in allowlist: %s", path.name) failure.add_event("not_in_allowlist", f"File not found in allowlist: {path.name}", True) return failure if codecs.encode(digest.hash, "hex").decode("utf-8") not in accept_list: logger.warning( "Hashes for file %s don't match %s not in %s", path.name, codecs.encode(digest.hash, "hex").decode("utf-8"), accept_list, ) failure.add_event( "allowlist_hash", { "message": "Hash not in allowlist found", "got": codecs.encode(digest.hash, "hex").decode("utf-8"), "expected": accept_list, }, True, ) return failure return failure
def validate_target_table(target, reference_values, failure: Failure) -> None: """ Validates a target table entry against reference_values in a policy. If a failure occurs it is added to the failure object. """ valid = True for key in reference_values.keys(): data = target # Non default target arguments are stored in target_attributes if key not in [ "target_index", "target_begin", "target_len", "target_name", "target_version" ]: data = target.target_attributes try: if not _check_attr(getattr(data, key), reference_values[key]): failure.add_event( "target_data_mismatch", { "got": getattr(data, key), "expected": reference_values[key], "context": key }, True, ) valid = False except AttributeError as e: failure.add_event( "target_attribute_not_found", {"context": f"Key {key} not found on target: {e}"}, True) if not valid: failure.add_event("target_data_invalid", "target data was not valid", True)
def validate_device_remove(self, event: "RemoveEvent", match_key) -> Failure: failure = Failure(Component.IMA, ["validation", "dm", "dm_device_remove"]) # TODO: check if we can always use the active table device_key = getattr(event.device_active_metadata, match_key) device_state = self.devices.get(device_key, None) if device_state is None: failure.add_event("remove_before_table_load", "Remove event before table was loaded", True) return failure policy = self.policies["rules"][device_state.policy_name] if not policy["device_remove"]["allow_removal"]: failure.add_event( "device_removed", f"Device {device_key} was remove, but that is not allowed", True) device_state.valid_state = False return failure # Remove device completely del self.devices[device_key] return failure
def validate_device_rename(self, event: "RenameEvent", match_key) -> Failure: failure = Failure(Component.IMA, ["validation", "dm", "dm_device_rename"]) device_key = getattr(event.device_metadata, match_key) device_state = self.devices.get(device_key, None) if device_state is None: failure.add_event("rename_before_table_load", "Rename event before table was loaded", True) return failure policy = self.policies["rules"][device_state.policy_name] # Only check if the name changed if event.device_metadata.name != event.new_name: if not _check_attr(event.new_name, policy["device_rename"]["valid_name"]): failure.add_event( "new_name_invalid", { "message": "New name is invalid", "got": event.new_name, "expected": policy["device_rename"]["valid_name"], }, True, ) device_state.valid_state = False elif match_key == "name": self.devices[event.new_name] = self.devices.pop(device_key) # Only check if uuid changed if event.device_metadata.uuid != event.new_uuid: if not _check_attr(event.new_uuid, policy["device_rename"]["valid_uuid"]): failure.add_event( "new_uuid_invalid", { "message": "New name is invalid", "got": event.new_uuid, "expected": policy["device_rename"]["valid_uuid"], }, True, ) device_state.valid_state = False elif match_key == "uuid": self.devices[event.new_name] = self.devices.pop(device_key) return failure
def validate_device_resume(self, event: "ResumeEvent", match_key) -> Failure: failure = Failure(Component.IMA, ["validation", "dm", "dm_device_resume"]) device_key = getattr(event.device_metadata, match_key) if device_key not in self.devices: failure.add_event("resume_before_table_load", "Resume event before table was loaded", True) return failure device_state = self.devices[device_key] # We only expect one resume event if device_state.active: failure.add_event("already_active", "table is already active", True) device_state.valid_state = False return failure # Check if the table hash is consistent if device_state.active_table_hash != event.active_table_hash: failure.add_event( "active_table_mismatch", { "got": event.active_table_hash, "expected": device_state.active_table_hash, "context": "resume does not match the table", }, True, ) device_state.valid_state = False return failure # TODO: Check also current capacity. # Mark now device as active and in a valid state device_state.active = True device_state.valid_state = True return failure
def validate_table_clear(self, event: "ClearEvent", match_key) -> Failure: failure = Failure(Component.IMA, ["validation", "dm", "dm_table_clear"]) device_key = getattr(event.device_metadata, match_key) device_state = self.devices.get(device_key, None) if device_state is None: failure.add_event("clear_before_table_load", "Clear event before table was loaded", True) return failure policy = self.policies["rules"][device_state.policy_name] if not policy["allow_clear"]: failure.add_event( "table_cleared", f"Table for device {device_key} was cleared, but that is not allowed", True) device_state.valid_state = False return failure return failure
def invalid(self) -> Failure: """ Check if the devices are in a consistent state """ failure = Failure(Component.IMA, ["validation", "dm"]) used_policies = set() # Check if the devices are in a valid state for device_name, device in self.devices.items(): if not device.valid_state: failure.add_event("device_invalid_state", {"context": device_name}, True) else: # Only add polices that are on valid states used_policies.add(device.policy_name) # Check for required active policies for policy_name, policy in self.policies["rules"].items(): if policy["required"] and policy_name not in used_policies: failure.add_event( "required_policy_not_in_use", f"policy {policy_name} required but not used", True) 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 _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 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 _true(*_): return Failure(Component.DEFAULT)
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 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
def validate_table_load(self, event: "LoadEvent", match_key, digest) -> Failure: failure = Failure(Component.IMA, ["validation", "dm", "dm_table_load"]) device_key = getattr(event.device_metadata, match_key) if device_key in self.devices and not self.devices[ device_key].allow_multiple_loads: failure.add_event( "multiple_table_loads", f"Multiple table load entries for device: {device_key}", True) return failure # Find matching policy used_policy_name = None used_policy = None for policy_name, policy in self.policies["rules"].items(): if re.fullmatch(policy["table_load"][match_key], device_key): used_policy = policy used_policy_name = policy_name break if used_policy_name is None: failure.add_event("no_matching_policy", "No policy found", True) return failure # Validate device metadata for entry in [ "name", "uuid", "major", "minor", "minor_count", "num_targets" ]: if not _check_attr(getattr(event.device_metadata, entry), used_policy["table_load"][entry]): failure.add_event( "invalid_entry", { "got": getattr(event.device_metadata, entry), "expected": used_policy["table_load"][entry], "context": entry, }, True, ) return failure # Check "num_targets" # Note that we get actually could get multiple lines, but this does not happen for our use cases so we # treat it as a failure. if event.device_metadata.num_targets != len( event.targets) or used_policy["table_load"][entry] != len( event.targets): failure.add_event("num_targets_mismatch", "lengths are not consistent", True) return failure # Validate targets for actual_target, should_values in zip( event.targets, used_policy["table_load"]["targets"]): self.validate_target_table(actual_target, should_values, failure) # Add device to validator self.devices[device_key] = DeviceState( active=False, valid_state=not used_policy["device_resume_required"], # Device normally has a resume to be in a fully validated state active_table_hash=digest, inactive_table_hash=None, policy_name=used_policy_name, allow_multiple_loads=used_policy["table_load"] ["allow_multiple_loads"], num_targets=event.device_metadata.num_targets, ) return failure