|
| 1 | +#!/usr/bin/python3 |
| 2 | + |
| 3 | +# cec1702-image.py - CEC1702 SPI flash image creater utility |
| 4 | +# Copyright (c) 2019 Crypta Labs Ltd. |
| 5 | +# |
| 6 | +# SPDX-License-Identifier: Apache-2.0 |
| 7 | + |
| 8 | +import os, argparse, struct, sys, crcmod |
| 9 | +from cryptography import x509 |
| 10 | +from cryptography.hazmat.backends import default_backend |
| 11 | +from cryptography.hazmat.primitives import hashes, serialization |
| 12 | +from cryptography.hazmat.primitives.asymmetric import ec, utils |
| 13 | + |
| 14 | +backend = default_backend() |
| 15 | + |
| 16 | +def int_to_bytes(val, num_bytes): |
| 17 | + # big-endian representation (ROM Addendum documentation is incorrect) |
| 18 | + return [(val >> (num_bytes-pos-1)*8) & 0xff for pos in range(num_bytes)] |
| 19 | + |
| 20 | +def digest(hashalg, blob): |
| 21 | + d = hashes.Hash(hashalg, backend=backend) |
| 22 | + d.update(blob) |
| 23 | + return d.finalize() |
| 24 | + |
| 25 | +def sign(blob, sign_key): |
| 26 | + # Sign or add checksum according to ROM Addendum |
| 27 | + if sign_key: |
| 28 | + # Raw EC-DSA signature using secp256R1 EC-key |
| 29 | + rfc = sign_key.sign(blob, ec.ECDSA(hashes.SHA256())) |
| 30 | + r, s = utils.decode_dss_signature(rfc) |
| 31 | + return bytes(bytearray(int_to_bytes(r, 32) + int_to_bytes(s, 32))) |
| 32 | + else: |
| 33 | + # Raw SHA256 digest |
| 34 | + return digest(hashes.SHA256(), blob) + b'\x00'*32 |
| 35 | + |
| 36 | +def private_key_to_raw_public_key(p): |
| 37 | + # Return RAW Uncompressed format without the leading 0x04 format byte of X962 |
| 38 | + pubkey = p.public_key() |
| 39 | + if not hasattr(serialization.Encoding, 'X962'): |
| 40 | + # Old API, deprecated in cryptography 2.5 |
| 41 | + return pubkey.public_numbers().encode_point()[1:] |
| 42 | + return pubkey.public_bytes(serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint)[1:] |
| 43 | + |
| 44 | +def encrypt(blob, peer_public_key): |
| 45 | + from cryptography.hazmat.primitives.kdf.x963kdf import X963KDF |
| 46 | + from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes |
| 47 | + |
| 48 | + # Encrypt firmware image according to ROM Addendum: |
| 49 | + # Ephemeral EC-DH over secp256R1, with ANSI X9.63 KDF to generate AES-256-CBC key and IV |
| 50 | + # The uncompressed public ephemeral key is appended to the end |
| 51 | + ephemeral_key = ec.generate_private_key(ec.SECP256R1(), backend) |
| 52 | + shared_secret = ephemeral_key.exchange(ec.ECDH(), peer_public_key) |
| 53 | + derived_key = X963KDF(algorithm=hashes.SHA256(), length=48, sharedinfo=None, backend=backend).derive(shared_secret) |
| 54 | + enc = Cipher(algorithms.AES(derived_key[0:32]), modes.CBC(derived_key[32:48]), backend=backend).encryptor() |
| 55 | + return enc.update(blob) + enc.finalize() + private_key_to_raw_public_key(ephemeral_key) |
| 56 | + |
| 57 | +def bootable_image(opts): |
| 58 | + # image size needs to be modulo 64 due to block size requirement |
| 59 | + # try to workaround bootloader bug in part C2 by increasing padding as needed |
| 60 | + img = opts.image.read() |
| 61 | + if opts.encrypt: |
| 62 | + img_pad = 256 |
| 63 | + elif opts.sign: |
| 64 | + img_pad = 128 |
| 65 | + else: |
| 66 | + img_pad = 64 |
| 67 | + x = len(img) % img_pad |
| 68 | + if x != 0: |
| 69 | + img = img + b'\0' * (img_pad - x) |
| 70 | + |
| 71 | + # header |
| 72 | + entry = struct.unpack_from("<L", img, 4)[0] # entry point |
| 73 | + vtr_byte = 0x00 # encryption & vtr control |
| 74 | + img_len = len(img) |
| 75 | + if opts.encrypt: |
| 76 | + vtr_byte |= 0x80 |
| 77 | + img = encrypt(img, opts.encrypt) |
| 78 | + |
| 79 | + # Create CEC boot header |
| 80 | + hdr = bytearray(b'\0'*0x40) |
| 81 | + struct.pack_into("<4sBBBBLLHHL", hdr, 0, |
| 82 | + b"PHCM", # magic |
| 83 | + 0x00, # version |
| 84 | + opts.spi_clock + (opts.spi_drive << 2), # SPI configuration |
| 85 | + vtr_byte, # VTR / encryption control |
| 86 | + opts.spi_command, # Flash read command |
| 87 | + opts.load_address, # Load Address (SRAM) |
| 88 | + entry, # Entry Address |
| 89 | + img_len // 64, # Image Length (blocks of 64 bytes) |
| 90 | + 0x0000, # Reserved |
| 91 | + opts.image_offset) # HDR size (offset to firmware image) |
| 92 | + hdr = bytes(hdr) |
| 93 | + hdr_sign = sign(hdr, opts.sign) |
| 94 | + img_sign = sign(img, opts.sign) |
| 95 | + return hdr + hdr_sign + b'\x00'*(opts.image_offset-len(hdr)-len(hdr_sign)) + img + img_sign |
| 96 | + |
| 97 | +def tag(offset): |
| 98 | + offset = offset >> 8 |
| 99 | + h = crcmod.predefined.Crc('crc-8-itu') |
| 100 | + h.update(struct.pack('BBB', offset & 0xff, (offset >> 8) & 0xff, (offset >> 16) & 0xff)) |
| 101 | + return offset | (ord(h.digest()) << 24) |
| 102 | + |
| 103 | +def flashable_image(args, img0, img1): |
| 104 | + img = bytearray(b'\xff' * args.flash_size) |
| 105 | + struct.pack_into("<LL", img, 0, tag(args.img0_offset), tag(args.img1_offset)) |
| 106 | + img[args.img0_offset:args.img0_offset+len(img0)] = img0 |
| 107 | + img[args.img1_offset:args.img1_offset+len(img1)] = img1 |
| 108 | + return bytes(img) |
| 109 | + |
| 110 | +def load_file(fn): |
| 111 | + return open(fn, 'rb').read() |
| 112 | + |
| 113 | +def load_pem_private_key(fn): |
| 114 | + blob = load_file(fn) |
| 115 | + for pwd in [ "ECPRIVKEY001", os.getenv("PEM_PASSPHRASE") ]: |
| 116 | + try: |
| 117 | + return serialization.load_pem_private_key(blob, password=pwd.encode(), backend=backend) |
| 118 | + except: |
| 119 | + pass |
| 120 | + raise argparse.ArgumentTypeError('Invalid private key passphrase') |
| 121 | + |
| 122 | +def load_pem_certificate(fn): |
| 123 | + return x509.load_pem_x509_certificate(load_file(fn), backend).public_key() |
| 124 | + |
| 125 | +def get_image_opts(args, n): |
| 126 | + opts = dict() |
| 127 | + vargs = vars(args) |
| 128 | + prefix = "img%d_" % n |
| 129 | + for k in vargs: |
| 130 | + if k.startswith(prefix): |
| 131 | + opts[k[len(prefix):]] = vargs[k] |
| 132 | + return argparse.Namespace(**opts) |
| 133 | + |
| 134 | +def main(): |
| 135 | + ap = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
| 136 | + ap.add_argument("--flash-size", help="Size of the flash", type=int, default=0x200000, metavar="SIZE") |
| 137 | + ap.add_argument("--image-out", help="Write raw flash image to file (out)", type=argparse.FileType('wb'), metavar="FILE") |
| 138 | + for x, req, offs in ('img0', True, 0x001000), ('img1', False, 0x081000): |
| 139 | + g = ap.add_argument_group(x, "Options for %s" % x) |
| 140 | + g.add_argument("--%s-image" % x, help="Executable image", type=argparse.FileType('rb'), metavar="IMG", required=req) |
| 141 | + g.add_argument("--%s-image-offset" % x, help="Firmware image offset from header", type=int, default=0x100, metavar="OFFSET") |
| 142 | + g.add_argument('--%s-load-address' % x, help="Image SRAM load address", type=int, default=0xB0000, metavar="ADDR") |
| 143 | + g.add_argument("--%s-offset" % x, help="Image offset in SPI flash", type=int, default=offs, metavar="OFFS") |
| 144 | + g.add_argument("--%s-encrypt" % x, help="Encrypt using certificate public key (X.509 PEM)", type=load_pem_certificate, metavar="PEM") |
| 145 | + g.add_argument("--%s-sign" % x, help="Signing private key (PEM)", type=load_pem_private_key, metavar="PEM") |
| 146 | + g.add_argument("--%s-spi-clock" % x, help="SPI clock (0=48MHz, 1=24MHz, 2=16MHz, 3=12MHz)", choices=range(4), type=int, default=3, metavar="CLK") |
| 147 | + g.add_argument("--%s-spi-drive" % x, help="SPI drive strength (0=2mA, 1=4mA, 2=8mA, 3=12mA)", choices=range(4), type=int, default=1, metavar="DRV") |
| 148 | + g.add_argument("--%s-spi-command" % x, help="SPI read command (0=normal, 1=fast, 2=double, 3=quad)", choices=range(4), type=int, default=1, metavar="CMD") |
| 149 | + g.add_argument("--%s-out" % x, help="Write OTA image to file (out)", type=argparse.FileType('wb'), metavar="FILE") |
| 150 | + |
| 151 | + args = ap.parse_args() |
| 152 | + |
| 153 | + img0 = bootable_image(get_image_opts(args, 0)) |
| 154 | + if args.img1_image: |
| 155 | + img1 = bootable_image(get_image_opts(args, 1)) |
| 156 | + else: |
| 157 | + img1 = img0 |
| 158 | + |
| 159 | + if args.image_out: |
| 160 | + args.image_out.write(flashable_image(args, img0, img1)) |
| 161 | + if args.img0_out: |
| 162 | + args.img0_out.write(img0) |
| 163 | + if args.img1_out: |
| 164 | + args.img1_out.write(img1) |
| 165 | + |
| 166 | +if __name__ == "__main__": |
| 167 | + main() |
0 commit comments