blob: dd807a151259175b82c50a430f707a738d87a464 [file] [log] [blame]
#!/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()