// SPDX-License-Identifier: BSD-2-Clause-Patent
#include "shim.h"

typedef struct {
	CHAR16 *VariableName;
	EFI_GUID *VendorGuid;
	VOID *Data;
	UINTN Size;
} VARIABLE_RECORD;

UINTN measuredcount = 0;
VARIABLE_RECORD *measureddata = NULL;
static BOOLEAN tpm_defective = FALSE;
static BOOLEAN log_full_already_warned = FALSE;

static BOOLEAN tpm_present(efi_tpm_protocol_t *tpm)
{
	EFI_STATUS efi_status;
	TCG_EFI_BOOT_SERVICE_CAPABILITY caps;
	UINT32 flags;
	EFI_PHYSICAL_ADDRESS eventlog, lastevent;

	if (tpm_defective)
		return FALSE;

	caps.Size = (UINT8)sizeof(caps);
	efi_status = tpm->status_check(tpm, &caps, &flags,
				       &eventlog, &lastevent);
	if (EFI_ERROR(efi_status) ||
	    caps.TPMDeactivatedFlag || !caps.TPMPresentFlag)
		return FALSE;

	return TRUE;
}

static EFI_STATUS tpm2_get_caps(efi_tpm2_protocol_t *tpm,
				EFI_TCG2_BOOT_SERVICE_CAPABILITY *caps,
				BOOLEAN *old_caps)
{
	EFI_STATUS efi_status;

	caps->Size = (UINT8)sizeof(*caps);

	efi_status = tpm->get_capability(tpm, caps);
	if (EFI_ERROR(efi_status))
		return efi_status;

	if (caps->StructureVersion.Major == 1 &&
	    caps->StructureVersion.Minor == 0)
		*old_caps = TRUE;
	else
		*old_caps = FALSE;

	return EFI_SUCCESS;
}

static BOOLEAN tpm2_present(EFI_TCG2_BOOT_SERVICE_CAPABILITY *caps,
			    BOOLEAN old_caps)
{
	TREE_BOOT_SERVICE_CAPABILITY *caps_1_0;

	if (old_caps) {
		caps_1_0 = (TREE_BOOT_SERVICE_CAPABILITY *)caps;
		if (caps_1_0->TrEEPresentFlag)
			return TRUE;
	}

	if (caps->TPMPresentFlag)
		return TRUE;

	return FALSE;
}

static EFI_STATUS tpm_locate_protocol(efi_tpm_protocol_t **tpm,
				      efi_tpm2_protocol_t **tpm2,
				      BOOLEAN *old_caps_p,
				      EFI_TCG2_BOOT_SERVICE_CAPABILITY *capsp)
{
	EFI_STATUS efi_status;

	*tpm = NULL;
	*tpm2 = NULL;
	efi_status = LibLocateProtocol(&EFI_TPM2_GUID, (VOID **)tpm2);
	/* TPM 2.0 */
	if (!EFI_ERROR(efi_status)) {
		BOOLEAN old_caps;
		EFI_TCG2_BOOT_SERVICE_CAPABILITY caps;

		efi_status = tpm2_get_caps(*tpm2, &caps, &old_caps);
		if (EFI_ERROR(efi_status))
			return efi_status;

		if (tpm2_present(&caps, old_caps)) {
			if (old_caps_p)
				*old_caps_p = old_caps;
			if (capsp)
				memcpy(capsp, &caps, sizeof(caps));
			return EFI_SUCCESS;
		}
	} else {
		efi_status = LibLocateProtocol(&EFI_TPM_GUID, (VOID **)tpm);
		if (EFI_ERROR(efi_status))
			return efi_status;

		if (tpm_present(*tpm))
			return EFI_SUCCESS;
	}

	return EFI_NOT_FOUND;
}

static void warn_first_log_full(void)
{
	if (!log_full_already_warned) {
		perror(L"TPM extend operation occurred, but the event could"
		       " not be written to one or more event logs. Applications"
		       " reliant on a valid event log will not function.\n");
		log_full_already_warned = TRUE;
	}
}

