diff --git a/components/esp_http_client/test_apps/README.md b/components/esp_http_client/test_apps/README.md index 6bcfea86348..c76778586ad 100644 --- a/components/esp_http_client/test_apps/README.md +++ b/components/esp_http_client/test_apps/README.md @@ -1,3 +1,5 @@ | Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 | | ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- | +See `examples/protocols/esp_http_client_mutual_auth` for mutual TLS (mTLS) examples including DS peripheral support. + diff --git a/docs/en/api-reference/protocols/esp_http_client.rst b/docs/en/api-reference/protocols/esp_http_client.rst index a1b755b3512..abcd1acd763 100644 --- a/docs/en/api-reference/protocols/esp_http_client.rst +++ b/docs/en/api-reference/protocols/esp_http_client.rst @@ -17,6 +17,7 @@ Application Examples -------------------- - :example:`protocols/esp_http_client` demonstrates how to use the ESP HTTP Client to make HTTP/S requests. +- :example:`protocols/esp_http_client_mutual_auth` demonstrates how to configure mutual TLS authentication with the ESP HTTP Client. Basic HTTP Request diff --git a/examples/protocols/.build-test-rules.yml b/examples/protocols/.build-test-rules.yml index 122b7c8d5b7..95fb9e1ef6d 100644 --- a/examples/protocols/.build-test-rules.yml +++ b/examples/protocols/.build-test-rules.yml @@ -25,6 +25,15 @@ examples/protocols/esp_http_client: depends_components+: - esp_http_client +examples/protocols/esp_http_client_mutual_auth: + <<: *default_rules + disable_test: + - if: IDF_TARGET != "esp32c3" + depends_components+: + - esp_http_client + - esp-tls + - esp_secure_cert_mgr + examples/protocols/esp_local_ctrl: <<: *default_rules disable+: diff --git a/examples/protocols/esp_http_client_mutual_auth/CMakeLists.txt b/examples/protocols/esp_http_client_mutual_auth/CMakeLists.txt new file mode 100644 index 00000000000..6b76a8c32f3 --- /dev/null +++ b/examples/protocols/esp_http_client_mutual_auth/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +idf_build_set_property(MINIMAL_BUILD ON) + +project(esp_http_client_mutual_auth) + +if(CONFIG_EXAMPLE_MUTUAL_AUTH_USE_HW_RSA_DS) + # Flash the pre-built esp_secure_cert partition (device cert + DS context) so it + # is included in the QEMU flash image created by esptool merge_bin. + esptool_py_flash_to_partition( + flash esp_secure_cert + "${CMAKE_CURRENT_SOURCE_DIR}/main/certs/esp_secure_cert_data/esp_secure_cert.bin" + ) +endif() diff --git a/examples/protocols/esp_http_client_mutual_auth/README.md b/examples/protocols/esp_http_client_mutual_auth/README.md new file mode 100644 index 00000000000..d005c96b202 --- /dev/null +++ b/examples/protocols/esp_http_client_mutual_auth/README.md @@ -0,0 +1,104 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | -------- | -------- | -------- | + +# ESP HTTP Client Mutual TLS Authentication Example + +This example demonstrates mutual TLS (mTLS) authentication using `esp_http_client`. The server verifies the client's identity via a client certificate, and the client verifies the server using a CA certificate. + +Two client key modes are supported, selectable via Kconfig: + +| Mode | Kconfig | Client key source | +|------|---------|-------------------| +| **Software keys** (default) | `EXAMPLE_MUTUAL_AUTH_USE_HW_RSA_DS=n` | PEM files embedded in firmware | +| **DS peripheral** | `EXAMPLE_MUTUAL_AUTH_USE_HW_RSA_DS=y` | Hardware Digital Signature peripheral via `esp_secure_cert` partition | + +## How it works + +1. Device connects to Wi-Fi or Ethernet (via `protocol_examples_common`) +2. Prompts for HTTPS server URL on serial console +3. Performs HTTPS GET with mutual TLS — sends client certificate to server +4. Logs the HTTP status code + +## Quick start (software key mode) + +```bash +idf.py set-target esp32c3 +idf.py build flash monitor +``` + +When prompted, enter the URL of an HTTPS server that requires client certificates. The pre-generated certificates in `main/certs/` are used automatically. + +## Certificates + +Pre-generated certificates with 10-year lifetime are in `main/certs/`. A single CA signs both client and server certificates. See `main/certs/README.md` for OpenSSL commands to regenerate. + +| File | Purpose | Embedded in firmware? | +|------|---------|----------------------| +| `ca_cert.pem` | CA certificate (verifies server) | Yes (both modes) | +| `client_cert.pem` | Client certificate | Yes (software mode only) | +| `client_key.pem` | Client private key | Yes (software mode only) | +| `server_cert.pem` | Server certificate (pytest) | No | +| `server_key.pem` | Server private key (pytest) | No | + +## DS peripheral mode + +The DS (Digital Signature) peripheral holds the client private key in hardware. The client certificate and DS context are stored in the `esp_secure_cert` partition. + +### Prerequisites + +- ESP32-C3 or other chip with `SOC_DIG_SIGN_SUPPORTED` +- `esp-secure-cert-tool` (`pip install esp-secure-cert-tool`) + +### Provisioning + +1. **Generate DS partition and HMAC key** (from the example directory): + +```bash +configure_esp_secure_cert.py \ + --device-cert main/certs/client_cert.pem \ + --target_chip esp32c3 \ + --configure_ds --priv_key_algo RSA 2048 \ + --skip_flash --keep_ds_data_on_host \ + --private-key main/certs/client_key.pem \ + --efuse_key_id 1 +``` + +Copy the output files to `main/certs/esp_secure_cert_data/`: +- `esp_secure_cert.bin` +- `hmac_key.bin` + +2. **Burn HMAC key to eFuse:** + +```bash +# QEMU +idf.py qemu efuse-burn-key --do-not-confirm BLOCK_KEY1 \ + main/certs/esp_secure_cert_data/hmac_key.bin HMAC_DOWN_DIGITAL_SIGNATURE + +# Hardware (irreversible!) +idf.py efuse-burn-key --do-not-confirm BLOCK_KEY1 \ + main/certs/esp_secure_cert_data/hmac_key.bin HMAC_DOWN_DIGITAL_SIGNATURE +``` + +3. **Build and flash:** + +```bash +idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.ci.qemu_ds" build flash monitor +``` + +## QEMU testing + +Three QEMU tests are included: + +- `test_mutual_auth_software_keys` — software key mTLS, expects HTTP 200 +- `test_mutual_auth_ds` — DS peripheral mTLS, expects HTTP 200 +- `test_mutual_auth_ds_fails_wrong_credentials` — DS with wrong server CA, expects failure + +The tests start a local HTTPS server with mutual TLS, inject the URL via serial, and verify device output. + +## Troubleshooting + +**TLS handshake failed: BAD_SIGNATURE** +The client certificate is not signed by the CA the server expects. Regenerate certs using the commands in `main/certs/README.md`. + +**ESP_ERR_HW_CRYPTO_DS_HMAC_FAIL** +The HMAC key in eFuse does not match the key used to create the DS partition. For QEMU, delete the eFuse image and regenerate it from the current `hmac_key.bin`. diff --git a/examples/protocols/esp_http_client_mutual_auth/main/CMakeLists.txt b/examples/protocols/esp_http_client_mutual_auth/main/CMakeLists.txt new file mode 100644 index 00000000000..f320bfe099c --- /dev/null +++ b/examples/protocols/esp_http_client_mutual_auth/main/CMakeLists.txt @@ -0,0 +1,14 @@ +set(requires esp-tls nvs_flash esp_event esp_netif esp_http_client) + +set(embed_txt certs/ca_cert.pem) + +if(CONFIG_EXAMPLE_MUTUAL_AUTH_USE_HW_RSA_DS) + list(APPEND requires esp_secure_cert_mgr) +else() + list(APPEND embed_txt certs/client_cert.pem certs/client_key.pem) +endif() + +idf_component_register(SRCS "mutual_auth_example.c" + INCLUDE_DIRS "." + PRIV_REQUIRES ${requires} + EMBED_TXTFILES ${embed_txt}) diff --git a/examples/protocols/esp_http_client_mutual_auth/main/Kconfig.projbuild b/examples/protocols/esp_http_client_mutual_auth/main/Kconfig.projbuild new file mode 100644 index 00000000000..f821f6fe594 --- /dev/null +++ b/examples/protocols/esp_http_client_mutual_auth/main/Kconfig.projbuild @@ -0,0 +1,12 @@ +menu "Example Configuration" + config EXAMPLE_MUTUAL_AUTH_USE_HW_RSA_DS + bool "Use DS peripheral for client authentication" if SOC_DIG_SIGN_SUPPORTED + default n + help + When enabled, the client private key is held by the DS (Digital Signature) + peripheral. The client certificate and DS context are loaded from the + esp_secure_cert partition at runtime. Requires provisioning; see the README. + + When disabled (default), the client certificate and private key are embedded + in the firmware as PEM files from main/certs/. +endmenu diff --git a/examples/protocols/esp_http_client_mutual_auth/main/certs/README.md b/examples/protocols/esp_http_client_mutual_auth/main/certs/README.md new file mode 100644 index 00000000000..a1be2db4a0e --- /dev/null +++ b/examples/protocols/esp_http_client_mutual_auth/main/certs/README.md @@ -0,0 +1,41 @@ +# Certificate Generation for Mutual TLS Example + +All certificates are pre-generated with a 10-year lifetime. To regenerate, run +the following OpenSSL commands from this directory (`main/certs/`). + +## Generate CA (signs both client and server certs) + + openssl req -newkey rsa:2048 -nodes -keyout ca_key.pem -x509 -days 3650 \ + -out ca_cert.pem -subj "/CN=Mutual Auth Test CA" + +## Generate client certificate (embedded in firmware) + + openssl genrsa -out client_key.pem 2048 + openssl req -out client.csr -key client_key.pem -new -subj "/CN=esp_mutual_auth_client" + openssl x509 -req -days 3650 -in client.csr -CA ca_cert.pem -CAkey ca_key.pem \ + -sha256 -CAcreateserial -out client_cert.pem + rm client.csr ca_cert.srl + +## Generate server certificate (used by pytest mTLS server) + + openssl genrsa -out server_key.pem 2048 + openssl req -out server.csr -key server_key.pem -new -subj "/CN=esp_mutual_auth_server" + openssl x509 -req -days 3650 -in server.csr -CA ca_cert.pem -CAkey ca_key.pem \ + -sha256 -CAcreateserial -out server_cert.pem + rm server.csr ca_cert.srl + +## Files + +| File | Purpose | Embedded in firmware? | +|------|---------|----------------------| +| `ca_cert.pem` | CA certificate (verifies server identity) | Yes | +| `ca_key.pem` | CA private key (signs certs; keep private) | No | +| `client_cert.pem` | Client certificate (sent to server) | Yes (software mode) | +| `client_key.pem` | Client private key | Yes (software mode) | +| `server_cert.pem` | Server certificate (pytest mTLS server) | No | +| `server_key.pem` | Server private key (pytest mTLS server) | No | + +## DS peripheral mode + +For DS peripheral mode, see `esp_secure_cert_data/` and the DS provisioning +section in the top-level README. diff --git a/examples/protocols/esp_http_client_mutual_auth/main/certs/ca_cert.pem b/examples/protocols/esp_http_client_mutual_auth/main/certs/ca_cert.pem new file mode 100644 index 00000000000..ffd2068b266 --- /dev/null +++ b/examples/protocols/esp_http_client_mutual_auth/main/certs/ca_cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDBTCCAe2gAwIBAgIUHrbEhg+kRb2wd66oi57asPHvrBYwDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNjAyMDQwNzMwMDRaFw0zNjAyMDIw +NzMwMDRaMBIxEDAOBgNVBAMMB1Rlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCpug883pCJKGj0cZ/zdJLnjBa9M6lPWscgDFh1wPO1wyOzu25Q +kUykgUE8GE3W7RC4zw4LCUkU06EeVnp6UkXsKktC94OvAeYczsq6Z95WwyF8ktXP +AzW/Lpz202MFpA4edcoczLJEf1GPmoI1RM//V19Op+6nLBNLLTj35yO3CUMjxeh6 +ptlxi3PbiFliOVff6DfPcNzp7rh1k+SWoNP5adTN8aHjpoO53wFHhBfiP4zqnLJG +uHFHApGcwyf1SAbERJ4F2tV+xMRVnp2i/Bx9pIY7qVRKdqOswQK6YGSlznfY8RCM +he9B0mZpSa9NFsPdm+Lpt2wxDvuSC/WtMLHBAgMBAAGjUzBRMB0GA1UdDgQWBBRG +ECXpPzDsNPv55ZHXAsKgCIYF6zAfBgNVHSMEGDAWgBRGECXpPzDsNPv55ZHXAsKg +CIYF6zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAFiLooiqC/ +LQBoN7J6FyQhsKvly+yX4Vh8ituhQQnt4kRD28ci+GmceqgNlX86e/MMTzTXOCMJ +EIhR4xHd1xg5lnPLPS1o+LXsbyOlU8QTCoYgVkjDVHi4fGnbMIoWaZBt8o3MYgAy +Fc+7A6ZoK9PIbbcRUYPSPGe1loop490tgyNrvyc29lLdg4ljO+Xg3Wpu95bTxe/F +bjWFWZgF6aqAfrT7QZXk9OIfOkDpcb5EsxBySQIZNC06FEtNkwanVWFSMdDm3Axh +Xjuv1HdGyHnS/jcFTZ4EfB76oU0+NuoYtDYF7Fns3Upvg8QZhhs+6Ob5mRKJefRD +HlIA+c6CBmsY +-----END CERTIFICATE----- diff --git a/examples/protocols/esp_http_client_mutual_auth/main/certs/ca_key.pem b/examples/protocols/esp_http_client_mutual_auth/main/certs/ca_key.pem new file mode 100644 index 00000000000..ed7aa03dad5 --- /dev/null +++ b/examples/protocols/esp_http_client_mutual_auth/main/certs/ca_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCpug883pCJKGj0 +cZ/zdJLnjBa9M6lPWscgDFh1wPO1wyOzu25QkUykgUE8GE3W7RC4zw4LCUkU06Ee +Vnp6UkXsKktC94OvAeYczsq6Z95WwyF8ktXPAzW/Lpz202MFpA4edcoczLJEf1GP +moI1RM//V19Op+6nLBNLLTj35yO3CUMjxeh6ptlxi3PbiFliOVff6DfPcNzp7rh1 +k+SWoNP5adTN8aHjpoO53wFHhBfiP4zqnLJGuHFHApGcwyf1SAbERJ4F2tV+xMRV +np2i/Bx9pIY7qVRKdqOswQK6YGSlznfY8RCMhe9B0mZpSa9NFsPdm+Lpt2wxDvuS +C/WtMLHBAgMBAAECggEAAt0DUwvvrQqwG5ibodIwhMS/oPVBSNgPli4zI1hsHB/E +x0xVD/mljPxrvvFrhcHV14JRurSvRZFM7Wu40P24lYOApcpyb6ZE7S09bQ/hx72u +v9Dj18RWeKlXB1B5Yg/al5+1107KPp7Vv8oT4oVsy2JcVqG9ZFdZY3optP/yoay4 +oRJprOV4jc8aaLB0+zze/7RJZ4DtpeOWhyNws/cGwwFvaDlncnylAoaDs20MBfsg +VV+glo69rqvn3SCfSXIG5/vyfHS52xPN04kPrpXpu7kvTeJHqCfq4w5LSS0GgBdM +5CbBSpW25AnX9Jqczxhu7Y2rt8kIx+4gnW05z0YB2QKBgQDn9yeQzsXK98R7R6hz +ogKyOYQuNVNjjR7guSD0285QTU+XB8jC2nqiTVPy8Xy1w6udh9SjvqS47yQyySzC +ZCFvT3rwwdWP5pUj89FG7XoX33hQrV4+jbBCcjajeyMxAmMM83ISL9NahsVLWx7K +gdPdxhBiCB8dueF+kVcBf1Bb2QKBgQC7UAjwl45oOIDoXBqN3TDGVodwQM6gn1rY +heYjEUeQmjL6j4e8PiUFl5rHCNavIUr0gkBhnN5hcq2mvfECBEVVm8dlrvHHa4Ti +ujhC6DGgrOUIPok+rXJoELvmrYa72H04uHwkOkESkuHO+gHuRJ7nIxH/BktGKsol +Unp+CNBcKQKBgQCtn+NvkjWeTII2vFYr5xIZkM+NPsDh/Nkl36v5WyU8GgH+zAbL +QnkUTskNSQ/NhV5JFUhmH+Zvvh/cG5RzFDuqc1VUK+HMSg1L0c3NRydiAxStXnby +X1+U/KRFDYAzyNOW+Alj74RFeCbo1pVfgnmwv/W3StjviRhtgiAbsM3XUQKBgQCg +E94P/vWtC9zetxfadVXhqsFEpZ3wlz9EG+p5vaKzaZR3nYIa1eE9zjpwLpWKRaGR +JF9xDGbgUOkmvtzhJFU8vEzEEaZ/DtwaB5tdUqJW9msliIwyDHjhhquOkG28y174 +wnEVZNOH1A82m2JbBjnmvon6sJ5T8O2gx8P3QgEPQQKBgQCcpSf/rJxMfEeZf4Kl +EeqSjPwFRKsC/8gBs63VJrmYh2XigL5tI2MnTdgts+4W1xC8avPiRoeaZ8YN6258 +n2tVyhJrm3F1cw/tFmLE0kuNkS0jOA/hzFeOkLJTZXR9npBzwNduQQ5i0HqQM9Qi +aIIVlhLZ0Jlqu6SLnJ74n34VnA== +-----END PRIVATE KEY----- diff --git a/examples/protocols/esp_http_client_mutual_auth/main/certs/client_cert.pem b/examples/protocols/esp_http_client_mutual_auth/main/certs/client_cert.pem new file mode 100644 index 00000000000..871e8952e75 --- /dev/null +++ b/examples/protocols/esp_http_client_mutual_auth/main/certs/client_cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDBDCCAeygAwIBAgIUa91CxumwjyUKK9AHnJ9upekrqlgwDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNjA1MjkwMzI5MTlaFw0zNjA1MjYw +MzI5MTlaMCIxIDAeBgNVBAMMF2VzcF9odHRwX2NsaWVudF9kc190ZXN0MIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsztCh5DaIh+KDzk6L5CEps8AJ6cw +xJffL/wjZc3yTOCjwDOX7UdDilizLobkzCBMm8lccafts2+jEAVaMrR1pQVO0c8J +5sDk1IHlNJVlq1UxnIl7JvbDjtAWp+cHYMRQSBXmHrkqJl45LPTdjC0FvdsB2oyj +HFmDHp3A3B/BwzdwyBzucG+Xa3OILZI0wsri0oDMyTw6Xg6SoXmLTQhbqCD38Ijt +d9AalLMElM+2j+IH5zVlnG+JpCrvzYy7pY7gIUmjLFBewSpEiPjxWjrFJ0teXyTj +smhd/IK5rVQw2v1wjKT1Wc5lgRDifZTVNRxDIepr+m4Ck3clRfyesSslhQIDAQAB +o0IwQDAdBgNVHQ4EFgQU7jBC4N9rFhRFkzBq0KcXGRsNatcwHwYDVR0jBBgwFoAU +RhAl6T8w7DT7+eWR1wLCoAiGBeswDQYJKoZIhvcNAQELBQADggEBAHWHKGJ0Z4+b +rw25oZ9ZK2pZkQcvAG1HBQxHoNBRmYruitvALnQhq7UuHlAoMFxBmkJ61FRjgA1x +zGAZsfTCy/Agv2lfVf6LiRd+Rl134rzHLlCoTg0tZC5uFcS/6W5mbfXIviYaPRaf +RMgpM1ZPYMF5VsX0B6CJH6I+UHoZVLTb1nz5gu01WiKKULTygW70xyk5B0osiK9C +UDCOQ7jcYrjYNZulwY2qv+Kg3PH6aFR9xT+RUp1fDb0HJk0/M0YDOdh4QUzm29UL +KCCM+E25tan4Nvgg1jQIw3dppWc6dCSE0IgCCVY+O3aZqnfRugHl7NCxJ5pHCyPr +zV9BSaFx8Uk= +-----END CERTIFICATE----- diff --git a/examples/protocols/esp_http_client_mutual_auth/main/certs/client_key.pem b/examples/protocols/esp_http_client_mutual_auth/main/certs/client_key.pem new file mode 100644 index 00000000000..1e72440c6b8 --- /dev/null +++ b/examples/protocols/esp_http_client_mutual_auth/main/certs/client_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCzO0KHkNoiH4oP +OTovkISmzwAnpzDEl98v/CNlzfJM4KPAM5ftR0OKWLMuhuTMIEybyVxxp+2zb6MQ +BVoytHWlBU7RzwnmwOTUgeU0lWWrVTGciXsm9sOO0Ban5wdgxFBIFeYeuSomXjks +9N2MLQW92wHajKMcWYMencDcH8HDN3DIHO5wb5drc4gtkjTCyuLSgMzJPDpeDpKh +eYtNCFuoIPfwiO130BqUswSUz7aP4gfnNWWcb4mkKu/NjLuljuAhSaMsUF7BKkSI ++PFaOsUnS15fJOOyaF38grmtVDDa/XCMpPVZzmWBEOJ9lNU1HEMh6mv6bgKTdyVF +/J6xKyWFAgMBAAECggEABhTu3dUbTx4dMVq37+Y/rL4IcNAFC8llgDuA41L43lqf +DoYRqTJfsdbvBEH9Nd9WCiaWivVvpO+XAROqwF626+nBXMWMPfbHV7NYA4/wOmny +fC/UFySMypCivCwkPXMxOt5Mso0w8iAEtlL8Hjtm3vmKrLpUoevNqGEiVWCEfYBv +h7Yc7uKcB90WyZ3LtgmghV46kDFvaSMMdPTqVU6jgCTpq6yBCntpeYz1VqcnDhZt +nU6i/vinJz8BMDXIDbVonie0lZ9tCWnYwHaWCkIPy9Cb5eqlldJfiN1CYz9MFGGs +oymeTF+lrtPphRTakpziDkxBM/AwnIkx5HQTsxF4jwKBgQD882SmrGvbshJLYMVl +4UznqDoP59M7G5kwQ/j579VKkhbrKrV0JvOEYQuJx18d7drDyY7eLRF6KYRLYOhG +FWznMpNhRYQMcWcAqNLHb5W4FSsGGRt8Q1qmmKQmO192FZVveG0czz2FAPY4uSBX +4va+mJszMrnfEApqW8TdOrEwYwKBgQC1ZF5qTQR9wZPj+65mGwfYei1EP1OEE5xc +DWo9fsdDVxv4ykHTpURF+EftMkOF+b/GTgJHyEceZfgm/9dx15EeQuHqpNDjffUD +y/b8GVT7NB2nR2m3ztXYBHsRK7uYGbgVtB+7w1gFpcZRx92SvITy+EjQRTs9z4n/ +Ru2y8P+S9wKBgF0bTfIXxz+/xQIf5akBjCg9ANo378Vy/CkK7As7n1vqeCsptk7B +w6L3gaK+UyGWGo80krTvC97Wh06jpfueCU70i9EjIF7gIxTYD3W/efGfQQ3mkfpk +ZGqsBsfX1OSHP1Efl7IiCjf5yafJZMFU1pQDYiUvR8F2iw7pJoZ0AyKXAoGBAKKv +RCaesLqBFTzSC5Y0BBNZcKPXD/ZTCFdfCLviqqBwzfuSmvtRLCx9AzVvcTQFzMP0 +TwNGUtKmrat8piPKLLMxVSF3dImz/D3NftSXe6pZEYdn+x8JeK1nR2EdEgDWgE2m +4RcrmhRmm7nZQZZLUgoAOH3iucE0FBZJ7QIiN3X9AoGBAJmGvBoa6ZQpbRXZbVfv +qvz+gzwN2fO3t7iWGaHVCGygnSaplCGl/MfVtb/ak3AG5NcjwDqjld8X7RE9dSk2 +l12Y3AtsyaDozLg5KXdwt+F1Pdhju9WB3pd5O25Ocn5JU1jUqL+huW+d7WhUDPO7 +PHTgtfI15eDu5LinkNPD73hE +-----END PRIVATE KEY----- diff --git a/examples/protocols/esp_http_client_mutual_auth/main/certs/esp_secure_cert_data/esp_secure_cert.bin b/examples/protocols/esp_http_client_mutual_auth/main/certs/esp_secure_cert_data/esp_secure_cert.bin new file mode 100644 index 00000000000..f2655a7b753 Binary files /dev/null and b/examples/protocols/esp_http_client_mutual_auth/main/certs/esp_secure_cert_data/esp_secure_cert.bin differ diff --git a/examples/protocols/esp_http_client_mutual_auth/main/certs/esp_secure_cert_data/hmac_key.bin b/examples/protocols/esp_http_client_mutual_auth/main/certs/esp_secure_cert_data/hmac_key.bin new file mode 100644 index 00000000000..50910577bcd --- /dev/null +++ b/examples/protocols/esp_http_client_mutual_auth/main/certs/esp_secure_cert_data/hmac_key.bin @@ -0,0 +1 @@ +BĔ<|Spt3Nksc d \ No newline at end of file diff --git a/examples/protocols/esp_http_client_mutual_auth/main/certs/esp_secure_cert_data/qemu_efuse.bin b/examples/protocols/esp_http_client_mutual_auth/main/certs/esp_secure_cert_data/qemu_efuse.bin new file mode 100644 index 00000000000..742c5e07f65 Binary files /dev/null and b/examples/protocols/esp_http_client_mutual_auth/main/certs/esp_secure_cert_data/qemu_efuse.bin differ diff --git a/examples/protocols/esp_http_client_mutual_auth/main/certs/server_cert.pem b/examples/protocols/esp_http_client_mutual_auth/main/certs/server_cert.pem new file mode 100644 index 00000000000..bb56c04e252 --- /dev/null +++ b/examples/protocols/esp_http_client_mutual_auth/main/certs/server_cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDAzCCAeugAwIBAgIUcDdlf0FDC5KPEcTnJG58JcXKGtAwDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNjA0MDgwMzAzMzdaFw0zNjA0MDUw +MzAzMzdaMCExHzAdBgNVBAMMFmVzcF9tdXR1YWxfYXV0aF9zZXJ2ZXIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4AfXPbz06C1kwJHSkMWgzVHgz85Pb +bF7aRzImfHR9nfz2hNJ2Yg8Apdxt/xWM1n2uDDlVWjg0vK50X3DK4oz1Lk32//IE +iYajZuiFsO6ZBNHgy1rehJUlk92Z4fklhpEgpVT67Ph6o8yhPDjNgPM03eJsl6yC +lawxETSXU+dOhjzfuiT0IA74UOhTVIfVe8E0AlXGqjrN5QkKP5Xb74w1ovIZPOzy +vWNjnzuwg2MKWHTFOolPpKft4GUqPvMD41qNfymUL46JxtONcKwx//Q5hO0bSAnB +GJ1Brqca366ObF6Xh+dAXALnxVkbpauoJ7ZqdyiJg6YANovawe/oZZXpAgMBAAGj +QjBAMB0GA1UdDgQWBBTOXVwKpLmhvv/2Nqe9E4Hx8VYsrzAfBgNVHSMEGDAWgBRG +ECXpPzDsNPv55ZHXAsKgCIYF6zANBgkqhkiG9w0BAQsFAAOCAQEAdgeCBgBgu8N7 +r9ycEytvs1AKxf2P3c6VFOlXF8rRNq/+8a8CoyGm4cF8u7AyvQQJZya6Luv+xg+/ +tguXIbw5goNenNkEuUOHRlo39vZ/pHe/HGCAxHatWG8qgoahXCZh3OSEmHfDwEDc +RbYTJXGb98VynFzuxQ4RSDXb+WxhslwGCV2NXrF6gLS1FgZDKpJChmhkvw/ubdxY +QS3fSToWlvs7PSyZi2Ei5XuGfthQPT+PZpyHZufXX9evLDdGOB/5SOz+p0SSBXcZ +he21O0jovx0RDSnnq6kdG+qHfUrwOt5TGJ8tIJQZuKi9VUIMEONC7RMV4u1T00j9 +8VtZEG2gfg== +-----END CERTIFICATE----- diff --git a/examples/protocols/esp_http_client_mutual_auth/main/certs/server_key.pem b/examples/protocols/esp_http_client_mutual_auth/main/certs/server_key.pem new file mode 100644 index 00000000000..a0bc9da5ec7 --- /dev/null +++ b/examples/protocols/esp_http_client_mutual_auth/main/certs/server_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC4AfXPbz06C1kw +JHSkMWgzVHgz85PbbF7aRzImfHR9nfz2hNJ2Yg8Apdxt/xWM1n2uDDlVWjg0vK50 +X3DK4oz1Lk32//IEiYajZuiFsO6ZBNHgy1rehJUlk92Z4fklhpEgpVT67Ph6o8yh +PDjNgPM03eJsl6yClawxETSXU+dOhjzfuiT0IA74UOhTVIfVe8E0AlXGqjrN5QkK +P5Xb74w1ovIZPOzyvWNjnzuwg2MKWHTFOolPpKft4GUqPvMD41qNfymUL46JxtON +cKwx//Q5hO0bSAnBGJ1Brqca366ObF6Xh+dAXALnxVkbpauoJ7ZqdyiJg6YANova +we/oZZXpAgMBAAECggEAHNtEgaUP9K6w7EmF9/Xo7HEpGfnvTHBAqSSUgHqbXiki +RvAU0rdADmRJyapA5pcAm1npZ9w/3O1gwnlGhrAOq6ijq2z/Yadw85ErkpkTt0WN +Ox6eVS/KSrFXWvmYM+1YNxwWfp9zvEU8eGX/ASNhgKnyB1ZQmc3/ygDJFEOhOQFe +rAMntZvoEiQkkQIL45iKLceFfnTQ4iJNLRDvkOxt98OYgjOkVfLZvYXgTTH0r1mn +x2M3ITVVcU4OGs5AFQgbV/9fB5raPCAnroQkRIG69XD+uOJmRX04WvW8Hk9BQXk0 +aAkPSa5yPdZbpjsV5aYaQYoQodblN2XKuVxoqmj6bQKBgQD+Bo5lDmhxNGC7x64B +6pTLAmSM2oT/mHRGRUyR+YB/Mf3mw5MhoBwZS5CuB1UaOaFBUx0b8SpfXfXMAhcF +483FcyYKK3B/ZV9u3EfvxCBiiLzwYqLQYaBp7efeQkAJzb+sIj0+ZE70Lh2IjspL +zhFh7WaMaxeU1IAfqvfRaATjewKBgQC5cBY19erRuztFkP2SeNYHlIj0Y1uF6QRX +8ds2QLQsnj7J4WU/5VgTKWTywx7gn82/hWcJAs2k1PEUspRfMLwpChtOxvYHEosM +4a9fCBKlA+KPc4NpSGaC2M9VoJIJrgo7JjzJMFS6qSJ3zbmWUQh1XCLliNWNey2s +LkiNrA0M6wKBgQCG5wxv9nrYw6wrjRuHwQBL33VuqA3Bf0EgoGTNkOcApZflGS/l +x5WkiVDIWvSC/N/6RR1MXYLXKpsCQInhgt0gYsps1CzmOvu3cBxz5IAeU+ei8X7t +kysRllpw2lYP3shPrc9AdxzG6Eae4tXj9AefLegr4iOf0kpIhw8cklUmSQKBgQC1 +5qRy9DMO3unqeKq0poHU17hsepZJymSvXBjbpCbZabVP1SC7x95YlY9nr003rKpo +B5UlurE80oFV+0MeCTFZ1IcrBHJMR71MuomL3+BiLGhurTIn8ZRVIBZp+WOnyShS +E1UnSZijrcuY154IPJ7eeK3mmQ5ahY0szA3xoub+VwKBgQDUqNuMGgDmZo5O2Jsz +0J43BbRfqWXyQ9Q/nQQftHoxPEWkEQSoa3y58zWlYJKN4JoUU/UNR7dqdP5rnbhZ +f/pPXWdmhWQQlImgAfqJNEAZscA2NgsW/BOcot7zl/ZMgMOyDNXHoDSHenKPksKb +9POv9y/PnUWSUfG/WFwMEH65iQ== +-----END PRIVATE KEY----- diff --git a/examples/protocols/esp_http_client_mutual_auth/main/idf_component.yml b/examples/protocols/esp_http_client_mutual_auth/main/idf_component.yml new file mode 100644 index 00000000000..dc9f88e897a --- /dev/null +++ b/examples/protocols/esp_http_client_mutual_auth/main/idf_component.yml @@ -0,0 +1,7 @@ +dependencies: + protocol_examples_common: + path: ${IDF_PATH}/examples/common_components/protocol_examples_common + esp_secure_cert_mgr: + version: ">=2.0.0" + rules: + - if: "idf_version >=5.0" diff --git a/examples/protocols/esp_http_client_mutual_auth/main/mutual_auth_example.c b/examples/protocols/esp_http_client_mutual_auth/main/mutual_auth_example.c new file mode 100644 index 00000000000..22dd79ea1b4 --- /dev/null +++ b/examples/protocols/esp_http_client_mutual_auth/main/mutual_auth_example.c @@ -0,0 +1,212 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +/* ESP HTTP Client Mutual Authentication Example + + 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 "esp_log.h" +#include "esp_err.h" +#include "nvs_flash.h" +#include "esp_event.h" +#include "esp_netif.h" +#include "protocol_examples_common.h" +#include "esp_http_client.h" + +#if CONFIG_EXAMPLE_MUTUAL_AUTH_USE_HW_RSA_DS +#include "esp_secure_cert_read.h" +#if CONFIG_MBEDTLS_VER_4_X_SUPPORT +#include "psa_crypto_driver_esp_rsa_ds_contexts.h" +#else +#include "esp_rsa_sign_alt.h" +#endif +#endif + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +static const char *TAG = "MUTUAL_AUTH"; + +#define URL_BUF_SIZE 256 + +/* CA cert for server verification (embedded in both modes) */ +extern const char ca_cert_pem_start[] asm("_binary_ca_cert_pem_start"); +extern const char ca_cert_pem_end[] asm("_binary_ca_cert_pem_end"); + +#if !CONFIG_EXAMPLE_MUTUAL_AUTH_USE_HW_RSA_DS +/* Software mode: client cert and key embedded as PEM */ +extern const char client_cert_pem_start[] asm("_binary_client_cert_pem_start"); +extern const char client_cert_pem_end[] asm("_binary_client_cert_pem_end"); +extern const char client_key_pem_start[] asm("_binary_client_key_pem_start"); +extern const char client_key_pem_end[] asm("_binary_client_key_pem_end"); +#endif + +/** + * Read the server URL from stdin. Used by pytest to inject the mTLS server address. + * Returns ESP_OK on success, ESP_FAIL on error or empty input. + */ +static esp_err_t read_url_from_stdin(char *url_buf, size_t buf_size) +{ + example_configure_stdin_stdout(); + ESP_LOGI(TAG, "Enter mutual auth server URL:"); + + if (fgets(url_buf, (int)buf_size, stdin) == NULL) { + ESP_LOGE(TAG, "Failed to read URL from stdin"); + return ESP_FAIL; + } + /* Strip trailing newline */ + size_t len = strlen(url_buf); + if (len > 0 && url_buf[len - 1] == '\n') { + url_buf[--len] = '\0'; + } + if (len == 0) { + ESP_LOGE(TAG, "Empty URL"); + return ESP_FAIL; + } + return ESP_OK; +} + +#if CONFIG_EXAMPLE_MUTUAL_AUTH_USE_HW_RSA_DS +/** + * DS peripheral mode: load client cert and DS context from esp_secure_cert partition. + */ +static void mutual_auth_with_ds(const char *url) +{ + char *dev_cert_buf = NULL; + uint32_t dev_cert_len = 0; + esp_ds_data_ctx_t *ds_ctx = NULL; + + esp_err_t ret = esp_secure_cert_get_device_cert(&dev_cert_buf, &dev_cert_len); + if (ret != ESP_OK || dev_cert_buf == NULL || dev_cert_len == 0) { + ESP_LOGE(TAG, "Failed to get device cert: %s", esp_err_to_name(ret)); + return; + } + + ds_ctx = esp_secure_cert_get_ds_ctx(); + if (ds_ctx == NULL) { + ESP_LOGE(TAG, "Failed to get DS context"); + esp_secure_cert_free_device_cert(dev_cert_buf); + return; + } + + size_t ca_cert_len = ca_cert_pem_end - ca_cert_pem_start; + esp_http_client_config_t config = { + .url = url, + .cert_pem = ca_cert_pem_start, + .cert_len = ca_cert_len, + .client_cert_pem = dev_cert_buf, + .client_cert_len = dev_cert_len, + .ds_data = ds_ctx, + .skip_cert_common_name_check = true, + .timeout_ms = 10000, + }; + + esp_http_client_handle_t client = esp_http_client_init(&config); + if (client == NULL) { + ESP_LOGE(TAG, "Failed to init HTTP client"); + goto cleanup; + } + + esp_err_t err = esp_http_client_perform(client); + int status = esp_http_client_get_status_code(client); + if (err == ESP_OK) { + ESP_LOGI(TAG, "HTTPS Mutual Auth Status = %d, content_length = %" PRId64, + status, esp_http_client_get_content_length(client)); + } else { + ESP_LOGE(TAG, "Request failed: %s", esp_err_to_name(err)); + } + + esp_http_client_cleanup(client); + +cleanup: + esp_secure_cert_free_ds_ctx(ds_ctx); + esp_secure_cert_free_device_cert(dev_cert_buf); +} + +#else /* Software key mode */ + +/** + * Software key mode: use embedded PEM client cert and key. + */ +static void mutual_auth_with_software_keys(const char *url) +{ + size_t ca_cert_len = ca_cert_pem_end - ca_cert_pem_start; + size_t client_cert_len = client_cert_pem_end - client_cert_pem_start; + size_t client_key_len = client_key_pem_end - client_key_pem_start; + + esp_http_client_config_t config = { + .url = url, + .cert_pem = ca_cert_pem_start, + .cert_len = ca_cert_len, + .client_cert_pem = client_cert_pem_start, + .client_cert_len = client_cert_len, + .client_key_pem = client_key_pem_start, + .client_key_len = client_key_len, + .skip_cert_common_name_check = true, + .timeout_ms = 10000, + }; + + esp_http_client_handle_t client = esp_http_client_init(&config); + if (client == NULL) { + ESP_LOGE(TAG, "Failed to init HTTP client"); + return; + } + + esp_err_t err = esp_http_client_perform(client); + int status = esp_http_client_get_status_code(client); + if (err == ESP_OK) { + ESP_LOGI(TAG, "HTTPS Mutual Auth Status = %d, content_length = %" PRId64, + status, esp_http_client_get_content_length(client)); + } else { + ESP_LOGE(TAG, "Request failed: %s", esp_err_to_name(err)); + } + + esp_http_client_cleanup(client); +} +#endif /* CONFIG_EXAMPLE_MUTUAL_AUTH_USE_HW_RSA_DS */ + +static void mutual_auth_task(void *pvParameters) +{ + char url_buf[URL_BUF_SIZE]; + if (read_url_from_stdin(url_buf, sizeof(url_buf)) != ESP_OK) { + goto done; + } + +#if CONFIG_EXAMPLE_MUTUAL_AUTH_USE_HW_RSA_DS + mutual_auth_with_ds(url_buf); +#else + mutual_auth_with_software_keys(url_buf); +#endif + +done: + ESP_LOGI(TAG, "Finish mutual auth example"); + vTaskDelete(NULL); +} + +void app_main(void) +{ + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + ESP_ERROR_CHECK(example_connect()); + + ESP_LOGI(TAG, "Connected, starting mutual auth example"); + xTaskCreate(mutual_auth_task, "mutual_auth", 8192, NULL, 5, NULL); +} diff --git a/examples/protocols/esp_http_client_mutual_auth/partitions_esp_secure_cert.csv b/examples/protocols/esp_http_client_mutual_auth/partitions_esp_secure_cert.csv new file mode 100644 index 00000000000..5afedd6fcc5 --- /dev/null +++ b/examples/protocols/esp_http_client_mutual_auth/partitions_esp_secure_cert.csv @@ -0,0 +1,8 @@ +# Partition table for DS test using esp_secure_cert partition. +# esp_secure_cert_mgr looks for type 0x3F (custom) and name "esp_secure_cert". +# Flash main/certs/esp_secure_cert_data/esp_secure_cert.bin to the esp_secure_cert partition. +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, , 0x6000, +phy_init, data, phy, , 0x1000, +esp_secure_cert, 0x3F, 0x00, , 0x4000, +factory, app, factory, , 0x100000, diff --git a/examples/protocols/esp_http_client_mutual_auth/pytest_esp_http_client_mutual_auth.py b/examples/protocols/esp_http_client_mutual_auth/pytest_esp_http_client_mutual_auth.py new file mode 100644 index 00000000000..8b49a6cd58b --- /dev/null +++ b/examples/protocols/esp_http_client_mutual_auth/pytest_esp_http_client_mutual_auth.py @@ -0,0 +1,230 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 +import http.server +import logging +import multiprocessing +import os +import socket +import ssl +from typing import Any + +import pytest +from common_test_methods import get_host_ip4_by_dest_ip +from pytest_embedded_idf.utils import idf_parametrize +from pytest_embedded_qemu.app import QemuApp +from pytest_embedded_qemu.dut import QemuDut + +# --------------------------------------------------------------------------- +# mTLS HTTPS server (runs in a subprocess during tests) +# --------------------------------------------------------------------------- + + +def _https_mtls_request_handler() -> type[http.server.BaseHTTPRequestHandler]: + """Request handler for mutual TLS: responds 200 OK to GET, logs at INFO.""" + + class RequestHandler(http.server.BaseHTTPRequestHandler): + protocol_version = 'HTTP/1.1' + + def do_GET(self) -> None: + logging.info('[HTTPS server] GET from %s', self.address_string()) + self.send_response(200) + self.send_header('Content-Length', '0') + self.end_headers() + + def log_message(self, _format: str, *args: object) -> None: + logging.info( + '[HTTPS server] %s - - [%s] %s', self.address_string(), self.log_date_time_string(), _format % args + ) + + return RequestHandler + + +class _MTLSHTTPServer(http.server.HTTPServer): + """HTTPServer that wraps each connection with TLS and logs handshake success/failure.""" + + def __init__( + self, + server_address: tuple[str, int], + RequestHandlerClass: type[http.server.BaseHTTPRequestHandler], + ssl_context: ssl.SSLContext, + ) -> None: + super().__init__(server_address, RequestHandlerClass) + self.ssl_context = ssl_context + + def get_request(self) -> tuple[socket.socket, Any]: + conn, addr = self.socket.accept() + try: + wrapped = self.ssl_context.wrap_socket(conn, server_side=True) + logging.info('[HTTPS server] TLS handshake OK from %s:%s', addr[0], addr[1]) + return wrapped, addr + except ssl.SSLError as e: + logging.warning('[HTTPS server] TLS handshake failed from %s:%s: %s', addr[0], addr[1], e) + conn.close() + raise + + def server_bind(self) -> None: + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + super().server_bind() + + +def start_https_server_mutual_tls( + server_cert: str, + server_key: str, + client_ca: str, + host: str, + port: int, +) -> None: + """ + Start an HTTPS server that requires and verifies client certificates (mutual TLS). + Uses server_cert/server_key for the server TLS identity and client_ca to verify the client cert. + """ + request_handler = _https_mtls_request_handler() + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ssl_context.load_cert_chain(certfile=server_cert, keyfile=server_key) + ssl_context.verify_mode = ssl.CERT_REQUIRED + ssl_context.load_verify_locations(cafile=client_ca) + httpd = _MTLSHTTPServer((host, port), request_handler, ssl_context) + logging.info('[HTTPS server] Listening on %s:%s (mTLS)', host, port) + httpd.serve_forever() + + +# --------------------------------------------------------------------------- +# Paths and constants +# --------------------------------------------------------------------------- + +_EXAMPLE_DIR = os.path.dirname(__file__) +_CERTS_DIR = os.path.join(_EXAMPLE_DIR, 'main', 'certs') +_DS_DATA_DIR = os.path.join(_CERTS_DIR, 'esp_secure_cert_data') + +_QEMU_EFUSE_FILE = os.path.join(_DS_DATA_DIR, 'qemu_efuse.bin') +_HMAC_KEY_FILE = os.path.join(_DS_DATA_DIR, 'hmac_key.bin') +_ESP_SECURE_CERT_PARTITION = os.path.join(_DS_DATA_DIR, 'esp_secure_cert.bin') + +# qemu_extra_args must be a single string (plugin passes it to shlex.split). +_QEMU_EXTRA_DS = ( + f'-drive file={_QEMU_EFUSE_FILE},if=none,format=raw,id=efuse ' + '-global driver=nvram.esp32c3.efuse,property=drive,value=efuse ' + '-global driver=timer.esp32c3.timg,property=wdt_disable,value=true' +) + + +# --------------------------------------------------------------------------- +# DS eFuse and artifact helpers +# --------------------------------------------------------------------------- + + +def _ensure_ds_efuse_image() -> None: + """ + Verify DS artifacts (eFuse image, HMAC key, secure cert partition) are present + before QEMU starts. All three are committed under main/certs/esp_secure_cert_data/; + see the DS provisioning section in the example README to regenerate them. + """ + for path in (_ESP_SECURE_CERT_PARTITION, _HMAC_KEY_FILE, _QEMU_EFUSE_FILE): + if not os.path.isfile(path): + pytest.skip( + f'DS test requires {os.path.basename(path)}. ' + 'See the DS peripheral mode section in the example README for provisioning steps.' + ) + + +@pytest.fixture(autouse=True) +def _ensure_ds_efuse_before_qemu(request: pytest.FixtureRequest) -> None: + """Ensure eFuse image exists before QEMU starts for DS tests.""" + if 'qemu_ds' not in request.node.name: + return + _ensure_ds_efuse_image() + + +# --------------------------------------------------------------------------- +# Helper: start mTLS server and send URL to device +# --------------------------------------------------------------------------- + + +def _run_mtls_test( + dut: QemuDut, + server_port: int, + client_ca: str, + expect_success: bool, +) -> None: + """ + Start mTLS server, wait for device to request URL, send it, and verify outcome. + """ + server_cert = os.path.join(_CERTS_DIR, 'server_cert.pem') + server_key = os.path.join(_CERTS_DIR, 'server_key.pem') + + server_proc = multiprocessing.Process( + target=start_https_server_mutual_tls, + args=(server_cert, server_key, client_ca, '0.0.0.0', server_port), + ) + server_proc.daemon = True + server_proc.start() + logging.info('HTTPS server with mutual TLS started on port %s', server_port) + try: + ip_address = dut.expect(r'IPv4 address: (\d+\.\d+\.\d+\.\d+)', timeout=60)[1].decode() + host_ip = get_host_ip4_by_dest_ip(ip_address) + dut.expect('Enter mutual auth server URL:', timeout=30) + dut.write(f'https://{host_ip}:{server_port}\n') + + if expect_success: + dut.expect('HTTPS Mutual Auth Status = 200', timeout=30) + logging.info('mTLS test passed: status 200') + else: + dut.expect('mbedtls_ssl_handshake returned -0x7780', timeout=30) + dut.expect('Request failed:', timeout=30) + logging.info('Negative test passed: connection failed as expected') + + dut.expect('Finish mutual auth example', timeout=10) + finally: + server_proc.terminate() + server_proc.join(timeout=5) + if server_proc.is_alive(): + server_proc.kill() + + +# --------------------------------------------------------------------------- +# Test: software key mutual TLS +# --------------------------------------------------------------------------- + + +@pytest.mark.host_test +@pytest.mark.qemu +@pytest.mark.parametrize('config', ['default'], indirect=True) +@idf_parametrize('target', ['esp32c3'], indirect=['target']) +def test_mutual_auth_software_keys(app: QemuApp, dut: QemuDut) -> None: + """QEMU test: mutual TLS with embedded software client cert + key.""" + if os.environ.get('IDF_TOOLCHAIN') == 'clang': + pytest.skip('QEMU mTLS test not supported with clang toolchain (Docker SLIRP networking issue)') + client_ca = os.path.join(_CERTS_DIR, 'ca_cert.pem') + _run_mtls_test(dut, server_port=8070, client_ca=client_ca, expect_success=True) + + +# --------------------------------------------------------------------------- +# Tests: DS peripheral mutual TLS (migrated from esp_http_client) +# --------------------------------------------------------------------------- + + +@pytest.mark.host_test +@pytest.mark.qemu +@pytest.mark.parametrize('config', ['qemu_ds'], indirect=True) +@idf_parametrize('target', ['esp32c3'], indirect=['target']) +@pytest.mark.parametrize('qemu_extra_args', [_QEMU_EXTRA_DS], indirect=True) +def test_mutual_auth_ds(app: QemuApp, dut: QemuDut) -> None: + """QEMU + DS peripheral test: mTLS using DS peripheral for client key.""" + if os.environ.get('IDF_TOOLCHAIN') == 'clang': + pytest.skip('DS QEMU test not supported with clang toolchain (Docker SLIRP networking issue)') + client_ca = os.path.join(_CERTS_DIR, 'ca_cert.pem') + _run_mtls_test(dut, server_port=8070, client_ca=client_ca, expect_success=True) + + +@pytest.mark.host_test +@pytest.mark.qemu +@pytest.mark.parametrize('config', ['qemu_ds'], indirect=True) +@idf_parametrize('target', ['esp32c3'], indirect=['target']) +@pytest.mark.parametrize('qemu_extra_args', [_QEMU_EXTRA_DS], indirect=True) +def test_mutual_auth_ds_fails_wrong_credentials(app: QemuApp, dut: QemuDut) -> None: + """Negative test: server uses wrong CA, rejects device client cert.""" + if os.environ.get('IDF_TOOLCHAIN') == 'clang': + pytest.skip('DS QEMU test not supported with clang toolchain (Docker SLIRP networking issue)') + # Wrong CA: server uses server_cert as client CA (won't verify our client cert) + wrong_ca = os.path.join(_CERTS_DIR, 'server_cert.pem') + _run_mtls_test(dut, server_port=8071, client_ca=wrong_ca, expect_success=False) diff --git a/examples/protocols/esp_http_client_mutual_auth/sdkconfig.ci.default b/examples/protocols/esp_http_client_mutual_auth/sdkconfig.ci.default new file mode 100644 index 00000000000..e4509cb5601 --- /dev/null +++ b/examples/protocols/esp_http_client_mutual_auth/sdkconfig.ci.default @@ -0,0 +1,5 @@ +# Software key mutual TLS test over QEMU ethernet +CONFIG_EXAMPLE_CONNECT_WIFI=n +CONFIG_ETHERNET_SPI_SUPPORT=n +CONFIG_ETHERNET_OPENETH_SUPPORT=y +CONFIG_MBEDTLS_TLS_CLIENT_ONLY=y diff --git a/examples/protocols/esp_http_client_mutual_auth/sdkconfig.ci.qemu_ds b/examples/protocols/esp_http_client_mutual_auth/sdkconfig.ci.qemu_ds new file mode 100644 index 00000000000..5f13c929ab9 --- /dev/null +++ b/examples/protocols/esp_http_client_mutual_auth/sdkconfig.ci.qemu_ds @@ -0,0 +1,8 @@ +# DS peripheral mutual TLS test over QEMU ethernet +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_esp_secure_cert.csv" +CONFIG_EXAMPLE_MUTUAL_AUTH_USE_HW_RSA_DS=y +CONFIG_EXAMPLE_CONNECT_WIFI=n +CONFIG_ETHERNET_SPI_SUPPORT=n +CONFIG_ETHERNET_OPENETH_SUPPORT=y +CONFIG_MBEDTLS_TLS_CLIENT_ONLY=y diff --git a/examples/protocols/esp_http_client_mutual_auth/sdkconfig.defaults b/examples/protocols/esp_http_client_mutual_auth/sdkconfig.defaults new file mode 100644 index 00000000000..5430aef3bca --- /dev/null +++ b/examples/protocols/esp_http_client_mutual_auth/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_MBEDTLS_TLS_CLIENT_ONLY=y