mirror of
https://github.com/espressif/esp-idf.git
synced 2024-10-05 20:47:46 -04:00
79b21fb624
Added test to verify exporting of ECDSA public key Added test to verify XTS_AES in random mode Added pytest test case for testing ECDH0 mode for XTS_128 and XTS_256 key Add test for ECDSA key in ECDH0 mode Update the key manager hal based tests Update key manager tests to add ECDH0 workflow
169 lines
7.8 KiB
Python
169 lines
7.8 KiB
Python
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
|
# SPDX-License-Identifier: CC0-1.0
|
|
import binascii
|
|
import os
|
|
import subprocess
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from cryptography import exceptions
|
|
from cryptography.hazmat.backends import default_backend
|
|
from cryptography.hazmat.primitives import hashes
|
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
from cryptography.hazmat.primitives.asymmetric import utils
|
|
from cryptography.hazmat.primitives.asymmetric.ec import SECP256R1
|
|
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
|
from ecdsa import NIST256p
|
|
from ecdsa.ellipticcurve import Point
|
|
from pytest_embedded import Dut
|
|
|
|
|
|
def load_ecdsa_key(filename: str) -> SECP256R1:
|
|
with open(filename, 'rb') as key_file:
|
|
return load_pem_private_key(key_file.read(), password=None, backend=default_backend())
|
|
|
|
|
|
def test_xts_aes_encryption(negotiated_key: bytes, plaintext_data: bytes, encrypted_data: bytes) -> None:
|
|
with open('test/negotiated_key.bin', 'wb+') as key_file:
|
|
key_file.write(negotiated_key)
|
|
|
|
with open('test/plaintext.bin', 'wb+') as plaintext_file:
|
|
plaintext_file.write(plaintext_data)
|
|
|
|
command = [
|
|
'espsecure.py',
|
|
'encrypt_flash_data',
|
|
'--aes_xts',
|
|
'--keyfile', 'test/negotiated_key.bin',
|
|
'--address', '0x120000',
|
|
'--output', 'test/enc-data.bin',
|
|
'test/plaintext.bin'
|
|
]
|
|
result = subprocess.run(command, capture_output=True, text=True)
|
|
assert result.returncode == 0, f'Command failed with error: {result.stderr}'
|
|
|
|
with open('test/enc-data.bin', 'rb') as enc_file:
|
|
calculated_enc_data = enc_file.read()
|
|
|
|
assert calculated_enc_data == encrypted_data, 'Calculated data does not match encrypted data obtained from firmware'
|
|
|
|
|
|
def calculate_key_manager_ecdh0_negotiated_key(k2_G_hex: str, k1_ecdsa_key: str) -> Any:
|
|
k2_G_bytes_le = binascii.unhexlify(k2_G_hex)
|
|
|
|
k2_G_bytes_x_be = bytes(reversed(k2_G_bytes_le[:32]))
|
|
k2_G_bytes_y_be = bytes(reversed(k2_G_bytes_le[32:]))
|
|
|
|
k2_G_bytes_be = k2_G_bytes_x_be + k2_G_bytes_y_be
|
|
|
|
curve = NIST256p.curve
|
|
k2_G = Point.from_bytes(curve, k2_G_bytes_be)
|
|
|
|
# Load the ECDSA private key (k1)
|
|
k1_key = load_ecdsa_key(k1_ecdsa_key)
|
|
k1_int = k1_key.private_numbers().private_value
|
|
|
|
# Convert the integer to bytes in big endian format
|
|
k1_bytes_big_endian = k1_int.to_bytes((k1_int.bit_length() + 7) // 8, byteorder='big')
|
|
|
|
# Reverse the bytes to get little endian format
|
|
k1_bytes_little_endian = k1_bytes_big_endian[::-1]
|
|
|
|
k1_int = int.from_bytes(k1_bytes_little_endian, byteorder='little')
|
|
|
|
# Calculate k1*k2*G
|
|
k1_k2_G = k1_int * k2_G
|
|
|
|
# Extract the x-coordinate of the result and save it as the shared secret
|
|
negotiated_key = k1_k2_G.to_bytes()[:32]
|
|
return negotiated_key
|
|
|
|
|
|
def test_ecdsa_key(negotiated_key: bytes, digest: bytes, signature_r_le: bytes, signature_s_le: bytes, pubx: bytes, puby: bytes) -> None:
|
|
r = int.from_bytes(signature_r_le, 'little')
|
|
s = int.from_bytes(signature_s_le, 'little')
|
|
signature = utils.encode_dss_signature(r, s)
|
|
pubx_int = int.from_bytes(pubx, 'little')
|
|
puby_int = int.from_bytes(puby, 'little')
|
|
private_number = int.from_bytes(negotiated_key, byteorder='big')
|
|
ecdsa_private_key = ec.derive_private_key(private_number, ec.SECP256R1())
|
|
# Get the public key
|
|
public_key = ecdsa_private_key.public_key()
|
|
# Extract the pubx and puby values
|
|
calc_pubx, calc_puby = public_key.public_numbers().x, public_key.public_numbers().y
|
|
|
|
assert calc_pubx == pubx_int, 'Public key calculated should match with public key obtained'
|
|
assert calc_puby == puby_int, 'Public key calculated should match with public key obtained'
|
|
|
|
try:
|
|
public_key.verify(signature, digest, ec.ECDSA(utils.Prehashed(hashes.SHA256())))
|
|
print('Valid signature')
|
|
except exceptions.InvalidSignature:
|
|
print('Invalid signature')
|
|
raise
|
|
|
|
|
|
@pytest.mark.supported_targets
|
|
@pytest.mark.generic
|
|
def test_crypto(dut: Dut) -> None:
|
|
# if the env variable IDF_FPGA_ENV is set, we would need a longer timeout
|
|
# as tests for efuses burning security peripherals would be run
|
|
timeout = 600 if os.environ.get('IDF_ENV_FPGA') else 60
|
|
# only expect key manager result if it is supported for the SoC
|
|
if dut.app.sdkconfig.get('SOC_KEY_MANAGER_SUPPORTED'):
|
|
print('Key Manager is supported')
|
|
|
|
# Test for ECDH0 deployment XTS-AES-128 key
|
|
dut.expect('Key Manager ECDH0 deployment: XTS_AES_128 key', timeout=timeout)
|
|
k2_G = dut.expect(r'K2_G: 0x([0-9a-fA-F]+)', timeout=timeout)[1].decode()
|
|
plaintext_data = dut.expect(r'Plaintext data: 0x([0-9a-fA-F]+)', timeout=timeout)[1]
|
|
plaintext_data = binascii.unhexlify(plaintext_data)
|
|
encrypted_data = dut.expect(r'Encrypted data: 0x([0-9a-fA-F]+)', timeout=timeout)[1].decode()
|
|
encrypted_data = binascii.unhexlify(encrypted_data)
|
|
negotiated_key = calculate_key_manager_ecdh0_negotiated_key(k2_G, 'main/key_manager/k1_ecdsa.pem')
|
|
test_xts_aes_encryption(negotiated_key, plaintext_data, encrypted_data)
|
|
|
|
# Test for ECDH0 deployment XTS-AES-256 key
|
|
dut.expect('Key Manager ECDH0 deployment: XTS_AES_256 key', timeout=timeout)
|
|
k2_G_0 = dut.expect(r'K2_G_0: 0x([0-9a-fA-F]+)', timeout=timeout)[1].decode()
|
|
k2_G_1 = dut.expect(r'K2_G_1: 0x([0-9a-fA-F]+)', timeout=timeout)[1].decode()
|
|
encrypted_data = dut.expect(r'Encrypted data: 0x([0-9a-fA-F]+)', timeout=timeout)[1].decode()
|
|
encrypted_data = binascii.unhexlify(encrypted_data)
|
|
negotiated_key_0 = calculate_key_manager_ecdh0_negotiated_key(k2_G_0, 'main/key_manager/k1_ecdsa.pem')
|
|
negotiated_key_1 = calculate_key_manager_ecdh0_negotiated_key(k2_G_1, 'main/key_manager/k1_ecdsa.pem')
|
|
negotiated_key = negotiated_key_0 + negotiated_key_1
|
|
test_xts_aes_encryption(negotiated_key, plaintext_data, encrypted_data)
|
|
# Test for ECDH0 deployment ECDSA-256 key
|
|
dut.expect('Key Manager ECDH0 deployment: ECDSA_256 key', timeout=timeout)
|
|
k2_G = dut.expect(r'K2_G: 0x([0-9a-fA-F]+)', timeout=timeout)[1].decode()
|
|
digest = dut.expect(r'ECDSA message sha256 digest: 0x([0-9a-fA-F]+)', timeout=timeout)[1].decode()
|
|
digest = binascii.unhexlify(digest)
|
|
signature_r_le = dut.expect(r'ECDSA signature r_le: 0x([0-9a-fA-F]+)', timeout=timeout)[1].decode()
|
|
signature_r_le = binascii.unhexlify(signature_r_le)
|
|
signature_s_le = dut.expect(r'ECDSA signature s_le: 0x([0-9a-fA-F]+)', timeout=timeout)[1].decode()
|
|
signature_s_le = binascii.unhexlify(signature_s_le)
|
|
pub_x = dut.expect(r'ECDSA key pubx: 0x([0-9a-fA-F]+)', timeout=timeout)[1].decode()
|
|
pub_x = binascii.unhexlify(pub_x)
|
|
pub_y = dut.expect(r'ECDSA key puby: 0x([0-9a-fA-F]+)', timeout=timeout)[1].decode()
|
|
pub_y = binascii.unhexlify(pub_y)
|
|
negotiated_key = calculate_key_manager_ecdh0_negotiated_key(k2_G, 'main/key_manager/k1_ecdsa.pem')
|
|
test_ecdsa_key(negotiated_key, digest, signature_r_le, signature_s_le, pub_x, pub_y)
|
|
|
|
test_numbers = dut.expect(r'(\d+) Tests (\d+) Failures (\d+) Ignored', timeout=timeout)
|
|
failures = test_numbers.group(2).decode()
|
|
ignored = test_numbers.group(3).decode()
|
|
assert failures == '0', f'No of failures must be 0 (is {failures})'
|
|
assert ignored == '0', f'No of Ignored test must be 0 (is {ignored})'
|
|
dut.expect('Tests finished', timeout=timeout)
|
|
|
|
|
|
@pytest.mark.supported_targets
|
|
@pytest.mark.generic
|
|
@pytest.mark.parametrize('config', ['long_aes_operations'], indirect=True)
|
|
def test_crypto_long_aes_operations(dut: Dut) -> None:
|
|
# if the env variable IDF_FPGA_ENV is set, we would need a longer timeout
|
|
# as tests for efuses burning security peripherals would be run
|
|
timeout = 600 if os.environ.get('IDF_ENV_FPGA') else 60
|
|
|
|
dut.expect('Tests finished', timeout=timeout)
|