blob: c8e1eba43cff33c43db08ea596102263e6c3bc2f [file] [log] [blame] [edit]
#!/usr/bin/env python3
# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
r"""Takes a compiled VMEM image and processes it for loading into flash.
Specifically, this takes a raw flash image and adds both layers of ECC
(integrity and reliablity), and optionally, scrambles the data using the
same XEX scrambling scheme used in the flash controller. This enables
backdoor loading the flash on simulation platforms (e.g., DV and Verilator).
"""
import argparse
import functools
import re
import sys
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import List
from pyfinite import ffield
from util.design.lib.Present import Present
import prince
import secded_gen
MUBI4_TRUE = 0x6
# Fixed OTP data / scrambling parameters.
OTP_WORD_SIZE = 16 # bits
OTP_FLASH_DATA_DEFAULT_CFG_RE = re.compile(
r"CREATOR_SW_CFG: CREATOR_SW_CFG_FLASH_DATA_DEFAULT_CFG")
OTP_FLASH_DATA_DEFAULT_CFG_BLOCK_SIZE = 32 # bits
OTP_SECRET1_RE = re.compile(r"SECRET1")
OTP_SECRET1_BLOCK_SIZE = 64 # bits
OTP_SECRET1_PRESENT_KEY = 0x5703C3EB2BB563689E00A67814EFBDE8
OTP_SECRET1_PRESENT_KEY_LENGTH = 128 # bits
OTP_SECRET1_PRESENT_NUM_ROUNDS = 32
OTP_FLASH_ADDR_KEY_SEED_SIZE = 256 # bits
OTP_FLASH_DATA_KEY_SEED_SIZE = 256 # bits
OTP_SECRET1_FLASH_ADDR_KEY_SEED_START = 0
# Flash data / scrambling parameters.
FLASH_ADDR_KEY_SIZE = 128 # bits
FLASH_DATA_KEY_SIZE = 128 # bits
FLASH_WORD_SIZE = 64 # bits
FLASH_ADDR_SIZE = 16 # bits
FLASH_INTEGRITY_ECC_SIZE = 4 # bits
FLASH_RELIABILITY_ECC_SIZE = 8 # bits
FLASH_PRINCE_NUM_HALF_ROUNDS = 5
class FlashScramblingKeyType(Enum):
ADDRESS = 1
DATA = 2
# Flash scrambling key computation parameters.
# ------------------------------------------------------------------------------
# DO NOT EDIT: edit fixed parameters above instead.
# ------------------------------------------------------------------------------
# Computed OTP data / scrambling parameters.
OTP_SECRET1_PRESENT_CIPHER = Present(OTP_SECRET1_PRESENT_KEY,
rounds=OTP_SECRET1_PRESENT_NUM_ROUNDS,
keylen=OTP_SECRET1_PRESENT_KEY_LENGTH)
OTP_SECRET1_FLASH_ADDR_KEY_SEED_STOP = (OTP_FLASH_ADDR_KEY_SEED_SIZE //
OTP_SECRET1_BLOCK_SIZE)
OTP_SECRET1_FLASH_DATA_KEY_SEED_START = (OTP_FLASH_ADDR_KEY_SEED_SIZE //
OTP_SECRET1_BLOCK_SIZE)
OTP_SECRET1_FLASH_DATA_KEY_SEED_STOP = OTP_SECRET1_FLASH_DATA_KEY_SEED_START + (
OTP_FLASH_DATA_KEY_SEED_SIZE // OTP_SECRET1_BLOCK_SIZE)
# Computed flash data / scrambling parameters.
KEY_TYPE_2_IV = {
FlashScramblingKeyType.ADDRESS: 0x97883548F536F544,
FlashScramblingKeyType.DATA: 0xC5F5C1D8AEF35040,
}
KEY_TYPE_2_FINALIZATION_CONST = {
FlashScramblingKeyType.ADDRESS: 0x39AED01B4B2277312E9480868216A281,
FlashScramblingKeyType.DATA: 0x1D888AC88259C44AAB06CB4A4C65A7EA,
}
FLASH_KEY_COMPUTATION_KEY_SIZE = OTP_FLASH_ADDR_KEY_SEED_SIZE // 2
FLASH_KEY_COMPUTATION_KEY_MASK = (2**FLASH_KEY_COMPUTATION_KEY_SIZE) - 1
FLASH_GF_OPERAND_B_MASK = (2**FLASH_WORD_SIZE) - 1
FLASH_GF_OPERAND_A_MASK = (
FLASH_GF_OPERAND_B_MASK &
~(0xffff << (FLASH_WORD_SIZE - FLASH_ADDR_SIZE))) << FLASH_WORD_SIZE
# Create GF(2^64) with irreducible_polynomial = x^64 + x^4 + x^3 + x + 1
FLASH_GF_2_64 = ffield.FField(64,
gen=((0x1 << 64) | (0x1 << 4) | (0x1 << 3) |
(0x1 << 1) | 0x1))
# Format string for generating new VMEM file.
FLASH_VMEM_WORD_SIZE = (FLASH_WORD_SIZE + FLASH_INTEGRITY_ECC_SIZE +
FLASH_RELIABILITY_ECC_SIZE)
VMEM_FORMAT_STR = " {:0" + f"{FLASH_VMEM_WORD_SIZE // 4}" + "X}"
# ------------------------------------------------------------------------------
@dataclass
class FlashScramblingConfigs:
scrambling_enabled: bool = False
addr_key_seed: int = None
data_key_seed: int = None
addr_key: int = None
data_key: int = None
@functools.lru_cache(maxsize=None)
def _xex_scramble(data: int, word_addr: int, flash_addr_key: int,
flash_data_key: int) -> int:
operand_a = ((flash_addr_key & FLASH_GF_OPERAND_A_MASK) >>
(FLASH_WORD_SIZE - FLASH_ADDR_SIZE)) | word_addr
operand_b = flash_addr_key & FLASH_GF_OPERAND_B_MASK
mask = FLASH_GF_2_64.Multiply(operand_a, operand_b)
masked_data = data ^ mask
return prince.prince(masked_data, flash_data_key,
FLASH_PRINCE_NUM_HALF_ROUNDS) ^ mask
def _convert_array_2_int(data_array: List[int],
data_size: int,
little_endian=True) -> int:
"""Converts array of data blocks to an int."""
reformatted_data = 0
if not little_endian:
data_array.reverse()
for i, data in enumerate(data_array):
reformatted_data |= (data << (i * data_size))
return reformatted_data
def _get_flash_scrambling_configs(otp_vmem_file: str,
configs: FlashScramblingConfigs) -> None:
# Open OTP VMEM file and read into memory, skipping comment lines.
try:
otp_vmem = Path(otp_vmem_file).read_text()
except IOError:
raise Exception(f"Unable to open {otp_vmem_file}")
otp_vmem_lines = re.findall(r"^@.*$", otp_vmem, flags=re.MULTILINE)
# Retrieve OTP data from the following partitions:
# - CREATOR_SW_CFG: which contains the flash scramble enablement flag, and
# - SECRET1: which contains the flash scrambling key seeds.
# Note, we strip ECC bits from each data word when processing.
flash_data_default_cfg = None
secret1_data_blocks = []
otp_data_block = 0
idx = 0
for line in otp_vmem_lines:
if (OTP_FLASH_DATA_DEFAULT_CFG_RE.search(line) or
OTP_SECRET1_RE.search(line)):
otp_data_word_w_ecc = int(line.split()[1], 16)
otp_data_word = otp_data_word_w_ecc & (2**OTP_WORD_SIZE - 1)
otp_data_block |= otp_data_word << (idx * OTP_WORD_SIZE)
idx += 1
if OTP_FLASH_DATA_DEFAULT_CFG_RE.search(line):
if idx == (OTP_FLASH_DATA_DEFAULT_CFG_BLOCK_SIZE //
OTP_WORD_SIZE):
flash_data_default_cfg = otp_data_block & 0xff
# If flash data scrambling is disabled, then we can return
# early to save execution time.
if flash_data_default_cfg != MUBI4_TRUE:
configs.scrambling_enabled = False
return
configs.scrambling_enabled = True
otp_data_block = 0
idx = 0
if OTP_SECRET1_RE.search(line):
if idx == (OTP_SECRET1_BLOCK_SIZE // OTP_WORD_SIZE):
secret1_data_blocks.append(otp_data_block)
otp_data_block = 0
idx = 0
# Check we found the data we were looking for in the OTP image.
if flash_data_default_cfg is None:
raise RuntimeError(
"Cannot read flash scrambling enablement state from OTP.")
if not secret1_data_blocks:
raise RuntimeError("Cannot read flash scrambling key seeds from OTP.")
# Descramble SECRET1 partition data blocks and extract flash scrambling key
# seeds. The SECRET1 partition layout looks like:
# {FLASH_ADDR_KEY_SEED, FLASH_DATA_KEY_SEED, SRAM_DATA_KEY_SEED, DIGEST}
descrambled_secret1_blocks = list(
map(OTP_SECRET1_PRESENT_CIPHER.decrypt, secret1_data_blocks))
configs.addr_key_seed = _convert_array_2_int(
descrambled_secret1_blocks[OTP_SECRET1_FLASH_ADDR_KEY_SEED_START:
OTP_SECRET1_FLASH_ADDR_KEY_SEED_STOP],
OTP_SECRET1_BLOCK_SIZE)
configs.data_key_seed = _convert_array_2_int(
descrambled_secret1_blocks[OTP_SECRET1_FLASH_DATA_KEY_SEED_START:
OTP_SECRET1_FLASH_DATA_KEY_SEED_STOP],
OTP_SECRET1_BLOCK_SIZE)
def _compute_flash_scrambling_key(scrambling_configs: FlashScramblingConfigs,
key_type: FlashScramblingKeyType) -> int:
if key_type == FlashScramblingKeyType.ADDRESS:
key_seed = scrambling_configs.addr_key_seed
else:
key_seed = scrambling_configs.data_key_seed
full_key = 0
for i in range(2):
round_1_present_key = (key_seed >>
(FLASH_KEY_COMPUTATION_KEY_SIZE *
i)) & FLASH_KEY_COMPUTATION_KEY_MASK
key_half = 0
for j in range(2):
if j == 0:
cipher = Present(round_1_present_key)
key_half = cipher.encrypt(
KEY_TYPE_2_IV[key_type]) ^ KEY_TYPE_2_IV[key_type]
else:
cipher = Present(KEY_TYPE_2_FINALIZATION_CONST[key_type])
key_half = cipher.encrypt(key_half) ^ key_half
full_key |= key_half << (64 * i)
return full_key
def _compute_flash_scrambling_keys(
scrambling_configs: FlashScramblingConfigs) -> None:
scrambling_configs.addr_key = _compute_flash_scrambling_key(
scrambling_configs, FlashScramblingKeyType.ADDRESS)
scrambling_configs.data_key = _compute_flash_scrambling_key(
scrambling_configs, FlashScramblingKeyType.DATA)
def _reformat_flash_vmem(
flash_vmem_file: str,
scrambling_configs: FlashScramblingConfigs) -> List[str]:
# Open (raw) flash VMEM file and read into memory, skipping comment lines.
try:
flash_vmem = Path(flash_vmem_file).read_text()
except IOError:
raise Exception(f"Unable to open {flash_vmem_file}")
flash_vmem_lines = re.findall(r"^@.*$", flash_vmem, flags=re.MULTILINE)
# Load project SECDED configuration.
ecc_configs = secded_gen.load_secded_config()
# Add integrity/reliability ECC, and potentially scramble, each flash word.
reformatted_vmem_lines = []
for line in flash_vmem_lines:
line_items = line.split()
reformatted_line = ""
address = None
data = None
for item in line_items:
# Process the address first.
if re.match(r"^@", item):
reformatted_line += item
address = int(item.lstrip("@"), 16)
# Process the data words.
else:
data = int(item, 16)
# `data_w_intg_ecc` will be in format {ECC bits, data bits}.
data_w_intg_ecc, _ = secded_gen.ecc_encode(
ecc_configs, "hamming", FLASH_WORD_SIZE, data)
# Due to storage constraints the first nibble of ECC is dropped.
data_w_intg_ecc &= 0xF_FFFF_FFFF_FFFF_FFFF
if scrambling_configs.scrambling_enabled:
intg_ecc = data_w_intg_ecc & (0xF << FLASH_WORD_SIZE)
data = _xex_scramble(data, address,
scrambling_configs.addr_key,
scrambling_configs.data_key)
data_w_intg_ecc = intg_ecc | data
# `data_w_full_ecc` will be in format {reliablity ECC bits,
# integrity ECC bits, data bits}.
data_w_full_ecc, _ = secded_gen.ecc_encode(
ecc_configs, "hamming",
FLASH_WORD_SIZE + FLASH_INTEGRITY_ECC_SIZE,
data_w_intg_ecc)
reformatted_line += str.format(VMEM_FORMAT_STR,
data_w_full_ecc)
# Append reformatted line to what will be the new output VMEM file.
reformatted_vmem_lines.append(reformatted_line)
return reformatted_vmem_lines
def main(argv: List[str]):
# Parse command line args.
parser = argparse.ArgumentParser()
parser.add_argument("--in-flash-vmem",
type=str,
help="Input VMEM file to reformat.")
parser.add_argument("--in-otp-vmem",
type=str,
help="Input OTP (VMEM) file to retrieve data from.")
parser.add_argument("--out-flash-vmem", type=str, help="Output VMEM file.")
args = parser.parse_args(argv)
# Read flash scrambling configurations (including: enablement, address and
# data key seeds) directly from OTP VMEM file.
scrambling_configs = FlashScramblingConfigs()
if args.in_otp_vmem:
_get_flash_scrambling_configs(args.in_otp_vmem, scrambling_configs)
# Compute flash scrambling keys from seeds.
if scrambling_configs.scrambling_enabled:
_compute_flash_scrambling_keys(scrambling_configs)
# Reformat flash VMEM file to add integrity/reliablity ECC and scrambling.
reformatted_vmem_lines = _reformat_flash_vmem(args.in_flash_vmem,
scrambling_configs)
# Write re-formatted output file.
with open(args.out_flash_vmem, "w") as of:
of.write("\n".join(reformatted_vmem_lines))
if __name__ == "__main__":
main(sys.argv[1:])