scripts: imgtool: Add command to dump private keys

This applies a few improvements to a commit previously included in
PR #596:

* Move functions to dump a private key to the private key classes
* Remove language option; always dumps in C format
* Add option to generate a minimal dump. This will remove extra
  parameters that are present in keys generated with the `keygen`
  command.
  For P256 this will remove the public point, which is already
  ignored by the parsing function. The resulting key dump shrinks
  from 138 to 70 bytes.
  For RSA it will remove the DP/DQ/QP parameters which are only
  used with CRT enabled, and if not available, can be calculated at
  runtime. This reduces the size of a key dump from around 1190
  bytes to somewhere close to 800 bytes. A patch to the RSA parsing
  routine will be added in another commit.

Signed-off-by: Fabio Utzig <utzig@apache.org>
Signed-off-by: Ioannis Konstantelias <ikonstadel@gmail.com>
diff --git a/scripts/imgtool/keys/ecdsa.py b/scripts/imgtool/keys/ecdsa.py
index f93783d..c1c1cac 100644
--- a/scripts/imgtool/keys/ecdsa.py
+++ b/scripts/imgtool/keys/ecdsa.py
@@ -31,6 +31,9 @@
                 encoding=serialization.Encoding.DER,
                 format=serialization.PublicFormat.SubjectPublicKeyInfo)
 
+    def get_private_bytes(self, minimal):
+        self._unsupported('get_private_bytes')
+
     def export_private(self, path, passwd=None):
         self._unsupported('export_private')
 
@@ -84,6 +87,39 @@
     def _get_public(self):
         return self.key.public_key()
 
