/* octeontxotp: OcteonTX OTP FUSE read/write for secure boot
 */
#include <error.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <inttypes.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

#define debug(args ...) if (verbose) printf(args)

#define ARRAY_SIZE(arr) (int)(sizeof(arr) / sizeof((arr)[0]))
#define BIT(x)			(1 << x)

/* RESET registers */
#define RST_OCX			0x87e006001618
#define RST_SOFT_RST		0x87e006001680

/* FUSF registers */
#define FUSF_CTL		0x87e004000000
#define FUSF_ROTPK		0x87e004000060
#define FUSF_SSK		0x87e004000080
#define FUSF_HUK		0x87e004000090
#define FUSF_EK			0x87e0040000a0
#define FUSF_SW			0x87e0040000c0
#define FUSF_RCMD		0x87e004000100
#define FUSF_WADR		0x87e004000108
#define FUSF_PROG		0x87e004000110
#define FUSF_BNK_DATX(x)	(0x87e004000120 | (x * 8))

/* FUSF_CTL bits (all 0 by default) */
#define FUSF_CTL_ROM_CNT(x)	((uint64_t)(x & 0x7fff) << 32)
#define FUSF_CTL_CRYPT_SSK_DIS	BIT(15)	/* SSK crypt disable */
#define FUSF_CTL_CRYPT_NO_DIS	BIT(14)	/* no crypt disable */
#define FUSF_CTL_FJ_DIS_HUK	BIT(13)	/* flash-jump HUK hiding */
#define FUSF_CTL_FJ_CORE0	BIT(12)	/* flash-jump core 0 only */
#define FUSF_CTL_FJ_TIMEOUT(x)	(( x & 0x3) << 10) /* flash-jump timeout */
#define FUSF_CTL_FJ_DIS		BIT(9)	/* flash-jump disable */
#define FUSF_CTL_TZ_FORCE2	BIT(8)	/* disable trustzone */
#define FUSF_CTL_MFG_LCK	BIT(6)
#define FUSF_CTL_SW_LCK		BIT(3)	/* Locks SW bits */
#define FUSF_CTL_ROT_LCK	BIT(2)	/* Locks ROTPK, HUK, and EK */
#define FUSF_CTL_SSK_LCK	BIT(1)	/* Locks SSK */
#define FUSF_CTL_FUSF_LCK	BIT(0)	/* Lock all */

/* FUSF_PROG bits */
#define FUSF_PROG_PROG		BIT(0)
#define FUSF_PROG_SFT		BIT(1)
#define FUSF_PROG_PROG_PIN	BIT(2)
#define FUSF_PROG_VOLT_EN	BIT(3)

/* FUSF_RCMD bits */
#define FUSF_RCMD_PEND		BIT(12)
#define FUSF_RCMD_EFUSE		BIT(8)
#define FUSF_RCMD_ADDR(x)	((x & 0xff) << 0)	// 7:0
#define FUSF_RCMD_ADDR_HI(x)	((x & 0x7) << 9)	//10:9

struct fuse_block {
	const char *name;
	unsigned long reg;
	uint lock;
	uint offset;
	uint bits;
};

struct fuse_block fuses[] = {
 	/* ROTPK (Root of Trust) is a SHA256 hash */
	{ "ROTPK", FUSF_ROTPK, FUSF_CTL_ROT_LCK, 0x300, 256 },
	/* SSK (Secrect Symmetric Key) is a AES128 key */
	{ "SSK", FUSF_SSK, FUSF_CTL_SSK_LCK, 0x400, 128 },
	/* HUK (Hardware Unique Key) is a AES128 key */
	{ "HUK", FUSF_HUK, FUSF_CTL_ROT_LCK, 0x480, 128 },
	/* EK (ECC Private Endorsement) is a AES128 key */
	{ "EK",	FUSF_EK, FUSF_CTL_ROT_LCK, 0x500, 256 },
	/* SW (Software) is 512 bits */
	{ "SW",	FUSF_SW, FUSF_CTL_SW_LCK, 0x600, 512 },
};

static int verbose;
static int force;
static int hard;

