Skip to content

Commit dbaa27e

Browse files
committed
imgtool: Add support for encrypting image with raw AES key
The change adds --aes-key option that allows to pass a key via command line. The key is used to encrypt the image and there is not key exchange TLV added to the image. The options is provided for encrypting images for devices that store AES key on them so they do not expect it to be passed with image, in encrypted form. Signed-off-by: Dominik Ermel <dominik.ermel@nordicsemi.no>
1 parent 7603613 commit dbaa27e

File tree

2 files changed

+72
-40
lines changed

2 files changed

+72
-40
lines changed

scripts/imgtool/image.py

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ def ecies_hkdf(self, enckey, plainkey, hmac_sha_alg):
513513

514514
def create(self, key, public_key_format, enckey, dependencies=None,
515515
sw_type=None, custom_tlvs=None, compression_tlvs=None,
516-
compression_type=None, encrypt_keylen=128, clear=False,
516+
compression_type=None, aes_key=None, clear=False,
517517
fixed_sig=None, pub_key=None, vector_to_sign=None,
518518
user_sha='auto', hmac_sha='auto', is_pure=False, keep_comp_size=False,
519519
dont_encrypt=False):
@@ -605,7 +605,7 @@ def create(self, key, public_key_format, enckey, dependencies=None,
605605
#
606606
# This adds the padding if image is not aligned to the 16 Bytes
607607
# in encrypted mode
608-
if self.enckey is not None and dont_encrypt is False:
608+
if aes_key is not None and dont_encrypt is False:
609609
pad_len = len(self.payload) % 16
610610
if pad_len > 0:
611611
pad = bytes(16 - pad_len)
@@ -620,10 +620,8 @@ def create(self, key, public_key_format, enckey, dependencies=None,
620620
if compression_type == "lzma2armthumb":
621621
compression_flags |= IMAGE_F['COMPRESSED_ARM_THUMB']
622622
# This adds the header to the payload as well
623-
if encrypt_keylen == 256:
624-
self.add_header(enckey, protected_tlv_size, compression_flags, 256)
625-
else:
626-
self.add_header(enckey, protected_tlv_size, compression_flags)
623+
aes_key_bits = 0 if aes_key is None else len(aes_key) * 8
624+
self.add_header(protected_tlv_size, compression_flags, aes_key_bits)
627625

628626
prot_tlv = TLV(self.endian, TLV_PROT_INFO_MAGIC)
629627

@@ -742,12 +740,18 @@ def create(self, key, public_key_format, enckey, dependencies=None,
742740
if protected_tlv_off is not None:
743741
self.payload = self.payload[:protected_tlv_off]
744742

745-
if enckey is not None and dont_encrypt is False:
746-
if encrypt_keylen == 256:
747-
plainkey = os.urandom(32)
748-
else:
749-
plainkey = os.urandom(16)
743+
# When passed AES key and clear flag, then do not encrypt, because the key
744+
# is only passed to be stored in encryption key TLV, the payload stays clear text.
745+
if aes_key and not clear:
746+
nonce = bytes([0] * 16)
747+
cipher = Cipher(algorithms.AES(aes_key), modes.CTR(nonce),
748+
backend=default_backend())
749+
encryptor = cipher.encryptor()
750+
img = bytes(self.payload[self.header_size:])
751+
self.payload[self.header_size:] = \
752+
encryptor.update(img) + encryptor.finalize()
750753

754+
if enckey is not None and dont_encrypt is False:
751755
if not isinstance(enckey, rsa.RSAPublic):
752756
if hmac_sha == 'auto' or hmac_sha == '256':
753757
hmac_sha = '256'
@@ -762,35 +766,26 @@ def create(self, key, public_key_format, enckey, dependencies=None,
762766

763767
if isinstance(enckey, rsa.RSAPublic):
764768
cipherkey = enckey._get_public().encrypt(
765-
plainkey, padding.OAEP(
769+
aes_key, padding.OAEP(
766770
mgf=padding.MGF1(algorithm=hashes.SHA256()),
767771
algorithm=hashes.SHA256(),
768772
label=None))
769773
self.enctlv_len = len(cipherkey)
770774
tlv.add('ENCRSA2048', cipherkey)
771775
elif isinstance(enckey, ecdsa.ECDSA256P1Public):
772-
cipherkey, mac, pubk = self.ecies_hkdf(enckey, plainkey, hmac_sha_alg)
776+
cipherkey, mac, pubk = self.ecies_hkdf(enckey, aes_key, hmac_sha_alg)
773777
enctlv = pubk + mac + cipherkey
774778
self.enctlv_len = len(enctlv)
775779
tlv.add('ENCEC256', enctlv)
776780
elif isinstance(enckey, x25519.X25519Public):
777-
cipherkey, mac, pubk = self.ecies_hkdf(enckey, plainkey, hmac_sha_alg)
781+
cipherkey, mac, pubk = self.ecies_hkdf(enckey, aes_key, hmac_sha_alg)
778782
enctlv = pubk + mac + cipherkey
779783
self.enctlv_len = len(enctlv)
780784
if (hmac_sha == '256'):
781785
tlv.add('ENCX25519', enctlv)
782786
else:
783787
tlv.add('ENCX25519_SHA512', enctlv)
784788

785-
if not clear:
786-
nonce = bytes([0] * 16)
787-
cipher = Cipher(algorithms.AES(plainkey), modes.CTR(nonce),
788-
backend=default_backend())
789-
encryptor = cipher.encryptor()
790-
img = bytes(self.payload[self.header_size:])
791-
self.payload[self.header_size:] = \
792-
encryptor.update(img) + encryptor.finalize()
793-
794789
self.payload += prot_tlv.get()
795790
self.payload += tlv.get()
796791

@@ -805,11 +800,11 @@ def get_signature(self):
805800
def get_infile_data(self):
806801
return self.infile_data
807802

808-
def add_header(self, enckey, protected_tlv_size, compression_flags, aes_length=128):
803+
def add_header(self, protected_tlv_size, compression_flags, aes_length=0):
809804
"""Install the image header."""
810805

811806
flags = 0
812-
if enckey is not None:
807+
if aes_length != 0:
813808
if aes_length == 128:
814809
flags |= IMAGE_F['ENCRYPTED_AES128']
815810
else:

scripts/imgtool/main.py

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import base64
2121
import getpass
2222
import lzma
23+
import os
2324
import re
2425
import struct
2526
import sys
@@ -322,6 +323,14 @@ def create_lzma2_header(dictsize, pb, lc, lp):
322323
header.append( ( pb * 5 + lp) * 9 + lc)
323324
return header
324325

326+
def match_sig_enc_key(skey, ekey):
327+
ok = ((isinstance(skey, keys.ECDSA256P1) and isinstance(ekey, keys.ECDSA256P1Public)) or
328+
(isinstance(skey, keys.ECDSA384P1) and isinstance(ekey, keys.ECDSA384P1Public)) or
329+
(isinstance(skey, keys.RSA) and isinstance(ekey, keys.RSAPublic))
330+
)
331+
332+
return ok
333+
325334
class BasedIntParamType(click.ParamType):
326335
name = 'integer'
327336

@@ -450,13 +459,17 @@ def convert(self, value, param, ctx):
450459
help='Unique vendor identifier, format: (<raw_uuid>|<domain_name)>')
451460
@click.option('--cid', default=None, required=False,
452461
help='Unique image class identifier, format: (<raw_uuid>|<image_class_name>)')
453-
def sign(key, public_key_format, align, version, pad_sig, header_size,
462+
@click.option('--aes-key', default=None, required=False,
463+
help='String representing raw AES key, format: hex byte string of 32 or 64'
464+
'hexadecimal characters')
465+
@click.pass_context
466+
def sign(ctx, key, public_key_format, align, version, pad_sig, header_size,
454467
pad_header, slot_size, pad, confirm, test, max_sectors, overwrite_only,
455468
endian, encrypt_keylen, encrypt, compression, infile, outfile,
456469
dependencies, load_addr, hex_addr, erased_val, save_enctlv,
457470
security_counter, boot_record, custom_tlv, rom_fixed, max_align,
458471
clear, fix_sig, fix_sig_pubkey, sig_out, user_sha, hmac_sha, is_pure,
459-
vector_to_sign, non_bootable, vid, cid):
472+
vector_to_sign, non_bootable, vid, cid, aes_key):
460473

461474
if confirm or test:
462475
# Confirmed but non-padded images don't make much sense, because
@@ -472,17 +485,24 @@ def sign(key, public_key_format, align, version, pad_sig, header_size,
472485
non_bootable=non_bootable, vid=vid, cid=cid)
473486
compression_tlvs = {}
474487
img.load(infile)
488+
475489
key = load_key(key) if key else None
476-
enckey = load_key(encrypt) if encrypt else None
477-
if enckey and key and ((isinstance(key, keys.ECDSA256P1) and
478-
not isinstance(enckey, keys.ECDSA256P1Public))
479-
or (isinstance(key, keys.ECDSA384P1) and
480-
not isinstance(enckey, keys.ECDSA384P1Public))
481-
or (isinstance(key, keys.RSA) and
482-
not isinstance(enckey, keys.RSAPublic))):
483-
# FIXME
484-
raise click.UsageError("Signing and encryption must use the same "
485-
"type of key")
490+
enckey = None
491+
if not aes_key:
492+
enckey = load_key(encrypt) if encrypt else None
493+
if enckey and not match_sig_enc_key(key, enckey):
494+
# FIXME
495+
raise click.UsageError("Signing and encryption must use the same "
496+
"type of key")
497+
else:
498+
if encrypt:
499+
encrypt = None
500+
print('Raw AES key overrides --key, there will be no encrypted key added to the image')
501+
if clear:
502+
clear = False
503+
print('Raw AES key overrides --clear, image will be encrypted')
504+
if ctx.get_parameter_source('encrypt_keylen') != click.core.ParameterSource.DEFAULT:
505+
print('Raw AES key len overrides --encrypt-keylen')
486506

487507
if pad_sig and hasattr(key, 'pad_sig'):
488508
key.pad_sig = True
@@ -527,9 +547,26 @@ def sign(key, public_key_format, align, version, pad_sig, header_size,
527547
'Pure signatures, currently, enforces preferred hash algorithm, '
528548
'and forbids sha selection by user.')
529549

550+
plainkey = None
551+
if aes_key:
552+
# Converting the command line provided raw AES key to byte array;
553+
# this aray will be truncated to desired len.
554+
plainkey = bytes.fromhex(aes_key)
555+
plainkey_len = len(plainkey)
556+
if plainkey_len not in (16, 32):
557+
raise click.UsageError("Provided keylen, {int(plainkey_len)} in bytes, not supported")
558+
elif enckey:
559+
if encrypt_keylen == 256:
560+
encrypt_keylen_bytes = 32
561+
else:
562+
encrypt_keylen_bytes = 16
563+
564+
# No AES plain key and there is request to encrypt, generate random AES key
565+
plainkey = os.urandom(encrypt_keylen_bytes)
566+
530567
if compression in ["lzma2", "lzma2armthumb"]:
531568
img.create(key, public_key_format, enckey, dependencies, boot_record,
532-
custom_tlvs, compression_tlvs, None, int(encrypt_keylen), clear,
569+
custom_tlvs, compression_tlvs, None, None, clear,
533570
baked_signature, pub_key, vector_to_sign, user_sha=user_sha,
534571
hmac_sha=hmac_sha, is_pure=is_pure, keep_comp_size=False, dont_encrypt=True)
535572
compressed_img = image.Image(version=decode_version(version),
@@ -575,13 +612,13 @@ def sign(key, public_key_format, align, version, pad_sig, header_size,
575612
keep_comp_size = True
576613
compressed_img.create(key, public_key_format, enckey,
577614
dependencies, boot_record, custom_tlvs, compression_tlvs,
578-
compression, int(encrypt_keylen), clear, baked_signature,
615+
compression, plainkey, clear, baked_signature,
579616
pub_key, vector_to_sign, user_sha=user_sha, hmac_sha=hmac_sha,
580617
is_pure=is_pure, keep_comp_size=keep_comp_size)
581618
img = compressed_img
582619
else:
583620
img.create(key, public_key_format, enckey, dependencies, boot_record,
584-
custom_tlvs, compression_tlvs, None, int(encrypt_keylen), clear,
621+
custom_tlvs, compression_tlvs, None, plainkey, clear,
585622
baked_signature, pub_key, vector_to_sign, user_sha=user_sha,
586623
hmac_sha=hmac_sha, is_pure=is_pure)
587624
img.save(outfile, hex_addr)

0 commit comments

Comments
 (0)