static EFI_STATUS cc_log_event_raw(EFI_PHYSICAL_ADDRESS buf, UINTN size,
				   UINT8 pcr, const CHAR8 *log, UINTN logsize,
				   UINT32 type, BOOLEAN is_pe_image)
{
	EFI_STATUS efi_status;
	EFI_CC_EVENT *event;
	efi_cc_protocol_t *cc;
	EFI_CC_MR_INDEX mr;
	uint64_t flags = is_pe_image ? EFI_CC_FLAG_PE_COFF_IMAGE : 0;

	efi_status = LibLocateProtocol(&EFI_CC_MEASUREMENT_PROTOCOL_GUID,
				       (VOID **)&cc);
	if (EFI_ERROR(efi_status) || !cc)
		return EFI_SUCCESS;

	efi_status = cc->map_pcr_to_mr_index(cc, pcr, &mr);
	if (EFI_ERROR(efi_status))
		return EFI_NOT_FOUND;

	UINTN event_size = sizeof(*event) - sizeof(event->Event) + logsize;

	event = AllocatePool(event_size);
	if (!event) {
		perror(L"Unable to allocate event structure\n");
		return EFI_OUT_OF_RESOURCES;
	}

	event->Header.HeaderSize = sizeof(EFI_CC_EVENT_HEADER);
	event->Header.HeaderVersion = EFI_CC_EVENT_HEADER_VERSION;
	event->Header.MrIndex = mr;
	event->Header.EventType = type;
	event->Size = event_size;
	CopyMem(event->Event, (VOID *)log, logsize);
	efi_status = cc->hash_log_extend_event(cc, flags, buf, (UINT64)size,
					       event);
	/* Per spec: The extend operation occurred, but the event could
	 * not be written to one or more event logs. We can still safely
	 * boot in this case, but also show a warning to let the user know.
	 */
	if (efi_status == EFI_VOLUME_FULL) {
		warn_first_log_full();
		efi_status = EFI_SUCCESS;
	}
	FreePool(event);
	return efi_status;
}

static EFI_STATUS tpm_log_event_raw(EFI_PHYSICAL_ADDRESS buf, UINTN size,
				    UINT8 pcr, const CHAR8 *log, UINTN logsize,
				    UINT32 type, CHAR8 *hash)
{
	EFI_STATUS efi_status;
	efi_tpm_protocol_t *tpm;
	efi_tpm2_protocol_t *tpm2;
	BOOLEAN old_caps;
	EFI_TCG2_BOOT_SERVICE_CAPABILITY caps;

	/* CC guest like TDX or SEV will measure the buffer and log the event,
	   extend the result into a specific CC MR like TCG's PCR. It could
	   coexists with TCG's TPM 1.2 and TPM 2.
	*/
	efi_status = cc_log_event_raw(buf, size, pcr, log, logsize, type,
				      (hash != NULL));
	if (EFI_ERROR(efi_status))
		return efi_status;

	efi_status = tpm_locate_protocol(&tpm, &tpm2, &old_caps, &caps);
	if (EFI_ERROR(efi_status)) {
#ifdef REQUIRE_TPM
		perror(L"TPM logging failed: %r\n", efi_status);
		return efi_status;
#else
		if (efi_status != EFI_NOT_FOUND) {
			perror(L"TPM logging failed: %r\n", efi_status);
			return efi_status;
		}
#endif
	} else if (tpm2) {
		EFI_TCG2_EVENT *event;
		UINTN event_size = sizeof(*event) - sizeof(event->Event) +
			logsize;

		event = AllocatePool(event_size);
		if (!event) {
			perror(L"Unable to allocate event structure\n");
			return EFI_OUT_OF_RESOURCES;
		}

		event->Header.HeaderSize = sizeof(EFI_TCG2_EVENT_HEADER);
		event->Header.HeaderVersion = 1;
		event->Header.PCRIndex = pcr;
		event->Header.EventType = type;
		event->Size = event_size;
		CopyMem(event->Event, (VOID *)log, logsize);
		if (hash) {
			/* TPM 2 systems will generate the appropriate hash
			   themselves if we pass PE_COFF_IMAGE.  In case that
			   fails we fall back to measuring without it.
			*/
			efi_status = tpm2->hash_log_extend_event(tpm2,
				PE_COFF_IMAGE, buf, (UINT64) size, event);
			if (efi_status == EFI_VOLUME_FULL) {
				warn_first_log_full();
				efi_status = EFI_SUCCESS;
			}
		}

	        if (!hash || EFI_ERROR(efi_status)) {
			efi_status = tpm2->hash_log_extend_event(tpm2,
				0, buf, (UINT64) size, event);
			if (efi_status == EFI_VOLUME_FULL) {
				warn_first_log_full();
				efi_status = EFI_SUCCESS;
			}
		}
		FreePool(event);
		return efi_status;
	} else if (tpm) {
		TCG_PCR_EVENT *event;
		UINT32 eventnum = 0;
		EFI_PHYSICAL_ADDRESS lastevent;

		efi_status = LibLocateProtocol(&EFI_TPM_GUID, (VOID **)&tpm);
		if (EFI_ERROR(efi_status))
			return EFI_SUCCESS;

		if (!tpm_present(tpm))
			return EFI_SUCCESS;

		event = AllocatePool(sizeof(*event) + logsize);

		if (!event) {
			perror(L"Unable to allocate event structure\n");
			return EFI_OUT_OF_RESOURCES;
		}

		event->PCRIndex = pcr;
		event->EventType = type;
		event->EventSize = logsize;
		CopyMem(event->Event, (VOID *)log, logsize);
		if (hash) {
			/* TPM 1.2 devices require us to pass the Authenticode
			   hash rather than allowing the firmware to attempt
			   to calculate it */
			CopyMem(event->digest, hash, sizeof(event->digest));
			efi_status = tpm->log_extend_event(tpm, 0, 0,
				TPM_ALG_SHA, event, &eventnum, &lastevent);
			if (efi_status == EFI_VOLUME_FULL) {
				warn_first_log_full();
				efi_status = EFI_SUCCESS;
			}
		} else {
			efi_status = tpm->log_extend_event(tpm, buf,
				(UINT64)size, TPM_ALG_SHA, event, &eventnum,
				&lastevent);
			if (efi_status == EFI_VOLUME_FULL) {
				warn_first_log_full();
				efi_status = EFI_SUCCESS;
			}
		}
		if (efi_status == EFI_UNSUPPORTED) {
			perror(L"Could not write TPM event: %r. Considering "
			       "the TPM as defective.\n", efi_status);
			tpm_defective = TRUE;
			efi_status = EFI_SUCCESS;
		}
		FreePool(event);
		return efi_status;
	}

	return EFI_SUCCESS;
}

