def main() -> None: parser = argparse.ArgumentParser() parser.add_argument('hjson') parser.add_argument('infile', type=argparse.FileType('rb')) parser.add_argument('outfile', type=argparse.FileType('w')) args = parser.parse_args() scrambler = Scrambler.from_hjson_path(args.hjson, ROM_SIZE_WORDS) # Load the input ELF file clr_mem = MemFile.load_elf32(args.infile, 4 * ROM_BASE_WORD) # Flatten the file, padding with pseudo-random data and ensuring it's # exactly scrambler.rom_size_words words long. clr_flat = scrambler.flatten(clr_mem) # Extend from 32 bits to 39 by adding Hsiao (39,32) ECC bits. clr_flat.add_ecc32() # Zero-extend the cleartext memory by one more bit (this is the size we # actually use in the physical ROM) assert clr_flat.width == 39 clr_flat.width = 40 # Scramble the memory scr_mem = scrambler.scramble40(clr_flat) # TODO: Calculate and insert the expected hash here. scr_mem.write_vmem(args.outfile)
def scramble40(self, mem: MemFile) -> MemFile: assert len(mem.chunks) == 1 assert len(mem.chunks[0].words) == self.rom_size_words word_width = 40 # Write addr_sp, data_sp for the S&P networks for address and data, # respectively. Write clr[i] for unscrambled data word i and scr[i] for # scrambled data word i. We need to construct scr[0], scr[1], ..., # scr[self.rom_size_words]. # # Then, for all i, we have: # # clr[i] = PRINCE(i) ^ data_sp(scr[addr_sp(i)]) # # Change coordinates by evaluating at addr_sp_inv(i): # # clr[addr_sp_inv(i)] = PRINCE(addr_sp_inv(i)) ^ data_sp(scr[i]) # # so # # scr[i] = data_sp_inv(clr[addr_sp_inv(i)] ^ PRINCE(addr_sp_inv(i))) subst_perm_rounds = 2 num_rounds_half = 2 assert word_width <= 64 word_mask = (1 << word_width) - 1 data_nonce_width = 64 - self._addr_width data_scr_nonce = self.nonce & ((1 << data_nonce_width) - 1) addr_scr_nonce = self.nonce >> data_nonce_width scrambled = [] for phy_addr in range(self.rom_size_words): log_addr = subst_perm_dec(phy_addr, addr_scr_nonce, self._addr_width, subst_perm_rounds) assert 0 <= log_addr < self.rom_size_words to_scramble = (data_scr_nonce << self._addr_width) | log_addr keystream = prince(to_scramble, self.key, num_rounds_half) keystream_trunc = keystream & word_mask clr_data = mem.chunks[0].words[log_addr] assert 0 <= clr_data < word_mask sp_scr_data = keystream_trunc ^ clr_data scr_data = subst_perm_enc(sp_scr_data, 0, word_width, subst_perm_rounds) assert 0 <= scr_data < word_mask scrambled.append(scr_data) return MemFile(mem.width, [MemChunk(0, scrambled)])
def main() -> int: parser = argparse.ArgumentParser() parser.add_argument('hjson') parser.add_argument('infile', type=argparse.FileType('rb')) parser.add_argument('outfile', type=argparse.FileType('w')) args = parser.parse_args() scrambler = Scrambler.from_hjson_path(args.hjson, ROM_SIZE_WORDS) # Load the input ELF file clr_mem = MemFile.load_elf32(args.infile, 4 * ROM_BASE_WORD) # Flatten the file, padding with pseudo-random data and ensuring it's # exactly scrambler.rom_size_words words long. clr_flat = scrambler.flatten(clr_mem) # Extend from 32 bits to 39 by adding Hsiao (39,32) ECC bits. clr_flat.add_ecc32() # Zero-extend the cleartext memory by one more bit (this is the size we # actually use in the physical ROM) assert clr_flat.width == 39 clr_flat.width = 40 # Scramble the memory scr_mem = scrambler.scramble40(clr_flat) # Insert the expected hash here to the top 8 words scrambler.add_hash(scr_mem) # Check for collisions collisions = scr_mem.collisions() if collisions: print( 'ERROR: This combination of ROM contents and scrambling\n' ' key results in one or more collisions where\n' ' different addresses have the same data.\n' '\n' ' Looks like we\'ve been (very) unlucky with the\n' ' birthday problem. As a work-around, try again after\n' ' generating some different RndCnst* parameters.\n', file=sys.stderr) print('{} colliding addresses:'.format(len(collisions)), file=sys.stderr) for addr0, addr1 in collisions: print(' {:#010x}, {:#010x}'.format(addr0, addr1), file=sys.stderr) return 1 scr_mem.write_vmem(args.outfile) return 0
def main() -> int: parser = argparse.ArgumentParser() parser.add_argument('infile', type=argparse.FileType('rb')) parser.add_argument('outfile', type=argparse.FileType('w')) parser.add_argument('--swap-nibbles', dest='swap_nibbles', action='store_true') args = parser.parse_args() # Extract width from ROM file name. match = re.search(r'([0-9]+).vmem', args.infile.name) if not match: raise ValueError('Cannot extract ROM word width from file name ' + args.infile.name) else: width = int(match.group(1)) # Load the input vmem file. vmem = MemFile.load_vmem(width, args.infile) # OpenTitan vmem files should always contain one single contiguous chunk. assert len(vmem.chunks) == 1 # Loop over all words, and: # 1) Generate the address, # 2) convert the endianness, and # 3) write this to the output file. addr_chars = 8 word_chars = math.ceil(width / 4) for idx, word in enumerate(vmem.chunks[0].words): # Generate the address. addr = idx * math.ceil(width / 8) # Convert endianness. data = swap_bytes(width, word, args.swap_nibbles) # Write to file. toks = [f'@{addr:0{addr_chars}X}'] toks.append(f'{data:0{word_chars}X}') args.outfile.write(' '.join(toks) + '\n') return 0
def scramble(self, mem: MemFile) -> MemFile: assert len(mem.chunks) == 1 assert len(mem.chunks[0].words) == self.rom_size_words width = mem.width # Write addr_sp, data_sp for the S&P networks for address and data, # respectively. Write clr[i] for unscrambled data word i and scr[i] for # scrambled data word i. We need to construct scr[0], scr[1], ..., # scr[self.rom_size_words]. # # Then, for all i, we have: # # clr[i] = PRINCE(i) ^ data_sp_dec(scr[addr_sp_enc(i)]) # # Change coordinates by evaluating at addr_sp_dec(i): # # clr[addr_sp_dec(i)] = PRINCE(addr_sp_dec(i)) ^ data_sp_dec(scr[i]) # # so # # scr[i] = data_sp_enc(clr[addr_sp_dec(i)] ^ PRINCE(addr_sp_dec(i))) # # Using the scramble_word helper function, this is: # # scr[i] = scramble_word(width, addr_sp_dec(i), clr[addr_sp_dec(i)]) assert width <= 64 scrambled = [] for phy_addr in range(self.rom_size_words): log_addr = self.addr_sp_dec(phy_addr) assert 0 <= log_addr < self.rom_size_words clr_data = mem.chunks[0].words[log_addr] assert 0 <= clr_data < (1 << width) scrambled.append(self.scramble_word(width, log_addr, clr_data)) return MemFile(mem.width, [MemChunk(0, scrambled)])
def flatten(self, mem: MemFile) -> MemFile: '''Flatten and pad mem up to the correct size This adds 8 trailing zero words as space to store the expected hash. These are (obviously!) not the right hash: we inject them properly later. ''' digest_size_words = 8 initial_len = self.rom_size_words - digest_size_words seed = self.key + self.nonce flattened = mem.flatten(initial_len, seed) assert len(flattened.chunks) == 1 assert len(flattened.chunks[0].words) == initial_len # Add the 8 trailing zero words. We do it here, rather than passing # rom_size_words to mem.flatten, to make sure that we see the error if # mem is too big. flattened.chunks[0].words += [0] * digest_size_words return flattened