Commit c6e7e9be authored by Antonio de Angelis's avatar Antonio de Angelis Committed by Fabio Utzig
Browse files

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: default avatarAntonio de Angelis <Antonio.deAngelis@arm.com>
parent 284b8fe3
Loading
Loading
Loading
Loading
+44 −20
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@ ECDSA key management
"""

# SPDX-License-Identifier: Apache-2.0
import os.path

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
@@ -102,17 +103,25 @@ class ECDSA256P1(ECDSA256P1Public):
    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 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]
        b = bytearray(der[:-offset_PUB])
            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)
@@ -123,16 +132,31 @@ class ECDSA256P1(ECDSA256P1Public):
            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):
+3 −2
Original line number Diff line number Diff line
@@ -6,6 +6,7 @@ import sys

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 @@ class KeyClass(object):
    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)

scripts/imgtool/main.py

100755 → 100644
+7 −2
Original line number Diff line number Diff line
@@ -69,6 +69,7 @@ keygens = {
    'ed25519':    gen_ed25519,
    'x25519':     gen_x25519,
}
valid_formats = ['openssl', 'pkcs8']

def load_signature(sigfile):
    with open(sigfile, 'rb') as f:
@@ -150,13 +151,17 @@ def getpub(key, encoding, lang):
                   '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)