hw/mcu/dialog/da1469x/scripts/otp_tool.py (456 lines of code) (raw):

#!/usr/bin/env python3 # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you 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. # import serial import io import click import struct import binascii import base64 from typing import NamedTuple import os from cryptography.hazmat.primitives import serialization # Add in path to mcuboot. This is required to pull in keys module from imgtool import sys sys.path.append(os.path.join(os.getcwd(), "repos", "mcuboot", "scripts", "imgtool")) import keys as keys DEFAULT_BAUDRATE = 500000 # Cmds that apply for the dialog BSP are defined here. # Custom Commands are defined in the custom BSP starting # at a different offset so that the default command set # can be expanded if needed. class Cmd(object): OTP_READ_KEY = 0 OTP_WRITE_KEY = 1 FLASH_READ = 2 FLASH_WRITE = 3 OTP_READ_CONFIG = 4 OTP_APPEND_VALUE = 5 OTP_INIT = 6 FLASH_ERASE = 7 TEST_ALIVE = 8 class cmd_no_payload(NamedTuple): som: int cmd: int class cmd_read_key(NamedTuple): som: int cmd: int segment: int slot: int class cmd_write_key(NamedTuple): som: int cmd: int segment: int slot: int class cmd_append_value(NamedTuple): som: int cmd: int length: int class cmd_response(NamedTuple): som: int cmd: int status: int length: int class cmd_flash_op(NamedTuple): som: int cmd: int index: int offset: int length: int def crc16(data: bytearray, offset, length): if data is None or offset < 0: return if offset > len(data) - 1 or offset + length > len(data): return 0 crc = 0 for i in range(length): crc ^= data[offset + i] << 8 for _ in range(8): if (crc & 0x8000) > 0: crc = (crc << 1) ^ 0x1021 else: crc = crc << 1 return crc & 0xFFFF def validate_slot_index(ctx, param, value): if value in range(8): return value else: raise click.BadParameter("Slot value has to be between 0 and 7") def read_exact(device, length): data = device.read(length) if len(data) != length: raise SystemExit("Failed to receive expected response, exiting") return data def validate_response(response): if response.som != 0x55aa55aa: raise SystemExit("SOM not valid, invalid response") return True def get_serial_port(port, baudrate, timeout, bytesize, stopbits): try: ser = serial.Serial(port=port, baudrate=baudrate, timeout=timeout, bytesize=bytesize, stopbits=stopbits) except serial.SerialException: raise SystemExit("Failed to open serial port") # drain serial port buffer try: while True: data = ser.read(1) if len(data) == 0: break except serial.SerialException: raise SystemExit("Failed to open serial port") return ser def otp_read_key(index, segment, uart, baudrate): seg_map = {'signature': 0, 'data': 1, 'qspi': 2} ser = get_serial_port(uart, baudrate, 1, 8, serial.STOPBITS_ONE) cmd = cmd_read_key(0xaa55aa55, Cmd.OTP_READ_KEY, seg_map[segment], index) data = struct.pack('IIII', *cmd) try: ser.write(data) except serial.SerialException: raise SystemExit("Failed to write to %s" % uart) data = read_exact(ser, 16) response = cmd_response._make(struct.unpack_from('IIII', data)) validate_response(response) if response.status == 0: key = ser.read(32) else: raise SystemExit("Error reading key with status %s" % hex(response.status)) return key @click.argument('infile') @click.option('-u', '--uart', required=True, help='uart port') @click.option('-i', '--index', required=True, type=int, help='key slot index', callback=validate_slot_index) @click.option('-s', '--segment', type=click.Choice(['signature', 'data', 'qspi']), help='OTP segment', required=True,) @click.option('-r', '--baudrate', default=DEFAULT_BAUDRATE, help='default baudrate') @click.command(help='Write a single key to OTP key segment') def otp_write_key(infile, index, segment, uart, baudrate): key = bytearray() try: with open(infile, "rb") as f: if segment == 'signature': # read key from ED25519 pem file try: sig = keys.load(infile) except ValueError: raise SystemExit("Invalid PEM file") buf = sig._get_public().public_bytes( encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw) else: # read key from base64 encoded AES key file buf = f.read() try: buf = base64.decodestring(buf) except ValueError: raise SystemExit("Invalid base 64 file") if len(buf) != 32: raise SystemExit("AES key file has incorrect length") key = struct.unpack('IIIIIIII', buf) except IOError: raise SystemExit("Failed to read key from file") seg_map = {'signature': 0, 'data': 1, 'qspi': 2} ser = get_serial_port(uart, baudrate, 1, 8, serial.STOPBITS_ONE) cmd = cmd_write_key(0xaa55aa55, Cmd.OTP_WRITE_KEY, seg_map[segment], index) # swap endianness of data to little endian msg = struct.pack('IIII', *cmd) key_bytes = struct.pack('>IIIIIIII', *key) msg += (key_bytes) msg += struct.pack('H', crc16(key_bytes, 0, 32)) ser.write(msg) data = read_exact(ser, 16) response = cmd_response._make(struct.unpack_from('IIII', data)) validate_response(response) if response.status == 0: print("Key successfully updated") else: raise SystemExit('Error writing key with status %s ' % hex(response.status)) def generate_payload(data): msg = bytearray() for entry in data: msg += entry.to_bytes(4, 'little') return msg @ click.argument('outfile') @click.option('-u', '--uart', required=True, help='uart port') @click.option('-r', '--baudrate', default=DEFAULT_BAUDRATE, help='default baudrate') @click.command(help='Read data from OTP configuration script area') def otp_read_config(uart, outfile, baudrate): ser = get_serial_port(uart, baudrate, 1, 8, serial.STOPBITS_ONE) # length is unused, so set to 0 cmd = cmd_append_value(0xaa55aa55, Cmd.OTP_READ_CONFIG, 0) data = struct.pack('III', *cmd) try: ser.write(data) except serial.SerialException: raise SystemExit("Failed to write to %s" % uart) data = read_exact(ser, 16) response = cmd_response._make(struct.unpack_from('IIII', data)) validate_response(response) if response.status == 0: data = ser.read(response.length) if len(data) != response.length: raise SystemExit("Failed to receive data, exiting") print("Successfully read configuration script, writing to file: " + outfile) try: with open(outfile, "wb") as f: f.write(data) except IOError: raise SystemExit("Failed to write config data to file") @click.argument('outfile') @click.option('-a', '--offset', type=str, required=True, help='flash address offset from base, hexadecimal') @click.option('-l', '--length', type=int, required=True, help='length to read') @click.option('-u', '--uart', required=True, help='uart port') @click.option('-r', '--baudrate', default=DEFAULT_BAUDRATE, help='default baudrate') @click.command(help='Read from flash') def flash_read(uart, length, outfile, offset, baudrate): ser = get_serial_port(uart, baudrate, 1, 8, serial.STOPBITS_ONE) try: f = open(outfile, "wb") except IOError: raise SystemExit("Failed to open output file") bytes_left = length offset = int(offset, 16) retry = 3 while bytes_left > 0: if bytes_left > 4096: trans_length = 4096 else: trans_length = bytes_left cmd = cmd_flash_op(0xaa55aa55, Cmd.FLASH_READ, 0, offset, trans_length) data = struct.pack('IIIII', *cmd) try: ser.write(data) except serial.SerialException: raise SystemExit("Failed to write cmd to %s" % uart) data = read_exact(ser, 16) response = cmd_response._make(struct.unpack_from('IIII', data)) validate_response(response) if response.status == 0: data = ser.read(response.length) if len(data) != response.length: raise SystemExit("Failed to receive response, exiting") # data has a crc on the end crc_computed = crc16(data[:-2], 0, response.length - 2) crc = struct.unpack('!H', data[response.length -2 :])[0] if crc == crc_computed: retry = 0 else: if retry == 0: raise SystemExit("Data corruption retries exceeded, exiting") print("Data crc failed, retrying\n"); retry -= 1 continue f.write(data[:-2]) else: raise SystemExit("Error in read response, exiting") break bytes_left -= trans_length offset += trans_length print("Successfully read flash, wrote contents to " + outfile) @click.option('-a', '--offset', type=str, required=True, help='flash address offset, in hex') @click.option('-l', '--length', type=int, required=True, help='size to erase') @click.option('-u', '--uart', required=True, help='uart port') @click.option('-r', '--baudrate', default=DEFAULT_BAUDRATE, help='default baudrate') @click.command(help='Erase flash') def flash_erase(uart, offset, length, baudrate): ser = get_serial_port(uart, baudrate, 1, 8, serial.STOPBITS_ONE) # length is unused, so set to 0 offset = int(offset, 16) cmd = cmd_flash_op(0xaa55aa55, Cmd.FLASH_ERASE, 0, offset, length) msg = struct.pack('IIIII', *cmd) try: ser.write(msg) except serial.SerialException: raise SystemExit("Failed to write to %s" % uart) data = read_exact(ser, 16) response = cmd_response._make(struct.unpack_from('IIII', data)) validate_response(response) if response.status != 0: raise SystemExit("Failed to erase flash, exiting") print("Successfully erased flash") @click.argument('infile') @click.option('-a', '--offset', type=str, required=True, help='flash address offset, in hex') @click.option('-b', '--block-size', type=int, default=4096, help='block size') @click.option('-u', '--uart', required=True, help='uart port') @click.option('-r', '--baudrate', default=DEFAULT_BAUDRATE, help='default baudrate') @click.command(help='Write to flash') def flash_write(uart, infile, offset, block_size, baudrate): ser = get_serial_port(uart, baudrate, 1, 8, serial.STOPBITS_ONE) try: f = open(infile, "rb") except IOError: raise SystemExit("Failed to open input file") length = os.path.getsize(infile) bytes_left = length offset = int(offset, 16) while bytes_left > 0: if bytes_left > block_size: trans_length = block_size else: trans_length = bytes_left cmd = cmd_flash_op(0xaa55aa55, Cmd.FLASH_WRITE, 0, offset, trans_length + 2) data = struct.pack('IIIII', *cmd) f_bytes = f.read(trans_length) data += f_bytes data += struct.pack('H', crc16(f_bytes, 0, trans_length)) try: ser.write(data) except serial.SerialException: raise SystemExit("Failed to write cmd to %s" % uart) data = read_exact(ser, 16) response = cmd_response._make(struct.unpack_from('IIII', data)) validate_response(response) if response.status != 0: raise SystemExit("Flash write failed w/ %s, exiting" % hex(response.status)) bytes_left -= trans_length offset += trans_length print("Successfully wrote flash") def send_otp_config_payload(uart, data, baudrate): ser = get_serial_port(uart, baudrate, 1, 8, serial.STOPBITS_ONE) data_bytes = generate_payload(data) # length is unused, so set to 0 cmd = cmd_append_value(0xaa55aa55, Cmd.OTP_APPEND_VALUE, len(data_bytes) + 2) msg = struct.pack('III', *cmd) msg += data_bytes msg += struct.pack('H', crc16(data_bytes, 0, len(data_bytes))) try: ser.write(msg) except serial.SerialException: raise SystemExit("Failed to write to %s" % uart) data = read_exact(ser, 16) response = cmd_response._make(struct.unpack_from('IIII', data)) validate_response(response) if response.status == 0: data = ser.read(response.length) if len(data) != response.length: raise SystemExit("Failed to receive data, exiting") @click.command(help='Append register value to OTP configuration script') @click.option('-a', '--address', type=str, required=True, help='register address in hexadecimal') @click.option('-v', '--value', type=str, required=True, help='register value in hexadecimal') @click.option('-u', '--uart', required=True, help='uart port') @click.option('-r', '--baudrate', default=DEFAULT_BAUDRATE, help='default baudrate') def otp_append_register(uart, address, value, baudrate): send_otp_config_payload(uart, [int(address, 16), int(value, 16)], baudrate) @click.option('-t', '--trim', type=str, required=True, multiple=True, help='trim value') @click.option('-i', '--index', type=int, required=True, help='Trim value id') @click.option('-u', '--uart', required=True, help='uart port') @click.option('-r', '--baudrate', default=DEFAULT_BAUDRATE, help='default baudrate') @click.command(help='Append trim value to OTP configuration script') def otp_append_trim(uart, trim, index, baudrate): data = [] data.append(0x90000000 + (len(trim) << 8) + index) for entry in trim: data.append(int(entry, 16)) send_otp_config_payload(uart, data, baudrate) @click.option('-u', '--uart', required=True, help='uart port') @click.option('-r', '--baudrate', default=DEFAULT_BAUDRATE, help='default baudrate') @click.command(help='Disable development mode') def disable_development_mode(uart, baudrate): send_otp_config_payload(uart, [0x70000000], baudrate) @click.option('-u', '--uart', required=True, help='uart port') @click.option('-r', '--baudrate', default=DEFAULT_BAUDRATE, help='default baudrate') @click.command(help='Enable secure boot') def enable_secure_boot(uart, baudrate): send_otp_config_payload(uart, [0x500000cc, 0x1], baudrate) @click.option('-u', '--uart', required=True, help='uart port') @click.option('-r', '--baudrate', default=DEFAULT_BAUDRATE, help='default baudrate') @click.command(help='Write lock OTP QSPI key area') def disable_qspi_key_write(uart, baudrate): send_otp_config_payload(uart, [0x500000cc, 0x40], baudrate) @click.option('-u', '--uart', required=True, help='uart port') @click.option('-r', '--baudrate', default=DEFAULT_BAUDRATE, help='default baudrate') @click.command(help='Read lock OTP QSPI key area') def disable_qspi_key_read(uart, baudrate): send_otp_config_payload(uart, [0x500000cc, 0x80], baudrate) @click.option('-u', '--uart', required=True, help='uart port') @click.option('-r', '--baudrate', default=DEFAULT_BAUDRATE, help='default baudrate') @click.command(help='Write lock OTP user key area') def disable_user_key_write(uart, baudrate): send_otp_config_payload(uart, [0x500000cc, 0x10], baudrate) @click.option('-u', '--uart', required=True, help='uart port') @click.option('-r', '--baudrate', default=DEFAULT_BAUDRATE, help='default baudrate') @click.command(help='Read lock OTP user key area') def disable_user_key_read(uart, baudrate): send_otp_config_payload(uart, [0x500000cc, 0x20], baudrate) @click.option('-u', '--uart', required=True, help='uart port') @click.option('-r', '--baudrate', default=DEFAULT_BAUDRATE, help='default baudrate') @click.command(help='Write lock OTP signature key area') def disable_signature_key_write(uart, baudrate): send_otp_config_payload(uart, [0x500000cc, 0x8], baudrate) @click.option('-u', '--uart', required=True, help='uart port') @click.option('-r', '--baudrate', default=DEFAULT_BAUDRATE, help='default baudrate') @click.command(help='Disable CMAC debugger') def disable_cmac_debugger(uart, baudrate): send_otp_config_payload(uart, [0x500000cc, 0x4], baudrate) @click.option('-u', '--uart', required=True, help='uart port') @click.option('-r', '--baudrate', default=DEFAULT_BAUDRATE, help='default baudrate') @click.command(help='Disable SWD debugger') def disable_swd_debugger(uart, baudrate): send_otp_config_payload(uart, [0x500000cc, 0x2], baudrate) @click.option('-u', '--uart', required=True, help='uart port') @click.option('-r', '--baudrate', default=DEFAULT_BAUDRATE, help='default baudrate') @click.command(help='Close out OTP configuration script') def close_config_script(uart, baudrate): send_otp_config_payload(uart, [0x0], baudrate) @click.option('-u', '--uart', required=True, help='uart port') @click.option('-r', '--baudrate', default=DEFAULT_BAUDRATE, help='default baudrate') @click.command(help='Initialize blank OTP Config script') def init_config_script(uart, baudrate): ser = get_serial_port(uart, baudrate, 1, 8, serial.STOPBITS_ONE) # length is unused, so set to 0 cmd = cmd_append_value(0xaa55aa55, Cmd.OTP_INIT, 0) msg = struct.pack('III', *cmd) try: ser.write(msg) except serial.SerialException: raise SystemExit("Failed to write to %s" % uart) data = read_exact(ser, 16) response = cmd_response._make(struct.unpack_from('IIII', data)) validate_response(response) if response.status != 0: raise SystemExit('Failed to initialize OTP with status %d' % response.status) print("Successfully initialized blank OTP") @click.option('-u', '--uart', required=True, help='uart port') @click.option('-r', '--baudrate', default=DEFAULT_BAUDRATE, help='default baudrate') @click.command(help='Test if the board is alive by sending and receving data') def test_alive_target(uart, baudrate): ser = get_serial_port(uart, baudrate, 1, 8, serial.STOPBITS_ONE) # length is unused, so set to 0 cmd = cmd_append_value(0xaa55aa55, Cmd.TEST_ALIVE, 0) msg = struct.pack('III', *cmd) # 20 payload bytes and 16 header bytes rlen = 36 try: ser.write(msg) except serial.SerialException: raise SystemExit("Failed to write to %s" % uart) data = ser.read(rlen) if len(data) != rlen: raise SystemExit("Failed to receive response, exiting") response = cmd_response._make(struct.unpack_from('IIII', data)) validate_response(response) if response.status != 0: raise SystemExit('Failed to verify system status over UART: %d' % response.status) else: print("Successfully communicated with target") @click.group() def cli(): pass cli.add_command(otp_read_config) cli.add_command(otp_write_key) cli.add_command(flash_read) cli.add_command(flash_write) cli.add_command(flash_erase) cli.add_command(otp_append_register) cli.add_command(otp_append_trim) cli.add_command(disable_development_mode) cli.add_command(enable_secure_boot) cli.add_command(disable_qspi_key_write) cli.add_command(disable_qspi_key_read) cli.add_command(disable_user_key_write) cli.add_command(disable_user_key_read) cli.add_command(disable_signature_key_write) cli.add_command(disable_cmac_debugger) cli.add_command(disable_swd_debugger) cli.add_command(close_config_script) cli.add_command(init_config_script) cli.add_command(test_alive_target) if __name__ == '__main__': cli()