From 96ecca2ffef6a342e4be1f769f772ac95f92a8d8 Mon Sep 17 00:00:00 2001 From: Mateusz Michalek Date: Fri, 10 Jan 2025 17:40:26 +0100 Subject: [PATCH 1/4] Revert "[nrf noup] imgtool: create image obj with image_hash" This reverts commit 38c662b4b81b5ac92adf16eb1975888b6d68fb30. --- scripts/imgtool/image.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/imgtool/image.py b/scripts/imgtool/image.py index 9946a2595..7bbcf34cc 100644 --- a/scripts/imgtool/image.py +++ b/scripts/imgtool/image.py @@ -605,8 +605,6 @@ def create(self, key, public_key_format, enckey, dependencies=None, sha.update(self.payload) digest = sha.digest() tlv.add(hash_tlv, digest) - # for external usage - self.image_hash = digest # Unless pure, we are signing digest. message = digest From 3fa44810f7fd35d07fae263d92831ad14f0006d2 Mon Sep 17 00:00:00 2001 From: Mateusz Michalek Date: Fri, 10 Jan 2025 17:45:49 +0100 Subject: [PATCH 2/4] Revert "[nrf fromlist] imgtool: Add pure signature support" This reverts commit fe61522a37e7c2cd26ad560fb1068e6c8875a216. --- scripts/imgtool/image.py | 75 ++++++++-------------------------------- scripts/imgtool/main.py | 23 +++--------- 2 files changed, 20 insertions(+), 78 deletions(-) diff --git a/scripts/imgtool/image.py b/scripts/imgtool/image.py index 7bbcf34cc..115d04ec9 100644 --- a/scripts/imgtool/image.py +++ b/scripts/imgtool/image.py @@ -189,15 +189,7 @@ def tlv_sha_to_sha(tlv): keys.X25519 : ['256', '512'] } -ALLOWED_PURE_KEY_SHA = { - keys.Ed25519 : ['512'] -} - -ALLOWED_PURE_SIG_TLVS = [ - TLV_VALUES['ED25519'] -] - -def key_and_user_sha_to_alg_and_tlv(key, user_sha, is_pure = False): +def key_and_user_sha_to_alg_and_tlv(key, user_sha): """Matches key and user requested sha to sha alogrithm and TLV name. The returned tuple will contain hash functions and TVL name. @@ -211,16 +203,12 @@ def key_and_user_sha_to_alg_and_tlv(key, user_sha, is_pure = False): # If key is not None, then we have to filter hash to only allowed allowed = None - allowed_key_ssh = ALLOWED_PURE_KEY_SHA if is_pure else ALLOWED_KEY_SHA try: - allowed = allowed_key_ssh[type(key)] - + allowed = ALLOWED_KEY_SHA[type(key)] except KeyError: raise click.UsageError("Colud not find allowed hash algorithms for {}" .format(type(key))) - - # Pure enforces auto, and user selection is ignored - if user_sha == 'auto' or is_pure: + if user_sha == 'auto': return USER_SHA_TO_ALG_AND_TLV[allowed[0]] if user_sha in allowed: @@ -458,13 +446,12 @@ def ecies_hkdf(self, enckey, plainkey): def create(self, key, public_key_format, enckey, dependencies=None, sw_type=None, custom_tlvs=None, compression_tlvs=None, compression_type=None, encrypt_keylen=128, clear=False, - fixed_sig=None, pub_key=None, vector_to_sign=None, - user_sha='auto', is_pure=False): + fixed_sig=None, pub_key=None, vector_to_sign=None, user_sha='auto'): self.enckey = enckey # key decides on sha, then pub_key; of both are none default is used check_key = key if key is not None else pub_key - hash_algorithm, hash_tlv = key_and_user_sha_to_alg_and_tlv(check_key, user_sha, is_pure) + hash_algorithm, hash_tlv = key_and_user_sha_to_alg_and_tlv(check_key, user_sha) # Calculate the hash of the public key if key is not None: @@ -604,16 +591,9 @@ def create(self, key, public_key_format, enckey, dependencies=None, sha = hash_algorithm() sha.update(self.payload) digest = sha.digest() + message = digest; tlv.add(hash_tlv, digest) - # Unless pure, we are signing digest. - message = digest - - if is_pure: - # Note that when Pure signature is used, hash TLV is not present. - message = bytes(self.payload) - e = STRUCT_ENDIAN_DICT[self.endian] - sig_pure = struct.pack(e + '?', True) - tlv.add('SIG_PURE', sig_pure) + self.image_hash = digest if vector_to_sign == 'payload': # Stop amending data to the image @@ -805,7 +785,7 @@ def verify(imgfile, key): version = struct.unpack('BBHI', b[20:28]) if magic != IMAGE_MAGIC: - return VerifyResult.INVALID_MAGIC, None, None, None + return VerifyResult.INVALID_MAGIC, None, None tlv_off = header_size + img_size tlv_info = b[tlv_off:tlv_off + TLV_INFO_SIZE] @@ -816,27 +796,11 @@ def verify(imgfile, key): magic, tlv_tot = struct.unpack('HH', tlv_info) if magic != TLV_INFO_MAGIC: - return VerifyResult.INVALID_TLV_INFO_MAGIC, None, None, None - - # This is set by existence of TLV SIG_PURE - is_pure = False + return VerifyResult.INVALID_TLV_INFO_MAGIC, None, None prot_tlv_size = tlv_off hash_region = b[:prot_tlv_size] - tlv_end = tlv_off + tlv_tot - tlv_off += TLV_INFO_SIZE # skip tlv info - - # First scan all TLVs in search of SIG_PURE - while tlv_off < tlv_end: - tlv = b[tlv_off:tlv_off + TLV_SIZE] - tlv_type, _, tlv_len = struct.unpack('BBH', tlv) - if tlv_type == TLV_VALUES['SIG_PURE']: - is_pure = True - break - tlv_off += TLV_SIZE + tlv_len - digest = None - tlv_off = header_size + img_size tlv_end = tlv_off + tlv_tot tlv_off += TLV_INFO_SIZE # skip tlv info while tlv_off < tlv_end: @@ -844,15 +808,15 @@ def verify(imgfile, key): tlv_type, _, tlv_len = struct.unpack('BBH', tlv) if is_sha_tlv(tlv_type): if not tlv_matches_key_type(tlv_type, key): - return VerifyResult.KEY_MISMATCH, None, None, None + return VerifyResult.KEY_MISMATCH, None, None off = tlv_off + TLV_SIZE digest = get_digest(tlv_type, hash_region) if digest == b[off:off + tlv_len]: if key is None: - return VerifyResult.OK, version, digest, None + return VerifyResult.OK, version, digest else: - return VerifyResult.INVALID_HASH, None, None, None - elif not is_pure and key is not None and tlv_type == TLV_VALUES[key.sig_tlv()]: + return VerifyResult.INVALID_HASH, None, None + elif key is not None and tlv_type == TLV_VALUES[key.sig_tlv()]: off = tlv_off + TLV_SIZE tlv_sig = b[off:off + tlv_len] payload = b[:prot_tlv_size] @@ -861,18 +825,9 @@ def verify(imgfile, key): key.verify(tlv_sig, payload) else: key.verify_digest(tlv_sig, digest) - return VerifyResult.OK, version, digest, None - except InvalidSignature: - # continue to next TLV - pass - elif is_pure and key is not None and tlv_type in ALLOWED_PURE_SIG_TLVS: - off = tlv_off + TLV_SIZE - tlv_sig = b[off:off + tlv_len] - try: - key.verify_digest(tlv_sig, hash_region) - return VerifyResult.OK, version, None, tlv_sig + return VerifyResult.OK, version, digest except InvalidSignature: # continue to next TLV pass tlv_off += TLV_SIZE + tlv_len - return VerifyResult.INVALID_SIGNATURE, None, None, None + return VerifyResult.INVALID_SIGNATURE, None, None diff --git a/scripts/imgtool/main.py b/scripts/imgtool/main.py index 03d46c907..009d9234c 100755 --- a/scripts/imgtool/main.py +++ b/scripts/imgtool/main.py @@ -226,14 +226,11 @@ def getpriv(key, minimal, format): @click.command(help="Check that signed image can be verified by given key") def verify(key, imgfile): key = load_key(key) if key else None - ret, version, digest, signature = image.Image.verify(imgfile, key) + ret, version, digest = image.Image.verify(imgfile, key) if ret == image.VerifyResult.OK: print("Image was correctly validated") print("Image version: {}.{}.{}+{}".format(*version)) - if digest: - print("Image digest: {}".format(digest.hex())) - if signature and digest is None: - print("Image signature over image: {}".format(signature.hex())) + print("Image digest: {}".format(digest.hex())) return elif ret == image.VerifyResult.INVALID_MAGIC: print("Invalid image magic; is this an MCUboot image?") @@ -426,10 +423,6 @@ def convert(self, value, param, ctx): 'the signature calculated using the public key') @click.option('--fix-sig-pubkey', metavar='filename', help='public key relevant to fixed signature') -@click.option('--pure', 'is_pure', is_flag=True, default=False, show_default=True, - help='Expected Pure variant of signature; the Pure variant is ' - 'expected to be signature done over an image rather than hash of ' - 'that image.') @click.option('--sig-out', metavar='filename', help='Path to the file to which signature will be written. ' 'The image signature will be encoded as base64 formatted string') @@ -448,8 +441,8 @@ def sign(key, public_key_format, align, version, pad_sig, header_size, endian, encrypt_keylen, encrypt, compression, infile, outfile, dependencies, load_addr, hex_addr, erased_val, save_enctlv, security_counter, boot_record, custom_tlv, rom_fixed, max_align, - clear, fix_sig, fix_sig_pubkey, sig_out, user_sha, is_pure, - vector_to_sign, non_bootable): + clear, fix_sig, fix_sig_pubkey, sig_out, user_sha, vector_to_sign, + non_bootable): if confirm: # Confirmed but non-padded images don't make much sense, because @@ -516,15 +509,9 @@ def sign(key, public_key_format, align, version, pad_sig, header_size, 'value': raw_signature } - if is_pure and user_sha != 'auto': - raise click.UsageError( - 'Pure signatures, currently, enforces preferred hash algorithm, ' - 'and forbids sha selection by user.') - img.create(key, public_key_format, enckey, dependencies, boot_record, custom_tlvs, compression_tlvs, None, int(encrypt_keylen), clear, - baked_signature, pub_key, vector_to_sign, user_sha=user_sha, - is_pure=is_pure) + baked_signature, pub_key, vector_to_sign, user_sha) if compression in ["lzma2", "lzma2armthumb"]: compressed_img = image.Image(version=decode_version(version), From c301b3bf5ca347d7731b03260934642f7f6b54a0 Mon Sep 17 00:00:00 2001 From: Mateusz Michalek Date: Tue, 10 Dec 2024 13:51:11 +0100 Subject: [PATCH 3/4] [nrf fromtree] scripts: imgtool: fix sha512 for compression passing user_sha parameters while creating compressed image Upstream PR #: 2141 Signed-off-by: Mateusz Michalek (cherry picked from commit 15909d60ef5ebccfbeaf90f6194111492a0c08e3) --- scripts/imgtool/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/imgtool/main.py b/scripts/imgtool/main.py index 009d9234c..e0f70945c 100755 --- a/scripts/imgtool/main.py +++ b/scripts/imgtool/main.py @@ -552,7 +552,7 @@ def sign(key, public_key_format, align, version, pad_sig, header_size, compressed_img.create(key, public_key_format, enckey, dependencies, boot_record, custom_tlvs, compression_tlvs, compression, int(encrypt_keylen), clear, baked_signature, - pub_key, vector_to_sign) + pub_key, vector_to_sign, user_sha=user_sha) img = compressed_img img.save(outfile, hex_addr) if sig_out is not None: From c6521838e1c04ad936315e0addaedcf999de540b Mon Sep 17 00:00:00 2001 From: Dominik Ermel Date: Thu, 12 Sep 2024 19:37:40 +0000 Subject: [PATCH 4/4] [nrf fromtree] imgtool: Add pure signature support Adds PureEdDSA signature support. The change includes implementation of SIG_PURE TLV that, when present, indicates the signature that is present is Pure type. Upstream PR #: 2063 Signed-off-by: Dominik Ermel Signed-off-by: Mateusz Michalek --- scripts/imgtool/image.py | 74 ++++++++++++++++++++++++++++++++-------- scripts/imgtool/main.py | 26 ++++++++++---- 2 files changed, 80 insertions(+), 20 deletions(-) diff --git a/scripts/imgtool/image.py b/scripts/imgtool/image.py index 115d04ec9..04af4e6e4 100644 --- a/scripts/imgtool/image.py +++ b/scripts/imgtool/image.py @@ -189,7 +189,15 @@ def tlv_sha_to_sha(tlv): keys.X25519 : ['256', '512'] } -def key_and_user_sha_to_alg_and_tlv(key, user_sha): +ALLOWED_PURE_KEY_SHA = { + keys.Ed25519 : ['512'] +} + +ALLOWED_PURE_SIG_TLVS = [ + TLV_VALUES['ED25519'] +] + +def key_and_user_sha_to_alg_and_tlv(key, user_sha, is_pure = False): """Matches key and user requested sha to sha alogrithm and TLV name. The returned tuple will contain hash functions and TVL name. @@ -203,12 +211,16 @@ def key_and_user_sha_to_alg_and_tlv(key, user_sha): # If key is not None, then we have to filter hash to only allowed allowed = None + allowed_key_ssh = ALLOWED_PURE_KEY_SHA if is_pure else ALLOWED_KEY_SHA try: - allowed = ALLOWED_KEY_SHA[type(key)] + allowed = allowed_key_ssh[type(key)] + except KeyError: raise click.UsageError("Colud not find allowed hash algorithms for {}" .format(type(key))) - if user_sha == 'auto': + + # Pure enforces auto, and user selection is ignored + if user_sha == 'auto' or is_pure: return USER_SHA_TO_ALG_AND_TLV[allowed[0]] if user_sha in allowed: @@ -446,12 +458,13 @@ def ecies_hkdf(self, enckey, plainkey): def create(self, key, public_key_format, enckey, dependencies=None, sw_type=None, custom_tlvs=None, compression_tlvs=None, compression_type=None, encrypt_keylen=128, clear=False, - fixed_sig=None, pub_key=None, vector_to_sign=None, user_sha='auto'): + fixed_sig=None, pub_key=None, vector_to_sign=None, + user_sha='auto', is_pure=False): self.enckey = enckey # key decides on sha, then pub_key; of both are none default is used check_key = key if key is not None else pub_key - hash_algorithm, hash_tlv = key_and_user_sha_to_alg_and_tlv(check_key, user_sha) + hash_algorithm, hash_tlv = key_and_user_sha_to_alg_and_tlv(check_key, user_sha, is_pure) # Calculate the hash of the public key if key is not None: @@ -591,9 +604,17 @@ def create(self, key, public_key_format, enckey, dependencies=None, sha = hash_algorithm() sha.update(self.payload) digest = sha.digest() - message = digest; tlv.add(hash_tlv, digest) self.image_hash = digest + # Unless pure, we are signing digest. + message = digest + + if is_pure: + # Note that when Pure signature is used, hash TLV is not present. + message = bytes(self.payload) + e = STRUCT_ENDIAN_DICT[self.endian] + sig_pure = struct.pack(e + '?', True) + tlv.add('SIG_PURE', sig_pure) if vector_to_sign == 'payload': # Stop amending data to the image @@ -785,7 +806,7 @@ def verify(imgfile, key): version = struct.unpack('BBHI', b[20:28]) if magic != IMAGE_MAGIC: - return VerifyResult.INVALID_MAGIC, None, None + return VerifyResult.INVALID_MAGIC, None, None, None tlv_off = header_size + img_size tlv_info = b[tlv_off:tlv_off + TLV_INFO_SIZE] @@ -796,11 +817,27 @@ def verify(imgfile, key): magic, tlv_tot = struct.unpack('HH', tlv_info) if magic != TLV_INFO_MAGIC: - return VerifyResult.INVALID_TLV_INFO_MAGIC, None, None + return VerifyResult.INVALID_TLV_INFO_MAGIC, None, None, None + + # This is set by existence of TLV SIG_PURE + is_pure = False prot_tlv_size = tlv_off hash_region = b[:prot_tlv_size] + tlv_end = tlv_off + tlv_tot + tlv_off += TLV_INFO_SIZE # skip tlv info + + # First scan all TLVs in search of SIG_PURE + while tlv_off < tlv_end: + tlv = b[tlv_off:tlv_off + TLV_SIZE] + tlv_type, _, tlv_len = struct.unpack('BBH', tlv) + if tlv_type == TLV_VALUES['SIG_PURE']: + is_pure = True + break + tlv_off += TLV_SIZE + tlv_len + digest = None + tlv_off = header_size + img_size tlv_end = tlv_off + tlv_tot tlv_off += TLV_INFO_SIZE # skip tlv info while tlv_off < tlv_end: @@ -808,15 +845,15 @@ def verify(imgfile, key): tlv_type, _, tlv_len = struct.unpack('BBH', tlv) if is_sha_tlv(tlv_type): if not tlv_matches_key_type(tlv_type, key): - return VerifyResult.KEY_MISMATCH, None, None + return VerifyResult.KEY_MISMATCH, None, None, None off = tlv_off + TLV_SIZE digest = get_digest(tlv_type, hash_region) if digest == b[off:off + tlv_len]: if key is None: - return VerifyResult.OK, version, digest + return VerifyResult.OK, version, digest, None else: - return VerifyResult.INVALID_HASH, None, None - elif key is not None and tlv_type == TLV_VALUES[key.sig_tlv()]: + return VerifyResult.INVALID_HASH, None, None, None + elif not is_pure and key is not None and tlv_type == TLV_VALUES[key.sig_tlv()]: off = tlv_off + TLV_SIZE tlv_sig = b[off:off + tlv_len] payload = b[:prot_tlv_size] @@ -825,9 +862,18 @@ def verify(imgfile, key): key.verify(tlv_sig, payload) else: key.verify_digest(tlv_sig, digest) - return VerifyResult.OK, version, digest + return VerifyResult.OK, version, digest, None + except InvalidSignature: + # continue to next TLV + pass + elif is_pure and key is not None and tlv_type in ALLOWED_PURE_SIG_TLVS: + off = tlv_off + TLV_SIZE + tlv_sig = b[off:off + tlv_len] + try: + key.verify_digest(tlv_sig, hash_region) + return VerifyResult.OK, version, None, tlv_sig except InvalidSignature: # continue to next TLV pass tlv_off += TLV_SIZE + tlv_len - return VerifyResult.INVALID_SIGNATURE, None, None + return VerifyResult.INVALID_SIGNATURE, None, None, None diff --git a/scripts/imgtool/main.py b/scripts/imgtool/main.py index e0f70945c..434530c7a 100755 --- a/scripts/imgtool/main.py +++ b/scripts/imgtool/main.py @@ -226,11 +226,14 @@ def getpriv(key, minimal, format): @click.command(help="Check that signed image can be verified by given key") def verify(key, imgfile): key = load_key(key) if key else None - ret, version, digest = image.Image.verify(imgfile, key) + ret, version, digest, signature = image.Image.verify(imgfile, key) if ret == image.VerifyResult.OK: print("Image was correctly validated") print("Image version: {}.{}.{}+{}".format(*version)) - print("Image digest: {}".format(digest.hex())) + if digest: + print("Image digest: {}".format(digest.hex())) + if signature and digest is None: + print("Image signature over image: {}".format(signature.hex())) return elif ret == image.VerifyResult.INVALID_MAGIC: print("Invalid image magic; is this an MCUboot image?") @@ -423,6 +426,10 @@ def convert(self, value, param, ctx): 'the signature calculated using the public key') @click.option('--fix-sig-pubkey', metavar='filename', help='public key relevant to fixed signature') +@click.option('--pure', 'is_pure', is_flag=True, default=False, show_default=True, + help='Expected Pure variant of signature; the Pure variant is ' + 'expected to be signature done over an image rather than hash of ' + 'that image.') @click.option('--sig-out', metavar='filename', help='Path to the file to which signature will be written. ' 'The image signature will be encoded as base64 formatted string') @@ -441,8 +448,8 @@ def sign(key, public_key_format, align, version, pad_sig, header_size, endian, encrypt_keylen, encrypt, compression, infile, outfile, dependencies, load_addr, hex_addr, erased_val, save_enctlv, security_counter, boot_record, custom_tlv, rom_fixed, max_align, - clear, fix_sig, fix_sig_pubkey, sig_out, user_sha, vector_to_sign, - non_bootable): + clear, fix_sig, fix_sig_pubkey, sig_out, user_sha, is_pure, + vector_to_sign, non_bootable): if confirm: # Confirmed but non-padded images don't make much sense, because @@ -509,9 +516,15 @@ def sign(key, public_key_format, align, version, pad_sig, header_size, 'value': raw_signature } + if is_pure and user_sha != 'auto': + raise click.UsageError( + 'Pure signatures, currently, enforces preferred hash algorithm, ' + 'and forbids sha selection by user.') + img.create(key, public_key_format, enckey, dependencies, boot_record, custom_tlvs, compression_tlvs, None, int(encrypt_keylen), clear, - baked_signature, pub_key, vector_to_sign, user_sha) + baked_signature, pub_key, vector_to_sign, user_sha=user_sha, + is_pure=is_pure) if compression in ["lzma2", "lzma2armthumb"]: compressed_img = image.Image(version=decode_version(version), @@ -552,7 +565,8 @@ def sign(key, public_key_format, align, version, pad_sig, header_size, compressed_img.create(key, public_key_format, enckey, dependencies, boot_record, custom_tlvs, compression_tlvs, compression, int(encrypt_keylen), clear, baked_signature, - pub_key, vector_to_sign, user_sha=user_sha) + pub_key, vector_to_sign, user_sha=user_sha, + is_pure=is_pure) img = compressed_img img.save(outfile, hex_addr) if sig_out is not None: