Skip to content

Commit 86e9121

Browse files
Merge pull request #16 from Valar-Systems/fix/github-ota-cert-bundle
fix(ota): validate GitHub against the CA bundle, not a pinned root (1.0.6)
2 parents 0036cba + 76f7abf commit 86e9121

3 files changed

Lines changed: 31 additions & 14 deletions

File tree

‎version/r1.1/firmware/platformio/esp32_firmware_platformio/platformio.ini‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ build_flags =
6363
-DARDUINO_USB_CDC_ON_BOOT=1
6464
; ESP32 firmware version: reported in the heartbeat and used by the OTA
6565
; flow to decide whether this board needs updating. Bump on every release.
66-
'-DFW_VERSION="1.0.5"'
66+
'-DFW_VERSION="1.0.6"'
6767
; Bootstrap token for register_device/capture transport auth. Sourced from
6868
; the environment so the secret is never committed: set MSC_BOOTSTRAP_TOKEN
6969
; before building (PowerShell: $env:MSC_BOOTSTRAP_TOKEN="..."; bash:

‎version/r1.1/firmware/platformio/esp32_firmware_platformio/src/api.h‎

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -90,23 +90,29 @@ static void applyTlsPolicy(WiFiClientSecure& client) {
9090
}
9191
}
9292

93+
// The ESP-IDF Mozilla root-CA bundle (esp_crt_bundle), embedded by the Arduino
94+
// framework when CONFIG_MBEDTLS_CERTIFICATE_BUNDLE is enabled (default). Linked
95+
// symbol; declared here so the GitHub update path can validate against the full
96+
// public trust store rather than one hand-pinned root.
97+
extern const uint8_t rootca_crt_bundle_start[] asm("_binary_x509_crt_bundle_start");
98+
extern const uint8_t rootca_crt_bundle_end[] asm("_binary_x509_crt_bundle_end");
99+
93100
/**
94101
* TLS trust policy for the GitHub auto-update path (ota.h).
95102
*
96-
* Pins the GitHub root bundle (config.h / GITHUB_CA_ROOT_CERT) so the whole
97-
* update path is authenticated: api.github.com (release metadata) and the
98-
* *.githubusercontent.com asset hosts (manifest.json + the .bin downloads) chain
99-
* to different roots, both in the bundle, and HTTPClient keeps this same client
100-
* across the github.com -> githubusercontent.com asset redirect, so one policy
101-
* validates every hop. Falls back to setInsecure() if the bundle was overridden
102-
* to empty (the binary is MD5-verified regardless, see ota.h).
103+
* Validates against the full Mozilla root-CA bundle, NOT a single pinned root.
104+
* GitHub rotates its CA, and a single pin breaks the whole update path on the
105+
* next rotation with MBEDTLS_ERR_X509_CERT_VERIFY_FAILED (tls -0x2700) -- which
106+
* is exactly what bricked the auto-update in the field. The bundle covers
107+
* api.github.com (release metadata) and the *.githubusercontent.com asset hosts
108+
* (manifest.json + .bin downloads), across the github.com -> githubusercontent
109+
* redirect HTTPClient follows on one client, and survives future CA rotations.
110+
* Still REAL certificate validation (not setInsecure): the firmware-update path
111+
* needs it, because the .bin's trusted MD5 is read from the TLS-fetched manifest.
103112
*/
104113
static void applyTlsPolicyGitHub(WiFiClientSecure& client) {
105-
if (kGithubCaRootCert != nullptr && kGithubCaRootCert[0] != '\0') {
106-
client.setCACert(kGithubCaRootCert);
107-
} else {
108-
client.setInsecure();
109-
}
114+
client.setCACertBundle(rootca_crt_bundle_start,
115+
(size_t)(rootca_crt_bundle_end - rootca_crt_bundle_start));
110116
}
111117

112118
/**

‎version/r1.1/firmware/platformio/esp32_firmware_platformio/src/ota.h‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,18 @@ bool otaCheckGithub() {
105105
https.addHeader("User-Agent", "MiniSpeedCam-OTA"); // GitHub 403s a missing UA
106106
https.addHeader("Accept", "application/vnd.github+json");
107107
int code = https.GET();
108-
if (code != 200) { otaSetStatus("check: API HTTP %d", code); https.end(); return false; }
108+
if (code != 200) {
109+
if (code < 0) { // negative = TLS/socket failure, not an HTTP status -- surface why
110+
char tlsbuf[64] = "";
111+
int tlserr = client.lastError(tlsbuf, sizeof(tlsbuf));
112+
Serial.printf("[OTA] github connect failed: code=%d tls=-0x%04x (%s)\n", code, -tlserr, tlsbuf);
113+
otaSetStatus("check: API HTTP %d (tls -0x%04x)", code, -tlserr);
114+
} else {
115+
otaSetStatus("check: API HTTP %d", code);
116+
}
117+
https.end();
118+
return false;
119+
}
109120

110121
JsonDocument filter;
111122
filter["tag_name"] = true;

0 commit comments

Comments
 (0)