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:
+16
-17
@@ -95,24 +95,23 @@ The Tools Server field accepts either:
|
|||||||
manage-menu overlay data, the QR codes' own links) uses whichever
|
manage-menu overlay data, the QR codes' own links) uses whichever
|
||||||
scheme you enter.
|
scheme you enter.
|
||||||
|
|
||||||
The firmware only trusts one certificate authority for the HTTPS case:
|
The firmware trusts the standard public CA bundle ESP-IDF ships
|
||||||
**Cloudflare's Origin CA** (both the RSA and ECC root, embedded at build
|
(`esp_crt_bundle_attach`, the same root store a browser trusts) -- so
|
||||||
time from [`main/certs/cloudflare_origin_ca.pem`](main/certs/cloudflare_origin_ca.pem),
|
any reverse proxy with a normal publicly-trusted certificate just
|
||||||
downloaded directly from Cloudflare's docs). This is the free, long-lived
|
works: Let's Encrypt, a Cloudflare-proxied hostname (Cloudflare's own
|
||||||
certificate Cloudflare issues for origin servers -- not a
|
edge certificate, issued by Google Trust Services or similar -- **not**
|
||||||
publicly-trusted cert, so ordinary browsers won't accept it directly, but
|
Cloudflare's Origin CA cert, which only ever sits on the Cloudflare-to-
|
||||||
it's exactly what a reverse proxy in front of a Cloudflare-proxied
|
origin leg and is never presented to a public client, ESP32 or browser
|
||||||
hostname presents. If your reverse proxy uses a different certificate
|
alike), or any other public CA. If your proxy uses a private/self-signed
|
||||||
(a real publicly-trusted one from Let's Encrypt, or a different private
|
cert instead (no public CA in the chain at all), the public bundle won't
|
||||||
CA), swap the PEM file's contents for that CA's root and rebuild.
|
trust it -- that's not supported today, would need switching back to
|
||||||
|
embedding that specific cert.
|
||||||
|
|
||||||
The device does perform normal hostname verification (it's not skipped),
|
The device does perform normal hostname verification (it's not
|
||||||
so the Tools Server field's hostname has to match what the certificate
|
skipped), so the Tools Server field's hostname has to match what the
|
||||||
was actually issued for -- a bare LAN IP address (`https://192.168.1.50`)
|
certificate was actually issued for -- a bare LAN IP address
|
||||||
will fail the handshake even with the right CA trusted, since an Origin
|
(`https://192.168.1.50`) will fail the handshake even against a
|
||||||
CA cert never covers a raw IP. Use whatever hostname the certificate's
|
perfectly valid cert for a different name.
|
||||||
SAN list actually covers (e.g. a local DNS/hosts entry pointing at the
|
|
||||||
frame's LAN IP, or the same public hostname the proxy is issued for).
|
|
||||||
|
|
||||||
## Access token
|
## Access token
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
idf_component_register(SRCS main.c wifi_provisioning.c frame_client.c qr_onboarding.c status_screen.c epd_draw.c reset_button.c next_button.c manage_button.c manage_qr_overlay.c
|
idf_component_register(SRCS main.c wifi_provisioning.c frame_client.c qr_onboarding.c status_screen.c epd_draw.c reset_button.c next_button.c manage_button.c manage_qr_overlay.c
|
||||||
PRIV_REQUIRES esp_event nvs_flash esp_wifi esp_netif esp_http_server esp_http_client dns_server epd7in3e qrcode epaper_fonts esp_driver_gpio
|
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_FILES root.html
|
||||||
EMBED_TXTFILES certs/cloudflare_origin_ca.pem)
|
EMBED_TXTFILES certs/tools_server_ca.pem)
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIEADCCAuigAwIBAgIID+rOSdTGfGcwDQYJKoZIhvcNAQELBQAwgYsxCzAJBgNV
|
|
||||||
BAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTQwMgYDVQQLEytDbG91
|
|
||||||
ZEZsYXJlIE9yaWdpbiBTU0wgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MRYwFAYDVQQH
|
|
||||||
Ew1TYW4gRnJhbmNpc2NvMRMwEQYDVQQIEwpDYWxpZm9ybmlhMB4XDTE5MDgyMzIx
|
|
||||||
MDgwMFoXDTI5MDgxNTE3MDAwMFowgYsxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBD
|
|
||||||
bG91ZEZsYXJlLCBJbmMuMTQwMgYDVQQLEytDbG91ZEZsYXJlIE9yaWdpbiBTU0wg
|
|
||||||
Q2VydGlmaWNhdGUgQXV0aG9yaXR5MRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRMw
|
|
||||||
EQYDVQQIEwpDYWxpZm9ybmlhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|
|
||||||
AQEAwEiVZ/UoQpHmFsHvk5isBxRehukP8DG9JhFev3WZtG76WoTthvLJFRKFCHXm
|
|
||||||
V6Z5/66Z4S09mgsUuFwvJzMnE6Ej6yIsYNCb9r9QORa8BdhrkNn6kdTly3mdnykb
|
|
||||||
OomnwbUfLlExVgNdlP0XoRoeMwbQ4598foiHblO2B/LKuNfJzAMfS7oZe34b+vLB
|
|
||||||
yrP/1bgCSLdc1AxQc1AC0EsQQhgcyTJNgnG4va1c7ogPlwKyhbDyZ4e59N5lbYPJ
|
|
||||||
SmXI/cAe3jXj1FBLJZkwnoDKe0v13xeF+nF32smSH0qB7aJX2tBMW4TWtFPmzs5I
|
|
||||||
lwrFSySWAdwYdgxw180yKU0dvwIDAQABo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYD
|
|
||||||
VR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQUJOhTV118NECHqeuU27rhFnj8KaQw
|
|
||||||
HwYDVR0jBBgwFoAUJOhTV118NECHqeuU27rhFnj8KaQwDQYJKoZIhvcNAQELBQAD
|
|
||||||
ggEBAHwOf9Ur1l0Ar5vFE6PNrZWrDfQIMyEfdgSKofCdTckbqXNTiXdgbHs+TWoQ
|
|
||||||
wAB0pfJDAHJDXOTCWRyTeXOseeOi5Btj5CnEuw3P0oXqdqevM1/+uWp0CM35zgZ8
|
|
||||||
VD4aITxity0djzE6Qnx3Syzz+ZkoBgTnNum7d9A66/V636x4vTeqbZFBr9erJzgz
|
|
||||||
hhurjcoacvRNhnjtDRM0dPeiCJ50CP3wEYuvUzDHUaowOsnLCjQIkWbR7Ni6KEIk
|
|
||||||
MOz2U0OBSif3FTkhCgZWQKOOLo1P42jHC3ssUZAtVNXrCk3fw9/E15k8NPkBazZ6
|
|
||||||
0iykLhH1trywrKRMVw67F44IE8Y=
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIICiTCCAi6gAwIBAgIUXZP3MWb8MKwBE1Qbawsp1sfA/Y4wCgYIKoZIzj0EAwIw
|
|
||||||
gY8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T
|
|
||||||
YW4gRnJhbmNpc2NvMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYDVQQL
|
|
||||||
Ey9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhvcml0
|
|
||||||
eTAeFw0xOTA4MjMyMTA4MDBaFw0yOTA4MTUxNzAwMDBaMIGPMQswCQYDVQQGEwJV
|
|
||||||
UzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEZ
|
|
||||||
MBcGA1UEChMQQ2xvdWRGbGFyZSwgSW5jLjE4MDYGA1UECxMvQ2xvdWRGbGFyZSBP
|
|
||||||
cmlnaW4gU1NMIEVDQyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwWTATBgcqhkjOPQIB
|
|
||||||
BggqhkjOPQMBBwNCAASR+sGALuaGshnUbcxKry+0LEXZ4NY6JUAtSeA6g87K3jaA
|
|
||||||
xpIg9G50PokpfWkhbarLfpcZu0UAoYy2su0EhN7wo2YwZDAOBgNVHQ8BAf8EBAMC
|
|
||||||
AQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQUhTBdOypw1O3VkmcH/es5
|
|
||||||
tBoOOKcwHwYDVR0jBBgwFoAUhTBdOypw1O3VkmcH/es5tBoOOKcwCgYIKoZIzj0E
|
|
||||||
AwIDSQAwRgIhAKilfntP2ILGZjwajktkBtXE1pB4Y/fjAfLkIRUzrI15AiEA5UCL
|
|
||||||
XYZZ9m2c3fKwIenMMojL1eqydsgqj/wK4p5kagQ=
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
-----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-----
|
||||||
@@ -25,12 +25,18 @@ static const char *TAG = "frame_client";
|
|||||||
|
|
||||||
static EventGroupHandle_t s_sta_event_group;
|
static EventGroupHandle_t s_sta_event_group;
|
||||||
|
|
||||||
/* Cloudflare's Origin CA root certs (RSA + ECC, both concatenated --
|
/* The exact CA certificate the tools server's reverse proxy presents,
|
||||||
* mbedtls_x509_crt_parse() chains every cert in a PEM buffer into the
|
* embedded verbatim (not sourced from ESP-IDF's built-in root bundle --
|
||||||
* trust store, so either one nginx presents validates), for trusting an
|
* that does an exact byte-level match against its compiled-in table,
|
||||||
* https tools server whose reverse proxy terminates TLS with a
|
* and a semantically-identical "same name, same public key" root
|
||||||
* Cloudflare-issued origin certificate. See firmware/main/certs/. */
|
* re-issued under a different serial/signature, as Google did for GTS
|
||||||
extern const char cloudflare_origin_ca_pem_start[] asm("_binary_cloudflare_origin_ca_pem_start");
|
* 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),
|
/* Builds a full URL from cfg->toolsserver + a path (no leading slash),
|
||||||
* appending cfg->access_token as ?token= if one's set. toolsserver is
|
* 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,
|
.url = url,
|
||||||
.method = HTTP_METHOD_GET,
|
.method = HTTP_METHOD_GET,
|
||||||
.timeout_ms = CONFIG_FRAME_SERVER_CHECK_TIMEOUT_MS,
|
.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);
|
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,
|
.url = url,
|
||||||
.method = HTTP_METHOD_GET,
|
.method = HTTP_METHOD_GET,
|
||||||
.timeout_ms = CONFIG_FRAME_SERVER_CHECK_TIMEOUT_MS,
|
.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);
|
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,
|
.url = url,
|
||||||
.method = HTTP_METHOD_GET,
|
.method = HTTP_METHOD_GET,
|
||||||
.timeout_ms = CONFIG_FRAME_SERVER_CHECK_TIMEOUT_MS,
|
.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);
|
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,
|
.url = url,
|
||||||
.method = force_advance ? HTTP_METHOD_POST : HTTP_METHOD_GET,
|
.method = force_advance ? HTTP_METHOD_POST : HTTP_METHOD_GET,
|
||||||
.timeout_ms = CONFIG_FRAME_FETCH_TIMEOUT_MS,
|
.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);
|
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user