From 9d04c024730610591cdf6382821aeded13aa8fc2 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Thu, 18 Jun 2026 19:04:21 +0000 Subject: [PATCH] fix(controller): verify download integrity + fail closed on TLS (D6) The rootfs is downloaded (latest_*.meta4 -> .tar.gz) and then extracted and executed as root, but Aria2Manager performed no integrity check and silently downgraded to --check-certificate=false when cacert.pem was missing. A compromised mirror or MITM could deliver a malicious rootfs (tech-debt D6). - TLS fail-closed: require cacert.pem (abort with a clear error if it cannot be provisioned) and always pass --check-certificate=true. The insecure fallback is removed. Applies to all aria2 downloads (rootfs + ZIM). - Integrity: add --check-integrity=true so aria2 verifies the SHA-256 checksums published in the .meta4 (file-level + per-piece) during the download. On mismatch aria2 exits non-zero, onError fires and the archive is never extracted/executed. This uses the server's published hash over a verified TLS channel, so no redundant on-device re-hash of the ~1.2 GB file is needed. Out of scope (separate items): ZIM content integrity (Kiwix publishes no embedded hash today) and the cleartext OTA APK path in MainActivity (F15). --- .../org/iiab/controller/Aria2Manager.java | 24 ++++++++++++------- controller/docs/TECH_DEBT_PLAN.md | 8 ++++++- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/Aria2Manager.java b/controller/app/src/main/java/org/iiab/controller/Aria2Manager.java index 1296c19..f804943 100644 --- a/controller/app/src/main/java/org/iiab/controller/Aria2Manager.java +++ b/controller/app/src/main/java/org/iiab/controller/Aria2Manager.java @@ -64,6 +64,12 @@ public void startDownload(Context context, String url, DownloadListener listener if (!caCertFile.exists()) { extractAsset(context, "cacert.pem", caCertFile); } + // D6: fail closed. The downloaded rootfs is extracted and executed as + // root, so we must never fall back to an unverified TLS connection + // where a MITM could swap it. If the CA bundle is unavailable, abort. + if (!caCertFile.exists()) { + throw new Exception("Secure download aborted: CA certificate bundle (cacert.pem) is unavailable."); + } Log.d(TAG, "Executing Native Aria2c..."); Log.d(TAG, "Target URL: " + url); @@ -83,20 +89,20 @@ public void startDownload(Context context, String url, DownloadListener listener command.add("--max-connection-per-server=4"); command.add("--split=4"); command.add("--follow-metalink=mem"); + // D6: verify the SHA-256 checksums embedded in the .meta4 (Metalink) + // while downloading. On mismatch aria2 exits non-zero, so onError fires + // and the archive is never extracted/executed. + command.add("--check-integrity=true"); command.add("--enable-dht=true"); command.add("--dht-file-path=" + dhtFile.getAbsolutePath()); command.add("--bt-enable-lpd=true"); command.add("--seed-time=0"); - // --- Apply SSL Certificate Validation --- - if (caCertFile.exists()) { - Log.d(TAG, "SSL certificates found. Enforcing strict validation."); - command.add("--check-certificate=true"); - command.add("--ca-certificate=" + caCertFile.getAbsolutePath()); - } else { - Log.w(TAG, "cacert.pem not found! Falling back to insecure connection."); - command.add("--check-certificate=false"); - } + // --- Apply SSL Certificate Validation (D6: always strict; cacert + // presence was already enforced above, so there is no insecure path) --- + Log.d(TAG, "Enforcing strict TLS certificate validation."); + command.add("--check-certificate=true"); + command.add("--ca-certificate=" + caCertFile.getAbsolutePath()); command.add("--console-log-level=warn"); command.add("--summary-interval=1"); diff --git a/controller/docs/TECH_DEBT_PLAN.md b/controller/docs/TECH_DEBT_PLAN.md index 241a30e..8663d5b 100644 --- a/controller/docs/TECH_DEBT_PLAN.md +++ b/controller/docs/TECH_DEBT_PLAN.md @@ -40,7 +40,13 @@ _Last updated: 2026-06-17. Tracks remediation work against the findings below. I - **S3** (`IIABAdbManager` key spec): the ADB identity key was created with `ENCRYPT | DECRYPT` + non-randomized encryption padding on top of sign/verify. Verified no `Cipher` uses this alias, so the spec is now `SIGN | VERIFY` only (encryption paddings / `setRandomizedEncryptionRequired(false)` removed). Alias kept at `v3` (non-disruptive: hardens new key generation without forcing existing devices to re-pair ADB). - No new unit tests: both are framework-bound legacy line-fixes with no extractable pure logic; verified by inspection + compile + CI lint. -**Phase 1 — Security hardening: IN PROGRESS.** Done so far: **S1** (PR #9), **M4**, **S3**. Remaining: **D2**, **D6**, **D12**, **S4**, **D11**. +**D6 — Download integrity + TLS fail-closed (Phase 1 security): DONE** (PR `fix/phase1-security-d6-download-integrity`) +- Closes **D6**: the rootfs (`latest_*.meta4` -> `.tar.gz`) is extracted and executed as root, but downloads had no integrity check and `Aria2Manager` silently fell back to `--check-certificate=false` when `cacert.pem` was missing. +- **TLS fail-closed**: `cacert.pem` is now required (fail with a clear error if it cannot be provisioned) and `--check-certificate=true` is always passed — the insecure fallback is removed. Applies to all aria2 downloads (rootfs + ZIM). +- **Integrity**: `--check-integrity=true` makes aria2 verify the SHA-256 checksums published in the `.meta4` (confirmed present: file-level + per-piece) during download; on mismatch aria2 exits non-zero, `onError` fires and the archive is never extracted. Uses the server's published hash + verified TLS, so no redundant on-device re-hash of the ~1.2 GB file. +- Follow-up (separate items): ZIM content integrity (Kiwix publishes no embedded hash today; needs `.sha256` sidecars) and the cleartext OTA APK path in `MainActivity` (**F15**). + +**Phase 1 — Security hardening: IN PROGRESS.** Done so far: **S1** (PR #9), **M4**, **S3** (PR #10), **D6**. Remaining: **D2**, **D12**, **S4**, **D11**. ## 1. Executive summary