static void usage(const char *cmd)
{
	fprintf(stderr, "\nUsage: %s [options]\n\n", cmd);
	fprintf(stderr, \
		"Show or blow (hard or soft) OcteonTX One Time Programable fuses.\n" \
		"By default these will be 'soft' blown which persist on a soft reset\n" \
		"unless the '--hard' option is used.\n" \
		"\n" \
		"Some settings require you to pass in the --force argument to verify\n" \
		"you understand the reprecusions of such actions.\n" \
		"\n" \
		"   --hard                          - hard blow fuse values\n" \
		"   --show                          - show fuse values\n" \
		"   --reset                         - perform a soft reset\n" \
		"   --force                         - force an operation\n" \
		"\n" \
		"   --rotpk 0x<256bit-fuse-hex-str> - program ROTPK SHA256 fuses\n" \
		"   --ssk 0x<128bit-fuse-hex-str>   - program SSK AES128 fuses\n" \
		"   --huk 0x<128bit-fuse-hex-str>   - program HUK AES128 fuses\n" \
		"   --ek 0x<256bit-fuse-hex-str>    - program EK 256bit fuses\n" \
		"   --sw 0x<512bit-fuse-hex-str>    - program SW 512bit fuses\n" \
		"\n" \
		"   --lock [all|rotpk|ssk|sw]       - lock fuses against future changes\n" \
		"   --rom_counter <val>             - set firmware counter (1-31)\n" \
		"   --crypt_ssk_dis                 - disable ssk key hiding\n" \
		"   --crypt_no_dis                  - disable no-crypt\n" \
		"   --fj_dis_huk                    - flash-jump and hide HUK\n" \
		"   --fj_core0                      - flash-jump core 0 only\n" \
		"   --fj_timeout <val>              - flash-jump timeout (0-3)\n" \
		"   --fj_dis                        - flash-jump disable\n" \
		"   --tz_force                      - enable secure boot\n" \
		"\n" \
	);

	exit(0);
}

/* Open devmem to map device memory to user space */
static int open_devmem () {
	int fd;

	fd = open("/dev/mem", O_RDWR | O_SYNC);
	if (fd < 0) {
		fprintf(stderr, "ERROR: Faied to open /dev/mem\n");
		return -1;
	}

	return fd;
}

/* Close devmem */
static int close_devmem (int fd)
{
	close (fd);

	return 0;
}

/*
 * Maps register hardware address to virtual user space accessible address.
 * Register address is mapped to user space from where register read/write
 * operation is performed.
 *
 * @reg_pa register h/w address
 * @fd     file discriptor to devmap
 *
 * returns: Virtual user space address to access register
 */
static void *map_reg_pa_to_va (uint64_t reg_pa, int fd)
{
	uint64_t reg_offset;
	uint8_t *hw_addr;
	off64_t  reg_base;
	size_t length;

	reg_offset = reg_pa % getpagesize();

	reg_base = reg_pa - reg_offset;
	length = getpagesize();

	hw_addr = mmap(NULL, length, PROT_READ|PROT_WRITE, MAP_SHARED, fd, reg_base);
	if (hw_addr == MAP_FAILED) {
		fprintf(stderr, "Mmap failed");
		exit(1);
	}

	debug("%s: pa=0x%llx va=0x%llx\n", __func__,
			(unsigned long long)reg_pa,
			(unsigned long long)(hw_addr + reg_offset));

	return (hw_addr + reg_offset);
}

/*
 * Read register value
 *
 * @reg_pa  : Register hardware address
 * @size    : Register size in bytes
 *
 * returns - register value
 */
static uint64_t _read_reg(uint64_t reg_pa, int size)
{
	int fd;
	volatile uint64_t *reg_va;
	uint64_t reg_val;

	fd = open_devmem();

	reg_va = map_reg_pa_to_va(reg_pa, fd);

	if (size == 8) {
		reg_val = *reg_va;
	} else if (size == 4) {
		volatile uint32_t *va_32bit = (volatile uint32_t *)reg_va;
		reg_val = *va_32bit;
	}
	debug("0x%016llx<=0x%016llx (%d)\n", (unsigned long long)reg_pa,
			(unsigned long long)reg_val, size);

	close_devmem(fd);

	return reg_val;
}
static uint64_t read_reg(uint64_t reg_pa)
{
	return _read_reg(reg_pa, 8);
}

