Skip to content

Commit 77f3866

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 f846e9e commit 77f3866

File tree

2 files changed

+77
-44
lines changed

2 files changed

+77
-44
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_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_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_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_key is None else len(aes_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 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()
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_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_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_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: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@
1818
# limitations under the License.
1919

2020
import base64
21+
import click
2122
import getpass
2223
import lzma
24+
import os
2325
import re
2426
import struct
2527
import sys
2628

27-
import click
2829

2930
import imgtool.keys as keys
3031
from imgtool import image, imgtool_version
@@ -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,11 +547,28 @@ 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,
534-
hmac_sha=hmac_sha, is_pure=is_pure, keep_comp_size=False, dont_encrypt=True)
571+
hmac_sha=hmac_sha, is_pure=is_pure, keep_comp_size=False)
535572
compressed_img = image.Image(version=decode_version(version),
536573
header_size=header_size, pad_header=pad_header,
537574
pad=pad, confirm=confirm, align=int(align),
@@ -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)