/* Copyright 2013-2017 IBM Corp.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#ifndef pr_fmt
#define pr_fmt(fmt) "TPMREL: " fmt
#endif

#include <skiboot.h>
#include <device.h>

#include "spira.h"
#include "hdata.h"
#include "hdif.h"

static void tpmrel_add_firmware_event_log(const struct HDIF_common_hdr *hdif_hdr)
{
	const struct secureboot_tpm_info *stinfo;
	struct dt_node *xscom, *node;
	uint64_t addr;
	int count, i;
	unsigned int asize;

	/* Are the hdat values populated? */
	if (!HDIF_get_idata(hdif_hdr, TPMREL_IDATA_SECUREBOOT_TPM_INFO, &asize))
		return;
	if (asize < sizeof(struct HDIF_array_hdr)) {
		prlog(PR_ERR, "secureboot_tpm_info idata not populated\n");
		return;
	}

	count = HDIF_get_iarray_size(hdif_hdr, TPMREL_IDATA_SECUREBOOT_TPM_INFO);
	if (count > 1) {
		prlog(PR_ERR, "multiple TPM not supported, count=%d\n", count);
		return;
	}

	/*
	 * There can be multiple secureboot_tpm_info entries with each entry
	 * corresponding to a master processor that has a tpm device.
	 * This looks for the tpm node that supposedly exists under the xscom
	 * node associated with the respective chip_id.
	 */
	for (i = 0; i < count; i++) {

		stinfo = HDIF_get_iarray_item(hdif_hdr,
					      TPMREL_IDATA_SECUREBOOT_TPM_INFO,
					      i, NULL);

		/*
		 * If tpm is not present, hostboot creates an empty
		 * secureboot_tpm_info entry, but setting
		 * tpm_status=TPM_NOT_PRESENT
		 */
		if (stinfo->tpm_status == TPM_NOT_PRESENT)
			continue;

		xscom = find_xscom_for_chip(be32_to_cpu(stinfo->chip_id));
		if (xscom) {
			dt_for_each_node(xscom, node) {
				if (dt_has_node_property(node, "label", "tpm"))
					break;
			}

			if (node) {
				addr = (uint64_t) stinfo +
					be32_to_cpu(stinfo->srtm_log_offset);
				dt_add_property_u64s(node, "linux,sml-base", addr);
				dt_add_property_cells(node, "linux,sml-size",
						      be32_to_cpu(stinfo->srtm_log_size));

				if (stinfo->tpm_status == TPM_PRESENT_AND_NOT_FUNCTIONAL)
					dt_add_property_string(node, "status", "disabled");
			} else {
				/**
				 * @fwts-label HDATNoTpmForChipId
				 * @fwts-advice HDAT secureboot_tpm_info
				 * structure described a chip id, but no tpm
				 * node was found under that xscom chip id.
				 * This is most certainly a hostboot bug.
				 */
				prlog(PR_ERR, "TPM node not found for "
				      "chip_id=%d (HB bug)\n", stinfo->chip_id);
				continue;
			}
		} else {
			/**
			 * @fwts-label HDATBadChipIdForTPM
			 * @fwts-advice HDAT secureboot_tpm_info structure
			 * described a chip id, but the xscom node for the
			 * chip_id was not found.
			 * This is most certainly a firmware bug.
			 */
			prlog(PR_ERR, "xscom node not found for chip_id=%d\n",
			      stinfo->chip_id);
			continue;
		}
	}
}

static struct dt_node *get_hb_reserved_memory(const char *label)
{
	struct dt_node *node, *hb_reserved_mem;

	hb_reserved_mem = dt_find_by_path(dt_root, "/ibm,hostboot/reserved-memory");
	if (!hb_reserved_mem) {
		prlog(PR_DEBUG, "/ibm,hostboot/reserved-memory node not found\n");
		return NULL;
	}

	dt_for_each_node(hb_reserved_mem, node) {
		const char *prd_label;
		if (!dt_find_property(node, "ibm,prd-label"))
			continue;
		prd_label = dt_prop_get(node, "ibm,prd-label");
		if (!strcmp(prd_label, label))
			return node;
	}
	return NULL;
}

struct {
	uint32_t type;
	const char *compat;
} cvc_services[] = {
	{ TPMREL_HV_SHA512, "ibm,cvc-sha512" },
	{ TPMREL_HV_VERIFY, "ibm,cvc-verify" },
};

static const char* cvc_service_map_compat(uint32_t type) {
	int i;
	for (i = 0; i < ARRAY_SIZE(cvc_services); i++) {
		if (cvc_services[i].type == type)
			return cvc_services[i].compat;
	}
	return NULL;
}

static void tpmrel_cvc_init(struct HDIF_common_hdr *hdif_hdr)
{
	struct dt_node *cvc_reserved_mem, *node, *parent;
	int count, i;
	unsigned int asize;

	/* Are the hdat values populated? */
	if (!HDIF_get_idata(hdif_hdr, TPMREL_IDATA_HASH_VERIF_OFFSETS, &asize))
		return;
	if (asize < sizeof(struct HDIF_array_hdr)) {
		prlog(PR_ERR, "hash_and_verification idata not populated\n");
		return;
	}

	node = dt_find_by_path(dt_root, "/ibm,secureboot");
	if (!node)
		return;

	cvc_reserved_mem = get_hb_reserved_memory("ibm,secure-crypt-algo-code");
	if (!cvc_reserved_mem) {
		prlog(PR_ERR, "CVC reserved memory not found\n");
		return;
	}

	parent = dt_new(node, "ibm,cvc");
	assert(parent);
	dt_add_property_cells(parent, "#address-cells", 1);
	dt_add_property_cells(parent, "#size-cells", 0);
	dt_add_property_strings(parent, "compatible", "ibm,container-verification-code");
	dt_add_property_cells(parent, "memory-region", cvc_reserved_mem->phandle);

	/*
	 * Initialize each service provided by the container verification code
	 */
	count = HDIF_get_iarray_size(hdif_hdr, TPMREL_IDATA_HASH_VERIF_OFFSETS);
	if (count <= 0 ) {
		prlog(PR_ERR, "no CVC service found\n");
		return;
	}

	for (i = 0; i < count; i++) {
		const struct hash_and_verification *hv;
		uint32_t type, offset, version;
		const char *compat;

		hv = HDIF_get_iarray_item(hdif_hdr,
					  TPMREL_IDATA_HASH_VERIF_OFFSETS,
					  i, NULL);
		type = be32_to_cpu(hv->type);
		offset = be32_to_cpu(hv->offset);
		version = be32_to_cpu(hv->version);

		compat = cvc_service_map_compat(type);

		if (!compat) {
			prlog(PR_WARNING, "CVC service type 0x%x unknown\n", type);
			continue;
		}

		node = dt_new_addr(parent, "ibm,cvc-service", offset);
		dt_add_property_strings(node, "compatible", compat);
		dt_add_property_cells(node, "reg", offset);
		dt_add_property_cells(node, "version", version);
	}
}

void node_stb_parse(void)
{
	struct HDIF_common_hdr *hdif_hdr;

	hdif_hdr = get_hdif(&spira.ntuples.node_stb_data, "TPMREL");
	if (!hdif_hdr) {
		prlog(PR_DEBUG, "TPMREL data not found\n");
		return;
	}

	tpmrel_add_firmware_event_log(hdif_hdr);
	tpmrel_cvc_init(hdif_hdr);
}