/*
 * Writes register value if user enters register hardware address.
 *
 * @reg_pa   : Register hardware address
 * @reg_value: Value to be written to register
 * @size     : Register size in bytes
 *
 * returns: 0 on success
 */
static int _write_reg(uint64_t reg_pa, uint64_t reg_value, int size)
{
        int fd;
        volatile uint64_t *reg_va;

	debug("0x%016lx=>0x%016lx (%d)\n", reg_pa, reg_value, size);
        fd = open_devmem();

        reg_va = map_reg_pa_to_va(reg_pa, fd);

	if (size == 8) {
		*reg_va = reg_value;
	} else if (size == 4) {
		volatile uint32_t *va_32bit = (volatile uint32_t *)reg_va;
		*va_32bit = (uint32_t)reg_value;
	}

        close_devmem(fd);

        return 0;
}
static int write_reg(uint64_t reg_pa, uint64_t reg_value)
{
	return _write_reg(reg_pa, reg_value, 8);
}

/* Wait for a bit to be clear */
static int wait_for_bit_clr(uint64_t reg, uint64_t val, int timeout_ms)
{
	int i;

	for (i = 0; i < timeout_ms; i++) {
		if ((read_reg(reg) & val) == 0)
			return 0;
	}

	return -ETIMEDOUT;
}

/* Wait for a bit to be set */
static int wait_for_bit_set(uint64_t reg, uint64_t val, int timeout_ms)
{
	int i;

	for (i = 0; i < timeout_ms; i++) {
		if ((read_reg(reg) & val) == val)
			return 0;
	}

	return -ETIMEDOUT;
}

/* verify that force flag is set or exit */
static int verify_force(const char *msg) {
	if (hard && !force) {
		fprintf(stderr, "Error: %s\n\n", msg);
		fprintf(stderr,
			"If this is really waht you want to do " \
			"run with the --force argument\n\n");
		exit(1);
	}

	return 0;
}

static int fuse_field_blow(int fuse)
{
	int start_fuse = fuse & -128;
	int index = fuse & 127;
	uint64_t fuses0 = (index < 64) ? 1ull << index : 0;
	uint64_t fuses1 = (index >= 64) ? 1ull << (index - 64) : 0;
	int val;

	debug("%s 0x%x fuses0=0x%016lx fuses1=0x%016lx %s\n", __func__,
		start_fuse, fuses0, fuses1, hard ? "hard" : "soft");

	if (hard) {
		verify_force("HARD blowing fuses means you can no longer unset those fuses");
	}

	if ((start_fuse & 127) != 0) {
		fprintf(stderr, "FUSF: Start fuse must be on a 128 bit boundary\n");
		return -1;
	}

	/* Select the fuse bank. there are 128 fuses per bank */
	write_reg(FUSF_WADR, start_fuse / 128);

	/* Select the fuse to program in the bank */
	write_reg(FUSF_BNK_DATX(0), fuses0);
	write_reg(FUSF_BNK_DATX(1), fuses1);

	/* Enable the external programming voltage */
	val = 0;
	if (!hard)
		val |= FUSF_PROG_SFT;
	else
		val |= FUSF_PROG_VOLT_EN;
	write_reg(FUSF_PROG, val);

	if (hard)
	{
		/* Wait for the voltage input to be recognized */
		if (wait_for_bit_set(FUSF_PROG, FUSF_PROG_PROG_PIN, 1000))
		{
			/* Timeout waiting for voltage, disable it and fail */
			write_reg(FUSF_PROG, 0x0);
			fprintf(stderr, "FUSF: Programming voltage not detected\n");
			return -1;
		}
	}

	/* Program the fuses */
	val |= FUSF_PROG_PROG;
	write_reg(FUSF_PROG, val);

	/* Wait for the program to complete */
	if (wait_for_bit_clr(FUSF_PROG, FUSF_PROG_PROG, 100000)) {
		/* Timeout waiting for program, disable power and fail */
		write_reg(FUSF_PROG, 0x0);
		fprintf(stderr, "FUSF: Timeout programming fuse\n");
		return -1;
	}

	/* Disable the external programming voltage */
	write_reg(FUSF_PROG, 0x0);

	/* Wait for the voltage input to be removed */
	if (wait_for_bit_clr(FUSF_PROG, FUSF_PROG_PROG_PIN, 10000)) {
		fprintf(stderr, "FUSF: Programming voltage not removed after program\n");
	}

	/* Read the fuses back to check programming */
	val = 0;
	if (hard)
		val |= FUSF_RCMD_EFUSE;
	/* In CN8XXX fuses take a byte address, not a 128 bit bank */
	val |= FUSF_RCMD_ADDR(start_fuse >> 3);
	val |= FUSF_RCMD_ADDR_HI(start_fuse >> 9);
	val |= FUSF_RCMD_PEND;
	write_reg(FUSF_RCMD, val);
	do {
		val = read_reg(FUSF_RCMD);
	} while (val & FUSF_RCMD_PEND);

	uint64_t dat0 = read_reg(FUSF_BNK_DATX(0));
	uint64_t dat1 = read_reg(FUSF_BNK_DATX(1));

	/* Make sure all fuses that were suppose to blow are blown */
	if (((dat0 & fuses0) != fuses0) || ((dat1 & fuses1) != fuses1))
	{
		fprintf(stderr, "FUSF: Mismatch after program (program 0x%lx 0x%lx, read 0x%lx 0x%lx)\n",
				fuses0, fuses1, dat0, dat1);
		return -1;
	}

	return 0;
}

