blob: aa1a8b1e246998401f2c866e9b77f658e7483aea [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2013 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""A script to pack EC binary into SPI flash image for MEC17xx
Based on MEC170x_ROM_Description.pdf DS00002225C (07-28-17).
"""
import argparse
import hashlib
import os
import struct
import subprocess
import tempfile
import zlib # CRC32
# pylint:disable=invalid-name
# MEC1701 has 256KB SRAM from 0xE0000 - 0x120000
# SRAM is divided into contiguous CODE & DATA
# CODE at [0xE0000, 0x117FFF] DATA at [0x118000, 0x11FFFF]
# SPI flash size for board is 512KB
# Boot-ROM TAG is located at SPI offset 0 (two 4-byte tags)
#
LFW_SIZE = 0x1000
LOAD_ADDR = 0x0E0000
LOAD_ADDR_RW = 0xE1000
HEADER_SIZE = 0x40
SPI_CLOCK_LIST = [48, 24, 16, 12]
SPI_READ_CMD_LIST = [0x3, 0xB, 0x3B, 0x6B]
CRC_TABLE = [
0x00,
0x07,
0x0E,
0x09,
0x1C,
0x1B,
0x12,
0x15,
0x38,
0x3F,
0x36,
0x31,
0x24,
0x23,
0x2A,
0x2D,
]
def mock_print(*_args, **_kwargs):
"""Does nothing."""
debug_print = mock_print
def Crc8(crc, data):
"""Update CRC8 value."""
for v in data:
crc = ((crc << 4) & 0xFF) ^ (CRC_TABLE[(crc >> 4) ^ (v >> 4)])
crc = ((crc << 4) & 0xFF) ^ (CRC_TABLE[(crc >> 4) ^ (v & 0xF)])
return crc ^ 0x55
def GetEntryPoint(payload_file):
"""Read entry point from payload EC image."""
with open(payload_file, "rb") as f:
f.seek(4)
s = f.read(4)
return struct.unpack("<I", s)[0]
def GetPayloadFromOffset(payload_file, offset):
"""Read payload and pad it to 64-byte aligned."""
with open(payload_file, "rb") as f:
f.seek(offset)
payload = bytearray(f.read())
rem_len = len(payload) % 64
if rem_len:
payload += b"\0" * (64 - rem_len)
return payload
def GetPayload(payload_file):
"""Read payload and pad it to 64-byte aligned."""
return GetPayloadFromOffset(payload_file, 0)
def GetPublicKey(pem_file):
"""Extract public exponent and modulus from PEM file."""
result = subprocess.run(
["openssl", "rsa", "-in", pem_file, "-text", "-noout"],
stdout=subprocess.PIPE,
encoding="utf-8",
check=True,
)
modulus_raw = []
in_modulus = False
exp = 0
for line in result.stdout.splitlines():
if line.startswith("modulus"):
in_modulus = True
elif not line.startswith(" "):
in_modulus = False
elif in_modulus:
modulus_raw.extend(line.strip().strip(":").split(":"))
if line.startswith("publicExponent"):
exp = int(line.split(" ")[1], 10)
modulus_raw.reverse()
modulus = bytearray((int(x, 16) for x in modulus_raw[:256]))
return struct.pack("<Q", exp), modulus
def GetSpiClockParameter(args):
"""Return the SPI clock parameter."""
assert (
args.spi_clock in SPI_CLOCK_LIST
), f"Unsupported SPI clock speed {args.spi_clock:d} MHz"
return SPI_CLOCK_LIST.index(args.spi_clock)
def GetSpiReadCmdParameter(args):
"""Return the SPI read cmd parameter."""
assert (
args.spi_read_cmd in SPI_READ_CMD_LIST
), f"Unsupported SPI read command 0x{args.spi_read_cmd:x}"
return SPI_READ_CMD_LIST.index(args.spi_read_cmd)
def PadZeroTo(data, size):
"""Pad data with zeros."""
data.extend(b"\0" * (size - len(data)))
def BuildHeader(args, payload_len, load_addr, rorofile):
"""Builds a header."""
# Identifier and header version
header = bytearray(b"PHCM\0")
# byte[5]
b = GetSpiClockParameter(args)
b |= 1 << 2
header.append(b)
# byte[6]
b = 0
header.append(b)
# byte[7]
header.append(GetSpiReadCmdParameter(args))
# bytes 0x08 - 0x0b
header.extend(struct.pack("<I", load_addr))
# bytes 0x0c - 0x0f
header.extend(struct.pack("<I", GetEntryPoint(rorofile)))
# bytes 0x10 - 0x13
header.append((payload_len >> 6) & 0xFF)
header.append((payload_len >> 14) & 0xFF)
PadZeroTo(header, 0x14)
# bytes 0x14 - 0x17
header.extend(struct.pack("<I", args.payload_offset))
# bytes 0x14 - 0x3F all 0
PadZeroTo(header, 0x40)
# header signature is appended by the caller
return header
def BuildHeader2(args, payload_len, load_addr, payload_entry):
"""Build a header, but differently than BuildHeader()."""
# Identifier and header version
header = bytearray(b"PHCM\0")
# byte[5]
b = GetSpiClockParameter(args)
b |= 1 << 2
header.append(b)
# byte[6]
b = 0
header.append(b)
# byte[7]
header.append(GetSpiReadCmdParameter(args))
# bytes 0x08 - 0x0b
header.extend(struct.pack("<I", load_addr))
# bytes 0x0c - 0x0f
header.extend(struct.pack("<I", payload_entry))
# bytes 0x10 - 0x13
header.append((payload_len >> 6) & 0xFF)
header.append((payload_len >> 14) & 0xFF)
PadZeroTo(header, 0x14)
# bytes 0x14 - 0x17
header.extend(struct.pack("<I", args.payload_offset))
# bytes 0x14 - 0x3F all 0
PadZeroTo(header, 0x40)
# header signature is appended by the caller
return header
def HashByteArray(data):
"""Compute SHA-256 of data and return digest as a bytearray"""
hasher = hashlib.sha256()
hasher.update(data)
h = hasher.digest()
bah = bytearray(h)
return bah
def SignByteArray(data):
"""Return 64-byte signature of byte array data.
Signature is SHA256 of data with 32 0 bytes appended
"""
debug_print("Signature is SHA-256 of data")
sigb = HashByteArray(data)
sigb.extend(b"\0" * 32)
return sigb
def BuildTag(args):
"""MEC1701H supports two 32-bit Tags located at offsets 0x0 and 0x4
in the SPI flash.
Tag format:
bits[23:0] correspond to bits[31:8] of the Header SPI address
Header is always on a 256-byte boundary.
bits[31:24] = CRC8-ITU of bits[23:0].
Notice there is no chip-select field in the Tag both Tag's point
to the same flash part.
"""
tag = bytearray(
[
(args.header_loc >> 8) & 0xFF,
(args.header_loc >> 16) & 0xFF,
(args.header_loc >> 24) & 0xFF,
]
)
tag.append(Crc8(0, tag))
return tag
def BuildTagFromHdrAddr(header_loc):
"""Build a tag from a header address."""
tag = bytearray(
[
(header_loc >> 8) & 0xFF,
(header_loc >> 16) & 0xFF,
(header_loc >> 24) & 0xFF,
]
)
tag.append(Crc8(0, tag))
return tag
#
# Creates temporary file for read/write
# Reads binary file containing LFW image_size (loader_file)
# Writes LFW image to temporary file
# Reads RO image at beginning of rorw_file up to image_size
# (assumes RO/RW images have been padded with 0xFF
# Returns temporary file name
#
def PacklfwRoImage(rorw_file, loader_file, image_size):
"""Create a temp file with the
first image_size bytes from the loader file and append bytes
from the rorw file.
return the filename"""
with tempfile.NamedTemporaryFile(
delete=False
) as fo: # Need to keep file around
with open(loader_file, "rb") as fin1: # read 4KB loader file
pro = fin1.read()
fo.write(pro) # write 4KB loader data to temp file
with open(rorw_file, "rb") as fin:
ro = fin.read(image_size)
fo.write(ro)
return fo.name
def gen_test_ecrw(pldrw):
"""Generate a test EC_RW image of same size as original.
Preserve image_data structure and fill all
other bytes with 0xA5.
useful for testing SPI read and EC build
process hash generation.
"""
debug_print("gen_test_ecrw: pldrw type =", type(pldrw))
debug_print("len pldrw =", len(pldrw), " = ", hex(len(pldrw)))
cookie1_pos = pldrw.find(b"\x99\x88\x77\xce")
cookie2_pos = pldrw.find(b"\xdd\xbb\xaa\xce", cookie1_pos + 4)
t = struct.unpack("<L", pldrw[cookie1_pos + 0x24 : cookie1_pos + 0x28])
size = t[0]
debug_print("EC_RW size =", size, " = ", hex(size))
debug_print("Found cookie1 at ", hex(cookie1_pos))
debug_print("Found cookie2 at ", hex(cookie2_pos))
if cookie2_pos > cookie1_pos > 0:
for i in range(0, cookie1_pos):
pldrw[i] = 0xA5
for i in range(cookie2_pos + 4, len(pldrw)):
pldrw[i] = 0xA5
with open("ec_RW_test.bin", "wb") as fecrw:
fecrw.write(pldrw[:size])
def parseargs():
"""Parse command line args."""
parser = argparse.ArgumentParser()
parser.add_argument(
"-i",
"--input",
help="EC binary to pack, usually ec.bin or ec.RO.flat.",
metavar="EC_BIN",
default="ec.bin",
)
parser.add_argument(
"-o",
"--output",
help="Output flash binary file",
metavar="EC_SPI_FLASH",
default="ec.packed.bin",
)
parser.add_argument(
"--loader_file", help="EC loader binary", default="ecloader.bin"
)
parser.add_argument(
"-s",
"--spi_size",
type=int,
help="Size of the SPI flash in KB",
default=512,
)
parser.add_argument(
"-l",
"--header_loc",
type=int,
help="Location of header in SPI flash",
default=0x1000,
)
parser.add_argument(
"-p",
"--payload_offset",
type=int,
help="The offset of payload from the start of header",
default=0x80,
)
parser.add_argument(
"-r",
"--rw_loc",
type=int,
help="Start offset of EC_RW. Default is -1 meaning 1/2 flash size",
default=-1,
)
parser.add_argument(
"--spi_clock",
type=int,
help="SPI clock speed. 8, 12, 24, or 48 MHz.",
default=24,
)
parser.add_argument(
"--spi_read_cmd",
type=int,
help="SPI read command. 0x3, 0xB, or 0x3B.",
default=0xB,
)
parser.add_argument(
"--image_size",
type=int,
help="Size of a single image. Default 220KB",
default=(220 * 1024),
)
parser.add_argument(
"--test_spi",
action="store_true",
help="Test SPI data integrity by adding CRC32 in last 4-bytes of RO/RW binaries",
default=False,
)
parser.add_argument(
"--test_ecrw",
action="store_true",
help="Use fixed pattern for EC_RW but preserve image_data",
default=False,
)
parser.add_argument(
"--verbose",
action="store_true",
help="Enable verbose output",
default=False,
)
return parser.parse_args()
def dumpsects(spi_list):
"""Debug helper routine"""
debug_print(f"spi_list has {len(spi_list)} entries")
for s in spi_list:
debug_print(f"0x{s[0]:x} 0x{len(s[1]):x} {s[2]:s}")
def printByteArrayAsHex(ba, title):
"""Print a byte array as hex."""
debug_print(title, "= ")
count = 0
for b in ba:
count = count + 1
debug_print(f"0x{b:02x}, ", end="")
if (count % 8) == 0:
debug_print("")
debug_print("\n")
def print_args(args):
"""Print all of the command line args."""
debug_print("parsed arguments:")
debug_print(".input = ", args.input)
debug_print(".output = ", args.output)
debug_print(".loader_file = ", args.loader_file)
debug_print(".spi_size (KB) = ", hex(args.spi_size))
debug_print(".image_size = ", hex(args.image_size))
debug_print(".header_loc = ", hex(args.header_loc))
debug_print(".payload_offset = ", hex(args.payload_offset))
if args.rw_loc < 0:
debug_print(".rw_loc = ", args.rw_loc)
else:
debug_print(".rw_loc = ", hex(args.rw_loc))
debug_print(".spi_clock = ", args.spi_clock)
debug_print(".spi_read_cmd = ", args.spi_read_cmd)
debug_print(".test_spi = ", args.test_spi)
debug_print(".verbose = ", args.verbose)
#
# Handle quiet mode build from Makefile
# Quiet mode when V is unset or V=0
# Verbose mode when V=1
#
def main():
"""Main function."""
global debug_print # pylint:disable=global-statement
args = parseargs()
if args.verbose:
debug_print = print
debug_print("Begin MEC17xx pack_ec.py script")
# MEC17xx maximum 192KB each for RO & RW
# mec1701 chip Makefile sets args.spi_size = 512
# Tags at offset 0
#
print_args(args)
spi_size = args.spi_size * 1024
debug_print("SPI Flash image size in bytes =", hex(spi_size))
# !!! IMPORTANT !!!
# These values MUST match chip/mec1701/config_flash_layout.h
# defines.
# MEC17xx Boot-ROM TAGs are at offset 0 and 4.
# lfw + EC_RO starts at beginning of second 4KB sector
# EC_RW starts at offset 0x40000 (256KB)
spi_list = []
debug_print("args.input = ", args.input)
debug_print("args.loader_file = ", args.loader_file)
debug_print("args.image_size = ", hex(args.image_size))
rorofile = PacklfwRoImage(args.input, args.loader_file, args.image_size)
payload = GetPayload(rorofile)
payload_len = len(payload)
# debug
debug_print("EC_LFW + EC_RO length = ", hex(payload_len))
# SPI image integrity test
# compute CRC32 of EC_RO except for last 4 bytes
# skip over 4KB LFW
# Store CRC32 in last 4 bytes
if args.test_spi:
crc = zlib.crc32(bytes(payload[LFW_SIZE : (payload_len - 4)]))
crc_ofs = payload_len - 4
debug_print(f"EC_RO CRC32 = 0x{crc:08x} @ 0x{crc_ofs:08x}")
for i in range(4):
payload[crc_ofs + i] = crc & 0xFF
crc = crc >> 8
# Chromebooks are not using MEC BootROM ECDSA.
# We implemented the ECDSA disabled case where
# the 64-byte signature contains a SHA-256 of the binary plus
# 32 zeros bytes.
payload_signature = SignByteArray(payload)
# debug
printByteArrayAsHex(payload_signature, "LFW + EC_RO payload_signature")
# MEC17xx Header is 0x80 bytes with an 64 byte signature
# (32 byte SHA256 + 32 zero bytes)
header = BuildHeader(args, payload_len, LOAD_ADDR, rorofile)
# debug
printByteArrayAsHex(header, "Header LFW + EC_RO")
# MEC17xx payload ECDSA not used, 64 byte signature is
# SHA256 + 32 zero bytes
header_signature = SignByteArray(header)
# debug
printByteArrayAsHex(header_signature, "header_signature")
tag = BuildTag(args)
# MEC17xx truncate RW length to args.image_size to not overwrite LFW
# offset may be different due to Header size and other changes
# MCHP we want to append a SHA-256 to the end of the actual payload
# to test SPI read routines.
debug_print("Call to GetPayloadFromOffset")
debug_print("args.input = ", args.input)
debug_print("args.image_size = ", hex(args.image_size))
payload_rw = GetPayloadFromOffset(args.input, args.image_size)
debug_print("type(payload_rw) is ", type(payload_rw))
debug_print("len(payload_rw) is ", hex(len(payload_rw)))
# truncate to args.image_size
rw_len = args.image_size
payload_rw = payload_rw[:rw_len]
payload_rw_len = len(payload_rw)
debug_print("Truncated size of EC_RW = ", hex(payload_rw_len))
payload_entry_tuple = struct.unpack_from("<I", payload_rw, 4)
debug_print("payload_entry_tuple = ", payload_entry_tuple)
payload_entry = payload_entry_tuple[0]
debug_print("payload_entry = ", hex(payload_entry))
# Note: payload_rw is a bytearray therefore is mutable
if args.test_ecrw:
gen_test_ecrw(payload_rw)
# SPI image integrity test
# compute CRC32 of EC_RW except for last 4 bytes
# Store CRC32 in last 4 bytes
if args.test_spi:
crc = zlib.crc32(bytes(payload_rw[: (payload_rw_len - 32)]))
crc_ofs = payload_rw_len - 4
debug_print(f"EC_RW CRC32 = 0x{crc:08x} at offset 0x{crc_ofs:08x}")
for i in range(4):
payload_rw[crc_ofs + i] = crc & 0xFF
crc = crc >> 8
payload_rw_sig = SignByteArray(payload_rw)
# debug
printByteArrayAsHex(payload_rw_sig, "payload_rw_sig")
os.remove(rorofile) # clean up the temp file
# MEC170x Boot-ROM Tags are located at SPI offset 0
spi_list.append((0, tag, "tag"))
spi_list.append((args.header_loc, header, "header(lwf + ro)"))
spi_list.append(
(
args.header_loc + HEADER_SIZE,
header_signature,
"header(lwf + ro) signature",
)
)
spi_list.append(
(args.header_loc + args.payload_offset, payload, "payload(lfw + ro)")
)
offset = args.header_loc + args.payload_offset + payload_len
# No SPI Header for EC_RW as its not loaded by BootROM
spi_list.append((offset, payload_signature, "payload(lfw_ro) signature"))
# EC_RW location
rw_offset = int(spi_size // 2)
if args.rw_loc >= 0:
rw_offset = args.rw_loc
debug_print(f"rw_offset = 0x{rw_offset:08x}")
if rw_offset < offset + len(payload_signature):
print("ERROR: EC_RW overlaps EC_RO")
spi_list.append((rw_offset, payload_rw, "payload(rw)"))
# don't add to EC_RW. We don't know if Google will process
# EC SPI flash binary with other tools during build of
# coreboot and OS.
# offset = rw_offset + payload_rw_len
# spi_list.append((offset, payload_rw_sig, "payload(rw) signature"))
spi_list = sorted(spi_list)
dumpsects(spi_list)
#
# MEC17xx Boot-ROM locates TAG at SPI offset 0 instead of end of SPI.
#
with open(args.output, "wb") as f:
debug_print("Write spi list to file", args.output)
addr = 0
for s in spi_list:
if addr < s[0]:
debug_print(
"Offset ",
hex(addr),
" Length",
hex(s[0] - addr),
"fill with 0xff",
)
f.write(b"\xff" * (s[0] - addr))
addr = s[0]
debug_print(
"Offset ",
hex(addr),
" Length",
hex(len(s[1])),
"write data",
)
f.write(s[1])
addr += len(s[1])
if addr < spi_size:
debug_print(
"Offset ",
hex(addr),
" Length",
hex(spi_size - addr),
"fill with 0xff",
)
f.write(b"\xff" * (spi_size - addr))
f.flush()
if __name__ == "__main__":
main()