mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-02 11:10:54 +03:00
feat(esp_http_client_mutual_auth): add mutual TLS example
Closes https://github.com/espressif/esp-idf/issues/18393
This commit is contained in:
@@ -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 |
|
| 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 |
|
||||||
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- |
|
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- |
|
||||||
|
|
||||||
|
See `examples/protocols/esp_http_client_mutual_auth` for mutual TLS (mTLS) examples including DS peripheral support.
|
||||||
|
|
||||||
|
|||||||
@@ -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` 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
|
Basic HTTP Request
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ examples/protocols/esp_http_client:
|
|||||||
depends_components+:
|
depends_components+:
|
||||||
- esp_http_client
|
- 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:
|
examples/protocols/esp_local_ctrl:
|
||||||
<<: *default_rules
|
<<: *default_rules
|
||||||
disable+:
|
disable+:
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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`.
|
||||||
@@ -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})
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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-----
|
||||||
@@ -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-----
|
||||||
@@ -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-----
|
||||||
@@ -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-----
|
||||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
|||||||
|
�¾Bÿ›Ä”<|S¾ßpt3®Nk”sc†dø�à
|
||||||
BIN
Binary file not shown.
@@ -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-----
|
||||||
@@ -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-----
|
||||||
@@ -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"
|
||||||
@@ -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 <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <inttypes.h>
|
||||||
|
|
||||||
|
#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);
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
Can't render this file because it contains an unexpected character in line 2 and column 61.
|
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
CONFIG_MBEDTLS_TLS_CLIENT_ONLY=y
|
||||||
Reference in New Issue
Block a user