/* Soft blow a range of fuses */
static int fuse_blow_range(int start_fuse, int num_fuses, uint64_t value)
{
	int fuse, ret;

	debug("%s:start_fuse:0x%0x num_fuses:%d value=0x%0lx\n", __func__,
	       start_fuse, num_fuses, value);
	for (fuse = num_fuses - 1; fuse >= 0; fuse--) {
		if (value & (1ull << fuse)) {
			ret = fuse_field_blow(start_fuse + fuse);
			if (ret) {
				fprintf(stderr, "Failed to program fuse: 0x%0x\n",
					start_fuse + fuse);
			}
		}
	}

	return 0;
}

static struct fuse_block *get_fuse_block(const char *name)
{
	int i;

	for (i = 0; i < ARRAY_SIZE(fuses); i++) {
		if (!strcasecmp(name, fuses[i].name))
			return &fuses[i];
	}

	fprintf(stderr, "Error: Invalid fuse block: %s\n", name);

	return NULL;
}

/* lock_fuse - lock a specific fuse block by blowing its OTP lock bit */
static int lock_ctl_fuse(const char *name, int val)
{
	int ret;
	uint64_t ctl = 0;

	if (!strcmp(name, "rom_counter"))
		ctl |= FUSF_CTL_ROM_CNT(val);
	else if (!strcmp(name, "crypt_ssk_dis"))
		ctl |= FUSF_CTL_CRYPT_SSK_DIS;
	else if (!strcmp(name, "crypt_no_dis"))
		ctl |= FUSF_CTL_CRYPT_NO_DIS;
	else if (!strcmp(name, "fj_dis_huk"))
		ctl |= FUSF_CTL_FJ_DIS_HUK;
	else if (!strcmp(name, "fj_core0"))
		ctl |= FUSF_CTL_FJ_CORE0;
	else if (!strcmp(name, "fj_timeout"))
		ctl |= FUSF_CTL_FJ_TIMEOUT(val);
	else if (!strcmp(name, "fj_dis"))
		ctl |= FUSF_CTL_FJ_DIS;
	else if (!strcmp(name, "tz_force"))
		ctl |= FUSF_CTL_TZ_FORCE2;

	ret = fuse_blow_range(0x0, 64, ctl);
	if (ret)
		return ret;

	printf("set %s\n", name);

	return 0;
}

