diff --git a/firmware/README.md b/firmware/README.md index 40771af..b6a3a35 100644 --- a/firmware/README.md +++ b/firmware/README.md @@ -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 :443 -showcerts :443 -showcerts 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); diff --git a/firmware/sdkconfig.defaults b/firmware/sdkconfig.defaults index c9fee64..c6f1838 100644 --- a/firmware/sdkconfig.defaults +++ b/firmware/sdkconfig.defaults @@ -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.