|  | #!/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"""OTP memory image class, used to create preload images for the OTP | 
|  | memory for simulations and FPGA emulation. | 
|  | """ | 
|  |  | 
|  | import copy | 
|  | import logging as log | 
|  | import random | 
|  | from typing import Tuple | 
|  |  | 
|  | from lib.common import (check_bool, check_int, ecc_encode, permute_bits, | 
|  | random_or_hexvalue) | 
|  | from lib.LcStEnc import LcStEnc | 
|  | from lib.OtpMemMap import OtpMemMap | 
|  | from lib.Present import Present | 
|  | from mubi.prim_mubi import mubi_value_as_int | 
|  |  | 
|  | # Seed diversification constant for OtpMemImg (this enables to use | 
|  | # the same seed for different classes) | 
|  | OTP_IMG_SEED_DIVERSIFIER = 1941661965323525198146 | 
|  |  | 
|  |  | 
|  | def _present_64bit_encrypt(plain, key): | 
|  | '''Scramble a 64bit block with PRESENT cipher''' | 
|  |  | 
|  | # Make sure data is within 64bit range | 
|  | assert (plain >= 0) and (plain < 2**64), \ | 
|  | 'Data block is out of 64bit range' | 
|  |  | 
|  | # Make sure key is within 128bit range | 
|  | assert (key >= 0) and (key < 2**128), \ | 
|  | 'Key is out of 128bit range' | 
|  |  | 
|  | # Make sure inputs are integers | 
|  | assert isinstance(plain, int) and isinstance(key, int), \ | 
|  | 'Data and key need to be of type int' | 
|  |  | 
|  | cipher = Present(key, rounds=32, keylen=128) | 
|  | return cipher.encrypt(plain) | 
|  |  | 
|  |  | 
|  | def _present_64bit_digest(data_blocks, iv, const): | 
|  | '''Compute digest over multiple 64bit data blocks''' | 
|  |  | 
|  | # Make a deepcopy since we're going to modify and pad the list. | 
|  | data_blocks = copy.deepcopy(data_blocks) | 
|  |  | 
|  | # We need to align the number of data blocks to 2x64bit | 
|  | # for the digest to work properly. | 
|  | if len(data_blocks) % 2 == 1: | 
|  | data_blocks.append(data_blocks[-1]) | 
|  |  | 
|  | # Append finalization constant. | 
|  | data_blocks.append(const & 0xFFFF_FFFF_FFFF_FFFF) | 
|  | data_blocks.append((const >> 64) & 0xFFFF_FFFF_FFFF_FFFF) | 
|  |  | 
|  | # This computes a digest according to a Merkle-Damgard construction | 
|  | # that uses the Davies-Meyer scheme to turn the PRESENT cipher into | 
|  | # a one-way compression function. Digest finalization consists of | 
|  | # a final digest round with a 128bit constant. | 
|  | # See also: https://docs.opentitan.org/hw/ip/otp_ctrl/doc/index.html#scrambling-datapath | 
|  | state = iv | 
|  | last_b64 = None | 
|  | for b64 in data_blocks: | 
|  | if last_b64 is None: | 
|  | last_b64 = b64 | 
|  | continue | 
|  |  | 
|  | b128 = last_b64 + (b64 << 64) | 
|  | state ^= _present_64bit_encrypt(state, b128) | 
|  | last_b64 = None | 
|  |  | 
|  | assert last_b64 is None | 
|  | return state | 
|  |  | 
|  |  | 
|  | def _to_memfile_with_ecc(data, annotation, config, | 
|  | data_perm) -> Tuple[str, int]: | 
|  | '''Compute ECC and convert into MEM file''' | 
|  |  | 
|  | log.info('Convert to MEM file.') | 
|  |  | 
|  | data_width = config['secded']['data_width'] | 
|  | ecc_width = config['secded']['ecc_width'] | 
|  | assert data_width % 8 == 0, \ | 
|  | 'OTP data width must be a multiple of 8' | 
|  | assert data_width <= 64, \ | 
|  | 'OTP data width cannot be larger than 64' | 
|  |  | 
|  | num_words = len(data) * 8 // data_width | 
|  | bytes_per_word = data_width // 8 | 
|  | # Byte aligned total width after adding ECC bits | 
|  | bytes_per_word_ecc = (data_width + ecc_width + 7) // 8 | 
|  | bit_padding = bytes_per_word_ecc * 8 - data_width - ecc_width | 
|  | bin_format_str = '0' + str(data_width) + 'b' | 
|  | hex_format_str = '0' + str(bytes_per_word_ecc * 2) + 'x' | 
|  | bitness = bytes_per_word_ecc * 8 | 
|  | mem_lines = [ | 
|  | '// OTP MEM file with {} x {}bit layout'.format(num_words, bitness), | 
|  | ] | 
|  |  | 
|  | log.info('Memory layout is {} x {}bit (with ECC)'.format( | 
|  | num_words, bitness)) | 
|  |  | 
|  | for i_word in range(num_words): | 
|  | # Assemble native OTP word and uniquify annotation for comments | 
|  | word_address = i_word * bytes_per_word | 
|  | word = 0 | 
|  | word_annotations = set() | 
|  | for i_byte in range(bytes_per_word): | 
|  | byte_address = word_address + i_byte | 
|  | word += data[byte_address] << (i_byte * 8) | 
|  | word_annotations.add(annotation[byte_address]) | 
|  |  | 
|  | # ECC encode | 
|  | word_bin = format(word, bin_format_str) | 
|  | word_bin = ecc_encode(config, word_bin) | 
|  | # Pad to word boundary and permute data if needed | 
|  | word_bin = ('0' * bit_padding) + word_bin | 
|  | word_bin = permute_bits(word_bin, data_perm) | 
|  | word_hex = format(int(word_bin, 2), hex_format_str) | 
|  |  | 
|  | # Build a MEM line containing this word's address in the memory map and | 
|  | # its value. Because this file will be read by Verilog's $readmemh, the | 
|  | # address is a *word* offset, not a byte offset. The address does not | 
|  | # count ECC bits. In a comment, we also include any annotations | 
|  | # associated with the word. | 
|  | line = '@{:06x} {} // {}'.format(i_word, word_hex, | 
|  | ', '.join(word_annotations)) | 
|  | mem_lines.append(line) | 
|  |  | 
|  | log.info('Done.') | 
|  |  | 
|  | return ('\n'.join(mem_lines), bitness) | 
|  |  | 
|  |  | 
|  | def _check_unused_keys(dict_to_check, msg_postfix=""): | 
|  | '''If there are unused keys, print their names and error out''' | 
|  | for key in dict_to_check.keys(): | 
|  | log.info("Unused key {} in {}".format(key, msg_postfix)) | 
|  | if dict_to_check: | 
|  | raise RuntimeError('Aborting due to unused keys in config dict') | 
|  |  | 
|  |  | 
|  | class OtpMemImg(OtpMemMap): | 
|  |  | 
|  | def __init__(self, lc_state_config, otp_mmap_config, img_config, | 
|  | data_perm): | 
|  | # Initialize memory map | 
|  | super().__init__(otp_mmap_config) | 
|  |  | 
|  | # Initialize the LC state and OTP memory map objects first, since | 
|  | # validation and image generation depends on them | 
|  | self.lc_state = LcStEnc(lc_state_config) | 
|  |  | 
|  | # Validate memory image configuration | 
|  | log.info('') | 
|  | log.info('Parse OTP image specification.') | 
|  |  | 
|  | # Encryption smoke test with known test vector | 
|  | enc_test = _present_64bit_encrypt(0x0123456789abcdef, | 
|  | 0x0123456789abcdef0123456789abcdef) | 
|  |  | 
|  | assert enc_test == 0x0e9d28685e671dd6, \ | 
|  | 'Encryption module test failed' | 
|  |  | 
|  | otp_width = self.config['otp']['width'] * 8 | 
|  | secded_width = self.lc_state.config['secded']['data_width'] | 
|  | if otp_width != secded_width: | 
|  | raise RuntimeError('OTP width and SECDED data width must be equal') | 
|  |  | 
|  | if 'seed' not in img_config: | 
|  | raise RuntimeError('Missing seed in configuration.') | 
|  |  | 
|  | img_config['seed'] = check_int(img_config['seed']) | 
|  | log.info('Seed: {0:x}'.format(img_config['seed'])) | 
|  | log.info('') | 
|  |  | 
|  | # Re-initialize with seed to make results reproducible. | 
|  | random.seed(OTP_IMG_SEED_DIVERSIFIER + img_config['seed']) | 
|  |  | 
|  | if 'partitions' not in img_config: | 
|  | raise RuntimeError('Missing partitions key in configuration.') | 
|  |  | 
|  | for part in img_config['partitions']: | 
|  | self.merge_part_data(part) | 
|  | log.info('Adding values to {} partition.'.format(part['name'])) | 
|  | for item in part['items']: | 
|  | self.merge_item_data(part, item) | 
|  |  | 
|  | # Key accounting | 
|  | img_config_check = img_config.copy() | 
|  | del img_config_check['seed'] | 
|  | del img_config_check['partitions'] | 
|  | _check_unused_keys(img_config_check, 'in image config') | 
|  |  | 
|  | log.info('') | 
|  | log.info('Parsing OTP image successfully completed.') | 
|  |  | 
|  | self.validate_data_perm(data_perm) | 
|  |  | 
|  | def merge_part_data(self, part): | 
|  | '''This validates and merges the partition data into the memory map dict''' | 
|  |  | 
|  | part.setdefault('items', []) | 
|  | if not isinstance(part['items'], list): | 
|  | raise RuntimeError('the "items" key must contain a list') | 
|  |  | 
|  | # Check if partition name exists in memory map | 
|  | part.setdefault('name', 'unknown_name') | 
|  | mmap_part = self.get_part(part['name']) | 
|  | if mmap_part is None: | 
|  | raise RuntimeError('Partition {} does not exist'.format( | 
|  | part['name'])) | 
|  |  | 
|  | # Only partitions with a hardware digest can be locked. | 
|  | part.setdefault('lock', 'false') | 
|  | part['lock'] = check_bool(part['lock']) | 
|  | if part['lock'] and not \ | 
|  | mmap_part['hw_digest']: | 
|  | raise RuntimeError( | 
|  | 'Partition {} does not contain a hardware digest'.format( | 
|  | part['name'])) | 
|  |  | 
|  | # Augment memory map datastructure with lock bit. | 
|  | mmap_part['lock'] = part['lock'] | 
|  |  | 
|  | if part['name'] == 'LIFE_CYCLE': | 
|  | part.setdefault('state', 'RAW') | 
|  | part.setdefault('count', 0) | 
|  |  | 
|  | part['count'] = check_int(part['count']) | 
|  | if len(part['items']) > 0: | 
|  | raise RuntimeError( | 
|  | 'Life cycle items cannot directly be overridden') | 
|  | if part['lock']: | 
|  | raise RuntimeError('Life cycle partition cannot be locked') | 
|  |  | 
|  | if part['count'] == 0 and part['state'] != "RAW": | 
|  | raise RuntimeError( | 
|  | 'Life cycle transition counter can only be zero in the RAW state' | 
|  | ) | 
|  |  | 
|  | # Augment life cycle partition with correct life cycle encoding | 
|  | state = self.lc_state.encode('lc_state', str(part['state'])) | 
|  | count = self.lc_state.encode('lc_cnt', str(part['count'])) | 
|  | part['items'] = [{ | 
|  | 'name': 'LC_STATE', | 
|  | 'value': '0x{:X}'.format(state) | 
|  | }, { | 
|  | 'name': 'LC_TRANSITION_CNT', | 
|  | 'value': '0x{:X}'.format(count) | 
|  | }] | 
|  |  | 
|  | # Key accounting | 
|  | part_check = part.copy() | 
|  | del part_check['state'] | 
|  | del part_check['count'] | 
|  | else: | 
|  | # Key accounting | 
|  | part_check = part.copy() | 
|  | if len(part['items']) == 0: | 
|  | log.warning("Partition does not contain any items.") | 
|  |  | 
|  | # Key accounting | 
|  | del part_check['items'] | 
|  | del part_check['name'] | 
|  | del part_check['lock'] | 
|  | _check_unused_keys(part_check, "in partition {}".format(part['name'])) | 
|  |  | 
|  | def merge_item_data(self, part, item): | 
|  | '''This validates and merges the item data into the memory map dict''' | 
|  | item.setdefault('name', 'unknown_name') | 
|  |  | 
|  | mmap_item = self.get_item(part['name'], item['name']) | 
|  | if mmap_item is None: | 
|  | raise RuntimeError('Item {} does not exist'.format(item['name'])) | 
|  |  | 
|  | item_size = mmap_item['size'] | 
|  | item_width = item_size * 8 | 
|  |  | 
|  | # if needed, resolve the mubi value first | 
|  | if mmap_item['ismubi']: | 
|  | mubi_str = "mubi " | 
|  | mubi_val_str = " kMultiBitBool{}".format(item_width) | 
|  | item.setdefault("value", "false") | 
|  | item["value"] = check_bool(item["value"]) | 
|  | mubi_val_str += "True" if item["value"] else "False" | 
|  | item["value"] = mubi_value_as_int(item["value"], item_width) | 
|  | else: | 
|  | mubi_str = "" | 
|  | mubi_val_str = "" | 
|  | item.setdefault('value', '0x0') | 
|  | random_or_hexvalue(item, 'value', item_width) | 
|  |  | 
|  | mmap_item['value'] = item['value'] | 
|  |  | 
|  | log.info('> Adding {}item {} with size {}B and value{}:'.format( | 
|  | mubi_str, item['name'], item_size, mubi_val_str)) | 
|  | fmt_str = '{:0' + str(item_size * 2) + 'x}' | 
|  | value_str = fmt_str.format(item['value']) | 
|  | bytes_per_line = 8 | 
|  | j = 0 | 
|  | while value_str: | 
|  | # Print out max 64bit per line | 
|  | line_str = '' | 
|  | for k in range(bytes_per_line): | 
|  | num_chars = min(len(value_str), 2) | 
|  | line_str += value_str[-num_chars:] | 
|  | if k < bytes_per_line - 1: | 
|  | line_str += ' ' | 
|  | value_str = value_str[:len(value_str) - num_chars] | 
|  | log.info('  {:06x}: '.format(j) + line_str) | 
|  | j += bytes_per_line | 
|  |  | 
|  | # Key accounting | 
|  | item_check = item.copy() | 
|  | del item_check['name'] | 
|  | del item_check['value'] | 
|  | _check_unused_keys(item_check, 'in item {}'.format(item['name'])) | 
|  |  | 
|  | def override_data(self, img_config): | 
|  | '''Override specific partition items''' | 
|  |  | 
|  | if 'partitions' not in img_config: | 
|  | raise RuntimeError('Missing partitions key in configuration.') | 
|  |  | 
|  | if not isinstance(img_config['partitions'], list): | 
|  | raise RuntimeError('the "partitions" key must contain a list') | 
|  |  | 
|  | for part in img_config['partitions']: | 
|  | self.merge_part_data(part) | 
|  | log.info('Overriding values of {} partition.'.format(part['name'])) | 
|  | for item in part['items']: | 
|  | self.merge_item_data(part, item) | 
|  |  | 
|  | # Key accounting | 
|  | img_config_check = img_config.copy() | 
|  | del img_config_check['seed'] | 
|  | del img_config_check['partitions'] | 
|  | _check_unused_keys(img_config_check, 'in image config') | 
|  |  | 
|  | def streamout_partition(self, part): | 
|  | '''Scramble and stream out partition data as a list of bytes''' | 
|  |  | 
|  | part_name = part['name'] | 
|  | log.info('Streamout of partition {}'.format(part_name)) | 
|  |  | 
|  | part_offset = part['offset'] | 
|  | part_size = part['size'] | 
|  | assert part_size % 8 == 0, 'Partition must be 64bit aligned' | 
|  |  | 
|  | # First chop up all items into individual bytes. | 
|  | data_bytes = [0] * part_size | 
|  | # Annotation is propagated into the MEM file as comments | 
|  | annotation = ['unallocated'] * part_size | 
|  | # Need to keep track of defined items for the scrambling. | 
|  | # Undefined regions are left blank (0x0) in the memory. | 
|  | defined = [False] * part_size | 
|  | for item in part['items']: | 
|  | for k in range(item['size']): | 
|  | idx = item['offset'] - part_offset + k | 
|  | annotation[idx] = part_name + ': ' + item['name'] | 
|  | if 'value' in item: | 
|  | data_bytes[idx] = ((item['value'] >> (8 * k)) & 0xFF) | 
|  | assert not defined[idx], "Unexpected item collision" | 
|  | defined[idx] = True | 
|  |  | 
|  | # Reshape this into 64bit blocks (this must be aligned at this point) | 
|  | assert len(data_bytes) % 8 == 0, 'data_bytes must be 64bit aligned' | 
|  |  | 
|  | data_blocks = [] | 
|  | data_block_defined = [] | 
|  | for k, b in enumerate(data_bytes): | 
|  | if (k % 8) == 0: | 
|  | data_blocks.append(b) | 
|  | data_block_defined.append(defined[k]) | 
|  | else: | 
|  | data_blocks[k // 8] += (b << 8 * (k % 8)) | 
|  | # If any of the individual bytes are defined, the | 
|  | # whole block is considered defined. | 
|  | data_block_defined[k // 8] |= defined[k] | 
|  |  | 
|  | # Check if scrambling is needed | 
|  | if part['secret']: | 
|  | part_name = part['name'] | 
|  | key_sel = part['key_sel'] | 
|  | log.info('> Scramble partition with key "{}"'.format(key_sel)) | 
|  |  | 
|  | for key in self.config['scrambling']['keys']: | 
|  | if key['name'] == key_sel: | 
|  | break | 
|  | else: | 
|  | raise RuntimeError( | 
|  | 'Scrambling key cannot be found {}'.format(key_sel)) | 
|  |  | 
|  | for k in range(len(data_blocks)): | 
|  | if data_block_defined[k]: | 
|  | data_blocks[k] = _present_64bit_encrypt( | 
|  | data_blocks[k], key['value']) | 
|  |  | 
|  | # Check if digest calculation is needed | 
|  | if part['hw_digest']: | 
|  | # Make sure that this HW-governed digest has not been | 
|  | # overridden manually | 
|  | if data_blocks[-1] != 0: | 
|  | raise RuntimeError( | 
|  | 'Digest of partition {} cannot be overridden manually'. | 
|  | format(part_name)) | 
|  |  | 
|  | # Digest is stored in last block of a partition | 
|  | if part.setdefault('lock', False): | 
|  | log.info('> Lock partition by computing digest') | 
|  | # Digest constants at index 0 are used to compute the | 
|  | # consistency digest | 
|  | iv = self.config['scrambling']['digests'][0]['iv_value'] | 
|  | const = self.config['scrambling']['digests'][0]['cnst_value'] | 
|  |  | 
|  | data_blocks[-1] = _present_64bit_digest( | 
|  | data_blocks[0:-1], iv, const) | 
|  | else: | 
|  | log.info( | 
|  | '> Partition is not locked, hence no digest is computed') | 
|  |  | 
|  | # Convert to a list of bytes to make final packing into | 
|  | # OTP memory words independent of the cipher block size. | 
|  | data = [] | 
|  | for block in data_blocks: | 
|  | for k in range(8): | 
|  | data.append((block >> (8 * k)) & 0xFF) | 
|  |  | 
|  | # Make sure this has the right size | 
|  | assert len(data) == part['size'], 'Partition size mismatch' | 
|  |  | 
|  | # The annotation list contains a string for each byte | 
|  | # that can be used to print out informative comments | 
|  | # in the memory hex file. | 
|  | return data, annotation | 
|  |  | 
|  | def validate_data_perm(self, data_perm): | 
|  | '''Validate data permutation option''' | 
|  |  | 
|  | # Byte aligned total width after adding ECC bits | 
|  | secded_cfg = self.lc_state.config['secded'] | 
|  | raw_bitlen = secded_cfg['data_width'] + secded_cfg['ecc_width'] | 
|  | total_bitlen = ((raw_bitlen + 7) // 8) * 8 | 
|  |  | 
|  | # If the permutation is undefined, use the default mapping. | 
|  | self.data_perm = list( | 
|  | range(total_bitlen)) if not data_perm else data_perm | 
|  |  | 
|  | # Check for bijectivity | 
|  | if len(self.data_perm) != total_bitlen: | 
|  | raise RuntimeError( | 
|  | 'Data permutation "{}" is not bijective, since' | 
|  | 'it does not have the same length as the data.'.format( | 
|  | data_perm)) | 
|  | for k in self.data_perm: | 
|  | if k >= total_bitlen: | 
|  | raise RuntimeError( | 
|  | 'Data permutation "{}" is not bijective,' | 
|  | 'since the index {} is out of bounds.'.format( | 
|  | data_perm, k)) | 
|  |  | 
|  | if len(set(self.data_perm)) != total_bitlen: | 
|  | raise RuntimeError( | 
|  | 'Data permutation "{}" is not bijective,' | 
|  | 'since it contains duplicated indices.'.format(data_perm)) | 
|  |  | 
|  | def streamout_memfile(self) -> Tuple[str, int]: | 
|  | '''Streamout of memory image in MEM file format | 
|  |  | 
|  | Returns a tuple of the file contents and architecture bitness. | 
|  | ''' | 
|  |  | 
|  | log.info('Scramble and stream out partitions.') | 
|  | log.info('') | 
|  | otp_size = self.config['otp']['size'] | 
|  | data = [0] * otp_size | 
|  | annotation = [''] * otp_size | 
|  | for part in self.config['partitions']: | 
|  | part_data, part_annotation = self.streamout_partition(part) | 
|  | assert part['offset'] <= otp_size, \ | 
|  | 'Partition offset out of bounds' | 
|  | idx_low = part['offset'] | 
|  | idx_high = part['offset'] + part['size'] | 
|  | data[idx_low:idx_high] = part_data | 
|  | annotation[idx_low:idx_high] = part_annotation | 
|  |  | 
|  | log.info('') | 
|  | log.info('Streamout successfully completed.') | 
|  |  | 
|  | # Smoke checks | 
|  | assert len(data) <= otp_size, 'Data size mismatch' | 
|  | assert len(annotation) <= otp_size, 'Annotation size mismatch' | 
|  | assert len(data) == len(annotation), 'Data/Annotation size mismatch' | 
|  |  | 
|  | return _to_memfile_with_ecc(data, annotation, self.lc_state.config, | 
|  | self.data_perm) |