diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 75db925f4..0f14b8879 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -224,6 +224,11 @@ build_esp_matter_examples_non_pytest_idf_v5_1: IDF_VERSION: "v5.1.1" script: - *build_external_platform_example + + # mfg_test_app needs an secure boot signing key, generating one here + - cd ${ESP_MATTER_PATH}/examples/mfg_test_app + - openssl genrsa -out secure_boot_signing_key.pem 3072 + - cd ${ESP_MATTER_PATH} - rm ./examples/*/dependencies.lock - pip install -r tools/ci/requirements-build.txt diff --git a/examples/.build-rules.yml b/examples/.build-rules.yml index 0d321deba..00736ee33 100644 --- a/examples/.build-rules.yml +++ b/examples/.build-rules.yml @@ -65,3 +65,9 @@ examples/all_device_types_app: - if: IDF_TARGET in ["esp32", "esp32c3", "esp32h2"] temporary: true reason: the other targets are not tested yet + +examples/mfg_test_app: + enable: + - if: IDF_TARGET in ["esp32c3", "esp32c2", "esp32c6", "esp32h2"] + temporary: true + reason: the other targets are not tested yet diff --git a/examples/mfg_test_app/CMakeLists.txt b/examples/mfg_test_app/CMakeLists.txt new file mode 100644 index 000000000..b87d2a271 --- /dev/null +++ b/examples/mfg_test_app/CMakeLists.txt @@ -0,0 +1,28 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +if(NOT DEFINED ENV{ESP_MATTER_PATH}) + message(FATAL_ERROR "Please set ESP_MATTER_PATH to the path of esp-matter repo") +endif(NOT DEFINED ENV{ESP_MATTER_PATH}) + +set(PROJECT_VER "v1.0") +set(PROJECT_VER_NUMBER 1) + +set(ESP_MATTER_PATH $ENV{ESP_MATTER_PATH}) +set(MATTER_SDK_PATH ${ESP_MATTER_PATH}/connectedhomeip/connectedhomeip) + +# This should be done before using the IDF_TARGET variable. +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +set(EXTRA_COMPONENT_DIRS + "${MATTER_SDK_PATH}/config/esp32/components" + "${ESP_MATTER_PATH}/components") + +project(mfg_test_app) + +idf_build_set_property(CXX_COMPILE_OPTIONS "-std=gnu++14;-Os;-DCHIP_HAVE_CONFIG_H" APPEND) +idf_build_set_property(C_COMPILE_OPTIONS "-Os" APPEND) +# For RISCV chips, project_include.cmake sets -Wno-format, but does not clear various +# flags that depend on -Wformat +idf_build_set_property(COMPILE_OPTIONS "-Wno-format-nonliteral;-Wno-format-security;-Wformat=0" APPEND) diff --git a/examples/mfg_test_app/README.md b/examples/mfg_test_app/README.md new file mode 100644 index 000000000..320af9f05 --- /dev/null +++ b/examples/mfg_test_app/README.md @@ -0,0 +1,38 @@ +## Matter Manufacturing Test Application + +The Matter Manufacturing Test Application is a tool used to verify the successful pre-provisioning of a Matter module. +The purpose of this application is to ensure that the module is only running signed firmware and that all necessary +security features are enabled and configured properly. + +It performs the tests listed below: + +- Reads DAC and PAI certificate from the secure cert partition and dumps few details: + - VID and PID + - Public Key + - Subject and Authority Key Identifier +- Signs the sample dataset with DAC private key (using DAC provider APIs) and verifies the signature using public key in DAC. +- Validates the certificate chain DAC -> PAI -> PAA. This example by default uses the Espressif's + [PAA](main/paa_cert.der). +- Makes sure that all the security bits like flash encryption and secure boot are enabled and they are configured in + *release mode* and corresponding efuses are burned. + + +### What is expected from the customers opting for Matter pre-provisioning service + +If a customer opts for the Matter pre-provisioning service, they must provide a signed firmware binary of this firmware +to test the pre-provisioning of their module. + +The secure boot signing key used for signing this firmware must match the key that was used to sign the bootloader +provided for pre-provisioning purposes. + +#### Steps to build the firmware + +- Copy the key for signing the firmware in the project directory with name `secure_boot_signing_key.pem`. + +- Configure Device Vendor Id and Device Product Id using `idf.py menuconfig`, + - Menu path: `(Top) -> Component config -> CHIP Device Layer -> Device Identification Options` + - Config options: `CONFIG_DEVICE_VENDOR_ID` and `CONFIG_DEVICE_PRODUCT_ID` + +- Build the firmware and provide the signed application binary (`build/mfg_test_app.bin`) to Espressif. + +- Please use esp-idf [v5.1](https://github.com/espressif/esp-idf/tree/v5.1). diff --git a/examples/mfg_test_app/main/CMakeLists.txt b/examples/mfg_test_app/main/CMakeLists.txt new file mode 100644 index 000000000..f82b3fe97 --- /dev/null +++ b/examples/mfg_test_app/main/CMakeLists.txt @@ -0,0 +1,9 @@ +set(PRIV_REQUIRES_LIST esp_matter bootloader_support) + +idf_component_register(SRC_DIRS "." + PRIV_INCLUDE_DIRS "." + PRIV_REQUIRES ${PRIV_REQUIRES_LIST} + EMBED_FILES "paa_cert.der") + +set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 14) +target_compile_options(${COMPONENT_LIB} PRIVATE "-DCHIP_HAVE_CONFIG_H") diff --git a/examples/mfg_test_app/main/main.cpp b/examples/mfg_test_app/main/main.cpp new file mode 100644 index 000000000..32fd6e788 --- /dev/null +++ b/examples/mfg_test_app/main/main.cpp @@ -0,0 +1,308 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace chip; +using namespace chip::Crypto; +using namespace chip::Credentials; + +namespace { +const char *TAG = "MFG-TEST-APP"; + +uint8_t s_dac_cert_buffer[kMaxDERCertLength]; // 600 bytes +uint8_t s_pai_cert_buffer[kMaxDERCertLength]; // 600 bytes +uint8_t s_paa_cert_buffer[kMaxDERCertLength]; // 600 bytes + +extern const uint8_t paa_cert_start[] asm("_binary_paa_cert_der_start"); +extern const uint8_t paa_cert_end[] asm("_binary_paa_cert_der_end"); + +MutableByteSpan paa_span; +MutableByteSpan pai_span; +MutableByteSpan dac_span; + +uint8_t s_garbage_buffer[128]; + +CHIP_ERROR read_certs_in_spans() +{ + // DAC Provider implementation + DeviceAttestationCredentialsProvider * dac_provider = GetDeviceAttestationCredentialsProvider(); + VerifyOrReturnError(dac_provider, CHIP_ERROR_INTERNAL, ESP_LOGE(TAG, "ERROR: Failed to get the DAC provider impl")); + + // Read DAC + dac_span = MutableByteSpan(s_dac_cert_buffer); + CHIP_ERROR err = dac_provider->GetDeviceAttestationCert(dac_span); + VerifyOrReturnError(err == CHIP_NO_ERROR, err, ESP_LOGE(TAG, "ERROR: Failed to read the DAC, %" CHIP_ERROR_FORMAT, err.Format())); + + // Read PAI + pai_span = MutableByteSpan(s_pai_cert_buffer); + err = dac_provider->GetProductAttestationIntermediateCert(pai_span); + VerifyOrReturnError(err == CHIP_NO_ERROR, err, ESP_LOGE(TAG, "ERROR: Failed to read the PAI Certificate %" CHIP_ERROR_FORMAT, err.Format())); + + // Read PAA + uint16_t paa_len = paa_cert_end - paa_cert_start; + memcpy(s_paa_cert_buffer, paa_cert_start, paa_len); + paa_span = MutableByteSpan(s_paa_cert_buffer, paa_len); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR dump_cert_details(const char *type, ByteSpan cert_span) +{ + ESP_LOGI(TAG, "---------- %s ----------", type); + + // Get VID, PID from the certificate + AttestationCertVidPid vidpid; + CHIP_ERROR err = ExtractVIDPIDFromX509Cert(cert_span, vidpid); + VerifyOrReturnError(err == CHIP_NO_ERROR, err, ESP_LOGE(TAG, "ERROR: Failed to extract VID and PID, error: %" CHIP_ERROR_FORMAT, err.Format())); + + if (vidpid.mVendorId.HasValue()) { + ESP_LOGI(TAG, "Vendor ID: 0x%04X", vidpid.mVendorId.Value()); + assert(CONFIG_DEVICE_VENDOR_ID == vidpid.mVendorId.Value()); + } else if (strncmp(type, "PAA", sizeof("PAA")) != 0) { + ESP_LOGE(TAG, "ERROR: Vendor ID: Unspecified"); + } + + if (vidpid.mProductId.HasValue()) { + ESP_LOGI(TAG, "Product ID: 0x%04X", vidpid.mProductId.Value()); + assert(CONFIG_DEVICE_PRODUCT_ID == vidpid.mProductId.Value()); + } else if ((strncmp(type, "PAA", sizeof("PAA")) != 0) && (strncmp(type, "PAI", sizeof("PAI")) != 0)) { + ESP_LOGE(TAG, "ERROR: Product ID: Unspecified"); + } + + // Get Public key from the certificate + P256PublicKey pubkey; + err = ExtractPubkeyFromX509Cert(cert_span, pubkey); + VerifyOrReturnError(err == CHIP_NO_ERROR, err, ESP_LOGE(TAG, "ERROR: Failed to extract public key, error: %" CHIP_ERROR_FORMAT, err.Format())); + + // Print public key + ESP_LOGI(TAG, "Public Key encoded as hex string:"); + for (uint8_t i = 0; i < pubkey.Length(); i++) { + printf("%02x", pubkey.ConstBytes()[i]); + } + printf("\n\n"); + + // Get AKID from the certificate + uint8_t akid_buffer[64]; + MutableByteSpan akid_span(akid_buffer); + err = ExtractAKIDFromX509Cert(cert_span, akid_span); + VerifyOrReturnError(err == CHIP_NO_ERROR, err, ESP_LOGE(TAG, "ERROR: Failed to extract AKID, error: %" CHIP_ERROR_FORMAT, err.Format())); + + // Print AKID + ESP_LOGI(TAG, "X509v3 Authority Key Identifier:"); + printf("%02x", akid_span.data()[0]); + for (uint8_t i = 1; i < akid_span.size(); i++) { + printf(":%02X", akid_span.data()[i]); + } + printf("\n\n"); + + // Get SKID from the certificate + uint8_t skid_buffer[64]; + MutableByteSpan skid_span(skid_buffer); + err = ExtractSKIDFromX509Cert(cert_span, skid_span); + VerifyOrReturnError(err == CHIP_NO_ERROR, err, ESP_LOGE(TAG, "ERROR: Failed to extract SKID, error: %" CHIP_ERROR_FORMAT, err.Format())); + + // Print SKID + ESP_LOGI(TAG, "X509v3 Subject Key Identifier:"); + printf("%02x", skid_span.data()[0]); + for (uint8_t i = 1; i < skid_span.size(); i++) { + printf(":%02X", skid_span.data()[i]); + } + printf("\n\n"); + + ESP_LOGI(TAG, "------------------------------"); + return CHIP_NO_ERROR; +} + +CHIP_ERROR test_dac(ByteSpan dac) +{ + // DAC Provider implementation + DeviceAttestationCredentialsProvider * dac_provider = GetDeviceAttestationCredentialsProvider(); + VerifyOrReturnError(dac_provider, CHIP_ERROR_INTERNAL, ESP_LOGE(TAG, "ERROR: Failed to get the DAC provider impl")); + + // Get Public key from the certificate + P256PublicKey pubkey; + CHIP_ERROR err = ExtractPubkeyFromX509Cert(dac, pubkey); + VerifyOrReturnError(err == CHIP_NO_ERROR, err, ESP_LOGE(TAG, "ERROR: Failed to get DAC public key, error: %" CHIP_ERROR_FORMAT, err.Format())); + + // Garbage + esp_fill_random(s_garbage_buffer, sizeof(s_garbage_buffer)); + ByteSpan mts_span(s_garbage_buffer); + + // signature + P256ECDSASignature signature; + MutableByteSpan signature_span{ signature.Bytes(), signature.Capacity() }; + + // Generate attestation signature + err = dac_provider->SignWithDeviceAttestationKey(mts_span, signature_span); + VerifyOrReturnError(err == CHIP_NO_ERROR, err, ESP_LOGE(TAG, "ERROR: Failed to sign the message with DAC key, error: %" CHIP_ERROR_FORMAT, err.Format())); + + ESP_LOGI(TAG, "Message signed with DAC key: OK"); + ESP_LOG_BUFFER_HEX(TAG, signature_span.data(), signature_span.size()); + + P256ECDSASignature signature_to_verify; + + ReturnErrorOnFailure(signature_to_verify.SetLength(signature_span.size())); + memcpy(signature_to_verify.Bytes(), signature_span.data(), signature_span.size()); + + err = pubkey.ECDSA_validate_msg_signature(s_garbage_buffer, sizeof(s_garbage_buffer), signature_to_verify); + VerifyOrReturnError(err == CHIP_NO_ERROR, err, ESP_LOGE(TAG, "ERROR: Failed to validate signature, error: %" CHIP_ERROR_FORMAT, err.Format())); + + ESP_LOGI(TAG, "Signature Verification: OK"); + return CHIP_NO_ERROR; +} + +AttestationVerificationResult MapError(CertificateChainValidationResult certificateChainValidationResult) +{ + switch (certificateChainValidationResult) + { + case CertificateChainValidationResult::kRootFormatInvalid: + return AttestationVerificationResult::kPaaFormatInvalid; + + case CertificateChainValidationResult::kRootArgumentInvalid: + return AttestationVerificationResult::kPaaArgumentInvalid; + + case CertificateChainValidationResult::kICAFormatInvalid: + return AttestationVerificationResult::kPaiFormatInvalid; + + case CertificateChainValidationResult::kICAArgumentInvalid: + return AttestationVerificationResult::kPaiArgumentInvalid; + + case CertificateChainValidationResult::kLeafFormatInvalid: + return AttestationVerificationResult::kDacFormatInvalid; + + case CertificateChainValidationResult::kLeafArgumentInvalid: + return AttestationVerificationResult::kDacArgumentInvalid; + + case CertificateChainValidationResult::kChainInvalid: + return AttestationVerificationResult::kDacSignatureInvalid; + + case CertificateChainValidationResult::kNoMemory: + return AttestationVerificationResult::kNoMemory; + + case CertificateChainValidationResult::kInternalFrameworkError: + return AttestationVerificationResult::kInternalError; + + default: + return AttestationVerificationResult::kInternalError; + } +} + +bool test_cert_chain(ByteSpan paa, ByteSpan pai, ByteSpan dac) +{ + AttestationVerificationResult err = AttestationVerificationResult::kSuccess; + + // Validate Proper Certificate Format + VerifyOrExit(VerifyAttestationCertificateFormat(paa, AttestationCertType::kPAA) == CHIP_NO_ERROR, + err = AttestationVerificationResult::kPaaFormatInvalid); + VerifyOrExit(VerifyAttestationCertificateFormat(pai, AttestationCertType::kPAI) == CHIP_NO_ERROR, + err = AttestationVerificationResult::kPaiFormatInvalid); + VerifyOrExit(VerifyAttestationCertificateFormat(dac, AttestationCertType::kDAC) == CHIP_NO_ERROR, + err = AttestationVerificationResult::kDacFormatInvalid); + + // Verify that VID and PID in the certificates match. + { + AttestationCertVidPid dacVidPid; + AttestationCertVidPid paiVidPid; + AttestationCertVidPid paaVidPid; + + VerifyOrExit(ExtractVIDPIDFromX509Cert(dac, dacVidPid) == CHIP_NO_ERROR, + err = AttestationVerificationResult::kDacFormatInvalid); + VerifyOrExit(ExtractVIDPIDFromX509Cert(pai, paiVidPid) == CHIP_NO_ERROR, + err = AttestationVerificationResult::kPaiFormatInvalid); + VerifyOrExit(ExtractVIDPIDFromX509Cert(paa, paaVidPid) == CHIP_NO_ERROR, + err = AttestationVerificationResult::kPaaFormatInvalid); + + VerifyOrExit(dacVidPid.mVendorId.HasValue() && dacVidPid.mVendorId == paiVidPid.mVendorId, + err = AttestationVerificationResult::kDacVendorIdMismatch); + + if (paaVidPid.mVendorId.HasValue()) + { + VerifyOrExit(dacVidPid.mVendorId == paaVidPid.mVendorId, + err = AttestationVerificationResult::kPaiVendorIdMismatch); + } + + if (paiVidPid.mProductId.HasValue()) + { + VerifyOrExit(dacVidPid.mProductId == paiVidPid.mProductId, + err = AttestationVerificationResult::kDacProductIdMismatch); + } + + VerifyOrExit(!paaVidPid.mProductId.HasValue(), err = AttestationVerificationResult::kPaaFormatInvalid); + } + + // Validate certificate chain. + CertificateChainValidationResult chainValidationResult; + VerifyOrExit(ValidateCertificateChain(paa.data(), paa.size(), pai.data(), pai.size(), dac.data(), dac.size(), + chainValidationResult) == CHIP_NO_ERROR, + err = MapError(chainValidationResult)); + +exit: + if (err != AttestationVerificationResult::kSuccess) { + ESP_LOGE(TAG, "ERROR: Certificates Chain Validation Failed with Error Code: %d\n", static_cast(err)); + ESP_LOGE(TAG, "Find the error enum here:https://github.com/project-chip/connectedhomeip/blob/c5216d1/src/credentials/attestation_verifier/DeviceAttestationVerifier.h#L30"); + return false; + } else { + ESP_LOGI(TAG, "DAC->PAI->PAA Certificate chain validation: OK"); + } + + return true; +} + +void test_security_bits() +{ + if (esp_flash_encryption_cfg_verify_release_mode()) { + ESP_LOGI(TAG, "Flash encryption in release mode: OK"); + } + + if (esp_secure_boot_cfg_verify_release_mode()) { + ESP_LOGI(TAG, "Secure Boot in release mode: OK"); + } +} + +} + +extern "C" void app_main() +{ + // Setup providers based on configuration options + esp_matter::setup_providers(); + + // read PAA, PAI, and DAC in spans + CHIP_ERROR err = read_certs_in_spans(); + VerifyOrReturn(err == CHIP_NO_ERROR, ESP_LOGE(TAG, "ERROR: Reading certificates failed, error: %" CHIP_ERROR_FORMAT, err.Format())); + + // Dump PAI details + err = dump_cert_details("PAI", pai_span); + VerifyOrReturn(err == CHIP_NO_ERROR, ESP_LOGE(TAG, "ERROR: Failed to dump PAI certificate details, error: %" CHIP_ERROR_FORMAT, err.Format())); + + // Dump DAC details + err = dump_cert_details("DAC", dac_span); + VerifyOrReturn(err == CHIP_NO_ERROR, ESP_LOGE(TAG, "ERROR: Failed to dump DAC certificate details, error: %" CHIP_ERROR_FORMAT, err.Format())); + + // Sign the message with DAC key and verify with public key in DAC certificate + err = test_dac(dac_span); + VerifyOrReturn(err == CHIP_NO_ERROR, ESP_LOGE(TAG, "ERROR: Failed to Sign and Verify using DAC keypair, error: %" CHIP_ERROR_FORMAT, err.Format())); + + // Test DAC -> PAI -> PAA chain validation + bool status = test_cert_chain(paa_span, pai_span, dac_span); + VerifyOrReturn(status, ESP_LOGE(TAG, "ERROR: Failed to validate attestation cert chain (DAC -> PAI -> PAA)")); + + test_security_bits(); +} diff --git a/examples/mfg_test_app/main/paa_cert.der b/examples/mfg_test_app/main/paa_cert.der new file mode 100644 index 000000000..f4299e637 Binary files /dev/null and b/examples/mfg_test_app/main/paa_cert.der differ diff --git a/examples/mfg_test_app/partitions.csv b/examples/mfg_test_app/partitions.csv new file mode 100644 index 000000000..f7ba16e45 --- /dev/null +++ b/examples/mfg_test_app/partitions.csv @@ -0,0 +1,7 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: Firmware partition offset needs to be 64K aligned, initial 36K (9 sectors) are reserved for bootloader and partition table +esp_secure_cert, 0x3F, ,0xd000, 0x2000, encrypted +nvs, data, nvs, 0x10000, 0xC000, +nvs_keys, data, nvs_keys,, 0x1000, encrypted +phy_init, data, phy, , 0x1000, +ota_0, app, ota_0, 0x20000, 0x100000, diff --git a/examples/mfg_test_app/sdkconfig.defaults b/examples/mfg_test_app/sdkconfig.defaults new file mode 100644 index 000000000..fd2bcb76c --- /dev/null +++ b/examples/mfg_test_app/sdkconfig.defaults @@ -0,0 +1,32 @@ +# Default to 921600 baud when flashing and monitoring device +CONFIG_ESPTOOLPY_BAUD_921600B=y +CONFIG_ESPTOOLPY_BAUD=921600 +CONFIG_ESPTOOLPY_COMPRESSED=y +CONFIG_ESPTOOLPY_MONITOR_BAUD_115200B=y +CONFIG_ESPTOOLPY_MONITOR_BAUD=115200 + +#enable lwip ipv6 autoconfig +CONFIG_LWIP_IPV6_AUTOCONFIG=y + +# Use a custom partition table +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_OFFSET=0xC000 + +#enable lwIP route hooks +CONFIG_LWIP_HOOK_IP6_ROUTE_DEFAULT=y +CONFIG_LWIP_HOOK_ND6_GET_GW_DEFAULT=y + +# Disable DS Peripheral +CONFIG_ESP_SECURE_CERT_DS_PERIPHERAL=n + +# Read attestation data from secure cert partition +CONFIG_SEC_CERT_DAC_PROVIDER=y + +# Enable flash encryption and secure boot in release mode +CONFIG_SECURE_BOOT=y +CONFIG_SECURE_BOOT_SIGNING_KEY="secure_boot_signing_key.pem" +CONFIG_SECURE_FLASH_ENC_ENABLED=y +CONFIG_SECURE_FLASH_ENCRYPTION_MODE_RELEASE=y + +CONFIG_DEVICE_VENDOR_ID=0x131B diff --git a/examples/mfg_test_app/sdkconfig.defaults.esp32h2 b/examples/mfg_test_app/sdkconfig.defaults.esp32h2 new file mode 100644 index 000000000..e6252d356 --- /dev/null +++ b/examples/mfg_test_app/sdkconfig.defaults.esp32h2 @@ -0,0 +1,2 @@ +# Enable DS Peripheral +CONFIG_ESP_SECURE_CERT_DS_PERIPHERAL=y