HTTPS trust: use the public CA bundle + one missing root, not a pinned cert
Build and push server image / build-and-push (push) Successful in 33s
Build and push server image / build-and-push (push) Successful in 33s
Root-caused the earlier "No matching trusted root certificate found" failure properly this time by reading ESP-IDF's actual bundle-matching code (esp_crt_bundle.c): it looks up a trusted root by the ISSUER name of whatever certificate it can't otherwise validate, not by matching the presented certificate itself. The live server's chain ends in a GTS Root R4 certificate cross-signed by the old GlobalSign Root CA R1 (common Cloudflare/Google Trust Services practice, for compatibility with older/embedded clients) -- and ESP-IDF's current bundle snapshot has dropped that old GlobalSign root entirely, so the lookup came up empty. This was a general gap, not something specific to this one deployment's cert. Fix: keep the standard public CA bundle (esp_crt_bundle_attach) as the trust mechanism -- so any normal reverse-proxy cert (Let's Encrypt, etc.) works out of the box -- and add the one missing root on top via ESP-IDF's CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE mechanism (sdkconfig.defaults), which appends a project-supplied cert file to the bundle at build time. Fetched GlobalSign's official Root CA R1 cert and cryptographically verified (openssl verify) it actually validates the live server's certificate before embedding it -- see firmware/main/certs/additional_root_ca.pem (replaces the old tools_server_ca.pem, which pinned one exact certificate directly and would've broken for anyone else's reverse proxy). Confirmed working against the real deployment on hardware.
This commit is contained in:
+27
-18
@@ -96,30 +96,39 @@ The Tools Server field accepts either:
|
||||
manage-menu overlay data, the QR codes' own links) uses whichever
|
||||
scheme you enter.
|
||||
|
||||
The firmware does **not** use ESP-IDF's general public CA bundle --
|
||||
it pins one specific certificate, embedded at build time from
|
||||
[`main/certs/tools_server_ca.pem`](main/certs/tools_server_ca.pem) and
|
||||
trusted directly via `cert_pem` on every request. (The public bundle
|
||||
was tried first and rejected: it does an exact byte-level match against
|
||||
its compiled-in table, and a real-world root that's been re-issued
|
||||
under a new serial/signature but the same name and key -- as Google did
|
||||
for GTS Root R4 -- doesn't match it, confirmed on hardware.)
|
||||
The firmware trusts ESP-IDF's standard public CA bundle
|
||||
(`esp_crt_bundle_attach`) -- so any reverse proxy with a normal
|
||||
publicly-trusted certificate just works out of the box: Let's Encrypt,
|
||||
a Cloudflare-proxied hostname, or any other public CA.
|
||||
|
||||
This means a normal publicly-trusted certificate (Let's Encrypt, a
|
||||
Cloudflare-proxied hostname, etc.) does **not** automatically work --
|
||||
only whichever certificate is actually embedded in
|
||||
`certs/tools_server_ca.pem` is trusted. To point the device at a
|
||||
different reverse proxy, extract that proxy's actual certificate and
|
||||
replace the file's contents:
|
||||
One specific root is added on top of that bundle at build time, from
|
||||
[`main/certs/additional_root_ca.pem`](main/certs/additional_root_ca.pem)
|
||||
(wired in via `CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE_PATH` in
|
||||
[`sdkconfig.defaults`](sdkconfig.defaults)): **GlobalSign Root CA R1**.
|
||||
This isn't a workaround specific to one deployment -- it's needed
|
||||
because Cloudflare (via Google Trust Services) commonly cross-signs its
|
||||
GTS Root R4 chain with this old GlobalSign root for backward
|
||||
compatibility with older/embedded clients, and ESP-IDF's current bundle
|
||||
snapshot has removed it (deprecated from modern trust stores). Without
|
||||
it, ESP-IDF's chain validation -- which looks up a trusted root by the
|
||||
*issuer name* of whatever certificate it can't otherwise validate, not
|
||||
by matching the presented certificate itself -- comes up empty and the
|
||||
handshake fails with "No matching trusted root certificate found",
|
||||
confirmed on hardware against a real Cloudflare-fronted deployment.
|
||||
Since this is a common, not deployment-specific, gap, it's likely worth
|
||||
keeping even if you're not using Cloudflare.
|
||||
|
||||
If your proxy presents a certificate chain the bundle (plus this one
|
||||
addition) still doesn't validate -- an unusual public CA, or a private/
|
||||
self-signed cert with no public CA in the chain at all -- diagnose with:
|
||||
|
||||
```
|
||||
openssl s_client -connect <host>:443 -showcerts </dev/null
|
||||
```
|
||||
|
||||
Pick whichever certificate in the printed chain you want as the trust
|
||||
anchor (typically the root) and rebuild. A private/self-signed cert
|
||||
works exactly the same way -- there's no requirement that it chain to
|
||||
a public CA at all, since the device trusts this one file directly.
|
||||
and add whichever certificate in the chain is missing to
|
||||
`additional_root_ca.pem` (or a file of your own, if you'd rather not
|
||||
touch the committed one) alongside the existing entry, then rebuild.
|
||||
|
||||
The device does perform normal hostname verification (it's not
|
||||
skipped), so the Tools Server field's hostname has to match what the
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
idf_component_register(SRCS main.c wifi_provisioning.c frame_client.c qr_onboarding.c status_screen.c epd_draw.c next_button.c back_button.c combo_button.c manage_qr_overlay.c
|
||||
PRIV_REQUIRES esp_event nvs_flash esp_wifi esp_netif esp_http_server esp_http_client mbedtls dns_server epd7in3e qrcode epaper_fonts esp_driver_gpio
|
||||
EMBED_FILES root.html
|
||||
EMBED_TXTFILES certs/tools_server_ca.pem)
|
||||
EMBED_FILES root.html)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG
|
||||
A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv
|
||||
b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw
|
||||
MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i
|
||||
YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT
|
||||
aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ
|
||||
jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp
|
||||
xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp
|
||||
1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG
|
||||
snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ
|
||||
U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8
|
||||
9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E
|
||||
BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B
|
||||
AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz
|
||||
yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE
|
||||
38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP
|
||||
AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad
|
||||
DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME
|
||||
HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,21 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDejCCAmKgAwIBAgIQf+UwvzMTQ77dghYQST2KGzANBgkqhkiG9w0BAQsFADBX
|
||||
MQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEQMA4GA1UE
|
||||
CxMHUm9vdCBDQTEbMBkGA1UEAxMSR2xvYmFsU2lnbiBSb290IENBMB4XDTIzMTEx
|
||||
NTAzNDMyMVoXDTI4MDEyODAwMDA0MlowRzELMAkGA1UEBhMCVVMxIjAgBgNVBAoT
|
||||
GUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBMTEMxFDASBgNVBAMTC0dUUyBSb290IFI0
|
||||
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE83Rzp2iLYK5DuDXFgTB7S0md+8Fhzube
|
||||
Rr1r1WEYNa5A3XP3iZEwWus87oV8okB2O6nGuEfYKueSkWpz6bFyOZ8pn6KY019e
|
||||
WIZlD6GEZQbR3IvJx3PIjGov5cSr0R2Ko4H/MIH8MA4GA1UdDwEB/wQEAwIBhjAd
|
||||
BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zAd
|
||||
BgNVHQ4EFgQUgEzW63T/STaj1dj8tT7FavCUHYwwHwYDVR0jBBgwFoAUYHtmGkUN
|
||||
l8qJUC99BM00qP/8/UswNgYIKwYBBQUHAQEEKjAoMCYGCCsGAQUFBzAChhpodHRw
|
||||
Oi8vaS5wa2kuZ29vZy9nc3IxLmNydDAtBgNVHR8EJjAkMCKgIKAehhxodHRwOi8v
|
||||
Yy5wa2kuZ29vZy9yL2dzcjEuY3JsMBMGA1UdIAQMMAowCAYGZ4EMAQIBMA0GCSqG
|
||||
SIb3DQEBCwUAA4IBAQAYQrsPBtYDh5bjP2OBDwmkoWhIDDkic574y04tfzHpn+cJ
|
||||
odI2D4SseesQ6bDrarZ7C30ddLibZatoKiws3UL9xnELz4ct92vID24FfVbiI1hY
|
||||
+SW6FoVHkNeWIP0GCbaM4C6uVdF5dTUsMVs/ZbzNnIdCp5Gxmx5ejvEau8otR/Cs
|
||||
kGN+hr/W5GvT1tMBjgWKZ1i4//emhA1JG1BbPzoLJQvyEotc03lXjTaCzv8mEbep
|
||||
8RqZ7a2CPsgRbuvTPBwcOMBBmuFeU88+FSBX6+7iP0il8b4Z0QFqIwwMHfs/L6K1
|
||||
vepuoxtGzi4CZ68zJpiq1UvSqTbFJjtbD4seiMHl
|
||||
-----END CERTIFICATE-----
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "esp_wifi_default.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_http_client.h"
|
||||
#include "esp_crt_bundle.h"
|
||||
#include "esp_sleep.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
@@ -25,19 +26,6 @@ static const char *TAG = "frame_client";
|
||||
|
||||
static EventGroupHandle_t s_sta_event_group;
|
||||
|
||||
/* The exact CA certificate the tools server's reverse proxy presents,
|
||||
* embedded verbatim (not sourced from ESP-IDF's built-in root bundle --
|
||||
* that does an exact byte-level match against its compiled-in table,
|
||||
* and a semantically-identical "same name, same public key" root
|
||||
* re-issued under a different serial/signature, as Google did for GTS
|
||||
* Root R4, doesn't match it; confirmed on hardware, see
|
||||
* firmware/main/certs/tools_server_ca.pem for how this was extracted).
|
||||
* If the proxy's CA ever changes, re-extract with:
|
||||
* openssl s_client -connect <host>:443 -showcerts </dev/null
|
||||
* and replace this file with whichever cert in the chain you want as
|
||||
* the trust anchor (typically the root). */
|
||||
extern const char tools_server_ca_pem_start[] asm("_binary_tools_server_ca_pem_start");
|
||||
|
||||
/* Builds a full URL from cfg->toolsserver + a path (no leading slash),
|
||||
* appending cfg->access_token as ?token= if one's set. toolsserver is
|
||||
* normally a bare "host:port", defaulting to plain http; it may instead
|
||||
@@ -243,7 +231,7 @@ static frame_server_config_t fetch_frame_config(const frame_config_t *cfg)
|
||||
.url = url,
|
||||
.method = HTTP_METHOD_GET,
|
||||
.timeout_ms = CONFIG_FRAME_SERVER_CHECK_TIMEOUT_MS,
|
||||
.cert_pem = tools_server_ca_pem_start,
|
||||
.crt_bundle_attach = esp_crt_bundle_attach,
|
||||
};
|
||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||
|
||||
@@ -304,7 +292,7 @@ static void fetch_photo_info(const frame_config_t *cfg, char *location_line1, si
|
||||
.url = url,
|
||||
.method = HTTP_METHOD_GET,
|
||||
.timeout_ms = CONFIG_FRAME_SERVER_CHECK_TIMEOUT_MS,
|
||||
.cert_pem = tools_server_ca_pem_start,
|
||||
.crt_bundle_attach = esp_crt_bundle_attach,
|
||||
};
|
||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||
|
||||
@@ -364,7 +352,7 @@ static int fetch_face_labels(const frame_config_t *cfg, manage_face_label_t *out
|
||||
.url = url,
|
||||
.method = HTTP_METHOD_GET,
|
||||
.timeout_ms = CONFIG_FRAME_SERVER_CHECK_TIMEOUT_MS,
|
||||
.cert_pem = tools_server_ca_pem_start,
|
||||
.crt_bundle_attach = esp_crt_bundle_attach,
|
||||
};
|
||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||
|
||||
@@ -513,7 +501,7 @@ static esp_err_t fetch_and_display(const frame_config_t *cfg, fetch_action_t act
|
||||
.url = url,
|
||||
.method = action == FETCH_NORMAL ? HTTP_METHOD_GET : HTTP_METHOD_POST,
|
||||
.timeout_ms = CONFIG_FRAME_FETCH_TIMEOUT_MS,
|
||||
.cert_pem = tools_server_ca_pem_start,
|
||||
.crt_bundle_attach = esp_crt_bundle_attach,
|
||||
};
|
||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
CONFIG_LWIP_MAX_SOCKETS=16
|
||||
|
||||
# HTTPS trust for the tools server: the standard public CA bundle (works
|
||||
# for any normal reverse-proxy cert -- Let's Encrypt, a typical
|
||||
# Cloudflare-issued edge cert, etc.) PLUS one specific root ESP-IDF's
|
||||
# current bundle snapshot is missing: GlobalSign Root CA R1, which
|
||||
# Cloudflare (via Google Trust Services) cross-signs its GTS Root R4
|
||||
# chain with for backward compatibility with older/embedded clients --
|
||||
# extremely common, not unique to this deployment. Without it, ESP-IDF's
|
||||
# own chain-validation lookup (by issuer name, not a literal cert match)
|
||||
# comes up empty and the handshake fails with "No matching trusted root
|
||||
# certificate found", confirmed on hardware. See
|
||||
# main/certs/additional_root_ca.pem and firmware/README.md's HTTPS
|
||||
# section for how this was diagnosed/verified.
|
||||
CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE=y
|
||||
CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE_PATH="main/certs/additional_root_ca.pem"
|
||||
|
||||
# ESP32-C6 dev board has 8MB flash. A 2MB app partition (vs. the ~1MB
|
||||
# "single app" default) gives headroom for the HTTP client, TLS, and image
|
||||
# buffers still to come, without needing to fight for space on every build.
|
||||
|
||||
Reference in New Issue
Block a user