Skip to content

Commit 9f0677c

Browse files
committed
imgtool: Add support for encrypting image with raw AES key
The change adds --aes-raw-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 56538cf commit 9f0677c

File tree

2 files changed

+74
-43
lines changed

2 files changed

+74
-43
lines changed

scripts/imgtool/image.py

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -513,12 +513,13 @@ 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_raw_key=None, clear=False,
517517
fixed_sig=None, pub_key=None, vector_to_sign=None,
518-
user_sha='auto', hmac_sha='auto', is_pure=False, keep_comp_size=False,
519-
dont_encrypt=False):
518+
user_sha='auto', hmac_sha='auto', is_pure=False, keep_comp_size=False):
520519
self.enckey = enckey
521520

521+
dont_encrypt = bool(not aes_raw_key)
522+
522523
# key decides on sha, then pub_key; of both are none default is used
523524
check_key = key if key is not None else pub_key
524525
hash_algorithm, hash_tlv = key_and_user_sha_to_alg_and_tlv(check_key, user_sha, is_pure)
@@ -605,7 +606,7 @@ def create(self, key, public_key_format, enckey, dependencies=None,
605606
#
606607
# This adds the padding if image is not aligned to the 16 Bytes
607608
# in encrypted mode
608-
if self.enckey is not None and dont_encrypt is False:
609+
if aes_raw_key is not None and dont_encrypt is False:
609610
pad_len = len(self.payload) % 16
610611
if pad_len > 0:
611612
pad = bytes(16 - pad_len)
@@ -620,10 +621,8 @@ def create(self, key, public_key_format, enckey, dependencies=None,
620621
if compression_type == "lzma2armthumb":
621622
compression_flags |= IMAGE_F['COMPRESSED_ARM_THUMB']
622623
# 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)
624+
aes_key_bits = 0 if aes_raw_key is None else len(aes_raw_key) * 8
625+
self.add_header(protected_tlv_size, compression_flags, aes_key_bits)
627626

628627
prot_tlv = TLV(self.endian, TLV_PROT_INFO_MAGIC)
629628

@@ -742,12 +741,18 @@ def create(self, key, public_key_format, enckey, dependencies=None,
742741
if protected_tlv_off is not None:
743742
self.payload = self.payload[:protected_tlv_off]
744743

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)
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 paylad stays clear text.
746+
if aes_raw_key and not clear:
747+
nonce = bytes([0] * 16)
748+
cipher = Cipher(algorithms.AES(aes_raw_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()
750754

755+
if enckey is not None and dont_encrypt is False:
751756
if not isinstance(enckey, rsa.RSAPublic):
752757
if hmac_sha == 'auto' or hmac_sha == '256':
753758
hmac_sha = '256'
@@ -762,35 +767,26 @@ def create(self, key, public_key_format, enckey, dependencies=None,
762767

763768
if isinstance(enckey, rsa.RSAPublic):
764769
cipherkey = enckey._get_public().encrypt(
765-
plainkey, padding.OAEP(
770+
aes_raw_key, padding.OAEP(
766771
mgf=padding.MGF1(algorithm=hashes.SHA256()),
767772
algorithm=hashes.SHA256(),
768773
label=None))
769774
self.enctlv_len = len(cipherkey)
770775
tlv.add('ENCRSA2048', cipherkey)
771776
elif isinstance(enckey, ecdsa.ECDSA256P1Public):
772-
cipherkey, mac, pubk = self.ecies_hkdf(enckey, plainkey, hmac_sha_alg)
777+
cipherkey, mac, pubk = self.ecies_hkdf(enckey, aes_raw_key, hmac_sha_alg)
773778
enctlv = pubk + mac + cipherkey
774779
self.enctlv_len = len(enctlv)
775780
tlv.add('ENCEC256', enctlv)
776781
elif isinstance(enckey, x25519.X25519Public):
777-
cipherkey, mac, pubk = self.ecies_hkdf(enckey, plainkey, hmac_sha_alg)
782+
cipherkey, mac, pubk = self.ecies_hkdf(enckey, aes_raw_key, hmac_sha_alg)
778783
enctlv = pubk + mac + cipherkey
779784
self.enctlv_len = len(enctlv)
780785
if (hmac_sha == '256'):
781786
tlv.add('ENCX25519', enctlv)
782787
else:
783788
tlv.add('ENCX25519_SHA512', enctlv)
784789

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-
794790
self.payload += prot_tlv.get()
795791
self.payload += tlv.get()
796792

@@ -805,11 +801,11 @@ def get_signature(self):
805801
def get_infile_data(self):
806802
return self.infile_data
807803

808-
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):
809805
"""Install the image header."""
810806

811807
flags = 0
812-
if enckey is not None:
808+
if aes_length != 0:
813809
if aes_length == 128:
814810
flags |= IMAGE_F['ENCRYPTED_AES128']
815811
else:

scripts/imgtool/main.py

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import re
2424
import struct
2525
import sys
26+
import os
2627

2728
import click
2829

@@ -450,13 +451,17 @@ def convert(self, value, param, ctx):
450451
help='Unique vendor identifier, format: (<raw_uuid>|<domain_name)>')
451452
@click.option('--cid', default=None, required=False,
452453
help='Unique image class identifier, format: (<raw_uuid>|<image_class_name>)')
453-
def sign(key, public_key_format, align, version, pad_sig, header_size,
454+
@click.option('--aes-raw-key', default=None, required=False,
455+
help='String representing raw AES key, format: hex byte string of 32 or 64'
456+
'hexadecimal characters')
457+
@click.pass_context
458+
def sign(ctx, key, public_key_format, align, version, pad_sig, header_size,
454459
pad_header, slot_size, pad, confirm, test, max_sectors, overwrite_only,
455460
endian, encrypt_keylen, encrypt, compression, infile, outfile,
456461
dependencies, load_addr, hex_addr, erased_val, save_enctlv,
457462
security_counter, boot_record, custom_tlv, rom_fixed, max_align,
458463
clear, fix_sig, fix_sig_pubkey, sig_out, user_sha, hmac_sha, is_pure,
459-
vector_to_sign, non_bootable, vid, cid):
464+
vector_to_sign, non_bootable, vid, cid, aes_raw_key):
460465

461466
if confirm or test:
462467
# Confirmed but non-padded images don't make much sense, because
@@ -472,17 +477,30 @@ def sign(key, public_key_format, align, version, pad_sig, header_size,
472477
non_bootable=non_bootable, vid=vid, cid=cid)
473478
compression_tlvs = {}
474479
img.load(infile)
480+
475481
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")
482+
enckey = None
483+
if not aes_raw_key:
484+
enckey = load_key(encrypt) if encrypt else None
485+
if enckey and key:
486+
if ((isinstance(key, keys.ECDSA256P1) and
487+
not isinstance(enckey, keys.ECDSA256P1Public))
488+
or (isinstance(key, keys.ECDSA384P1) and
489+
not isinstance(enckey, keys.ECDSA384P1Public))
490+
or (isinstance(key, keys.RSA) and
491+
not isinstance(enckey, keys.RSAPublic))):
492+
# FIXME
493+
raise click.UsageError("Signing and encryption must use the same "
494+
"type of key")
495+
else:
496+
if encrypt:
497+
encrypt = None
498+
print('Raw AES key overrides --key, there will be no encrypted key added to the image')
499+
if clear:
500+
clear = False
501+
print('Raw AES key overrides --clear, image will be encrypted')
502+
if ctx.get_parameter_source('encrypt_keylen') != click.core.ParameterSource.DEFAULT:
503+
print('Raw AES key len overrides --encrypt-keylen')
486504

487505
if pad_sig and hasattr(key, 'pad_sig'):
488506
key.pad_sig = True
@@ -527,11 +545,28 @@ def sign(key, public_key_format, align, version, pad_sig, header_size,
527545
'Pure signatures, currently, enforces preferred hash algorithm, '
528546
'and forbids sha selection by user.')
529547

548+
plainkey = None
549+
if aes_raw_key:
550+
# Converting the command line provided raw AES key to byte array;
551+
# this aray will be truncated to desired len.
552+
plainkey = bytes.fromhex(aes_raw_key)
553+
plainkey_len = len(plainkey)
554+
if plainkey_len not in (16, 32):
555+
raise click.UsageError("Provided keylen, {int(plainkey_len)} in bytes, not supported")
556+
elif enckey:
557+
if encrypt_keylen == 256:
558+
encrypt_keylen_bytes = 32
559+
else:
560+
encrypt_keylen_bytes = 16
561+
562+
# No AES plain key and there is request to encrypt, generate random AES key
563+
plainkey = os.urandom(encrypt_keylen_bytes)
564+
530565
if compression in ["lzma2", "lzma2armthumb"]:
531566
img.create(key, public_key_format, enckey, dependencies, boot_record,
532-
custom_tlvs, compression_tlvs, None, int(encrypt_keylen), clear,
567+
custom_tlvs, compression_tlvs, None, None, clear,
533568
baked_signature, pub_key, vector_to_sign, user_sha=user_sha,
534-
hmac_sha=hmac_sha, is_pure=is_pure, keep_comp_size=False, dont_encrypt=True)
569+
hmac_sha=hmac_sha, is_pure=is_pure, keep_comp_size=False)
535570
compressed_img = image.Image(version=decode_version(version),
536571
header_size=header_size, pad_header=pad_header,
537572
pad=pad, confirm=confirm, align=int(align),
@@ -575,13 +610,13 @@ def sign(key, public_key_format, align, version, pad_sig, header_size,
575610
keep_comp_size = True
576611
compressed_img.create(key, public_key_format, enckey,
577612
dependencies, boot_record, custom_tlvs, compression_tlvs,
578-
compression, int(encrypt_keylen), clear, baked_signature,
613+
compression, plainkey, clear, baked_signature,
579614
pub_key, vector_to_sign, user_sha=user_sha, hmac_sha=hmac_sha,
580615
is_pure=is_pure, keep_comp_size=keep_comp_size)
581616
img = compressed_img
582617
else:
583618
img.create(key, public_key_format, enckey, dependencies, boot_record,
584-
custom_tlvs, compression_tlvs, None, int(encrypt_keylen), clear,
619+
custom_tlvs, compression_tlvs, None, plainkey, clear,
585620
baked_signature, pub_key, vector_to_sign, user_sha=user_sha,
586621
hmac_sha=hmac_sha, is_pure=is_pure)
587622
img.save(outfile, hex_addr)

0 commit comments

Comments
 (0)