Skip to content

Commit 985cc85

Browse files
committed
imgtool: Add a possibility to attach manifest TLV
Add a simple logic that allows to attach a manifest TLV to an image. Signed-off-by: Tomasz Chyrowicz <tomasz.chyrowicz@nordicsemi.no>
1 parent bb740f4 commit 985cc85

File tree

2 files changed

+89
-6
lines changed

2 files changed

+89
-6
lines changed

scripts/imgtool/image.py

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
4040
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
4141
from intelhex import IntelHex
42+
from yaml import safe_load as yaml_safe_load
4243

4344
from . import keys
4445
from . import version as versmod
@@ -93,6 +94,7 @@
9394
'COMP_DEC_SIZE' : 0x73,
9495
'UUID_VID': 0x74,
9596
'UUID_CID': 0x75,
97+
'MANIFEST': 0x76,
9698
}
9799

98100
TLV_SIZE = 4
@@ -282,6 +284,73 @@ def parse_uuid(namespace, value):
282284

283285
return uuid_bytes
284286

287+
class Manifest:
288+
def __init__(self, endian, path):
289+
self.path = path
290+
self.format = 1
291+
self.data = None
292+
self.config = None
293+
self.endian = endian
294+
self.load()
295+
296+
def load(self):
297+
try:
298+
with open(self.path) as f:
299+
self.config = yaml_safe_load(f)
300+
format = self.config.get('format', 0)
301+
if isinstance(format, str) and format.isdigit():
302+
format = int(format)
303+
if format != self.format:
304+
raise click.UsageError(f"Unsupported manifest format: {format}")
305+
306+
# Encode manifest format
307+
e = STRUCT_ENDIAN_DICT[self.endian]
308+
self.data = struct.pack(e + 'I', format)
309+
310+
# Encode number of images/hashes
311+
n_images = len(self.config.get('images', []))
312+
self.data += struct.pack(e + 'I', n_images)
313+
314+
# Encode each image hash
315+
exp_hash_len = None
316+
for image in self.config.get('images', []):
317+
if 'path' not in image and 'hash' not in image:
318+
raise click.UsageError(
319+
"Manifest image entry must contain either 'path' or 'hash'")
320+
321+
# Encode hash, based on the signed image path
322+
if 'path' in image:
323+
(result, version, digest, _) = Image.verify(image['path'], None)
324+
if result != VerifyResult.OK:
325+
raise click.UsageError(f"Failed to verify image: {image['path']}")
326+
327+
if exp_hash_len is None:
328+
exp_hash_len = len(digest)
329+
elif exp_hash_len != len(digest):
330+
raise click.UsageError("All image hashes must have the same length")
331+
self.data += struct.pack(e + f'{exp_hash_len}s', digest)
332+
333+
# Encode RAW image hash
334+
if 'hash' in image:
335+
if exp_hash_len is None:
336+
exp_hash_len = len(bytes.fromhex(image['hash']))
337+
elif exp_hash_len != len(bytes.fromhex(image['hash'])):
338+
raise click.UsageError("All image hashes must have the same length")
339+
self.data += struct.pack(e + f'{exp_hash_len}s', bytes.fromhex(image['hash']))
340+
341+
except FileNotFoundError:
342+
raise click.UsageError(f"Manifest file {self.path} not found") from None
343+
344+
def encode(self):
345+
if self.data is None:
346+
raise click.UsageError("Manifest data is empty")
347+
return self.data
348+
349+
def __len__(self):
350+
return len(self.data) if self.data is not None else 0
351+
352+
def __repr__(self):
353+
return f"<Manifest path={self.path}, format={self.format}, len={len(self)}>"
285354

286355
class Image:
287356

@@ -291,7 +360,7 @@ def __init__(self, version=None, header_size=IMAGE_HEADER_SIZE,
291360
overwrite_only=False, endian="little", load_addr=0,
292361
rom_fixed=None, erased_val=None, save_enctlv=False,
293362
security_counter=None, max_align=None,
294-
non_bootable=False, vid=None, cid=None):
363+
non_bootable=False, vid=None, cid=None, manifest=None):
295364

