Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

imgtool: Add --pure switch to allow calculating signatures over entire images #2063

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 60 additions & 15 deletions scripts/imgtool/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -591,9 +604,16 @@ 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.image_hash is used externally

# Unless pure, we are signing digest.
message = digest

if is_pure:
# Note that when Pure signature is used, hash TLV is not present.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nordicjm @nvlsianpu I am bypassing addition of hash TLV in case when signature is pure; pure signature is not using it, but maybe the hash may be useful for something else is worth leaving it as it is?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@de-nordic Yes, it might be usefull: Hash is for check the integrity of the image, while the signature is for checking authenticity of the image. Usually HASH calculation is faster and doesn't require any key. I can expect that some cases might require just integrity calculation.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now the TLV sha is there, planning, for the future - not here, to add switch to not include it.

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
Expand Down Expand Up @@ -785,7 +805,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]
Expand All @@ -796,27 +816,43 @@ 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:
tlv = b[tlv_off:tlv_off + TLV_SIZE]
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]
Expand All @@ -825,9 +861,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
23 changes: 18 additions & 5 deletions scripts/imgtool/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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?")
Expand Down Expand Up @@ -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')
Expand All @@ -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
Expand Down Expand Up @@ -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),
Expand Down
Loading