+    def _build_minimal_ecdsa_privkey(self, der):
+        '''
+        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
+        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
+        return b
+
+    def get_private_bytes(self, minimal):
+        priv = self.key.private_bytes(
+                encoding=serialization.Encoding.DER,
+                format=serialization.PrivateFormat.PKCS8,
+                encryption_algorithm=serialization.NoEncryption())
+        if minimal:
+            priv = self._build_minimal_ecdsa_privkey(priv)
+        return priv
+
     def export_private(self, path, passwd=None):
         """Write the private key to the given file, protecting it with the optional password."""
         if passwd is None:
diff --git a/scripts/imgtool/keys/ed25519.py b/scripts/imgtool/keys/ed25519.py
index 57eb99a..6745a89 100644
--- a/scripts/imgtool/keys/ed25519.py
+++ b/scripts/imgtool/keys/ed25519.py
@@ -32,6 +32,9 @@
                 encoding=serialization.Encoding.DER,
                 format=serialization.PublicFormat.SubjectPublicKeyInfo)
 
+    def get_private_bytes(self, minimal):
+        self._unsupported('get_private_bytes')
+
     def export_private(self, path, passwd=None):
         self._unsupported('export_private')
 
@@ -70,6 +73,10 @@
     def _get_public(self):
         return self.key.public_key()
 
+    def get_private_bytes(self, minimal):
+        raise Ed25519UsageError("Operation not supported with {} keys".format(
+            self.shortname()))
+
     def export_private(self, path, passwd=None):
         """
         Write the private key to the given file, protecting it with the
diff --git a/scripts/imgtool/keys/general.py b/scripts/imgtool/keys/general.py
index 3ba34cb..f6b8a09 100644
--- a/scripts/imgtool/keys/general.py
+++ b/scripts/imgtool/keys/general.py
@@ -5,11 +5,10 @@
 AUTOGEN_MESSAGE = "/* Autogenerated by imgtool.py, do not edit. */"
 
 class KeyClass(object):
-    def _public_emit(self, header, trailer, indent, file=sys.stdout, len_format=None):
+    def _emit(self, header, trailer, encoded_bytes, indent, file=sys.stdout, len_format=None):
         print(AUTOGEN_MESSAGE, file=file)
         print(header, end='', file=file)
-        encoded = self.get_public_bytes()
-        for count, b in enumerate(encoded):
+        for count, b in enumerate(encoded_bytes):
             if count % 8 == 0:
                 print("\n" + indent, end='', file=file)
             else:
@@ -17,19 +16,30 @@
             print("0x{:02x},".format(b), end='', file=file)
         print("\n" + trailer, file=file)
         if len_format is not None:
-            print(len_format.format(len(encoded)), file=file)
+            print(len_format.format(len(encoded_bytes)), file=file)
 
-    def emit_c(self, file=sys.stdout):
-        self._public_emit(
+    def emit_c_public(self, file=sys.stdout):
+        self._emit(
                 header="const unsigned char {}_pub_key[] = {{".format(self.shortname()),
                 trailer="};",
+                encoded_bytes=self.get_public_bytes(),
                 indent="    ",
                 len_format="const unsigned int {}_pub_key_len = {{}};".format(self.shortname()),
                 file=file)
 
-    def emit_rust(self, file=sys.stdout):
-        self._public_emit(
+    def emit_rust_public(self, file=sys.stdout):
+        self._emit(
                 header="static {}_PUB_KEY: &'static [u8] = &[".format(self.shortname().upper()),
                 trailer="];",
+                encoded_bytes=self.get_public_bytes(),
                 indent="    ",
                 file=file)
+
+    def emit_private(self, minimal, file=sys.stdout):
+        self._emit(
+                header="const unsigned char enc_priv_key[] = {",
+                trailer="};",
+                encoded_bytes=self.get_private_bytes(minimal),
+                indent="    ",
+                len_format="const unsigned int enc_priv_key_len = {};",
+                file=file)
diff --git a/scripts/imgtool/keys/rsa.py b/scripts/imgtool/keys/rsa.py
index 0f9a905..85c0342 100644
--- a/scripts/imgtool/keys/rsa.py
+++ b/scripts/imgtool/keys/rsa.py
@@ -42,6 +42,9 @@
                 encoding=serialization.Encoding.DER,
                 format=serialization.PublicFormat.PKCS1)
 
+    def get_private_bytes(self, minimal):
+        self._unsupported('get_private_bytes')
+
     def export_private(self, path, passwd=None):
         self._unsupported('export_private')
 
@@ -94,6 +97,49 @@
     def _get_public(self):
         return self.key.public_key()
 
+    def _build_minimal_rsa_privkey(self, der):
+        '''
+        Builds a new DER that only includes N/E/D/P/Q RSA parameters;
+        standard DER private bytes provided by OpenSSL also includes
+        CRT params (DP/DQ/QP) which can be removed.
+        '''
+        OFFSET_N = 7  # N is always located at this offset
+        b = bytearray(der)
+        off = OFFSET_N
+        if b[off + 1] != 0x82:
+            raise RSAUsageError("Error parsing N while minimizing")
+        len_N = (b[off + 2] << 8) + b[off + 3] + 4
+        off += len_N
+        if b[off + 1] != 0x03:
+            raise RSAUsageError("Error parsing E while minimizing")
+        len_E = b[off + 2] + 4
+        off += len_E
+        if b[off + 1] != 0x82:
+            raise RSAUsageError("Error parsing D while minimizing")
+        len_D = (b[off + 2] << 8) + b[off + 3] + 4
+        off += len_D
+        if b[off + 1] != 0x81:
+            raise RSAUsageError("Error parsing P while minimizing")
+        len_P = b[off + 2] + 3
+        off += len_P
+        if b[off + 1] != 0x81:
+            raise RSAUsageError("Error parsing Q while minimizing")
+        len_Q = b[off + 2] + 3
+        off += len_Q
+        # adjust DER size for removed elements
+        b[2] = (off - 4) >> 8
+        b[3] = (off - 4) & 0xff
+        return b[:off]
+
+    def get_private_bytes(self, minimal):
+        priv = self.key.private_bytes(
+                encoding=serialization.Encoding.DER,
+                format=serialization.PrivateFormat.TraditionalOpenSSL,
+                encryption_algorithm=serialization.NoEncryption())
+        if minimal:
+            priv = self._build_minimal_rsa_privkey(priv)
+        return priv
+
     def export_private(self, path, passwd=None):
         """Write the private key to the given file, protecting it with the
         optional password."""
diff --git a/scripts/imgtool/main.py b/scripts/imgtool/main.py
index 2c1e049..61ed282 100755
--- a/scripts/imgtool/main.py
+++ b/scripts/imgtool/main.py
@@ -92,19 +92,33 @@
 @click.option('-l', '--lang', metavar='lang', default=valid_langs[0],
               type=click.Choice(valid_langs))
 @click.option('-k', '--key', metavar='filename', required=True)
-@click.command(help='Get public key from keypair')
+@click.command(help='Dump public key from keypair')
 def getpub(key, lang):
     key = load_key(key)
     if key is None:
         print("Invalid passphrase")
     elif lang == 'c':
-        key.emit_c()
+        key.emit_c_public()
     elif lang == 'rust':
-        key.emit_rust()
+        key.emit_rust_public()
     else:
         raise ValueError("BUG: should never get here!")
 
 
+@click.option('--minimal', default=False, is_flag=True,
+              help='Reduce the size of the dumped private key to include only '
+                   'the minimum amount of data required to decrypt. This '
+                   'might require changes to the build config. Check the docs!'
+              )
+@click.option('-k', '--key', metavar='filename', required=True)
+@click.command(help='Dump private key from keypair')
+def getpriv(key, minimal):
+    key = load_key(key)
+    if key is None:
+        print("Invalid passphrase")
+    key.emit_private(minimal)
+
+
 @click.argument('imgfile')
 @click.option('-k', '--key', metavar='filename')
 @click.command(help="Check that signed image can be verified by given key")
@@ -271,6 +285,7 @@
 
 imgtool.add_command(keygen)
 imgtool.add_command(getpub)
+imgtool.add_command(getpriv)
 imgtool.add_command(verify)
 imgtool.add_command(sign)
 imgtool.add_command(version)