Skip to content

Commit 8a3f3bb

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 234c66e commit 8a3f3bb

File tree

2 files changed

+71
-40
lines changed

2 files changed

+71
-40
lines changed

scripts/imgtool/image.py

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

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

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

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

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

755+
if enckey is not None and dont_encrypt is False:
752756
if not isinstance(enckey, rsa.RSAPublic):
753757
if hmac_sha == 'auto' or hmac_sha == '256':
754758
hmac_sha = '256'
@@ -763,35 +767,26 @@ def create(self, key, public_key_format, enckey, dependencies=None,
763767

764768
if isinstance(enckey, rsa.RSAPublic):
765769
cipherkey = enckey._get_public().encrypt(
766-
plainkey, padding.OAEP(
770+
aes_key, padding.OAEP(
767771
mgf=padding.MGF1(algorithm=hashes.SHA256()),
768772
algorithm=hashes.SHA256(),
769773
label=None))
770774
self.enctlv_len = len(cipherkey)
771775
tlv.add('ENCRSA2048', cipherkey)
772776
elif isinstance(enckey, ecdsa.ECDSA256P1Public):
773-
cipherkey, mac, pubk = self.ecies_hkdf(enckey, plainkey, hmac_sha_alg)
777+
cipherkey, mac, pubk = self.ecies_hkdf(enckey, aes_key, hmac_sha_alg)
774778
enctlv = pubk + mac + cipherkey
775779
self.enctlv_len = len(enctlv)
776780
tlv.add('ENCEC256', enctlv)
777781
elif isinstance(enckey, x25519.X25519Public):
778-
cipherkey, mac, pubk = self.ecies_hkdf(enckey, plainkey, hmac_sha_alg)
782+
cipherkey, mac, pubk = self.ecies_hkdf(enckey, aes_key, hmac_sha_alg)
779783
enctlv = pubk + mac + cipherkey
780784
self.enctlv_len = len(enctlv)
781785
if (hmac_sha == '256'):
782786
tlv.add('ENCX25519', enctlv)
783787
else:
784788
tlv.add('ENCX25519_SHA512', enctlv)
785789

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

@@ -806,11 +801,11 @@ def get_signature(self):
806801
def get_infile_data(self):
807802
return self.infile_data
808803

809-
def add_header(self, enckey, protected_tlv_size, compression_flags, aes_length=128):
804+
def add_header(self, protected_tlv_size, compression_flags, aes_length=0):
810805
"""Install the image header."""
811806

812807
flags = 0
813-
if enckey is not None:
808+
if aes_length != 0:
814809
if aes_length == 128:
815810
flags |= IMAGE_F['ENCRYPTED_AES128']
816811
else:

scripts/imgtool/main.py

Lines changed: 51 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,23 @@ 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+
print('--clear overrides raw AES key, image will not be encrypted')
503+
if ctx.get_parameter_source('encrypt_keylen') != click.core.ParameterSource.DEFAULT:
504+
print('Raw AES key len overrides --encrypt-keylen')
486505

487506
if pad_sig and hasattr(key, 'pad_sig'):
488507
key.pad_sig = True
@@ -527,9 +546,26 @@ def sign(key, public_key_format, align, version, pad_sig, header_size,
527546
'Pure signatures, currently, enforces preferred hash algorithm, '
528547
'and forbids sha selection by user.')
529548

549+
plainkey = None
550+
if aes_key:
551+
# Converting the command line provided raw AES key to byte array;
552+
# this aray will be truncated to desired len.
553+
plainkey = bytes.fromhex(aes_key)
554+
plainkey_len = len(plainkey)
555+
if plainkey_len not in (16, 32):
556+
raise click.UsageError("Provided keylen, {int(plainkey_len)} in bytes, not supported")
557+
elif enckey:
558+
if encrypt_keylen == 256:
559+
encrypt_keylen_bytes = 32
560+
else:
561+
encrypt_keylen_bytes = 16
562+
563+
# No AES plain key and there is request to encrypt, generate random AES key
564+
plainkey = os.urandom(encrypt_keylen_bytes)
565+
530566
if compression in ["lzma2", "lzma2armthumb"]:
531567
img.create(key, public_key_format, enckey, dependencies, boot_record,
532-
custom_tlvs, compression_tlvs, None, int(encrypt_keylen), clear,
568+
custom_tlvs, compression_tlvs, None, None, clear,
533569
baked_signature, pub_key, vector_to_sign, user_sha=user_sha,
534570
hmac_sha=hmac_sha, is_pure=is_pure, keep_comp_size=False, dont_encrypt=True)
535571
compressed_img = image.Image(version=decode_version(version),
@@ -575,13 +611,13 @@ def sign(key, public_key_format, align, version, pad_sig, header_size,
575611
keep_comp_size = True
576612
compressed_img.create(key, public_key_format, enckey,
577613
dependencies, boot_record, custom_tlvs, compression_tlvs,
578-
compression, int(encrypt_keylen), clear, baked_signature,
614+
compression, plainkey, clear, baked_signature,
579615
pub_key, vector_to_sign, user_sha=user_sha, hmac_sha=hmac_sha,
580616
is_pure=is_pure, keep_comp_size=keep_comp_size)
581617
img = compressed_img
582618
else:
583619
img.create(key, public_key_format, enckey, dependencies, boot_record,
584-
custom_tlvs, compression_tlvs, None, int(encrypt_keylen), clear,
620+
custom_tlvs, compression_tlvs, None, plainkey, clear,
585621
baked_signature, pub_key, vector_to_sign, user_sha=user_sha,
586622
hmac_sha=hmac_sha, is_pure=is_pure)
587623
img.save(outfile, hex_addr)

0 commit comments

Comments
 (0)