Skip to content

Commit 63fa7e4

Browse files
michalek-nonvlsianpu
authored andcommitted
scripts: imgtool: compression
Adds LZMA2 compression to imgtool. Python lzma library is unable to compress with proper parameters while using "ALONE" container, therefore 2 header bytes are calculated and added to payload by imgtool. Signed-off-by: Mateusz Michalek <[email protected]>
1 parent 30bcd46 commit 63fa7e4

File tree

2 files changed

+132
-14
lines changed

2 files changed

+132
-14
lines changed

scripts/imgtool/image.py

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,15 @@
2020
Image signing and management.
2121
"""
2222

23+
from . import version as versmod
24+
from .boot_record import create_sw_component_data
25+
import click
26+
import copy
27+
from enum import Enum
28+
import array
29+
from intelhex import IntelHex
2330
import hashlib
31+
import array
2432
import os.path
2533
import struct
2634
from enum import Enum
@@ -60,6 +68,8 @@
6068
'NON_BOOTABLE': 0x0000010,
6169
'RAM_LOAD': 0x0000020,
6270
'ROM_FIXED': 0x0000100,
71+
'COMPRESSED_LZMA1': 0x0000200,
72+
'COMPRESSED_LZMA2': 0x0000400,
6373
}
6474

6575
TLV_VALUES = {
@@ -80,6 +90,9 @@
8090
'DEPENDENCY': 0x40,
8191
'SEC_CNT': 0x50,
8292
'BOOT_RECORD': 0x60,
93+
'DECOMP_SIZE': 0x70,
94+
'DECOMP_SHA': 0x71,
95+
'DECOMP_SIGNATURE': 0x72,
8396
}
8497

8598
TLV_SIZE = 4
@@ -238,6 +251,9 @@ def __init__(self, version=None, header_size=IMAGE_HEADER_SIZE,
238251
if load_addr and rom_fixed:
239252
raise click.UsageError("Can not set rom_fixed and load_addr at the same time")
240253

254+
self.image_hash = None
255+
self.image_size = None
256+
self.signature = None
241257
self.version = version or versmod.decode_version("0")
242258
self.header_size = header_size
243259
self.pad_header = pad_header
@@ -253,6 +269,7 @@ def __init__(self, version=None, header_size=IMAGE_HEADER_SIZE,
253269
self.rom_fixed = rom_fixed
254270
self.erased_val = 0xff if erased_val is None else int(erased_val, 0)
255271
self.payload = []
272+
self.infile_data = []
256273
self.enckey = None
257274
self.save_enctlv = save_enctlv
258275
self.enctlv_len = 0
@@ -307,13 +324,31 @@ def load(self, path):
307324
try:
308325
if ext == INTEL_HEX_EXT:
309326
ih = IntelHex(path)
310-
self.payload = ih.tobinarray()
327+
self.infile_data = ih.tobinarray()
328+
self.payload = copy.copy(self.infile_data)
311329
self.base_addr = ih.minaddr()
312330
else:
313331
with open(path, 'rb') as f:
314-
self.payload = f.read()
332+
self.infile_data = f.read()
333+
self.payload = copy.copy(self.infile_data)
315334
except FileNotFoundError:
316335
raise click.UsageError("Input file not found")
336+
self.image_size = len(self.payload)
337+
338+
# Add the image header if needed.
339+
if self.pad_header and self.header_size > 0:
340+
if self.base_addr:
341+
# Adjust base_addr for new header
342+
self.base_addr -= self.header_size
343+
self.payload = bytes([self.erased_val] * self.header_size) + \
344+
self.payload
345+
346+
self.check_header()
347+
348+
def load_compressed(self, data, compression_header):
349+
"""Load an image from buffer"""
350+
self.payload = compression_header + data
351+
self.image_size = len(self.payload)
317352

318353
# Add the image header if needed.
319354
if self.pad_header and self.header_size > 0:
@@ -408,7 +443,8 @@ def ecies_hkdf(self, enckey, plainkey):
408443
return cipherkey, ciphermac, pubk
409444

410445
def create(self, key, public_key_format, enckey, dependencies=None,
411-
sw_type=None, custom_tlvs=None, encrypt_keylen=128, clear=False,
446+
sw_type=None, custom_tlvs=None, compression_tlvs=None,
447+
compression_type=None, encrypt_keylen=128, clear=False,
412448
fixed_sig=None, pub_key=None, vector_to_sign=None, user_sha='auto'):
413449
self.enckey = enckey
414450

@@ -471,6 +507,9 @@ def create(self, key, public_key_format, enckey, dependencies=None,
471507
dependencies_num = len(dependencies[DEP_IMAGES_KEY])
472508
protected_tlv_size += (dependencies_num * 16)
473509

510+
if compression_tlvs is not None:
511+
for value in compression_tlvs.values():
512+
protected_tlv_size += TLV_SIZE + len(value)
474513
if custom_tlvs is not None:
475514
for value in custom_tlvs.values():
476515
protected_tlv_size += TLV_SIZE + len(value)
@@ -492,11 +531,15 @@ def create(self, key, public_key_format, enckey, dependencies=None,
492531
else:
493532
self.payload.extend(pad)
494533

534+
compression_flags = 0x0
535+
if compression_tlvs is not None:
536+
if compression_type == "lzma2":
537+
compression_flags = IMAGE_F['COMPRESSED_LZMA2']
495538
# This adds the header to the payload as well
496539
if encrypt_keylen == 256:
497-
self.add_header(enckey, protected_tlv_size, 256)
540+
self.add_header(enckey, protected_tlv_size, compression_flags, 256)
498541
else:
499-
self.add_header(enckey, protected_tlv_size)
542+
self.add_header(enckey, protected_tlv_size, compression_flags)
500543

501544
prot_tlv = TLV(self.endian, TLV_PROT_INFO_MAGIC)
502545

@@ -526,6 +569,9 @@ def create(self, key, public_key_format, enckey, dependencies=None,
526569
)
527570
prot_tlv.add('DEPENDENCY', payload)
528571

572+
if compression_tlvs is not None:
573+
for tag, value in compression_tlvs.items():
574+
prot_tlv.add(tag, value)
529575
if custom_tlvs is not None:
530576
for tag, value in custom_tlvs.items():
531577
prot_tlv.add(tag, value)
@@ -544,6 +590,7 @@ def create(self, key, public_key_format, enckey, dependencies=None,
544590
digest = sha.digest()
545591
message = digest;
546592
tlv.add(hash_tlv, digest)
593+
self.image_hash = digest
547594

548595
if vector_to_sign == 'payload':
549596
# Stop amending data to the image
@@ -623,10 +670,16 @@ def create(self, key, public_key_format, enckey, dependencies=None,
623670

624671
self.check_trailer()
625672

673+
def get_struct_endian(self):
674+
return STRUCT_ENDIAN_DICT[self.endian]
675+
626676
def get_signature(self):
627677
return self.signature
628678

629-
def add_header(self, enckey, protected_tlv_size, aes_length=128):
679+
def get_infile_data(self):
680+
return self.infile_data
681+
682+
def add_header(self, enckey, protected_tlv_size, compression_flags, aes_length=128):
630683
"""Install the image header."""
631684

632685
flags = 0
@@ -664,7 +717,7 @@ def add_header(self, enckey, protected_tlv_size, aes_length=128):
664717
protected_tlv_size, # TLV Info header +
665718
# Protected TLVs
666719
len(self.payload) - self.header_size, # ImageSz
667-
flags,
720+
flags | compression_flags,
668721
self.version.major,
669722
self.version.minor or 0,
670723
self.version.revision or 0,

scripts/imgtool/main.py

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,24 @@
2222
import getpass
2323
import imgtool.keys as keys
2424
import sys
25+
import struct
26+
import os
27+
import lzma
28+
import hashlib
2529
import base64
2630
from imgtool import image, imgtool_version
2731
from imgtool.version import decode_version
2832
from imgtool.dumpinfo import dump_imginfo
2933
from .keys import (
3034
RSAUsageError, ECDSAUsageError, Ed25519UsageError, X25519UsageError)
3135

36+
comp_default_dictsize=131072
37+
comp_default_pb=2
38+
comp_default_lc=3
39+
comp_default_lp=1
40+
comp_default_preset=9
41+
42+
3243
MIN_PYTHON_VERSION = (3, 6)
3344
if sys.version_info < MIN_PYTHON_VERSION:
3445
sys.exit("Python %s.%s or newer is required by imgtool."
@@ -300,6 +311,14 @@ def get_dependencies(ctx, param, value):
300311
dependencies[image.DEP_VERSIONS_KEY] = versions
301312
return dependencies
302313

314+
def create_lzma2_header(dictsize, pb, lc, lp):
315+
header = bytearray()
316+
for i in range(0, 40):
317+
if dictsize <= ((2 | ((i) & 1)) << int((i) / 2 + 11)):
318+
header.append(i)
319+
break
320+
header.append( ( pb * 5 + lp) * 9 + lc)
321+
return header
303322

304323
class BasedIntParamType(click.ParamType):
305324
name = 'integer'
@@ -343,6 +362,11 @@ def convert(self, value, param, ctx):
343362
type=click.Choice(['128', '256']),
344363
help='When encrypting the image using AES, select a 128 bit or '
345364
'256 bit key len.')
365+
@click.option('--compression', default='disabled',
366+
type=click.Choice(['disabled', 'lzma2']),
367+
help='Enable image compression using specified type. '
368+
'Will fall back without image compression automatically '
369+
'if the compression increases the image size.')
346370
@click.option('-c', '--clear', required=False, is_flag=True, default=False,
347371
help='Output a non-encrypted image with encryption capabilities,'
348372
'so it can be installed in the primary slot, and encrypted '
@@ -414,10 +438,11 @@ def convert(self, value, param, ctx):
414438
.hex extension, otherwise binary format is used''')
415439
def sign(key, public_key_format, align, version, pad_sig, header_size,
416440
pad_header, slot_size, pad, confirm, max_sectors, overwrite_only,
417-
endian, encrypt_keylen, encrypt, infile, outfile, dependencies,
418-
load_addr, hex_addr, erased_val, save_enctlv, security_counter,
419-
boot_record, custom_tlv, rom_fixed, max_align, clear, fix_sig,
420-
fix_sig_pubkey, sig_out, user_sha, vector_to_sign, non_bootable):
441+
endian, encrypt_keylen, encrypt, compression, infile, outfile,
442+
dependencies, load_addr, hex_addr, erased_val, save_enctlv,
443+
security_counter, boot_record, custom_tlv, rom_fixed, max_align,
444+
clear, fix_sig, fix_sig_pubkey, sig_out, user_sha, vector_to_sign,
445+
non_bootable):
421446

422447
if confirm:
423448
# Confirmed but non-padded images don't make much sense, because
@@ -431,6 +456,7 @@ def sign(key, public_key_format, align, version, pad_sig, header_size,
431456
erased_val=erased_val, save_enctlv=save_enctlv,
432457
security_counter=security_counter, max_align=max_align,
433458
non_bootable=non_bootable)
459+
compression_tlvs = {}
434460
img.load(infile)
435461
key = load_key(key) if key else None
436462
enckey = load_key(encrypt) if encrypt else None
@@ -484,10 +510,49 @@ def sign(key, public_key_format, align, version, pad_sig, header_size,
484510
}
485511

486512
img.create(key, public_key_format, enckey, dependencies, boot_record,
487-
custom_tlvs, int(encrypt_keylen), clear, baked_signature,
488-
pub_key, vector_to_sign, user_sha)
513+
custom_tlvs, compression_tlvs, int(encrypt_keylen), clear,
514+
baked_signature, pub_key, vector_to_sign, user_sha)
515+
516+
if compression == "lzma2":
517+
compressed_img = image.Image(version=decode_version(version),
518+
header_size=header_size, pad_header=pad_header,
519+
pad=pad, confirm=confirm, align=int(align),
520+
slot_size=slot_size, max_sectors=max_sectors,
521+
overwrite_only=overwrite_only, endian=endian,
522+
load_addr=load_addr, rom_fixed=rom_fixed,
523+
erased_val=erased_val, save_enctlv=save_enctlv,
524+
security_counter=security_counter, max_align=max_align)
525+
compression_filters = [
526+
{"id": lzma.FILTER_LZMA2, "preset": comp_default_preset,
527+
"dict_size": comp_default_dictsize, "lp": comp_default_lp,
528+
"lc": comp_default_lc}
529+
]
530+
compressed_data = lzma.compress(img.get_infile_data(),filters=compression_filters,
531+
format=lzma.FORMAT_RAW)
532+
uncompressed_size = len(img.get_infile_data())
533+
compressed_size = len(compressed_data)
534+
print(f"compressed image size: {compressed_size} bytes")
535+
print(f"original image size: {uncompressed_size} bytes")
536+
compression_tlvs["DECOMP_SIZE"] = struct.pack(
537+
img.get_struct_endian() + 'L', img.image_size)
538+
compression_tlvs["DECOMP_SHA"] = img.image_hash
539+
compression_tlvs_size = len(compression_tlvs["DECOMP_SIZE"])
540+
compression_tlvs_size += len(compression_tlvs["DECOMP_SHA"])
541+
if img.get_signature():
542+
compression_tlvs["DECOMP_SIGNATURE"] = img.get_signature()
543+
compression_tlvs_size += len(compression_tlvs["DECOMP_SIGNATURE"])
544+
if (compressed_size + compression_tlvs_size) < uncompressed_size:
545+
compression_header = create_lzma2_header(
546+
dictsize = comp_default_dictsize, pb = comp_default_pb,
547+
lc = comp_default_lc, lp = comp_default_lp)
548+
compressed_img.load_compressed(compressed_data, compression_header)
549+
compressed_img.base_addr = img.base_addr
550+
compressed_img.create(key, public_key_format, enckey,
551+
dependencies, boot_record, custom_tlvs, compression_tlvs,
552+
compression, int(encrypt_keylen), clear, baked_signature,
553+
pub_key, vector_to_sign)
554+
img = compressed_img
489555
img.save(outfile, hex_addr)
490-
491556
if sig_out is not None:
492557
new_signature = img.get_signature()
493558
save_signature(sig_out, new_signature)

0 commit comments

Comments
 (0)