From d978421db9e90051fa988211e17a81751b28eab3 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Thu, 20 Dec 2018 15:29:48 +1100 Subject: [PATCH] espsecure: Add support and tests for using verify_signature with binary public key file Previously, keyfile could only be in PEM format Closes https://github.com/espressif/esptool/issues/357 --- espsecure.py | 20 ++-- test/ecdsa_secure_boot_signing_key2.pem | 5 + .../ecdsa_secure_boot_signing_key2.pem | 5 + .../ecdsa_secure_boot_signing_pubkey.pem | 4 + .../ecdsa_secure_boot_signing_pubkey2.pem | 4 + test/test_espsecure.py | 97 +++++++++++++++---- 6 files changed, 111 insertions(+), 24 deletions(-) create mode 100644 test/ecdsa_secure_boot_signing_key2.pem create mode 100644 test/secure_images/ecdsa_secure_boot_signing_key2.pem create mode 100644 test/secure_images/ecdsa_secure_boot_signing_pubkey.pem create mode 100644 test/secure_images/ecdsa_secure_boot_signing_pubkey2.pem diff --git a/espsecure.py b/espsecure.py index 0880c4bc0..79f2fa317 100755 --- a/espsecure.py +++ b/espsecure.py @@ -170,11 +170,16 @@ def sign_data(args): def verify_signature(args): """ Verify a previously signed binary image, using the ECDSA public key """ key_data = args.keyfile.read() - try: - vk = ecdsa.VerifyingKey.from_pem(key_data) - except ecdsa.der.UnexpectedDER: + if b"-BEGIN EC PRIVATE KEY" in key_data: sk = ecdsa.SigningKey.from_pem(key_data) vk = sk.get_verifying_key() + elif b"-BEGIN PUBLIC KEY" in key_data: + vk = ecdsa.VerifyingKey.from_pem(key_data) + elif len(key_data) == 64: + vk = ecdsa.VerifyingKey.from_string(key_data, + curve=ecdsa.NIST256p) + else: + raise esptool.FatalError("Verification key does not appear to be an EC key in PEM format or binary EC public key data. Unsupported") if vk.curve != ecdsa.NIST256p: raise esptool.FatalError("Public key uses incorrect curve. ESP32 Secure Boot only supports NIST256p (openssl calls this curve 'prime256v1") @@ -363,19 +368,20 @@ def main(): p = subparsers.add_parser('verify_signature', help='Verify a data file previously signed by "sign_data", using the public key.') - p.add_argument('--keyfile', '-k', help="Public key file for verification. Can be the private key file (public key is embedded).", + p.add_argument('--keyfile', '-k', help="Public key file for verification. Can be private or public key in PEM format, " + + "or a binary public key produced by extract_public_key command.", type=argparse.FileType('rb'), required=True) p.add_argument('datafile', help="Signed data file to verify signature.", type=argparse.FileType('rb')) p = subparsers.add_parser('extract_public_key', help='Extract the public verification key for signatures, save it as a raw binary file.') - p.add_argument('--keyfile', '-k', help="Private key file to extract the public verification key from.", type=argparse.FileType('rb'), + p.add_argument('--keyfile', '-k', help="Private key file (PEM format) to extract the public verification key from.", type=argparse.FileType('rb'), required=True) - p.add_argument('public_keyfile', help="File to save new public key) into", type=argparse.FileType('wb')) + p.add_argument('public_keyfile', help="File to save new public key into", type=argparse.FileType('wb')) p = subparsers.add_parser('digest_private_key', help='Generate an SHA-256 digest of the private signing key. ' + 'This can be used as a reproducible secure bootloader or flash encryption key.') - p.add_argument('--keyfile', '-k', help="Private key file to generate a digest from.", type=argparse.FileType('rb'), + p.add_argument('--keyfile', '-k', help="Private key file (PEM format) to generate a digest from.", type=argparse.FileType('rb'), required=True) p.add_argument('--keylen', '-l', help="Length of private key digest file to generate (in bits).", choices=['192','256'], default='256') diff --git a/test/ecdsa_secure_boot_signing_key2.pem b/test/ecdsa_secure_boot_signing_key2.pem new file mode 100644 index 000000000..32f3e3633 --- /dev/null +++ b/test/ecdsa_secure_boot_signing_key2.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIINRYjKaXM0dQJn4FLwoHYj3ovmpRee7UWv8ksQ9IS2toAoGCCqGSM49 +AwEHoUQDQgAEYWCmqAxxAmbr8cZk4AjTYkQJ0pCZOsESk2bPAe6lrzHrFKKR2t2W +ge+cNvXU2dYswEdgWr/fdnyAbHHEVEBSrA== +-----END EC PRIVATE KEY----- diff --git a/test/secure_images/ecdsa_secure_boot_signing_key2.pem b/test/secure_images/ecdsa_secure_boot_signing_key2.pem new file mode 100644 index 000000000..51d73884c --- /dev/null +++ b/test/secure_images/ecdsa_secure_boot_signing_key2.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIP9iY7XY8g3qSaUkKwTbq6HEq/AwenIxrssLqXGTS0z3oAoGCCqGSM49 +AwEHoUQDQgAEG+Ah4OAejTBYKQNvJkEOP9AifgulBMr4E9f+OqRU1Uno9Efi1kMc +fzwZyx0A4mib0HfLUg9JNh8dNrUxLeVb4Q== +-----END EC PRIVATE KEY----- diff --git a/test/secure_images/ecdsa_secure_boot_signing_pubkey.pem b/test/secure_images/ecdsa_secure_boot_signing_pubkey.pem new file mode 100644 index 000000000..ec8e37ed1 --- /dev/null +++ b/test/secure_images/ecdsa_secure_boot_signing_pubkey.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENM762z/ushk+c0XOIYpi8wLSWuF5 +COnU0VAQnt3spTSX6l3bpwfuppsemDdwy+aKbdgeyMYDxFbROLOPTRbYJw== +-----END PUBLIC KEY----- diff --git a/test/secure_images/ecdsa_secure_boot_signing_pubkey2.pem b/test/secure_images/ecdsa_secure_boot_signing_pubkey2.pem new file mode 100644 index 000000000..15abeeb96 --- /dev/null +++ b/test/secure_images/ecdsa_secure_boot_signing_pubkey2.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEG+Ah4OAejTBYKQNvJkEOP9Aifgul +BMr4E9f+OqRU1Uno9Efi1kMcfzwZyx0A4mib0HfLUg9JNh8dNrUxLeVb4Q== +-----END PUBLIC KEY----- diff --git a/test/test_espsecure.py b/test/test_espsecure.py index 394d6c3da..d093f82e9 100755 --- a/test/test_espsecure.py +++ b/test/test_espsecure.py @@ -43,10 +43,20 @@ def run_espsecure(self, args): print(e.output) raise e -def _open(image_file): - return open(os.path.join('secure_images', image_file), 'rb') + def setUp(self): + self.cleanup_files = [] # keep a list of files _open()ed by each test case -class ESP32SecureBootloaderTests(unittest.TestCase): + def tearDown(self): + for f in self.cleanup_files: + f.close() + + def _open(self, image_file): + f = open(os.path.join('secure_images', image_file), 'rb') + self.cleanup_files.append(f) + return f + + +class ESP32SecureBootloaderTests(EspSecureTestCase): def test_digest_bootloader(self): DBArgs = namedtuple('digest_bootloader_args', [ @@ -59,19 +69,23 @@ def test_digest_bootloader(self): output_file = tempfile.NamedTemporaryFile(delete=False) output_file.close() - args = DBArgs(_open('256bit_key.bin'), + args = DBArgs(self._open('256bit_key.bin'), output_file.name, - _open('256bit_iv.bin'), - _open('bootloader.bin')) + self._open('256bit_iv.bin'), + self._open('bootloader.bin')) espsecure.digest_secure_bootloader(args) with open(output_file.name, 'rb') as of: - with _open('bootloader_digested.bin') as ef: + with self._open('bootloader_digested.bin') as ef: self.assertEqual(ef.read(), of.read()) finally: os.unlink(output_file.name) -class ECDSASigningTests(unittest.TestCase): +class ECDSASigningTests(EspSecureTestCase): + + VerifyArgs = namedtuple('verify_signature_args', [ + 'keyfile', + 'datafile' ]) def test_sign_data(self): SignArgs = namedtuple('sign_data_args', [ @@ -85,27 +99,76 @@ def test_sign_data(self): # Note: signing bootloader is not actually needed # for ESP32, it's just a handy file to sign - args = SignArgs(_open('ecdsa_secure_boot_signing_key.pem'), + args = SignArgs(self._open('ecdsa_secure_boot_signing_key.pem'), output_file.name, - _open('bootloader.bin')) + self._open('bootloader.bin')) espsecure.sign_data(args) with open(output_file.name, 'rb') as of: - with _open('bootloader_signed.bin') as ef: + with self._open('bootloader_signed.bin') as ef: self.assertEqual(ef.read(), of.read()) finally: os.unlink(output_file.name) - def test_verify_signature(self): - VerifyArgs = namedtuple('verify_signature_args', [ - 'keyfile', - 'datafile' ]) + def test_verify_signature_signing_key(self): + # correct key + args = self.VerifyArgs(self._open('ecdsa_secure_boot_signing_key.pem'), + self._open('bootloader_signed.bin')) + espsecure.verify_signature(args) - args = VerifyArgs(_open('ecdsa_secure_boot_signing_key.pem'), - _open('bootloader_signed.bin')) + # wrong key + args = self.VerifyArgs(self._open('ecdsa_secure_boot_signing_key2.pem'), + self._open('bootloader_signed.bin')) + with self.assertRaises(esptool.FatalError) as cm: + espsecure.verify_signature(args) + self.assertIn("Signature is not valid", str(cm.exception)) + + def test_verify_signature_public_key(self): + # correct key + args = self.VerifyArgs(self._open('ecdsa_secure_boot_signing_pubkey.pem'), + self._open('bootloader_signed.bin')) espsecure.verify_signature(args) + # wrong key + args = self.VerifyArgs(self._open('ecdsa_secure_boot_signing_pubkey2.pem'), + self._open('bootloader_signed.bin')) + with self.assertRaises(esptool.FatalError) as cm: + espsecure.verify_signature(args) + self.assertIn("Signature is not valid", str(cm.exception)) + + def test_extract_binary_public_key(self): + ExtractKeyArgs = namedtuple('extract_public_key_args', + [ 'keyfile', 'public_keyfile' ]) + + pub_keyfile = tempfile.NamedTemporaryFile(delete=False) + pub_keyfile2 = tempfile.NamedTemporaryFile(delete=False) + try: + args = ExtractKeyArgs(self._open('ecdsa_secure_boot_signing_key.pem'), + pub_keyfile) + espsecure.extract_public_key(args) + + args = ExtractKeyArgs(self._open('ecdsa_secure_boot_signing_key2.pem'), + pub_keyfile2) + espsecure.extract_public_key(args) + + pub_keyfile.seek(0) + pub_keyfile2.seek(0) + + # use correct extracted public key to verify + args = self.VerifyArgs(pub_keyfile, self._open('bootloader_signed.bin')) + espsecure.verify_signature(args) + + # use wrong extracted public key to try and verify + args = self.VerifyArgs(pub_keyfile2, self._open('bootloader_signed.bin')) + with self.assertRaises(esptool.FatalError) as cm: + espsecure.verify_signature(args) + self.assertIn("Signature is not valid", str(cm.exception)) + + finally: + os.unlink(pub_keyfile.name) + os.unlink(pub_keyfile2.name) + if __name__ == '__main__': print("Running espsecure tests...")