Fix HTTPS handshake: trust the actual cert the proxy presents, not Origin CA

The Tools Server hostname turned out to be Cloudflare-proxied, not a
direct connection to nginx -- so the ESP32 (and any browser) sees
Cloudflare's own edge certificate (issued by Google Trust Services),
never the Origin CA cert, which only ever sits on the Cloudflare-to-
origin leg. Confirmed on hardware: ESP_ERR_HTTP_CONNECT.

Tried switching to ESP-IDF's built-in public CA bundle instead
(esp_crt_bundle_attach) as the more general fix, but that also failed
on hardware ("No matching trusted root certificate found") -- the
bundle's copy of the relevant Google root has the same name and public
key as the live one but a different serial/signature (a reissue), and
the bundle does an exact byte-level match, not a semantic one.

Simplest reliable fix: embed the exact certificate the proxy actually
presents (extracted live via openssl s_client, see
firmware/main/certs/tools_server_ca.pem) and trust that directly via
cert_pem, sidestepping bundle-matching semantics entirely. Documented
in firmware/README.md how to re-extract if the proxy's CA ever changes.
This commit is contained in:
2026-07-19 12:09:08 -04:00
parent 62cf907d88
commit bb4f473bfa
5 changed files with 55 additions and 69 deletions
+16 -10
View File
@@ -25,12 +25,18 @@ static const char *TAG = "frame_client";
static EventGroupHandle_t s_sta_event_group;
/* Cloudflare's Origin CA root certs (RSA + ECC, both concatenated --
* mbedtls_x509_crt_parse() chains every cert in a PEM buffer into the
* trust store, so either one nginx presents validates), for trusting an
* https tools server whose reverse proxy terminates TLS with a
* Cloudflare-issued origin certificate. See firmware/main/certs/. */
extern const char cloudflare_origin_ca_pem_start[] asm("_binary_cloudflare_origin_ca_pem_start");
/* 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
@@ -237,7 +243,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 = cloudflare_origin_ca_pem_start,
.cert_pem = tools_server_ca_pem_start,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
@@ -298,7 +304,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 = cloudflare_origin_ca_pem_start,
.cert_pem = tools_server_ca_pem_start,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
@@ -358,7 +364,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 = cloudflare_origin_ca_pem_start,
.cert_pem = tools_server_ca_pem_start,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
@@ -498,7 +504,7 @@ static esp_err_t fetch_and_display(const frame_config_t *cfg, bool force_advance
.url = url,
.method = force_advance ? HTTP_METHOD_POST : HTTP_METHOD_GET,
.timeout_ms = CONFIG_FRAME_FETCH_TIMEOUT_MS,
.cert_pem = cloudflare_origin_ca_pem_start,
.cert_pem = tools_server_ca_pem_start,
};
esp_http_client_handle_t client = esp_http_client_init(&config);