Skip to content

Commit

Permalink
imgtool: Add pure signature support
Browse files Browse the repository at this point in the history
Adds PureEdDSA signature support.

The change includes implementation of SIG_PURE TLV that, when present,
indicates the signature that is present is Pure type.

Signed-off-by: Dominik Ermel <[email protected]>
Signed-off-by: Mateusz Michalek <[email protected]>
  • Loading branch information
de-nordic authored and michalek-no committed Dec 23, 2024
1 parent a2bc982 commit f757950
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 20 deletions.
74 changes: 60 additions & 14 deletions scripts/imgtool/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,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 @@ -204,12 +212,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 @@ -447,12 +459,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 @@ -592,9 +605,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
Expand Down Expand Up @@ -786,7 +807,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 @@ -797,27 +818,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 @@ -826,9 +863,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
26 changes: 20 additions & 6 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 Expand Up @@ -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:
Expand Down

0 comments on commit f757950

Please sign in to comment.