fix: Add back ECDSA signature support
Replace the (now broken) ECDSA code with code using the python
'cryptography' library.
Similar to the change to RSA, this changes the format that private keys
are stored, again using PKCS#8. This supports the stronger password
protection as well.
Again, this code will still support reading the older style of public
keys, but other tools that use keys generated by this change will need
to be updated to work with the new format.
Signed-off-by: David Brown <david.brown@linaro.org>
diff --git a/scripts/imgtool/keys/__init__.py b/scripts/imgtool/keys/__init__.py
index 371af12..da5b083 100644
--- a/scripts/imgtool/keys/__init__.py
+++ b/scripts/imgtool/keys/__init__.py
@@ -19,74 +19,10 @@
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
+from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey, EllipticCurvePublicKey
from .rsa import RSA2048, RSA2048Public, RSAUsageError
-
-class ECDSA256P1():
- def __init__(self, key):
- """Construct an ECDSA P-256 private key"""
- self.key = key
-
- @staticmethod
- def generate():
- return ECDSA256P1(SigningKey.generate(curve=NIST256p))
-
- def export_private(self, path):
- with open(path, 'wb') as f:
- f.write(self.key.to_pem())
-
- def get_public_bytes(self):
- vk = self.key.get_verifying_key()
- return bytes(vk.to_der())
-
- def emit_c(self):
- vk = self.key.get_verifying_key()
- print(AUTOGEN_MESSAGE)
- print("const unsigned char ecdsa_pub_key[] = {", end='')
- encoded = bytes(vk.to_der())
- for count, b in enumerate(encoded):
- if count % 8 == 0:
- print("\n\t", end='')
- else:
- print(" ", end='')
- print("0x{:02x},".format(b), end='')
- print("\n};")
- print("const unsigned int ecdsa_pub_key_len = {};".format(len(encoded)))
-
- def emit_rust(self):
- vk = self.key.get_verifying_key()
- print(AUTOGEN_MESSAGE)
- print("static ECDSA_PUB_KEY: &'static [u8] = &[", end='')
- encoded = bytes(vk.to_der())
- for count, b in enumerate(encoded):
- if count % 8 == 0:
- print("\n ", end='')
- else:
- print(" ", end='')
- print("0x{:02x},".format(b), end='')
- print("\n];")
-
- def sign(self, payload):
- # To make this fixed length, possibly pad with zeros.
- sig = self.key.sign(payload, hashfunc=hashlib.sha256, sigencode=util.sigencode_der)
- sig += b'\000' * (self.sig_len() - len(sig))
- return sig
-
- def sig_len(self):
- # The DER encoding depends on the high bit, and can be
- # anywhere from 70 to 72 bytes. Because we have to fill in
- # the length field before computing the signature, however,
- # we'll give the largest, and the sig checking code will allow
- # for it to be up to two bytes larger than the actual
- # signature.
- return 72
-
- def sig_type(self):
- """Return the type of this signature (as a string)"""
- return "ECDSA256_SHA256"
-
- def sig_tlv(self):
- return "ECDSA256"
+from .ecdsa import ECDSA256P1, ECDSA256P1Public, ECDSAUsageError
class PasswordRequired(Exception):
"""Raised to indicate that the key is password protected, but a
@@ -124,5 +60,17 @@
if pk.key_size != 2048:
raise Exception("Unsupported RSA key size: " + pk.key_size)
return RSA2048Public(pk)
+ elif isinstance(pk, EllipticCurvePrivateKey):
+ if pk.curve.name != 'secp256r1':
+ raise Exception("Unsupported EC curve: " + pk.curve.name)
+ if pk.key_size != 256:
+ raise Exception("Unsupported EC size: " + pk.key_size)
+ return ECDSA256P1(pk)
+ elif isinstance(pk, EllipticCurvePublicKey):
+ if pk.curve.name != 'secp256r1':
+ raise Exception("Unsupported EC curve: " + pk.curve.name)
+ if pk.key_size != 256:
+ raise Exception("Unsupported EC size: " + pk.key_size)
+ return ECDSA256P1Public(pk)
else:
raise Exception("Unknown key type: " + str(type(pk)))
diff --git a/scripts/imgtool/keys/ecdsa.py b/scripts/imgtool/keys/ecdsa.py
new file mode 100644
index 0000000..7066e30
--- /dev/null
+++ b/scripts/imgtool/keys/ecdsa.py
@@ -0,0 +1,95 @@
+"""
+ECDSA key management
+"""
+
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.primitives.hashes import SHA256
+
+from .general import KeyClass
+
+class ECDSAUsageError(Exception):
+ pass
+
+class ECDSA256P1Public(KeyClass):
+ def __init__(self, key):
+ self.key = key
+
+ def shortname(self):
+ return "ecdsa"
+
+ def _unsupported(self, name):
+ raise ECDSAUsageError("Operation {} requires private key".format(name))
+
+ def _get_public(self):
+ return self.key
+
+ def get_public_bytes(self):
+ # The key is embedded into MBUboot in "SubjectPublicKeyInfo" format
+ return self._get_public().public_bytes(
+ encoding=serialization.Encoding.DER,
+ format=serialization.PublicFormat.SubjectPublicKeyInfo)
+
+ def export_private(self, path, passwd=None):
+ self._unsupported('export_private')
+
+ def export_public(self, path):
+ """Write the public key to the given file."""
+ pem = self._get_public().public_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PublicFormat.SubjectPublicKeyInfo)
+ with open(path, 'wb') as f:
+ f.write(pem)
+
+ def sig_type(self):
+ return "ECDSA256_SHA256"
+
+ def sig_tlv(self):
+ return "ECDSA256"
+
+ def sig_len(self):
+ # The DER encoding depends on the high bit, and can be
+ # anywhere from 70 to 72 bytes. Because we have to fill in
+ # the length field before computing the signature, however,
+ # we'll give the largest, and the sig checking code will allow
+ # for it to be up to two bytes larger than the actual
+ # signature.
+ return 72
+
+class ECDSA256P1(ECDSA256P1Public):
+ """
+ Wrapper around an ECDSA private key.
+ """
+
+ def __init__(self, key):
+ """key should be an instance of EllipticCurvePrivateKey"""
+ self.key = key
+
+ @staticmethod
+ def generate():
+ pk = ec.generate_private_key(
+ ec.SECP256R1(),
+ backend=default_backend())
+ return ECDSA256P1(pk)
+
+ def _get_public(self):
+ return self.key.public_key()
+
+ 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:
+ enc = serialization.NoEncryption()
+ else:
+ enc = serialization.BestAvailableEncryption(passwd)
+ pem = self.key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.PKCS8,
+ encryption_algorithm=enc)
+ with open(path, 'wb') as f:
+ f.write(pem)
+
+ def sign(self, payload):
+ return self.key.sign(
+ data=payload,
+ signature_algorithm=ec.ECDSA(SHA256()))
diff --git a/scripts/imgtool/keys/ecdsa_test.py b/scripts/imgtool/keys/ecdsa_test.py
new file mode 100644
index 0000000..326d73f
--- /dev/null
+++ b/scripts/imgtool/keys/ecdsa_test.py
@@ -0,0 +1,99 @@
+"""
+Tests for ECDSA keys
+"""
+
+import io
+import os.path
+import sys
+import tempfile
+import unittest
+
+from cryptography.exceptions import InvalidSignature
+from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.primitives.hashes import SHA256
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
+
+from imgtool.keys import load, ECDSA256P1, ECDSAUsageError
+
+class EcKeyGeneration(unittest.TestCase):
+
+ def setUp(self):
+ self.test_dir = tempfile.TemporaryDirectory()
+
+ def tname(self, base):
+ return os.path.join(self.test_dir.name, base)
+
+ def tearDown(self):
+ self.test_dir.cleanup()
+
+ def test_keygen(self):
+ name1 = self.tname("keygen.pem")
+ k = ECDSA256P1.generate()
+ k.export_private(name1, b'secret')
+
+ self.assertIsNone(load(name1))
+
+ k2 = load(name1, b'secret')
+
+ pubname = self.tname('keygen-pub.pem')
+ k2.export_public(pubname)
+ pk2 = load(pubname)
+
+ # We should be able to export the public key from the loaded
+ # public key, but not the private key.
+ pk2.export_public(self.tname('keygen-pub2.pem'))
+ self.assertRaises(ECDSAUsageError,
+ pk2.export_private, self.tname('keygen-priv2.pem'))
+
+ def test_emit(self):
+ """Basic sanity check on the code emitters."""
+ k = ECDSA256P1.generate()
+
+ ccode = io.StringIO()
+ k.emit_c(ccode)
+ self.assertIn("ecdsa_pub_key", ccode.getvalue())
+ self.assertIn("ecdsa_pub_key_len", ccode.getvalue())
+
+ rustcode = io.StringIO()
+ k.emit_rust(rustcode)
+ self.assertIn("ECDSA_PUB_KEY", rustcode.getvalue())
+
+ def test_emit_pub(self):
+ """Basic sanity check on the code emitters."""
+ pubname = self.tname("public.pem")
+ k = ECDSA256P1.generate()
+ k.export_public(pubname)
+
+ k2 = load(pubname)
+
+ ccode = io.StringIO()
+ k2.emit_c(ccode)
+ self.assertIn("ecdsa_pub_key", ccode.getvalue())
+ self.assertIn("ecdsa_pub_key_len", ccode.getvalue())
+
+ rustcode = io.StringIO()
+ k2.emit_rust(rustcode)
+ self.assertIn("ECDSA_PUB_KEY", rustcode.getvalue())
+
+ def test_sig(self):
+ k = ECDSA256P1.generate()
+ buf = b'This is the message'
+ sig = k.sign(buf)
+
+ # The code doesn't have any verification, so verify this
+ # manually.
+ k.key.public_key().verify(
+ signature=sig,
+ data=buf,
+ signature_algorithm=ec.ECDSA(SHA256()))
+
+ # Modify the message to make sure the signature fails.
+ self.assertRaises(InvalidSignature,
+ k.key.public_key().verify,
+ signature=sig,
+ data=b'This is thE message',
+ signature_algorithm=ec.ECDSA(SHA256()))
+
+if __name__ == '__main__':
+ unittest.main()