296365
if load_addr and rom_fixed:
297366
raise click.UsageError("Can not set rom_fixed and load_addr at the same time")
@@ -323,6 +392,7 @@ def __init__(self, version=None, header_size=IMAGE_HEADER_SIZE,
323392
self.non_bootable = non_bootable
324393
self.vid = vid
325394
self.cid = cid
395+
self.manifest = Manifest(endian=endian, path=manifest) if manifest is not None else None
326396

327397
if self.max_align == DEFAULT_MAX_ALIGN:
328398
self.boot_magic = bytes([
@@ -352,7 +422,7 @@ def __repr__(self):
352422
return "<Image version={}, header_size={}, security_counter={}, \
353423
base_addr={}, load_addr={}, align={}, slot_size={}, \
354424
max_sectors={}, overwrite_only={}, endian={} format={}, \
355-
payloadlen=0x{:x}, vid={}, cid={}>".format(
425+
payloadlen=0x{:x}, vid={}, cid={}, manifest={}>".format(
356426
self.version,
357427
self.header_size,
358428
self.security_counter,
@@ -366,7 +436,8 @@ def __repr__(self):
366436
self.__class__.__name__,
367437
len(self.payload),
368438
self.vid,
369-
self.cid)
439+
self.cid,
440+
self.manifest)
370441

371442
def load(self, path):
372443
"""Load an image from a given file"""
@@ -556,6 +627,11 @@ def create(self, key, public_key_format, enckey, dependencies=None,
556627
# = 4 + 16 = 20 Bytes
557628
protected_tlv_size += TLV_SIZE + 16
558629

630+
if self.manifest is not None:
631+
# Size of the MANIFEST TLV: header ('HH') + payload (len(manifest))
632+
# = 4 + len(manifest) Bytes
633+
protected_tlv_size += TLV_SIZE + len(self.manifest.encode())
634+
559635
if sw_type is not None:
560636
if len(sw_type) > MAX_SW_TYPE_LENGTH:
561637
msg = f"'{sw_type}' is too long ({len(sw_type)} characters) for sw_type. Its " \
@@ -671,6 +747,10 @@ def create(self, key, public_key_format, enckey, dependencies=None,
671747
payload = struct.pack(e + '16s', cid)
672748
prot_tlv.add('UUID_CID', payload)
673749

750+
if self.manifest is not None:
751+
payload = self.manifest.encode()
752+
prot_tlv.add('MANIFEST', payload)
753+
674754
if custom_tlvs is not None:
675755
for tag, value in custom_tlvs.items():
676756
prot_tlv.add(tag, value)

scripts/imgtool/main.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -450,13 +450,15 @@ def convert(self, value, param, ctx):
450450
help='Unique vendor identifier, format: (<raw_uuid>|<domain_name)>')
451451
@click.option('--cid', default=None, required=False,
452452
help='Unique image class identifier, format: (<raw_uuid>|<image_class_name>)')
453+
@click.option('--manifest', default=None, required=False,
454+
help='Path to the update manifest file')
453455
def sign(key, public_key_format, align, version, pad_sig, header_size,
454456
pad_header, slot_size, pad, confirm, test, max_sectors, overwrite_only,
455457
endian, encrypt_keylen, encrypt, compression, infile, outfile,
456458
dependencies, load_addr, hex_addr, erased_val, save_enctlv,
457459
security_counter, boot_record, custom_tlv, rom_fixed, max_align,
458460
clear, fix_sig, fix_sig_pubkey, sig_out, user_sha, hmac_sha, is_pure,
459-
vector_to_sign, non_bootable, vid, cid):
461+
vector_to_sign, non_bootable, vid, cid, manifest):
460462

461463
if confirm or test:
462464
# Confirmed but non-padded images don't make much sense, because
@@ -469,7 +471,8 @@ def sign(key, public_key_format, align, version, pad_sig, header_size,
469471
endian=endian, load_addr=load_addr, rom_fixed=rom_fixed,
470472
erased_val=erased_val, save_enctlv=save_enctlv,
471473
security_counter=security_counter, max_align=max_align,
472-
non_bootable=non_bootable, vid=vid, cid=cid)
474+
non_bootable=non_bootable, vid=vid, cid=cid,
475+
manifest=manifest)
473476
compression_tlvs = {}
474477
img.load(infile)
475478
key = load_key(key) if key else None
@@ -540,7 +543,7 @@ def sign(key, public_key_format, align, version, pad_sig, header_size,
540543
load_addr=load_addr, rom_fixed=rom_fixed,
541544
erased_val=erased_val, save_enctlv=save_enctlv,
542545
security_counter=security_counter, max_align=max_align,
543-
vid=vid, cid=cid)
546+
vid=vid, cid=cid, manifest=manifest)
544547
compression_filters = [
545548
{"id": lzma.FILTER_LZMA2, "preset": comp_default_preset,
546549
"dict_size": comp_default_dictsize, "lp": comp_default_lp,

0 commit comments

Comments
 (0)