static int lock_fuse(const char *name) {
	int ret;
	uint64_t ctl = 0;
	struct fuse_block *fuse = get_fuse_block(name);

	if (!strcmp(name, "all")) {
		verify_force("blowing FUSF_LCK means you can no longer update " \
			     "ROM trusted counter or SW bits");
		ctl |= FUSF_CTL_FUSF_LCK;
	} else if (fuse && !strcmp(fuse->name, "ROTPK")) {
		ctl |= fuse->lock;
	} else if (fuse && !strcmp(name, "SSK")) {
		ctl |= fuse->lock;
	} else if (fuse && !strcmp(name, "SW")) {
		verify_force("blowing FUSF_SW means you can no longer update SW bits");
		ctl |= fuse->lock;
	} else {
		fprintf(stderr, "invalid lock request: %s\n", name);
		return -EINVAL;
	}

	ret = fuse_blow_range(0x0, 64, ctl);
	if (ret)
		return ret;

	printf("Locked %s\n", name);

	return 0;
}

static int blow_fuse(const char *name, const char *val)
{
	int i;
	char tmp[32];
	struct fuse_block *fuse = get_fuse_block(name);

	if (!fuse || !val)
		return -EINVAL;

	/* verify hex string */
	if (strncasecmp("0x", val, 2) != 0) {
		fprintf(stderr, "Error: %s invalid key "
			"(must be hex key starting with '0x')\n", fuse->name);
		return -EINVAL;
	}

	/* verify string length; each char reprsents 4 bits plus 2 chars for the 0x) */
	if (strlen(val) != (2 + (fuse->bits / 4))) {
		fprintf(stderr, "Error: %s invalid key length "
			"(must be %d chars for %d bits)\n",
			fuse->name, fuse->bits / 4, fuse->bits);
		return -EINVAL;
	}

	/* verify fuse block not already locked */
	if (read_reg(FUSF_CTL) & (fuse->lock | FUSF_CTL_FUSF_LCK)) {
		fprintf(stderr, "Error: %s already locked\n", fuse->name);
		return -EPERM;
	}

	/* iterate over 64bit values */
	for (i = 0; i < (fuse->bits / 64); i++) {
		strncpy(tmp, val + 2 + (16 * ((fuse->bits / 64) - i - 1)), 16);
		fuse_blow_range(fuse->offset + (i * 64), 64,
				(uint64_t)strtoull(tmp, NULL, 16));
	}

	printf("%s fuses blown: %s\n", fuse->name, val);

	return 0;
}

static int show_fuse(const char *name) {
	struct fuse_block *fuse = get_fuse_block(name);
	uint64_t ctl = read_reg(FUSF_CTL);
	int i;

	if (!fuse)
		return -EINVAL;

	printf("%s:0x", fuse->name);
	for (i = (fuse->bits / 64); i > 0; i--) {
		printf("%016llx",
			(unsigned long long)read_reg(fuse->reg + ((i - 1) * 8)));
	}

	printf("\n");

	/* all keys other than ROTPK are unreadable if DIS_HUK set */
	if ( (ctl & FUSF_CTL_FJ_DIS_HUK) && strcmp(name, "ROTPK") ) {
		fprintf(stderr, "WARNING: %s can't be read due to FJ_DIS_HUK set\n", name);
	}

	return 0;
}

static int show_fuses() {
	uint64_t ctl = read_reg(FUSF_CTL);
	int i;

	printf("FUSF_CTL:0x%016llx", (unsigned long long)read_reg(FUSF_CTL));
	if (ctl & FUSF_CTL_CRYPT_SSK_DIS)
		printf(" CRYPT_SSK_DIS");
	if (ctl & FUSF_CTL_CRYPT_NO_DIS)
		printf(" CRYPT_NO_DIS");
	if (ctl & FUSF_CTL_FJ_DIS_HUK)
		printf(" FJ_DIS_HUK");
	if (ctl & FUSF_CTL_FJ_CORE0)
		printf(" FJ_CORE0");
	if (ctl & FUSF_CTL_FJ_DIS)
		printf(" FJ_DIS");
	if (ctl & FUSF_CTL_ROM_CNT(0xffff))
		printf(" ROM_CNT(0x%04x)", (uint32_t)(ctl >> 32) & 0xffff);
	if (ctl & FUSF_CTL_FJ_TIMEOUT(0x3))
		printf(" FJ_TIMEOUT(0x%02x)", (uint8_t)(ctl >> 10) & 0x3);
	if (ctl & FUSF_CTL_TZ_FORCE2)
		printf(" TZ_FORCE");
	if (ctl & FUSF_CTL_FUSF_LCK)
		printf(" ALL_LCK");
	if (ctl & FUSF_CTL_SSK_LCK)
		printf(" SSK_LCK");
	if (ctl & FUSF_CTL_ROT_LCK)
		printf(" ROT_LCK");
	if (ctl & FUSF_CTL_SW_LCK)
		printf(" SW_LCK");
	printf("\n");
	for (i = 0; i < ARRAY_SIZE(fuses); i++)
		show_fuse(fuses[i].name);

	return 0;
}

