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 <dominik.ermel@nordicsemi.no>
  • Loading branch information
de-nordic committed Oct 2, 2024
1 parent c894d04 commit 99bc965
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 22 deletions.
87 changes: 69 additions & 18 deletions scripts/imgtool/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
'ECDSASIG': 0x22,
'RSA3072': 0x23,
'ED25519': 0x24,
'SIG_PURE' : 0x25,
'ENCRSA2048': 0x30,
'ENCKW': 0x31,
'ENCEC256': 0x32,
Expand Down Expand Up @@ -174,7 +175,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 @@ -189,11 +198,17 @@ 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
try:
allowed = ALLOWED_KEY_SHA[type(key)]
if is_pure:
allowed = ALLOWED_PURE_KEY_SHA[type(key)]
else:
allowed = ALLOWED_KEY_SHA[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 @@ -408,12 +423,13 @@ def ecies_hkdf(self, enckey, plainkey):

def create(self, key, public_key_format, enckey, dependencies=None,
sw_type=None, custom_tlvs=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 @@ -538,11 +554,18 @@ def create(self, key, public_key_format, enckey, dependencies=None,
# EC signatures so called Pure algorithm, designated to be run
# over entire message is used with sha of image as message,
# so, for example, in case of ED25519 we have here SHAxxx-ED25519-SHA512.
sha = hash_algorithm()
sha.update(self.payload)
digest = sha.digest()
message = digest;
tlv.add(hash_tlv, digest)
if not is_pure:
sha = hash_algorithm()
sha.update(self.payload)
digest = sha.digest()
message = digest
tlv.add(hash_tlv, digest)
else:
# Note that when Pure signature is used, there is not hash encoded
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 @@ -728,7 +751,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 @@ -739,27 +762,46 @@ 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 is_pure:
print("SHA is not expected for signature over image (pure), ignoring")
continue
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 @@ -768,9 +810,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
20 changes: 16 additions & 4 deletions scripts/imgtool/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,11 +215,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 @@ -399,6 +402,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 @@ -417,7 +424,7 @@ def sign(key, public_key_format, align, version, pad_sig, header_size,
endian, encrypt_keylen, encrypt, 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):
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 @@ -483,9 +490,14 @@ 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, int(encrypt_keylen), clear, baked_signature,
pub_key, vector_to_sign, user_sha)
pub_key, vector_to_sign, user_sha, is_pure)
img.save(outfile, hex_addr)

if sig_out is not None:
Expand Down

0 comments on commit 99bc965

Please sign in to comment.