EFI_STATUS tpm_log_event(EFI_PHYSICAL_ADDRESS buf, UINTN size, UINT8 pcr,
			 const CHAR8 *description)
{
	return tpm_log_event_raw(buf, size, pcr, description,
				 strlen(description) + 1, EV_IPL, NULL);
}

EFI_STATUS tpm_log_pe(EFI_PHYSICAL_ADDRESS buf, UINTN size,
		      EFI_PHYSICAL_ADDRESS addr, EFI_DEVICE_PATH *path,
		      UINT8 *sha1hash, UINT8 pcr)
{
	EFI_IMAGE_LOAD_EVENT *ImageLoad = NULL;
	EFI_STATUS efi_status;
	UINTN path_size = 0;

	if (path)
		path_size = DevicePathSize(path);

	ImageLoad = AllocateZeroPool(sizeof(*ImageLoad) + path_size);
	if (!ImageLoad) {
		perror(L"Unable to allocate image load event structure\n");
		return EFI_OUT_OF_RESOURCES;
	}

	ImageLoad->ImageLocationInMemory = buf;
	ImageLoad->ImageLengthInMemory = size;
	ImageLoad->ImageLinkTimeAddress = addr;

	if (path_size > 0) {
		CopyMem(ImageLoad->DevicePath, path, path_size);
		ImageLoad->LengthOfDevicePath = path_size;
	}

	efi_status = tpm_log_event_raw(buf, size, pcr, (CHAR8 *)ImageLoad,
				       sizeof(*ImageLoad) + path_size,
				       EV_EFI_BOOT_SERVICES_APPLICATION,
				       (CHAR8 *)sha1hash);
	FreePool(ImageLoad);

	return efi_status;
}

typedef struct {
	EFI_GUID VariableName;
	UINT64 UnicodeNameLength;
	UINT64 VariableDataLength;
	CHAR16 UnicodeName[1];
	INT8 VariableData[1];
} __attribute__ ((packed)) EFI_VARIABLE_DATA_TREE;

static BOOLEAN tpm_data_measured(CHAR16 *VarName, EFI_GUID VendorGuid, UINTN VarSize, VOID *VarData)
{
	UINTN i;

	for (i=0; i<measuredcount; i++) {
		if ((StrCmp (VarName, measureddata[i].VariableName) == 0) &&
		    (CompareGuid (&VendorGuid, measureddata[i].VendorGuid) == 0) &&
		    (VarSize == measureddata[i].Size) &&
		    (CompareMem (VarData, measureddata[i].Data, VarSize) == 0)) {
			return TRUE;
		}
	}

	return FALSE;
}