/* Perform a soft reset (used to test effect of soft-blown fuses) */
static void soft_reset(void)
{
	printf("%s\n", __func__);

	write_reg(RST_OCX, 0);
	read_reg(RST_OCX);
	write_reg(RST_SOFT_RST, 0x1);
}

int main (int argc, char **argv)
{
	uint64_t ctl = read_reg(FUSF_CTL);
	const char *name;
	int c;

	if (argc < 2) {
		usage(argv[0]);
	}

	while (1) {
		static struct option long_options[] = {
			/* flags */
			{ "verbose", no_argument, &verbose, 1},
			{ "force", no_argument, &force, 1},
			{ "hard", no_argument, &hard, 1 },
			/* short opts */
			{ "help", no_argument, 0, 'h' },
			{ "reset", no_argument, 0, 'r' },
			{ "show", no_argument, 0, 's' },
			/* long opts */
			{ "rotpk", required_argument, 0, 0 },
			{ "ssk", required_argument, 0, 0 },
			{ "huk", required_argument, 0, 0 },
			{ "ek", required_argument, 0, 0 },
			{ "sw", required_argument, 0, 0 },
			{ "lock", required_argument, 0, 0 },
			{ "rom_counter", required_argument, 0, 0 },
			{ "ft_timeout", required_argument, 0, 0 },
			{ "crypt_ssk_dis", no_argument, 0, 0 },
			{ "fj_dis_huk", no_argument, 0, 0 },
			{ "fj_core0", no_argument, 0, 0 },
			{ "fj_dis", no_argument, 0, 0 },
			{ "tz_force", no_argument, 0, 0 },
			{ 0, 0, 0, 0 }
		};

		int option_index = 0;

		c = getopt_long(argc, argv, "r:hp:", long_options, &option_index);

		if (c == -1)
			break;
		switch(c) {
		case 0:
			name = long_options[option_index].name;
			if (long_options[option_index].flag != 0)
				break;

			if (hard && (ctl & FUSF_CTL_FUSF_LCK)) {
				fprintf(stderr, "Error: Fuses are locked\n");
				exit(1);
			}

			if (!strcmp(name, "rotpk"))
				blow_fuse("rotpk", optarg);
			else if (!strcmp(name, "ssk"))
				blow_fuse(name, optarg);
			else if (!strcmp(name, "huk"))
				blow_fuse(name, optarg);
			else if (!strcmp(name, "ek"))
				blow_fuse(name, optarg);
			else if (!strcmp(name, "sw"))
				blow_fuse(name, optarg);
			else if (!strcmp(name, "lock"))
				lock_fuse(optarg);
			else if (!strcmp(name, "rom_counter"))
				lock_ctl_fuse(name, strtol(optarg, NULL, 0));
			else if (!strcmp(name, "crypt_ssk_dis"))
				lock_ctl_fuse(name, 0);
			else if (!strcmp(name, "crypt_no_dis"))
				lock_ctl_fuse(name, 0);
			else if (!strcmp(name, "fj_dis_huk"))
				lock_ctl_fuse(name, 0);
			else if (!strcmp(name, "fj_core0"))
				lock_ctl_fuse(name, 0);
			else if (!strcmp(name, "fj_timeout"))
				lock_ctl_fuse(name, strtol(optarg, NULL, 0));
			else if (!strcmp(name, "fj_dis"))
				lock_ctl_fuse(name, 0);
			else if (!strcmp(name, "tz_force"))
				lock_ctl_fuse(name, 0);
			break;
		case 'r':
			soft_reset();
			break;
		case 'h':
			usage(argv[0]);
			break;
		case 's':
			show_fuses();
			break;
		default:
			usage(argv[0]);
			break;
		}
	}

	exit(0);
}
