imgtool: Improve ECDSA key generation
This patch improves the existing ECDSA key generation feature
in the imgtool by:
- Fix a bug in the 'minimal' representation of PKCS#8 keys where
the resulting ASN.1 DER encoding is not compliant
- Add the option to export ECDSA private keys in SEC1 format by
providing a command line option -f or --format that can be
'openssl' (for SEC1 format) or 'pkcs8'. This format ends up in
key encodings which are generally smaller than PKCS#8.
Signed-off-by: Antonio de Angelis <Antonio.deAngelis@arm.com>
diff --git a/scripts/imgtool/keys/ecdsa.py b/scripts/imgtool/keys/ecdsa.py
index 9198bb4..0433beb 100644
--- a/scripts/imgtool/keys/ecdsa.py
+++ b/scripts/imgtool/keys/ecdsa.py
@@ -3,6 +3,7 @@
"""
# SPDX-License-Identifier: Apache-2.0
+import os.path
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
@@ -102,37 +103,60 @@
def _get_public(self):
return self.key.public_key()
- def _build_minimal_ecdsa_privkey(self, der):
+ def _build_minimal_ecdsa_privkey(self, der, format):
'''
Builds a new DER that only includes the EC private key, removing the
public key that is added as an "optional" BITSTRING.
'''
- offset_PUB = 68
+
+ if format == serialization.PrivateFormat.OpenSSH:
+ print(os.path.basename(__file__) +
+ ': Warning: --minimal is supported only for PKCS8 '
+ 'or TraditionalOpenSSL formats')
+ return bytearray(der)
+
EXCEPTION_TEXT = "Error parsing ecdsa key. Please submit an issue!"
- if der[offset_PUB] != 0xa1:
- raise ECDSAUsageError(EXCEPTION_TEXT)
- len_PUB = der[offset_PUB + 1]
- b = bytearray(der[:-offset_PUB])
- offset_SEQ = 29
- if b[offset_SEQ] != 0x30:
- raise ECDSAUsageError(EXCEPTION_TEXT)
- b[offset_SEQ + 1] -= len_PUB
- offset_OCT_STR = 27
- if b[offset_OCT_STR] != 0x04:
- raise ECDSAUsageError(EXCEPTION_TEXT)
- b[offset_OCT_STR + 1] -= len_PUB
- if b[0] != 0x30 or b[1] != 0x81:
- raise ECDSAUsageError(EXCEPTION_TEXT)
- b[2] -= len_PUB
+ if format == serialization.PrivateFormat.PKCS8:
+ offset_PUB = 68 # where the context specific TLV starts (tag 0xA1)
+ if der[offset_PUB] != 0xa1:
+ raise ECDSAUsageError(EXCEPTION_TEXT)
+ len_PUB = der[offset_PUB + 1] + 2 # + 2 for 0xA1 0x44 bytes
+ b = bytearray(der[:offset_PUB]) # remove the TLV with the PUB key
+ offset_SEQ = 29
+ if b[offset_SEQ] != 0x30:
+ raise ECDSAUsageError(EXCEPTION_TEXT)
+ b[offset_SEQ + 1] -= len_PUB
+ offset_OCT_STR = 27
+ if b[offset_OCT_STR] != 0x04:
+ raise ECDSAUsageError(EXCEPTION_TEXT)
+ b[offset_OCT_STR + 1] -= len_PUB
+ if b[0] != 0x30 or b[1] != 0x81:
+ raise ECDSAUsageError(EXCEPTION_TEXT)
+ # as b[1] has bit7 set, the length is on b[2]
+ b[2] -= len_PUB
+ if b[2] < 0x80:
+ del(b[1])
+
+ elif format == serialization.PrivateFormat.TraditionalOpenSSL:
+ offset_PUB = 51
+ if der[offset_PUB] != 0xA1:
+ raise ECDSAUsageError(EXCEPTION_TEXT)
+ len_PUB = der[offset_PUB + 1] + 2
+ b = bytearray(der[0:offset_PUB])
+ b[1] -= len_PUB
+
return b
- def get_private_bytes(self, minimal):
+ def get_private_bytes(self, minimal, format):
+ formats = {'pkcs8': serialization.PrivateFormat.PKCS8,
+ 'openssl': serialization.PrivateFormat.TraditionalOpenSSL
+ }
priv = self.key.private_bytes(
encoding=serialization.Encoding.DER,
- format=serialization.PrivateFormat.PKCS8,
+ format=formats[format],
encryption_algorithm=serialization.NoEncryption())
if minimal:
- priv = self._build_minimal_ecdsa_privkey(priv)
+ priv = self._build_minimal_ecdsa_privkey(priv, formats[format])
return priv
def export_private(self, path, passwd=None):
diff --git a/scripts/imgtool/keys/general.py b/scripts/imgtool/keys/general.py
index 77caf5d..033a70f 100644
--- a/scripts/imgtool/keys/general.py
+++ b/scripts/imgtool/keys/general.py
@@ -6,6 +6,7 @@
AUTOGEN_MESSAGE = "/* Autogenerated by imgtool.py, do not edit. */"
+
class KeyClass(object):
def _emit(self, header, trailer, encoded_bytes, indent, file=sys.stdout, len_format=None):
print(AUTOGEN_MESSAGE, file=file)
@@ -40,11 +41,11 @@
def emit_public_pem(self, file=sys.stdout):
print(str(self.get_public_pem(), 'utf-8'), file=file, end='')
- def emit_private(self, minimal, file=sys.stdout):
+ def emit_private(self, minimal, format, file=sys.stdout):
self._emit(
header="const unsigned char enc_priv_key[] = {",
trailer="};",
- encoded_bytes=self.get_private_bytes(minimal),
+ encoded_bytes=self.get_private_bytes(minimal, format),
indent=" ",
len_format="const unsigned int enc_priv_key_len = {};",
file=file)
diff --git a/scripts/imgtool/main.py b/scripts/imgtool/main.py
old mode 100755
new mode 100644
index f7d8eea..e43eaf1
--- a/scripts/imgtool/main.py
+++ b/scripts/imgtool/main.py
@@ -69,6 +69,7 @@
'ed25519': gen_ed25519,
'x25519': gen_x25519,
}
+valid_formats = ['openssl', 'pkcs8']
def load_signature(sigfile):
with open(sigfile, 'rb') as f:
@@ -150,13 +151,17 @@
'might require changes to the build config. Check the docs!'
)
@click.option('-k', '--key', metavar='filename', required=True)
+@click.option('-f', '--format',
+ type=click.Choice(valid_formats),
+ help='Valid formats: {}'.format(', '.join(valid_formats)),
+ default='pkcs8')
@click.command(help='Dump private key from keypair')
-def getpriv(key, minimal):
+def getpriv(key, minimal, format):
key = load_key(key)
if key is None:
print("Invalid passphrase")
try:
- key.emit_private(minimal)
+ key.emit_private(minimal, format)
except (RSAUsageError, ECDSAUsageError, Ed25519UsageError,
X25519UsageError) as e:
raise click.UsageError(e)