static EFI_STATUS tpm_record_data_measurement(CHAR16 *VarName, EFI_GUID VendorGuid, UINTN VarSize, VOID *VarData)
{
	if (measureddata == NULL) {
		measureddata = AllocatePool(sizeof(*measureddata));
	} else {
		measureddata = ReallocatePool(measureddata, measuredcount * sizeof(*measureddata),
					      (measuredcount + 1) * sizeof(*measureddata));
	}

	if (measureddata == NULL)
		return EFI_OUT_OF_RESOURCES;

	measureddata[measuredcount].VariableName = AllocatePool(StrSize(VarName));
	measureddata[measuredcount].VendorGuid = AllocatePool(sizeof(EFI_GUID));
	measureddata[measuredcount].Data = AllocatePool(VarSize);

	if (measureddata[measuredcount].VariableName == NULL ||
	    measureddata[measuredcount].VendorGuid == NULL ||
	    measureddata[measuredcount].Data == NULL) {
		return EFI_OUT_OF_RESOURCES;
	}

	StrCpy(measureddata[measuredcount].VariableName, VarName);
	CopyMem(measureddata[measuredcount].VendorGuid, &VendorGuid, sizeof(EFI_GUID));
	CopyMem(measureddata[measuredcount].Data, VarData, VarSize);
	measureddata[measuredcount].Size = VarSize;
	measuredcount++;

	return EFI_SUCCESS;
}

EFI_STATUS tpm_measure_variable(CHAR16 *VarName, EFI_GUID VendorGuid, UINTN VarSize, VOID *VarData)
{
	EFI_STATUS efi_status;
	UINTN VarNameLength;
	EFI_VARIABLE_DATA_TREE *VarLog;
	UINT32 VarLogSize;

	/* Don't measure something that we've already measured */
	if (tpm_data_measured(VarName, VendorGuid, VarSize, VarData))
		return EFI_SUCCESS;

	VarNameLength = StrLen (VarName);
	VarLogSize = (UINT32)(sizeof (*VarLog) +
			      VarNameLength * sizeof (*VarName) +
			      VarSize -
			      sizeof (VarLog->UnicodeName) -
			      sizeof (VarLog->VariableData));

	VarLog = (EFI_VARIABLE_DATA_TREE *) AllocateZeroPool (VarLogSize);
	if (VarLog == NULL) {
		return EFI_OUT_OF_RESOURCES;
	}

	CopyMem (&VarLog->VariableName, &VendorGuid,
		 sizeof(VarLog->VariableName));
	VarLog->UnicodeNameLength  = VarNameLength;
	VarLog->VariableDataLength = VarSize;
	CopyMem (VarLog->UnicodeName, VarName,
		 VarNameLength * sizeof (*VarName));
	CopyMem ((CHAR16 *)VarLog->UnicodeName + VarNameLength, VarData,
		 VarSize);

	efi_status = tpm_log_event_raw((EFI_PHYSICAL_ADDRESS)(intptr_t)VarLog,
				       VarLogSize, 7, (CHAR8 *)VarLog, VarLogSize,
				       EV_EFI_VARIABLE_AUTHORITY, NULL);

	FreePool(VarLog);

	if (EFI_ERROR(efi_status))
		return efi_status;

	return tpm_record_data_measurement(VarName, VendorGuid, VarSize,
					   VarData);
}

EFI_STATUS
fallback_should_prefer_reset(void)
{
	EFI_STATUS efi_status;
	efi_tpm_protocol_t *tpm;
	efi_tpm2_protocol_t *tpm2;

	efi_status = tpm_locate_protocol(&tpm, &tpm2, NULL, NULL);
	if (EFI_ERROR(efi_status))
		return EFI_NOT_FOUND;
	return EFI_SUCCESS;
}

#ifdef SHIM_UNIT_TEST
static void DESTRUCTOR
tpm_clean_up_measurements(void)
{
	for (UINTN i = 0; i < measuredcount; i++) {
		VARIABLE_RECORD *vr = &measureddata[i];

		if (vr->VariableName)
			FreePool(vr->VariableName);
		if (vr->VendorGuid)
			FreePool(vr->VendorGuid);
		if (vr->Data)
			FreePool(vr->Data);
	}
	if (measureddata)
		FreePool(measureddata);

	measuredcount = 0;
	measureddata = NULL;
}
#endif
