mirror of
https://github.com/espressif/esp-idf.git
synced 2026-09-22 13:01:16 +03:00
fix(esp_http_server): merge release/v6.0 to resolve backport conflict
Merge branch 'release/v6.0' into backport-48352-resolve to resolve a conflict in esp_httpd_priv.h between this backport's sdkconfig.h include and the ctrl-socket semaphore's freertos/semphr.h include.
This commit is contained in:
@@ -26,6 +26,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+:
|
||||
|
||||
@@ -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()
|
||||
104
examples/protocols/esp_http_client_mutual_auth/README.md
Normal file
104
examples/protocols/esp_http_client_mutual_auth/README.md
Normal file
@@ -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-----
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
<EFBFBD><EFBFBD>B<EFBFBD><EFBFBD>Ĕ<|S<><53>pt3<74>Nk<4E>sc<73>d<><64><10>
|
||||
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
|
||||
@@ -22,7 +22,7 @@ def test_examples_protocol_http_request(dut: Dut) -> None:
|
||||
# check and log bin size
|
||||
binary_file = os.path.join(dut.app.binary_path, 'http_request.bin')
|
||||
bin_size = os.path.getsize(binary_file)
|
||||
logging.info('http_request_bin_size : {}KB'.format(bin_size // 1024))
|
||||
logging.info(f'http_request_bin_size : {bin_size // 1024}KB')
|
||||
# start test
|
||||
dut.expect(r'DNS lookup succeeded.', timeout=30)
|
||||
# check if connected or not
|
||||
@@ -30,7 +30,7 @@ def test_examples_protocol_http_request(dut: Dut) -> None:
|
||||
dut.expect(' ... socket send success')
|
||||
dut.expect(' ... set socket receiving timeout success')
|
||||
# check server response
|
||||
dut.expect(r'HTTP/1.0 200 OK')
|
||||
dut.expect(r'HTTP/1.1 200 OK')
|
||||
# read from the socket completed
|
||||
dut.expect('... done reading from socket. Last read return=0 errno=128')
|
||||
dut.expect(r'(\d)...')
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"unplugin-fonts": "~1.4.0",
|
||||
"unplugin-vue-components": "~30.0.0",
|
||||
"unplugin-vue-router": "~0.16.0",
|
||||
"vite": "~7.1.11",
|
||||
"vite": "~7.3.2",
|
||||
"vite-plugin-vue-layouts-next": "~1.0.0",
|
||||
"vite-plugin-vuetify": "~2.1.2",
|
||||
"vue-router": "~4.6.3"
|
||||
|
||||
@@ -389,6 +389,7 @@ def test_examples_protocol_https_request(dut: Dut) -> None:
|
||||
|
||||
|
||||
@pytest.mark.wifi_ap
|
||||
@pytest.mark.esp32c2_rev2
|
||||
@pytest.mark.xtal_26mhz
|
||||
@pytest.mark.parametrize(
|
||||
'config, baud',
|
||||
@@ -399,6 +400,7 @@ def test_examples_protocol_https_request(dut: Dut) -> None:
|
||||
)
|
||||
@idf_parametrize('target', ['esp32c2'], indirect=['target'])
|
||||
def test_examples_protocol_https_request_rom_impl(dut: Dut) -> None:
|
||||
write_time_to_nvs(dut)
|
||||
# Connect to AP
|
||||
if dut.app.sdkconfig.get('EXAMPLE_WIFI_SSID_PWD_FROM_STDIN') is True:
|
||||
dut.expect('Please input ssid password:')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
CONFIG_IDF_TARGET="esp32c2"
|
||||
CONFIG_XTAL_FREQ_26=y
|
||||
CONFIG_ESP32C2_REV_MIN_200=y
|
||||
CONFIG_EXAMPLE_CONNECT_WIFI=y
|
||||
CONFIG_EXAMPLE_WIFI_SSID_PWD_FROM_STDIN=y
|
||||
# TODO: IDF-15012
|
||||
CONFIG_MBEDTLS_USE_CRYPTO_ROM_IMPL=n
|
||||
CONFIG_MBEDTLS_USE_CRYPTO_ROM_IMPL=y
|
||||
|
||||
Reference in New Issue
Block a user