summaryrefslogtreecommitdiff
path: root/chip/mchp/util/pack_ec.py
blob: 7908b0bf375437c6d5f508a3ec2a505cbe0e3908 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
#!/usr/bin/env python3

# Copyright 2013 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# Ignore indention messages, since legacy scripts use 2 spaces instead of 4.
# pylint: disable=bad-indentation,docstring-section-indent
# pylint: disable=docstring-trailing-quotes

# 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

# 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):
  pass

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')
  modulus_raw = []
  in_modulus = False
  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):
  assert args.spi_clock in SPI_CLOCK_LIST, \
         "Unsupported SPI clock speed %d MHz" % args.spi_clock
  return SPI_CLOCK_LIST.index(args.spi_clock)

def GetSpiReadCmdParameter(args):
  assert args.spi_read_cmd in SPI_READ_CMD_LIST, \
         "Unsupported SPI read command 0x%x" % args.spi_read_cmd
  return SPI_READ_CMD_LIST.index(args.spi_read_cmd)

def PadZeroTo(data, size):
  data.extend(b'\0' * (size - len(data)))

def BuildHeader(args, payload_len, load_addr, rorofile):
  # 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):
  # 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

#
# Compute SHA-256 of data and return digest
# as a bytearray
#
def HashByteArray(data):
  hasher = hashlib.sha256()
  hasher.update(data)
  h = hasher.digest()
  bah = bytearray(h)
  return bah

#
# Return 64-byte signature of byte array data.
# Signature is SHA256 of data with 32 0 bytes appended
#
def SignByteArray(data):
  debug_print("Signature is SHA-256 of data")
  sigb = HashByteArray(data)
  sigb.extend(b'\0' * 32)
  return sigb


# 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.
#
def BuildTag(args):
  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):
    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"""
  fo=tempfile.NamedTemporaryFile(delete=False) # 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)
  fo.close()
  return fo.name

#
# 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.
#
def gen_test_ecrw(pldrw):
  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 cookie1_pos > 0 and cookie2_pos > cookie1_pos:
    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():
  rpath = os.path.dirname(os.path.relpath(__file__))

  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()

# Debug helper routine
def dumpsects(spi_list):
  debug_print("spi_list has {0} entries".format(len(spi_list)))
  for s in spi_list:
    debug_print("0x{0:x} 0x{1:x} {2:s}".format(s[0],len(s[1]),s[2]))

def printByteArrayAsHex(ba, title):
  debug_print(title,"= ")
  count = 0
  for b in ba:
    count = count + 1
    debug_print("0x{0:02x}, ".format(b),end="")
    if (count % 8) == 0:
      debug_print("")
  debug_print("\n")

def print_args(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():
  global debug_print

  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 == True:
    crc = zlib.crc32(bytes(payload[LFW_SIZE:(payload_len - 4)]))
    crc_ofs = payload_len - 4
    debug_print("EC_RO CRC32 = 0x{0:08x} @ 0x{1:08x}".format(crc, crc_ofs))
    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 == True:
    crc = zlib.crc32(bytes(payload_rw[:(payload_rw_len - 32)]))
    crc_ofs = payload_rw_len - 4
    debug_print("EC_RW CRC32 = 0x{0:08x} at offset 0x{1:08x}".format(crc, crc_ofs))
    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("rw_offset = 0x{0:08x}".format(rw_offset))

  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()