mirror of
https://github.com/espressif/esptool.git
synced 2025-10-14 19:28:40 +08:00
903 lines
34 KiB
Python
Executable File
903 lines
34 KiB
Python
Executable File
# Tests for espsecure using the pytest framework
|
|
#
|
|
# Assumes openssl binary is in the PATH
|
|
|
|
import binascii
|
|
import io
|
|
import os
|
|
import os.path
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
from conftest import need_to_install_package_err
|
|
|
|
import pytest
|
|
|
|
try:
|
|
import espsecure
|
|
except ImportError:
|
|
need_to_install_package_err()
|
|
|
|
TEST_DIR = os.path.abspath(os.path.dirname(__file__))
|
|
|
|
|
|
@pytest.mark.host_test
|
|
class EspSecureTestCase:
|
|
def run_espsecure(self, args):
|
|
"""
|
|
Run espsecure with the specified arguments
|
|
|
|
Returns output as a string if there is any,
|
|
raises an exception if espsecure fails
|
|
"""
|
|
cmd = [sys.executable, "-m", "espsecure"] + args.split(" ")
|
|
print("\nExecuting {}...".format(" ".join(cmd)))
|
|
|
|
try:
|
|
output = subprocess.check_output(
|
|
[str(s) for s in cmd], cwd=TEST_DIR, stderr=subprocess.STDOUT
|
|
)
|
|
output = output.decode("utf-8")
|
|
print(output)
|
|
return output
|
|
except subprocess.CalledProcessError as e:
|
|
print(e.output.decode("utf-8"))
|
|
raise e
|
|
|
|
@classmethod
|
|
def setup_class(self):
|
|
self.cleanup_files = [] # keep a list of files _open()ed by each test case
|
|
|
|
@classmethod
|
|
def teardown_class(self):
|
|
for f in self.cleanup_files:
|
|
f.close()
|
|
|
|
def _get_imagepath(self, image_file):
|
|
return os.path.join(TEST_DIR, "secure_images", image_file)
|
|
|
|
def _open(self, image_file):
|
|
f = open(self._get_imagepath(image_file), "rb")
|
|
self.cleanup_files.append(f)
|
|
return f
|
|
|
|
|
|
class TestESP32SecureBootloader(EspSecureTestCase):
|
|
def test_digest_bootloader(self):
|
|
try:
|
|
output_file = tempfile.NamedTemporaryFile(delete=False)
|
|
output_file.close()
|
|
|
|
self.run_espsecure(
|
|
f"digest-secure-bootloader "
|
|
f"--keyfile {self._get_imagepath('256bit_key.bin')} "
|
|
f"--output {output_file.name} "
|
|
f"--iv {self._get_imagepath('256bit_iv.bin')} "
|
|
f"{self._get_imagepath('bootloader.bin')}"
|
|
)
|
|
|
|
with open(output_file.name, "rb") as of:
|
|
with self._open("bootloader_digested.bin") as ef:
|
|
assert ef.read() == of.read()
|
|
finally:
|
|
os.unlink(output_file.name)
|
|
|
|
def test_digest_rsa_public_key(self):
|
|
try:
|
|
output_file = tempfile.NamedTemporaryFile(delete=False)
|
|
output_file.close()
|
|
|
|
out = self.run_espsecure(
|
|
"digest-rsa-public-key "
|
|
f"--keyfile {self._get_imagepath('rsa_secure_boot_signing_key.pem')} "
|
|
f"-o {output_file.name}"
|
|
)
|
|
assert (
|
|
"DeprecationWarning: The command 'digest-rsa-public-key' is deprecated."
|
|
in out
|
|
)
|
|
|
|
with open(output_file.name, "rb") as of:
|
|
with self._open("rsa_public_key_digest.bin") as ef:
|
|
assert ef.read() == of.read()
|
|
finally:
|
|
os.unlink(output_file.name)
|
|
|
|
def test_extract_public_key_v1(self):
|
|
"""Test that extract-public-key CLI command produces raw output for version 1"""
|
|
with tempfile.TemporaryDirectory() as keydir:
|
|
# Generate a version 1 ECDSA256 key
|
|
keyfile_name = os.path.join(keydir, "v1_key.pem")
|
|
self.run_espsecure(
|
|
f"generate-signing-key --version 1 --scheme ecdsa256 {keyfile_name}"
|
|
)
|
|
|
|
output_file = os.path.join(keydir, "v1_public_key.bin")
|
|
output = self.run_espsecure(
|
|
f"extract-public-key --version 1 --keyfile {keyfile_name} {output_file}"
|
|
)
|
|
|
|
# Check that the command succeeded
|
|
assert "public key extracted" in output.lower()
|
|
|
|
# Read the output file
|
|
with open(output_file, "rb") as f:
|
|
v1_output = f.read()
|
|
|
|
# Version 1 should produce raw binary (64 bytes for ECDSA256)
|
|
assert len(v1_output) == 64, (
|
|
f"Expected 64 bytes for ECDSA256, got {len(v1_output)}"
|
|
)
|
|
|
|
# Raw binary should not contain PEM markers
|
|
assert b"-----BEGIN PUBLIC KEY-----" not in v1_output
|
|
assert b"-----END PUBLIC KEY-----" not in v1_output
|
|
assert b"PUBLIC KEY" not in v1_output
|
|
|
|
# Raw binary should contain only binary data (not text)
|
|
printable_count = sum(1 for b in v1_output if 32 <= b <= 126)
|
|
assert printable_count < len(v1_output), (
|
|
"Raw binary should not be all printable ASCII"
|
|
)
|
|
|
|
def test_extract_public_key_v2(self):
|
|
"""Test that extract-public-key CLI command produces PEM output for version 2"""
|
|
with tempfile.TemporaryDirectory() as keydir:
|
|
# Generate a version 2 ECDSA256 key
|
|
keyfile_name = os.path.join(keydir, "v2_key.pem")
|
|
self.run_espsecure(
|
|
f"generate-signing-key --version 2 --scheme ecdsa256 {keyfile_name}"
|
|
)
|
|
|
|
output_file = os.path.join(keydir, "v2_public_key.pem")
|
|
output = self.run_espsecure(
|
|
f"extract-public-key --version 2 --keyfile {keyfile_name} {output_file}"
|
|
)
|
|
|
|
# Check that the command succeeded
|
|
assert "public key extracted" in output.lower()
|
|
|
|
# Read the output file
|
|
with open(output_file, "rb") as f:
|
|
v2_output = f.read()
|
|
|
|
# Version 2 should produce PEM format
|
|
assert b"-----BEGIN PUBLIC KEY-----" in v2_output
|
|
assert b"-----END PUBLIC KEY-----" in v2_output
|
|
assert b"PUBLIC KEY" in v2_output
|
|
|
|
# PEM format should be longer than raw binary
|
|
assert len(v2_output) > 64, "PEM format should be longer than raw binary"
|
|
|
|
# PEM format should be mostly printable ASCII
|
|
printable_count = sum(1 for b in v2_output if 32 <= b <= 126)
|
|
assert printable_count > len(v2_output) * 0.8, (
|
|
"PEM format should be mostly printable ASCII"
|
|
)
|
|
|
|
|
|
class TestSigning(EspSecureTestCase):
|
|
def test_key_generation_v1(self):
|
|
with tempfile.TemporaryDirectory() as keydir:
|
|
# keyfile cannot exist before generation -> tempfile.NamedTemporaryFile()
|
|
# cannot be used for keyfile
|
|
keyfile_name = os.path.join(keydir, "key.pem")
|
|
self.run_espsecure(f"generate-signing-key --version 1 {keyfile_name}")
|
|
|
|
@pytest.mark.parametrize("scheme", ["rsa3072", "ecdsa192", "ecdsa256", "ecdsa384"])
|
|
def test_key_generation_v2(self, scheme):
|
|
with tempfile.TemporaryDirectory() as keydir:
|
|
# keyfile cannot exist before generation -> tempfile.NamedTemporaryFile()
|
|
# cannot be used for keyfile
|
|
keyfile_name = os.path.join(keydir, "key.pem")
|
|
self.run_espsecure(
|
|
f"generate-signing-key --version 2 --scheme {scheme} {keyfile_name}"
|
|
)
|
|
|
|
def _test_sign_v1_data(self, key_name):
|
|
try:
|
|
output_file = tempfile.NamedTemporaryFile(delete=False)
|
|
output_file.close()
|
|
|
|
# Note: signing bootloader is not actually needed
|
|
# for ESP32, it's just a handy file to sign
|
|
self.run_espsecure(
|
|
f"sign-data --version 1 --keyfile {self._get_imagepath(key_name)} "
|
|
f"--output {output_file.name} {self._get_imagepath('bootloader.bin')}"
|
|
)
|
|
|
|
with open(output_file.name, "rb") as of:
|
|
with self._open("bootloader_signed.bin") as ef:
|
|
assert ef.read() == of.read()
|
|
|
|
finally:
|
|
os.unlink(output_file.name)
|
|
|
|
def test_sign_v1_data(self):
|
|
self._test_sign_v1_data("ecdsa256_secure_boot_signing_key.pem")
|
|
|
|
def test_sign_v1_data_pkcs8(self):
|
|
self._test_sign_v1_data("ecdsa256_secure_boot_signing_key_pkcs8.pem")
|
|
|
|
def test_sign_v1_with_pre_calculated_signature(self):
|
|
# Sign using pre-calculated signature + Verify
|
|
signing_pubkey = "ecdsa256_secure_boot_signing_pubkey.pem"
|
|
pre_calculated_signature = "pre_calculated_bootloader_signature.bin"
|
|
|
|
try:
|
|
output_file = tempfile.NamedTemporaryFile(delete=False)
|
|
self.run_espsecure(
|
|
f"sign-data --version 1 "
|
|
f"--pub-key {self._get_imagepath(signing_pubkey)} "
|
|
f"--signature {self._get_imagepath(pre_calculated_signature)} "
|
|
f"--output {output_file.name} "
|
|
f"{self._get_imagepath('bootloader.bin')}"
|
|
)
|
|
|
|
self.run_espsecure(
|
|
f"verify-signature --version 1 "
|
|
f"--keyfile {self._get_imagepath(signing_pubkey)} "
|
|
f"{output_file.name}"
|
|
)
|
|
finally:
|
|
output_file.close()
|
|
os.unlink(output_file.name)
|
|
|
|
@pytest.mark.parametrize("scheme", ["rsa", "ecdsa192", "ecdsa256", "ecdsa384"])
|
|
def test_sign_v2_data(self, scheme):
|
|
key = f"{scheme}_secure_boot_signing_key.pem"
|
|
try:
|
|
output_file = tempfile.NamedTemporaryFile(delete=False)
|
|
self.run_espsecure(
|
|
f"sign-data --version 2 "
|
|
f"--keyfile {self._get_imagepath(key)} "
|
|
f"--output {output_file.name} "
|
|
f"{self._get_imagepath('bootloader_unsigned_v2.bin')}"
|
|
)
|
|
|
|
self.run_espsecure(
|
|
f"verify-signature --version 2 "
|
|
f"--keyfile {self._get_imagepath(key)} "
|
|
f"{output_file.name}"
|
|
)
|
|
finally:
|
|
output_file.close()
|
|
os.unlink(output_file.name)
|
|
|
|
@pytest.mark.parametrize("scheme", ["rsa", "ecdsa192", "ecdsa256", "ecdsa384"])
|
|
def test_sign_v2_multiple_keys_cli(self, scheme):
|
|
keydir = os.path.join(TEST_DIR, "secure_images")
|
|
with tempfile.NamedTemporaryFile(delete=False) as output_file:
|
|
keyfiles = [
|
|
os.path.join(keydir, f"{scheme}_secure_boot_signing_key.pem"),
|
|
os.path.join(keydir, f"{scheme}_secure_boot_signing_key2.pem"),
|
|
os.path.join(keydir, f"{scheme}_secure_boot_signing_key3.pem"),
|
|
]
|
|
|
|
self.run_espsecure(
|
|
f"sign-data --version 2 --keyfile {' '.join(keyfiles)} "
|
|
f"--output {output_file.name} "
|
|
f"{os.path.join(keydir, 'bootloader_unsigned_v2.bin')}"
|
|
)
|
|
self.run_espsecure(
|
|
"verify-signature --version 2 --keyfile "
|
|
f"{os.path.join(keydir, f'{scheme}_secure_boot_signing_key.pem')} "
|
|
f"{output_file.name}"
|
|
)
|
|
self.run_espsecure(
|
|
"verify-signature --version 2 --keyfile "
|
|
f"{os.path.join(keydir, f'{scheme}_secure_boot_signing_key2.pem')} "
|
|
f"{output_file.name}"
|
|
)
|
|
self.run_espsecure(
|
|
"verify-signature --version 2 --keyfile "
|
|
f"{os.path.join(keydir, f'{scheme}_secure_boot_signing_key3.pem')} "
|
|
f"{output_file.name}"
|
|
)
|
|
|
|
@pytest.mark.parametrize("scheme", ["rsa", "ecdsa192", "ecdsa256", "ecdsa384"])
|
|
def test_sign_v2_multiple_keys(self, scheme):
|
|
# 3 keys + Verify with 3rd key
|
|
try:
|
|
output_file = tempfile.NamedTemporaryFile(delete=False)
|
|
keyfiles = [
|
|
self._get_imagepath(f"{scheme}_secure_boot_signing_key.pem"),
|
|
self._get_imagepath(f"{scheme}_secure_boot_signing_key2.pem"),
|
|
self._get_imagepath(f"{scheme}_secure_boot_signing_key3.pem"),
|
|
]
|
|
|
|
self.run_espsecure(
|
|
f"sign-data --version 2 "
|
|
f"--keyfile {' '.join(keyfiles)} "
|
|
f"--output {output_file.name} "
|
|
f"{self._get_imagepath('bootloader_unsigned_v2.bin')}"
|
|
)
|
|
|
|
self.run_espsecure(
|
|
f"verify-signature --version 2 "
|
|
f"--keyfile "
|
|
f"{self._get_imagepath(f'{scheme}_secure_boot_signing_key3.pem')} "
|
|
f"{output_file.name}"
|
|
)
|
|
|
|
self.run_espsecure(
|
|
f"verify-signature --version 2 "
|
|
f"--keyfile "
|
|
f"{self._get_imagepath(f'{scheme}_secure_boot_signing_key2.pem')} "
|
|
f"{output_file.name}"
|
|
)
|
|
|
|
self.run_espsecure(
|
|
f"verify-signature --version 2 "
|
|
f"--keyfile "
|
|
f"{self._get_imagepath(f'{scheme}_secure_boot_signing_key.pem')} "
|
|
f"{output_file.name}"
|
|
)
|
|
finally:
|
|
output_file.close()
|
|
os.unlink(output_file.name)
|
|
|
|
@pytest.mark.parametrize("scheme", ["rsa", "ecdsa192", "ecdsa256", "ecdsa384"])
|
|
def test_sign_v2_append_signatures(self, scheme):
|
|
# Append signatures + Verify with an appended key
|
|
# (bootloader_signed_v2_rsa.bin already signed with
|
|
# rsa_secure_boot_signing_key.pem)
|
|
try:
|
|
output_file = tempfile.NamedTemporaryFile(delete=False)
|
|
keyfiles = [
|
|
self._get_imagepath(f"{scheme}_secure_boot_signing_key2.pem"),
|
|
self._get_imagepath(f"{scheme}_secure_boot_signing_key3.pem"),
|
|
]
|
|
|
|
self.run_espsecure(
|
|
f"sign-data --version 2 --append-signatures "
|
|
f"--keyfile {' '.join(keyfiles)} "
|
|
f"--output {output_file.name} "
|
|
f"{self._get_imagepath(f'bootloader_signed_v2_{scheme}.bin')}"
|
|
)
|
|
|
|
self.run_espsecure(
|
|
f"verify-signature --version 2 "
|
|
f"--keyfile "
|
|
f"{self._get_imagepath(f'{scheme}_secure_boot_signing_key.pem')} "
|
|
f"{output_file.name}"
|
|
)
|
|
|
|
self.run_espsecure(
|
|
f"verify-signature --version 2 "
|
|
f"--keyfile "
|
|
f"{self._get_imagepath(f'{scheme}_secure_boot_signing_key2.pem')} "
|
|
f"{output_file.name}"
|
|
)
|
|
|
|
self.run_espsecure(
|
|
f"verify-signature --version 2 "
|
|
f"--keyfile "
|
|
f"{self._get_imagepath(f'{scheme}_secure_boot_signing_key3.pem')} "
|
|
f"{output_file.name}"
|
|
)
|
|
finally:
|
|
output_file.close()
|
|
os.unlink(output_file.name)
|
|
|
|
@pytest.mark.parametrize("scheme", ["rsa", "ecdsa192", "ecdsa256", "ecdsa384"])
|
|
def test_sign_v2_append_signatures_multiple_steps(self, scheme):
|
|
# similar to previous test, but sign in two invocations
|
|
try:
|
|
output_file1 = tempfile.NamedTemporaryFile(delete=False)
|
|
output_file2 = tempfile.NamedTemporaryFile(delete=False)
|
|
output_file1.close()
|
|
output_file2.close()
|
|
|
|
self.run_espsecure(
|
|
f"sign-data --version 2 --append-signatures "
|
|
f"--keyfile "
|
|
f"{self._get_imagepath(f'{scheme}_secure_boot_signing_key2.pem')} "
|
|
f"--output {output_file1.name} "
|
|
f"{self._get_imagepath(f'bootloader_signed_v2_{scheme}.bin')}"
|
|
)
|
|
|
|
self.run_espsecure(
|
|
f"sign-data --version 2 --append-signatures "
|
|
f"--keyfile "
|
|
f"{self._get_imagepath(f'{scheme}_secure_boot_signing_key3.pem')} "
|
|
f"--output {output_file2.name} {output_file1.name}"
|
|
)
|
|
|
|
self.run_espsecure(
|
|
f"verify-signature --version 2 "
|
|
f"--keyfile "
|
|
f"{self._get_imagepath(f'{scheme}_secure_boot_signing_key.pem')} "
|
|
f"{output_file2.name}"
|
|
)
|
|
|
|
self.run_espsecure(
|
|
f"verify-signature --version 2 "
|
|
f"--keyfile "
|
|
f"{self._get_imagepath(f'{scheme}_secure_boot_signing_key2.pem')} "
|
|
f"{output_file2.name}"
|
|
)
|
|
|
|
self.run_espsecure(
|
|
f"verify-signature --version 2 "
|
|
f"--keyfile "
|
|
f"{self._get_imagepath(f'{scheme}_secure_boot_signing_key3.pem')} "
|
|
f"{output_file2.name}"
|
|
)
|
|
finally:
|
|
output_file1.close()
|
|
os.unlink(output_file1.name)
|
|
output_file2.close()
|
|
os.unlink(output_file2.name)
|
|
|
|
@pytest.mark.parametrize("scheme", ["rsa", "ecdsa192", "ecdsa256", "ecdsa384"])
|
|
def test_sign_v2_with_pre_calculated_signature(self, scheme):
|
|
# Sign using pre-calculated signature + Verify
|
|
pub_key = f"{scheme}_secure_boot_signing_pubkey.pem"
|
|
signature = f"pre_calculated_bootloader_signature_{scheme}.bin"
|
|
try:
|
|
output_file = tempfile.NamedTemporaryFile(delete=False)
|
|
output_file.close()
|
|
|
|
self.run_espsecure(
|
|
f"sign-data --version 2 "
|
|
f"--pub-key {self._get_imagepath(pub_key)} "
|
|
f"--signature {self._get_imagepath(signature)} "
|
|
f"--output {output_file.name} "
|
|
f"{self._get_imagepath('bootloader_unsigned_v2.bin')}"
|
|
)
|
|
|
|
self.run_espsecure(
|
|
f"verify-signature --version 2 "
|
|
f"--keyfile {self._get_imagepath(pub_key)} "
|
|
f"{output_file.name}"
|
|
)
|
|
finally:
|
|
output_file.close()
|
|
os.unlink(output_file.name)
|
|
|
|
@pytest.mark.parametrize("scheme", ["rsa", "ecdsa192", "ecdsa256", "ecdsa384"])
|
|
def test_sign_v2_with_multiple_pre_calculated_signatures(self, scheme):
|
|
# Sign using multiple pre-calculated signatures + Verify
|
|
signing_pubkeys = [
|
|
f"{scheme}_secure_boot_signing_pubkey.pem",
|
|
f"{scheme}_secure_boot_signing_pubkey.pem",
|
|
f"{scheme}_secure_boot_signing_pubkey.pem",
|
|
]
|
|
pre_calculated_signatures = [
|
|
f"pre_calculated_bootloader_signature_{scheme}.bin",
|
|
f"pre_calculated_bootloader_signature_{scheme}.bin",
|
|
f"pre_calculated_bootloader_signature_{scheme}.bin",
|
|
]
|
|
try:
|
|
output_file = tempfile.NamedTemporaryFile(delete=False)
|
|
output_file.close()
|
|
|
|
pubkey_args = " ".join(
|
|
f"--pub-key {self._get_imagepath(pub_key)}"
|
|
for pub_key in signing_pubkeys
|
|
)
|
|
signature_args = " ".join(
|
|
f"--signature {self._get_imagepath(signature)}"
|
|
for signature in pre_calculated_signatures
|
|
)
|
|
|
|
self.run_espsecure(
|
|
f"sign-data --version 2 {pubkey_args} {signature_args} "
|
|
f"--output {output_file.name} "
|
|
f"{self._get_imagepath('bootloader_unsigned_v2.bin')}"
|
|
)
|
|
|
|
self.run_espsecure(
|
|
f"verify-signature --version 2 "
|
|
f"--keyfile {self._get_imagepath(signing_pubkeys[0])} "
|
|
f"{output_file.name}"
|
|
)
|
|
finally:
|
|
output_file.close()
|
|
os.unlink(output_file.name)
|
|
|
|
@pytest.mark.parametrize(
|
|
"version, keyfile, datafile",
|
|
[
|
|
("1", "ecdsa256_secure_boot_signing_key.pem", "bootloader_signed.bin"),
|
|
(
|
|
"1",
|
|
"ecdsa256_secure_boot_signing_pubkey_raw.bin",
|
|
"bootloader_signed.bin",
|
|
),
|
|
("2", "rsa_secure_boot_signing_key.pem", "bootloader_signed_v2_rsa.bin"),
|
|
(
|
|
"2",
|
|
"ecdsa384_secure_boot_signing_key.pem",
|
|
"bootloader_signed_v2_ecdsa384.bin",
|
|
),
|
|
(
|
|
"2",
|
|
"ecdsa256_secure_boot_signing_key.pem",
|
|
"bootloader_signed_v2_ecdsa256.bin",
|
|
),
|
|
(
|
|
"2",
|
|
"ecdsa192_secure_boot_signing_key.pem",
|
|
"bootloader_signed_v2_ecdsa192.bin",
|
|
),
|
|
],
|
|
ids=[
|
|
"v1_pem",
|
|
"v1_raw",
|
|
"v2_rsa",
|
|
"v2_ecdsa384",
|
|
"v2_ecdsa256",
|
|
"v2_ecdsa192",
|
|
],
|
|
)
|
|
def test_verify_signature_correct_key(self, version, keyfile, datafile):
|
|
self.run_espsecure(
|
|
f"verify-signature --version {version} "
|
|
f"--keyfile {self._get_imagepath(keyfile)} "
|
|
f"{self._get_imagepath(datafile)}"
|
|
)
|
|
|
|
def test_verify_signature_wrong_key_v1(self):
|
|
with pytest.raises(subprocess.CalledProcessError) as cm:
|
|
self.run_espsecure(
|
|
f"verify-signature --version 1 "
|
|
f"--keyfile "
|
|
f"{self._get_imagepath('ecdsa256_secure_boot_signing_key2.pem')} "
|
|
f"{self._get_imagepath('bootloader_signed.bin')}"
|
|
)
|
|
assert "Signature is not valid" in cm.value.output.decode("utf-8")
|
|
|
|
@pytest.mark.parametrize("scheme", ["rsa", "ecdsa192", "ecdsa256", "ecdsa384"])
|
|
def test_verify_signature_wrong_key_v2(self, scheme):
|
|
with pytest.raises(subprocess.CalledProcessError) as cm:
|
|
self.run_espsecure(
|
|
f"verify-signature --version 2 "
|
|
f"--keyfile "
|
|
f"{self._get_imagepath(f'{scheme}_secure_boot_signing_key2.pem')} "
|
|
f"{self._get_imagepath(f'bootloader_signed_v2_{scheme}.bin')}"
|
|
)
|
|
assert (
|
|
"Signature could not be verified with the provided key."
|
|
in cm.value.output.decode("utf-8")
|
|
)
|
|
|
|
def test_verify_signature_wrong_scheme(self):
|
|
with pytest.raises(subprocess.CalledProcessError) as cm:
|
|
self.run_espsecure(
|
|
f"verify-signature --version 2 "
|
|
f"--keyfile "
|
|
f"{self._get_imagepath('ecdsa256_secure_boot_signing_key.pem')} "
|
|
f"{self._get_imagepath('bootloader_signed.bin')}"
|
|
)
|
|
assert "Invalid datafile" in cm.value.output.decode("utf-8")
|
|
|
|
@pytest.mark.parametrize("scheme", ["rsa", "ecdsa192", "ecdsa256", "ecdsa384"])
|
|
def test_verify_signature_multi_signed_wrong_key(self, scheme):
|
|
with pytest.raises(subprocess.CalledProcessError) as cm:
|
|
self.run_espsecure(
|
|
f"verify-signature --version 2 "
|
|
f"--keyfile "
|
|
f"{self._get_imagepath(f'{scheme}_secure_boot_signing_key4.pem')} "
|
|
f"{self._get_imagepath(f'bootloader_multi_signed_v2_{scheme}.bin')}"
|
|
)
|
|
assert (
|
|
"Signature could not be verified with the provided key."
|
|
in cm.value.output.decode("utf-8")
|
|
)
|
|
|
|
@pytest.mark.parametrize(
|
|
"version, keyfile, datafile",
|
|
[
|
|
("1", "ecdsa256_secure_boot_signing_pubkey.pem", "bootloader_signed.bin"),
|
|
("2", "rsa_secure_boot_signing_pubkey.pem", "bootloader_signed_v2_rsa.bin"),
|
|
(
|
|
"2",
|
|
"ecdsa384_secure_boot_signing_pubkey.pem",
|
|
"bootloader_signed_v2_ecdsa384.bin",
|
|
),
|
|
(
|
|
"2",
|
|
"ecdsa256_secure_boot_signing_pubkey.pem",
|
|
"bootloader_signed_v2_ecdsa256.bin",
|
|
),
|
|
(
|
|
"2",
|
|
"ecdsa192_secure_boot_signing_pubkey.pem",
|
|
"bootloader_signed_v2_ecdsa192.bin",
|
|
),
|
|
],
|
|
ids=["v1", "v2_rsa", "v2_ecdsa384", "v2_ecdsa256", "v2_ecdsa192"],
|
|
)
|
|
def test_verify_signature_correct_pubkey(self, version, keyfile, datafile):
|
|
self.run_espsecure(
|
|
f"verify-signature --version {version} "
|
|
f"--keyfile {self._get_imagepath(keyfile)} "
|
|
f"{self._get_imagepath(datafile)}"
|
|
)
|
|
|
|
def test_verify_signature_wrong_pubkey_v1(self):
|
|
with pytest.raises(subprocess.CalledProcessError) as cm:
|
|
self.run_espsecure(
|
|
f"verify-signature --version 1 "
|
|
f"--keyfile "
|
|
f"{self._get_imagepath('ecdsa256_secure_boot_signing_pubkey2.pem')} "
|
|
f"{self._get_imagepath('bootloader_signed.bin')}"
|
|
)
|
|
assert "Signature is not valid" in cm.value.output.decode("utf-8")
|
|
|
|
@pytest.mark.parametrize("scheme", ["rsa", "ecdsa192", "ecdsa256", "ecdsa384"])
|
|
def test_verify_signature_wrong_pubkey_v2(self, scheme):
|
|
with pytest.raises(subprocess.CalledProcessError) as cm:
|
|
self.run_espsecure(
|
|
f"verify-signature --version 2 "
|
|
f"--keyfile "
|
|
f"{self._get_imagepath(f'{scheme}_secure_boot_signing_pubkey2.pem')} "
|
|
f"{self._get_imagepath(f'bootloader_signed_v2_{scheme}.bin')}"
|
|
)
|
|
assert (
|
|
"Signature could not be verified with the provided key."
|
|
in cm.value.output.decode("utf-8")
|
|
)
|
|
|
|
@pytest.mark.parametrize("scheme", ["rsa", "ecdsa192", "ecdsa256", "ecdsa384"])
|
|
def test_verify_signature_multi_signed_wrong_pubkey(self, scheme):
|
|
with pytest.raises(subprocess.CalledProcessError) as cm:
|
|
self.run_espsecure(
|
|
f"verify-signature --version 2 "
|
|
f"--keyfile "
|
|
f"{self._get_imagepath(f'{scheme}_secure_boot_signing_pubkey4.pem')} "
|
|
f"{self._get_imagepath(f'bootloader_multi_signed_v2_{scheme}.bin')}"
|
|
)
|
|
assert (
|
|
"Signature could not be verified with the provided key."
|
|
in cm.value.output.decode("utf-8")
|
|
)
|
|
|
|
def test_extract_binary_public_key(self):
|
|
with tempfile.TemporaryDirectory() as keydir:
|
|
pub_keyfile_path = os.path.join(keydir, "pubkey1.bin")
|
|
pub_keyfile2_path = os.path.join(keydir, "pubkey2.bin")
|
|
|
|
self.run_espsecure(
|
|
f"extract-public-key --version 1 "
|
|
f"--keyfile "
|
|
f"{self._get_imagepath('ecdsa256_secure_boot_signing_key.pem')} "
|
|
f"{pub_keyfile_path}"
|
|
)
|
|
|
|
self.run_espsecure(
|
|
f"extract-public-key --version 1 "
|
|
f"--keyfile "
|
|
f"{self._get_imagepath('ecdsa256_secure_boot_signing_key2.pem')} "
|
|
f"{pub_keyfile2_path}"
|
|
)
|
|
|
|
# use correct extracted public key to verify
|
|
self.run_espsecure(
|
|
f"verify-signature --version 1 --keyfile {pub_keyfile_path} "
|
|
f"{self._get_imagepath('bootloader_signed.bin')}"
|
|
)
|
|
|
|
# use wrong extracted public key to try and verify
|
|
with pytest.raises(subprocess.CalledProcessError) as cm:
|
|
self.run_espsecure(
|
|
f"verify-signature --version 1 --keyfile {pub_keyfile2_path} "
|
|
f"{self._get_imagepath('bootloader_signed.bin')}"
|
|
)
|
|
assert "Signature is not valid" in cm.value.output.decode("utf-8")
|
|
|
|
@pytest.mark.parametrize("scheme", ["rsa3072", "ecdsa192", "ecdsa256", "ecdsa384"])
|
|
def test_generate_and_extract_key_v2(self, scheme):
|
|
with tempfile.TemporaryDirectory() as keydir:
|
|
# keyfile cannot exist before generation -> tempfile.NamedTemporaryFile()
|
|
# cannot be used for keyfile
|
|
keyfile_name = os.path.join(keydir, "key.pem")
|
|
pub_keyfile_name = os.path.join(keydir, "pubkey.pem")
|
|
|
|
self.run_espsecure(
|
|
f"generate-signing-key --version 2 --scheme {scheme} {keyfile_name}"
|
|
)
|
|
|
|
self.run_espsecure(
|
|
f"extract-public-key --version 2 "
|
|
f"--keyfile {keyfile_name} {pub_keyfile_name}"
|
|
)
|
|
|
|
|
|
class TestFlashEncryption(EspSecureTestCase):
|
|
def _test_encrypt_decrypt(
|
|
self,
|
|
input_plaintext,
|
|
expected_ciphertext,
|
|
key_path,
|
|
offset,
|
|
flash_crypt_conf=0xF,
|
|
aes_xts=None,
|
|
):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
ciphertext_path = os.path.join(tmpdir, "ciphertext.bin")
|
|
plaintext_path = os.path.join(tmpdir, "plaintext.bin")
|
|
|
|
# Build encrypt command
|
|
encrypt_cmd = (
|
|
f"encrypt-flash-data --keyfile {self._get_imagepath(key_path)} "
|
|
f"--output {ciphertext_path} --address {offset}"
|
|
)
|
|
if flash_crypt_conf is not None:
|
|
encrypt_cmd += f" --flash-crypt-conf {flash_crypt_conf}"
|
|
if aes_xts:
|
|
encrypt_cmd += " --aes-xts"
|
|
encrypt_cmd += f" {self._get_imagepath(input_plaintext)}"
|
|
|
|
self.run_espsecure(encrypt_cmd)
|
|
|
|
# Verify encrypted content differs from original and matches expected
|
|
with open(self._get_imagepath(input_plaintext), "rb") as orig:
|
|
original_data = orig.read()
|
|
with open(ciphertext_path, "rb") as cipher:
|
|
cipher_data = cipher.read()
|
|
with self._open(expected_ciphertext) as expected:
|
|
expected_data = expected.read()
|
|
|
|
assert original_data != cipher_data
|
|
assert cipher_data == expected_data
|
|
|
|
# Build decrypt command
|
|
decrypt_cmd = (
|
|
f"decrypt-flash-data --keyfile {self._get_imagepath(key_path)} "
|
|
f"--output {plaintext_path} --address {offset}"
|
|
)
|
|
if flash_crypt_conf is not None:
|
|
decrypt_cmd += f" --flash-crypt-conf {flash_crypt_conf}"
|
|
if aes_xts:
|
|
decrypt_cmd += " --aes-xts"
|
|
decrypt_cmd += f" {ciphertext_path}"
|
|
|
|
self.run_espsecure(decrypt_cmd)
|
|
|
|
# Verify decrypted content matches original
|
|
with open(plaintext_path, "rb") as decrypted:
|
|
decrypted_data = decrypted.read()
|
|
assert original_data == decrypted_data
|
|
|
|
|
|
class TestESP32FlashEncryption(TestFlashEncryption):
|
|
def test_encrypt_decrypt_bootloader(self):
|
|
self._test_encrypt_decrypt(
|
|
"bootloader.bin", "bootloader-encrypted.bin", "256bit_key.bin", 0x1000, 0xF
|
|
)
|
|
|
|
def test_encrypt_decrypt_app(self):
|
|
self._test_encrypt_decrypt(
|
|
"hello-world-signed.bin",
|
|
"hello-world-signed-encrypted.bin",
|
|
"ef-flashencryption-key.bin",
|
|
0x20000,
|
|
0xF,
|
|
)
|
|
|
|
def test_encrypt_decrypt_non_default_conf(self):
|
|
"""Try some non-default (non-recommended) flash_crypt_conf settings"""
|
|
for conf in [0x0, 0x3, 0x9, 0xC]:
|
|
self._test_encrypt_decrypt(
|
|
"bootloader.bin",
|
|
f"bootloader-encrypted-conf{conf:x}.bin",
|
|
"256bit_key.bin",
|
|
0x1000,
|
|
conf,
|
|
)
|
|
|
|
|
|
class TestAesXtsFlashEncryption(TestFlashEncryption):
|
|
def test_encrypt_decrypt_bootloader(self):
|
|
self._test_encrypt_decrypt(
|
|
"bootloader.bin",
|
|
"bootloader-encrypted-aes-xts.bin",
|
|
"256bit_key.bin",
|
|
0x1000,
|
|
aes_xts=True,
|
|
)
|
|
|
|
def test_encrypt_decrypt_app(self):
|
|
self._test_encrypt_decrypt(
|
|
"hello-world-signed.bin",
|
|
"hello-world-signed-encrypted-aes-xts.bin",
|
|
"ef-flashencryption-key.bin",
|
|
0x20000,
|
|
aes_xts=True,
|
|
)
|
|
|
|
def test_encrypt_decrypt_app_512_bit_key(self):
|
|
self._test_encrypt_decrypt(
|
|
"hello-world-signed.bin",
|
|
"hello-world-signed-encrypted-aes-xts-256.bin",
|
|
"512bit_key.bin",
|
|
0x10000,
|
|
aes_xts=True,
|
|
)
|
|
|
|
def test_padding(self):
|
|
# Random 2048 bits hex string
|
|
plaintext = binascii.unhexlify(
|
|
"c33b7c49f12a969a9bb45af5f660b73f"
|
|
"3b372685012da570df1cf99d1a82eabb"
|
|
"fdf6aa16b9675bd8a2f95e871513e175"
|
|
"3bc89f57986ecfb2707a3d3b59a46968"
|
|
"5e6609d2e9c21d4b2310571175e6e3de"
|
|
"2656ee22243f557b925ef39ff782ab56"
|
|
"f821e6859ee852000daae7c03a7c77ce"
|
|
"58744f15fbdf0ad4ae6e964aedd6316a"
|
|
"cf0e36935eef895cd14a60fe682fb971"
|
|
"eb239eae38b770bdf969017c9decfd91"
|
|
"b7c60329fb0c896684f0e7415f99dec1"
|
|
"da0572fac360a3e6d7219973a7de07e5"
|
|
"33b5abfdf5917ed5bfe54d660a6f5047"
|
|
"32fdb8d07259bfcdc67da87293857c11"
|
|
"427b2bae5f00da4a4b2b00b588ff5109"
|
|
"4c41f07f02f680f8826841b43da3f25b"
|
|
)
|
|
|
|
plaintext_file = io.BytesIO(plaintext)
|
|
ciphertext_full_block = io.BytesIO()
|
|
|
|
keyfile = self._open("256bit_key.bin")
|
|
address = 0x1000
|
|
|
|
espsecure.encrypt_flash_data(
|
|
keyfile, ciphertext_full_block, address, None, "aes_xts", plaintext_file
|
|
)
|
|
|
|
# Test with different number of bytes per encryption call
|
|
# Final ciphertext should still be the same if padding is done correctly
|
|
bytes_per_encrypt = [16, 32, 64, 128]
|
|
|
|
for b in bytes_per_encrypt:
|
|
ciphertext = io.BytesIO()
|
|
num_enc_calls = len(plaintext) // b
|
|
|
|
for i in range(0, num_enc_calls):
|
|
keyfile.seek(0)
|
|
offset = b * i
|
|
|
|
# encrypt the whole plaintext a substring of b bytes at a time
|
|
plaintext_sub = io.BytesIO(plaintext[offset : offset + b])
|
|
|
|
espsecure.encrypt_flash_data(
|
|
keyfile,
|
|
ciphertext,
|
|
address + offset,
|
|
None,
|
|
"aes_xts",
|
|
plaintext_sub,
|
|
)
|
|
|
|
assert ciphertext_full_block.getvalue() == ciphertext.getvalue()
|
|
|
|
|
|
class TestDigest(EspSecureTestCase):
|
|
def test_digest_private_key(self):
|
|
with tempfile.NamedTemporaryFile() as f:
|
|
outfile_name = f.name
|
|
|
|
self.run_espsecure(
|
|
"digest-private-key "
|
|
f"--keyfile {self._get_imagepath('ecdsa256_secure_boot_signing_key.pem')} "
|
|
f"{outfile_name}"
|
|
)
|
|
|
|
with open(outfile_name, "rb") as f:
|
|
assert f.read() == binascii.unhexlify(
|
|
"7b7b53708fc89d5e0b2df2571fb8f9d778f61a422ff1101a22159c4b34aad0aa"
|
|
)
|
|
|
|
def test_digest_private_key_with_invalid_output(self, capsys):
|
|
fname = self._get_imagepath("ecdsa256_secure_boot_signing_key.pem")
|
|
|
|
with pytest.raises(subprocess.CalledProcessError):
|
|
self.run_espsecure(f"digest-private-key --keyfile {fname} {fname}")
|
|
output = capsys.readouterr().out
|
|
assert "should not be the same!" in output
|