blob: 435094c88b340b874370e2dea463711d16757983 [file] [log] [blame]
#!/usr/bin/env python3
# SPDX-License-Identifier: BSD-2-Clause
#
# Copyright (C) 2023, STMicroelectronics
#
try:
from elftools.elf.elffile import ELFFile
from elftools.elf.sections import SymbolTableSection
from elftools.elf.enums import ENUM_P_TYPE_ARM
from elftools.elf.enums import *
except ImportError:
print("""
***
ERROR: "pyelftools" python module is not installed or version < 0.25.
***
""")
raise
try:
from Cryptodome.Hash import SHA256
from Cryptodome.Signature import pkcs1_15
from Cryptodome.PublicKey import RSA
from Cryptodome.Signature import DSS
from Cryptodome.PublicKey import ECC
except ImportError:
print("""
***
ERROR: "pycryptodomex" python module should be installed.
***
""")
raise
import os
import sys
import struct
import logging
import binascii
# Generated file structure:
#
# -----+-------------+
# / | Magic | 32-bit word, magic value equal to
# / +-------------+ 0x3543A468
# / +-------------+
# / | version | 32-bit word, version of the format
# / +-------------+
# +-----------+ +-------------+
# | Header | | TLV size | 32-bit word, size of the TLV
# +-----------+ +-------------+ (aligned on 64-bit), in bytes.
# \ +-------------+
# \ | sign size | 32-bit word, size of the signature
# \ +-------------+ (aligned on 64-bit), in bytes.
# \ +-------------+
# \ | images size | 32-bit word, size of the images to
# -----+-------------+ load (aligned on 64-bit), in bytes.
#
# +-------------+ Information used to authenticate the
# | TLV | images and boot the remote processor,
# | | stored in Type-Length-Value format.
# +-------------+ 'Type' and 'Length' are 32-bit words.
#
# +-------------+
# | Signature | Signature of the header and the TLV.
# +-------------+
#
# +-------------+
# | Firmware |
# | image 1 |
# +-------------+
# ...
# +-------------+
# | Firmware |
# | image n |
# +-------------+
# Generic type definitions
TLV_TYPES = {
'SIGNTYPE': 0x00000001, # algorithm used for signature
'HASHTYPE': 0x00000002, # algorithm used for computing segment's hash
'NUM_IMG': 0x00000003, # number of images to load
'IMGTYPE': 0x00000004, # array of type of images to load
'IMGSIZE': 0x00000005, # array of the size of the images to load
'HASHTABLE': 0x000000010, # segment hash table for authentication
'PKEYINFO': 0x0000000011, # information to retrieve signature key
}
GENERIC_TLV_TYPE_RANGE = range(0x00000000, 0x00010000)
PLATFORM_TLV_TYPE_RANGE = range(0x00010000, 0x00020000)
HEADER_MAGIC = 0x3543A468
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
ENUM_HASH_TYPE = dict(
SHA256=1,
)
ENUM_SIGNATURE_TYPE = dict(
RSA=1,
ECC=2,
)
ENUM_BINARY_TYPE = dict(
ELF=1,
)
def dump_buffer(buf, step=16, name="", logger=logging.debug, indent=""):
logger("%s%s:" % (indent, name))
for i in range(0, len(buf), step):
logger("%s " % (indent) + " ".
join(["%02X" % c for c in buf[i:i+step]]))
logger("\n")
class TLV():
def __init__(self):
self.buf = bytearray()
self.tlvs = {}
def add(self, kind, payload):
"""
Add a TLV record. Argument type is either the type scalar ID or a
matching string defined in TLV_TYPES.
"""
if isinstance(kind, int):
buf = struct.pack('II', kind, len(payload))
else:
buf = struct.pack('II', TLV_TYPES[kind], len(payload))
# Ensure that each TLV is 64-bit aligned
align_64b = (len(payload) + len(buf)) % 8
self.buf += buf
self.buf += payload
if align_64b:
self.buf += bytearray(8 - align_64b)
def add_plat_tlv(self, cust_tlv):
# Get list of custom protected TLVs from the command-line
for tlv in cust_tlv:
type_id = int(tlv[0], 0)
if type_id not in PLATFORM_TLV_TYPE_RANGE:
raise Exception('TLV %s not in range' % hex(type_id))
value = tlv[1]
if value.startswith('0x'):
int_val = int(value[2:], 16)
self.tlvs[type_id] = int_val.to_bytes(4, 'little')
else:
self.tlvs[type_id] = value.encode('utf-8')
if self.tlvs is not None:
for type_id, value in self.tlvs.items():
self.add(type_id, value)
def get(self):
"""
Get a byte-array that concatenates all the TLV added.
"""
if len(self.buf) == 0:
return bytes()
return bytes(self.buf)
class RSA_Signature(object):
def __init__(self, key):
self._hasher = SHA256.new()
self.signer = pkcs1_15.new(key)
def hash_compute(self, segment):
self._hasher.update(segment)
def sign(self):
return self.signer.sign(self._hasher)
class ECC_Signature(object):
def __init__(self, key):
self._hasher = SHA256.new()
self.signer = DSS.new(key, 'fips-186-3')
def hash_compute(self, segment):
self._hasher.update(segment)
def sign(self):
return self.signer.sign(self._hasher)
Signature = {
1: RSA_Signature,
2: ECC_Signature,
}
class SegmentHashStruct:
pass
class SegmentHash(object):
'''
Hash table based on Elf program segments
'''
def __init__(self, img):
self._num_segments = img.num_segments()
self._pack_fmt = '<%dL' % 8
self.img = img
self.hashProgTable = bytes()
self._offset = 0
def get_table(self):
'''
Create a segment hash table containing for each segment:
- the segments header
- a hash of the segment
'''
h = SHA256.new()
seg = SegmentHashStruct()
self.size = (h.digest_size + 32) * self._num_segments
logging.debug("hash section size %d" % self.size)
del h
self.buf = bytearray(self.size)
self._bufview_ = memoryview(self.buf)
for i in range(self._num_segments):
h = SHA256.new()
segment = self.img.get_segment(i)
seg.header = self.img.get_segment(i).header
logging.debug("compute hash for segment offset %s" % seg.header)
h.update(segment.data())
seg.hash = h.digest()
logging.debug("hash computed: %s" % seg.hash)
del h
struct.pack_into('<I', self._bufview_, self._offset,
ENUM_P_TYPE_ARM[seg.header.p_type])
self._offset += 4
struct.pack_into('<7I', self._bufview_, self._offset,
seg.header.p_offset, seg.header.p_vaddr,
seg.header.p_paddr, seg.header.p_filesz,
seg.header.p_memsz, seg.header.p_flags,
seg.header.p_align)
self._offset += 28
struct.pack_into('<32B', self._bufview_, self._offset, *seg.hash)
self._offset += 32
dump_buffer(self.buf, name='hash table', indent="\t")
return self.buf
class ImageHeader(object):
'''
Image header
'''
magic = 'HELF' # SHDR_MAGIC
version = 1
MAGIC_OFFSET = 0
VERSION_OFFSET = 4
SIGN_LEN_OFFSET = 8
IMG_LEN_OFFSET = 12
TLV_LEN_OFFSET = 16
PTLV_LEN_OFFSET = 20
def __init__(self):
self.size = 56
self.magic = HEADER_MAGIC
self.version = 1
self.tlv_length = 0
self.sign_length = 0
self.img_length = 0
self.shdr = struct.pack('<IIIII',
self.magic, self.version,
self.tlv_length, self.sign_length,
self.img_length)
def dump(self):
logging.debug("\tMAGIC\t\t= %08X" % (self.magic))
logging.debug("\tHEADER_VERSION\t= %08X" % (self.version))
logging.debug("\tTLV_LENGTH\t= %08X" % (self.tlv_length))
logging.debug("\tSIGN_LENGTH\t= %08X" % (self.sign_length))
logging.debug("\tIMAGE_LENGTH\t= %08X" % (self.img_length))
def get_packed(self):
return struct.pack('<IIIII',
self.magic, self.version,
self.tlv_length, self.sign_length, self.img_length)
def get_args(logger):
from argparse import ArgumentParser, RawDescriptionHelpFormatter
import textwrap
parser = ArgumentParser(
description='Sign a remote processor firmware loadable by OP-TEE.',
usage='\n %(prog)s [ arguments ]\n\n'
' Generate signed loadable binary \n' +
' Takes arguments --in, --out --key\n' +
' %(prog)s --help show available arguments\n\n')
parser.add_argument('--in', required=True, dest='in_file',
help='Name of firmware input file ' +
'(can be used multiple times)', action='append')
parser.add_argument('--out', required=True, dest='out_file',
help='Name of the signed firmware output file')
parser.add_argument('--key', required=True,
help='Name of signing key file',
dest='key_file')
parser.add_argument('--key_info', required=False,
help='Name file containing extra key information',
dest='key_info')
parser.add_argument('--key_type', required=False,
help='Type of signing key: should be RSA or ECC',
default='RSA',
dest='key_type')
parser.add_argument('--key_pwd', required=False,
help='passphrase for the private key decryption',
dest='key_pwd')
parser.add_argument('--plat-tlv', required=False, nargs=2,
metavar=("ID", "value"), action='append',
help='Platform TLV that will be placed into image '
'plat_tlv area. Add "0x" prefix to interpret '
'the value as an integer, otherwise it will be '
'interpreted as a string. Option can be used '
'multiple times to add multiple TLVs.',
default=[], dest='plat_tlv')
parsed = parser.parse_args()
# Set defaults for optional arguments.
if parsed.out_file is None:
parsed.out_file = str(parsed.in_file)+'.sig'
return parsed
def rsa_key(key_file, key_pwd):
return RSA.importKey(open(key_file).read(), key_pwd)
def ecc_key(key_file, key_pwd):
return ECC.import_key(open(key_file).read(), key_pwd)
key_type = {
1: rsa_key,
2: ecc_key,
}
def rsa_sig_size(key):
return key.size_in_bytes()
def ecc_sig_size(key):
# to be improve...
# DSA size is N/4 so 64 for DSA (L,N) = (2048, 256)
return 64
sig_size_type = {
1: rsa_sig_size,
2: ecc_sig_size,
}
def main():
from Cryptodome.Signature import pss
from Cryptodome.Hash import SHA256
from Cryptodome.PublicKey import RSA
import base64
import logging
import struct
logging.basicConfig()
logger = logging.getLogger(os.path.basename(__file__))
args = get_args(logger)
# Initialise the header */
s_header = ImageHeader()
tlv = TLV()
sign_type = ENUM_SIGNATURE_TYPE[args.key_type]
get_key = key_type.get(sign_type, lambda: "Invalid sign type")
key = get_key(args.key_file, args.key_pwd)
if not key.has_private():
logger.error('Provided key cannot be used for signing, ')
sys.exit(1)
tlv.add('SIGNTYPE', sign_type.to_bytes(1, 'little'))
images_type = []
hash_tlv = bytearray()
images_size = []
# Firmware image
for inputf in args.in_file:
logging.debug("image %s" % inputf)
input_file = open(inputf, 'rb')
img = ELFFile(input_file)
# Only ARM machine has been tested and well supported yet.
# Indeed this script uses of ENUM_P_TYPE_ARM dic
assert img.get_machine_arch() in ["ARM"]
# Need to reopen the file to get the raw data
with open(inputf, 'rb') as f:
bin_img = f.read()
size = len(bin_img)
align_64b = size % 8
if align_64b:
size += 8 - align_64b
images_size.extend(size.to_bytes(4, 'little'))
s_header.img_length += size
f.close()
# Store image type information
bin_type = ENUM_BINARY_TYPE['ELF']
images_type += bin_type.to_bytes(1, 'little')
# Compute the hash table and add it to TLV blob
hash_table = SegmentHash(img)
hash_tlv.extend(hash_table.get_table())
# Add image information
# The 'IMGTYPE' contains a byte array of the image type (ENUM_BINARY_TYPE).
# The 'IMGSIZE' contains a byte array of the size (32-bit) of each image.
tlv.add('NUM_IMG', len(args.in_file).to_bytes(1, 'little'))
tlv.add('IMGTYPE', bytearray(images_type))
tlv.add('IMGSIZE', bytearray(images_size))
# Add hash type information in TLV blob
# The 'HASHTYPE' TLV contains a byte associated to ENUM_HASH_TYPE.
hash_type = ENUM_HASH_TYPE['SHA256']
tlv.add('HASHTYPE', hash_type.to_bytes(1, 'little'))
# Add hash table information in TLV blob
# The HASHTABLE TLV contains a byte array containing all the ELF segment
# with associated hash.
tlv.add('HASHTABLE', hash_tlv)
# Add optional key information to TLV
if args.key_info:
with open(args.key_info, 'rb') as f:
key_info = f.read()
tlv.add('PKEYINFO', key_info)
# Compute custom TLV that will be passed to the platform PTA
# Get list of custom protected TLVs from the command-line
if args.plat_tlv:
tlv.add_plat_tlv(args.plat_tlv)
# Get the TLV area and compute its size (with 64 bit alignment)
tlvs_buff = tlv.get()
s_header.tlv_length = len(tlvs_buff)
align_64b = 8 - (s_header.tlv_length % 8)
if align_64b:
s_header.tlv_length += 8 - align_64b
tlvs_buff += bytearray(8 - align_64b)
dump_buffer(tlvs_buff, name='TLVS', indent="\t")
# Signature chunk
sign_size = sig_size_type.get(ENUM_SIGNATURE_TYPE[args.key_type],
lambda: "Invalid sign type")(key)
s_header.sign_length = sign_size
# Construct the Header
header = s_header.get_packed()
# Generate signature
signer = Signature.get(ENUM_SIGNATURE_TYPE[args.key_type])(key)
signer.hash_compute(header)
signer.hash_compute(tlvs_buff)
signature = signer.sign()
if len(signature) != sign_size:
raise Exception(("Actual signature length is not equal to ",
"the computed one: {} != {}".
format(len(signature), sign_size)))
s_header.dump()
with open(args.out_file, 'wb') as f:
f.write(header)
f.write(tlvs_buff)
f.write(signature)
align_64b = sign_size % 8
if align_64b:
f.write(bytearray(8 - align_64b))
for inputf in args.in_file:
with open(inputf, 'rb') as fin:
bin_img = fin.read()
f.write(bin_img)
fin.close()
align_64b = len(bin_img) % 8
if align_64b:
f.write(bytearray(8 - align_64b))
if __name__ == "__main__":
main()