Skip to content

Commit 58a01bf

Browse files
committed
[nrf noup] 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 0812a81 commit 58a01bf

2 files changed

Lines changed: 71 additions & 40 deletions

File tree

scripts/imgtool/image.py

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

571571
def create(self, key, public_key_format, enckey, dependencies=None,
572572
sw_type=None, custom_tlvs=None, compression_tlvs=None,
573-
compression_type=None, encrypt_keylen=128, clear=False,
573+
compression_type=None, aes_key=None, clear=False,
574574
fixed_sig=None, pub_key=None, vector_to_sign=None,
575575
user_sha='auto', hmac_sha='auto', is_pure=False, keep_comp_size=False,
576576
dont_encrypt=False):
@@ -667,7 +667,7 @@ def create(self, key, public_key_format, enckey, dependencies=None,
667667
#
668668
# This adds the padding if image is not aligned to the 16 Bytes
669669
# in encrypted mode
670-
if self.enckey is not None and dont_encrypt is False:
670+
if aes_key is not None and dont_encrypt is False:
671671
pad_len = len(self.payload) % 16
672672
if pad_len > 0:
673673
pad = bytes(16 - pad_len)
@@ -682,10 +682,8 @@ def create(self, key, public_key_format, enckey, dependencies=None,
682682
if compression_type == "lzma2armthumb":
683683
compression_flags |= IMAGE_F['COMPRESSED_ARM_THUMB']
684684
# This adds the header to the payload as well
685-
if encrypt_keylen == 256:
686-
self.add_header(enckey, protected_tlv_size, compression_flags, 256)
687-
else:
688-
self.add_header(enckey, protected_tlv_size, compression_flags)
685+
aes_key_bits = 0 if aes_key is None else len(aes_key) * 8
686+
self.add_header(protected_tlv_size, compression_flags, aes_key_bits)
689687

690688
prot_tlv = TLV(self.endian, TLV_PROT_INFO_MAGIC)
691689

@@ -806,12 +804,18 @@ def create(self, key, public_key_format, enckey, dependencies=None,
806804
if protected_tlv_off is not None:
807805
self.payload = self.payload[:protected_tlv_off]
808806

809-
if enckey is not None and dont_encrypt is False:
810-
if encrypt_keylen == 256:
811-
plainkey = os.urandom(32)
812-
else:
813-
plainkey = os.urandom(16)
807+
# When passed AES key and clear flag, then do not encrypt, because the key
808+
# is only passed to be stored in encryption key TLV, the payload stays clear text.
809+
if aes_key and not clear:
810+
nonce = bytes([0] * 16)
811+
cipher = Cipher(algorithms.AES(aes_key), modes.CTR(nonce),
812+
backend=default_backend())
813+
encryptor = cipher.encryptor()
814+
img = bytes(self.payload[self.header_size:])
815+
self.payload[self.header_size:] = \
816+
encryptor.update(img) + encryptor.finalize()
814817

818+
if enckey is not None and dont_encrypt is False:
815819
if not isinstance(enckey, rsa.RSAPublic):
816820
if hmac_sha == 'auto' or hmac_sha == '256':
817821
hmac_sha = '256'
@@ -825,35 +829,26 @@ def create(self, key, public_key_format, enckey, dependencies=None,
825829

826830
if isinstance(enckey, rsa.RSAPublic):
827831
cipherkey = enckey._get_public().encrypt(
828-
plainkey, padding.OAEP(
832+
aes_key, padding.OAEP(
829833
mgf=padding.MGF1(algorithm=hashes.SHA256()),
830834
algorithm=hashes.SHA256(),
831835
label=None))
832836
self.enctlv_len = len(cipherkey)
833837
tlv.add('ENCRSA2048', cipherkey)
834838
elif isinstance(enckey, ecdsa.ECDSA256P1Public):
835-
cipherkey, mac, pubk = self.ecies_hkdf(enckey, plainkey, hmac_sha_alg)
839+
cipherkey, mac, pubk = self.ecies_hkdf(enckey, aes_key, hmac_sha_alg)
836840
enctlv = pubk + mac + cipherkey
837841
self.enctlv_len = len(enctlv)
838842
tlv.add('ENCEC256', enctlv)
839843
elif isinstance(enckey, x25519.X25519Public):
840-
cipherkey, mac, pubk = self.ecies_hkdf(enckey, plainkey, hmac_sha_alg)
844+
cipherkey, mac, pubk = self.ecies_hkdf(enckey, aes_key, hmac_sha_alg)
841845
enctlv = pubk + mac + cipherkey
842846
self.enctlv_len = len(enctlv)
843847
if (hmac_sha == '256'):
844848
tlv.add('ENCX25519', enctlv)
845849
else:
846850
tlv.add('ENCX25519_SHA512', enctlv)
847851

848-
if not clear:
849-
nonce = bytes([0] * 16)
850-
cipher = Cipher(algorithms.AES(plainkey), modes.CTR(nonce),
851-
backend=default_backend())
852-
encryptor = cipher.encryptor()
853-
img = bytes(self.payload[self.header_size:])
854-
self.payload[self.header_size:] = \
855-
encryptor.update(img) + encryptor.finalize()
856-
857852
self.payload += prot_tlv.get()
858853
self.payload += tlv.get()
859854

@@ -868,11 +863,11 @@ def get_signature(self):
868863
def get_infile_data(self):
869864
return self.infile_data
870865

871-
def add_header(self, enckey, protected_tlv_size, compression_flags, aes_length=128):
866+
def add_header(self, protected_tlv_size, compression_flags, aes_length=0):
872867
"""Install the image header."""
873868

874869
flags = 0
875-
if enckey is not None:
870+
if aes_length != 0:
876871
if aes_length == 128:
877872
flags |= IMAGE_F['ENCRYPTED_AES128']
878873
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
@@ -324,6 +325,14 @@ def create_lzma2_header(dictsize, pb, lc, lp):
324325
header.append( ( pb * 5 + lp) * 9 + lc)
325326
return header
326327

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

@@ -454,13 +463,17 @@ def convert(self, value, param, ctx):
454463
help='Unique image class identifier, format: (<raw_uuid>|<image_class_name>)')
455464
@click.option('--manifest', default=None, required=False,
456465
help='Path to the update manifest file')
457-
def sign(key, public_key_format, align, version, pad_sig, header_size,
466+
@click.option('--aes-key', default=None, required=False,
467+
help='String representing raw AES key, format: hex byte string of 32 or 64'
468+
'hexadecimal characters')
469+
@click.pass_context
470+
def sign(ctx, key, public_key_format, align, version, pad_sig, header_size,
458471
pad_header, slot_size, pad, confirm, test, max_sectors, overwrite_only,
459472
endian, encrypt_keylen, encrypt, compression, infile, outfile,
460473
dependencies, load_addr, hex_addr, erased_val, save_enctlv,
461474
security_counter, boot_record, custom_tlv, rom_fixed, max_align,
462475
clear, fix_sig, fix_sig_pubkey, sig_out, user_sha, hmac_sha, is_pure,
463-
vector_to_sign, non_bootable, vid, cid, manifest):
476+
vector_to_sign, non_bootable, vid, cid, manifest, aes_key):
464477

465478
if confirm or test:
466479
# Confirmed but non-padded images don't make much sense, because
@@ -477,17 +490,23 @@ def sign(key, public_key_format, align, version, pad_sig, header_size,
477490
manifest=manifest)
478491
compression_tlvs = {}
479492
img.load(infile)
493+
480494
key = load_key(key) if key else None
481-
enckey = load_key(encrypt) if encrypt else None
482-
if enckey and key and ((isinstance(key, keys.ECDSA256P1) and
483-
not isinstance(enckey, keys.ECDSA256P1Public))
484-
or (isinstance(key, keys.ECDSA384P1) and
485-
not isinstance(enckey, keys.ECDSA384P1Public))
486-
or (isinstance(key, keys.RSA) and
487-
not isinstance(enckey, keys.RSAPublic))):
488-
# FIXME
489-
raise click.UsageError("Signing and encryption must use the same "
490-
"type of key")
495+
enckey = None
496+
if not aes_key:
497+
enckey = load_key(encrypt) if encrypt else None
498+
if enckey and not match_sig_enc_key(key, enckey):
499+
# FIXME
500+
raise click.UsageError("Signing and encryption must use the same "
501+
"type of key")
502+
else:
503+
if encrypt:
504+
encrypt = None
505+
print('Raw AES key overrides --key, there will be no encrypted key added to the image')
506+
if clear:
507+
print('--clear overrides raw AES key, image will not be encrypted')
508+
if ctx.get_parameter_source('encrypt_keylen') != click.core.ParameterSource.DEFAULT:
509+
print('Raw AES key len overrides --encrypt-keylen')
491510

492511
if pad_sig and hasattr(key, 'pad_sig'):
493512
key.pad_sig = True
@@ -532,9 +551,26 @@ def sign(key, public_key_format, align, version, pad_sig, header_size,
532551
'Pure signatures, currently, enforces preferred hash algorithm, '
533552
'and forbids sha selection by user.')
534553

554+
plainkey = None
555+
if aes_key:
556+
# Converting the command line provided raw AES key to byte array;
557+
# this aray will be truncated to desired len.
558+
plainkey = bytes.fromhex(aes_key)
559+
plainkey_len = len(plainkey)
560+
if plainkey_len not in (16, 32):
561+
raise click.UsageError("Provided keylen, {int(plainkey_len)} in bytes, not supported")
562+
elif enckey:
563+
if encrypt_keylen == 256:
564+
encrypt_keylen_bytes = 32
565+
else:
566+
encrypt_keylen_bytes = 16
567+
568+
# No AES plain key and there is request to encrypt, generate random AES key
569+
plainkey = os.urandom(encrypt_keylen_bytes)
570+
535571
if compression in ["lzma2", "lzma2armthumb"]:
536572
img.create(key, public_key_format, enckey, dependencies, boot_record,
537-
custom_tlvs, compression_tlvs, None, int(encrypt_keylen), clear,
573+
custom_tlvs, compression_tlvs, None, None, clear,
538574
baked_signature, pub_key, vector_to_sign, user_sha=user_sha,
539575
hmac_sha=hmac_sha, is_pure=is_pure, keep_comp_size=False, dont_encrypt=True)
540576
compressed_img = image.Image(version=decode_version(version),
@@ -580,13 +616,13 @@ def sign(key, public_key_format, align, version, pad_sig, header_size,
580616
keep_comp_size = True
581617
compressed_img.create(key, public_key_format, enckey,
582618
dependencies, boot_record, custom_tlvs, compression_tlvs,
583-
compression, int(encrypt_keylen), clear, baked_signature,
619+
compression, plainkey, clear, baked_signature,
584620
pub_key, vector_to_sign, user_sha=user_sha, hmac_sha=hmac_sha,
585621
is_pure=is_pure, keep_comp_size=keep_comp_size)
586622
img = compressed_img
587623
else:
588624
img.create(key, public_key_format, enckey, dependencies, boot_record,
589-
custom_tlvs, compression_tlvs, None, int(encrypt_keylen), clear,
625+
custom_tlvs, compression_tlvs, None, plainkey, clear,
590626
baked_signature, pub_key, vector_to_sign, user_sha=user_sha,
591627
hmac_sha=hmac_sha, is_pure=is_pure)
592628
img.save(outfile, hex_addr)

0 commit comments

Comments
 (0)