Skip to content

feat(controller): OTA updater security + correctness redesign (F15, PR A)#30

Merged
luisguzman-adfa merged 1 commit into
mainfrom
feat/ota-updater-security-redesign
Jun 23, 2026
Merged

feat(controller): OTA updater security + correctness redesign (F15, PR A)#30
luisguzman-adfa merged 1 commit into
mainfrom
feat/ota-updater-security-redesign

Conversation

@luisguzman-adfa

Copy link
Copy Markdown
Collaborator

Summary

First of two PRs redesigning the in-app OTA self-updater (tech-debt F15). The updater downloaded the APK to public Downloads and installed it with no integrity check; DownloadManager reports "complete" even when the server returns an HTML/text error page, so a wrong/MITM'd response got installed as garbage (the "downloaded a text file" bug). The completion receiver was also registered EXPORTED.

PR A = security + functional correctness. PR B (separate) adds the UpdateViewModel + in-app download-progress UX.

Changes (layered org.iiab.controller.update slice)

  • domain/ (pure JVM, unit-tested): UpdateCheck (is the server build newer?) and CertDigests.sameSigner (signer-set comparison).
  • data/ApkVerifier: the downloaded APK must be signed by the same certificate as the running app — public certs via PackageManager (no private key, no secret, no server change), handling API <28 (GET_SIGNATURES) vs ≥28 (GET_SIGNING_CERTIFICATES). Rejects MITM/tampered APKs and non-APK downloads (kills the text-file bug).
  • MainActivity seam: stage the APK in the app's private external dir (not public Downloads); install only when the DownloadManager status is SUCCESSFUL; verify the signature before install (delete + clear error on failure); handle the API 26+ "install unknown apps" permission; register the receiver NOT_EXPORTED. New strings (en + es); REQUEST_INSTALL_PACKAGES.

How the signature check works (no secrets)

Every APK embeds its public signing certificate. We read the running app's cert and the downloaded APK's cert via PackageManager at runtime and compare — Android already requires an update to be signed by the same key, so we just enforce it early with a clean error. No GitHub Actions secret / private key involved.

Testing

UpdateCheckTest + CertDigestsTest (pure JVM, CI gate). The ApkVerifier/MainActivity parts are Android-bound — validated by inspection + CI compile and on-device (a text/HTML response is now rejected with a clear message instead of installing garbage).

Out of scope (follow-ups)

  • PR B: UpdateViewModel + in-app progress bar / cancel / state UX.
  • A network-security-config to scope cleartext to the local box hosts (S18) — deferred to avoid risking box connectivity; the updater URLs are HTTPS so this PR doesn't depend on it.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

…R A)

The in-app OTA self-updater downloaded the new APK to public Downloads and
installed it with no integrity check. DownloadManager reports "complete" even
when the server returned an HTML/text error page, so a wrong or MITM'd response
was installed as garbage (the "downloaded a text file" bug); the download
receiver was also registered EXPORTED.

PR A — security + functional correctness, as a layered `update` slice:
- domain: UpdateCheck (version rule) and CertDigests.sameSigner — pure JVM,
  unit-tested (UpdateCheckTest, CertDigestsTest).
- data/ApkVerifier: the downloaded APK must be signed by the SAME certificate as
  the running app (public certs via PackageManager — no secrets, no server
  change). Rejects MITM/tampered APKs and non-APK downloads (kills the text-file
  bug), handling API <28 (GET_SIGNATURES) vs >=28 (GET_SIGNING_CERTIFICATES).
- MainActivity seam: stage the APK in the app's PRIVATE external dir (not public
  Downloads); only install when the DownloadManager status is SUCCESSFUL; verify
  the signature before install (delete + clear error on failure); handle the
  API 26+ "install unknown apps" permission; register the completion receiver
  NOT_EXPORTED. New strings (en + es); REQUEST_INSTALL_PACKAGES permission.

Out of scope (follow-ups): PR B (UpdateViewModel + in-app download-progress UX)
and a network-security-config to scope cleartext to the local box hosts (S18),
deferred to avoid risking box connectivity.
@luisguzman-adfa luisguzman-adfa force-pushed the feat/ota-updater-security-redesign branch from 899e43f to 0c69557 Compare June 23, 2026 04:04
@luisguzman-adfa luisguzman-adfa merged commit 105fc46 into main Jun 23, 2026
1 check passed
luisguzman-adfa added a commit that referenced this pull request Jun 23, 2026
…ress (PR B)

Presentation/data slice for PR B (on top of PR A #30), behavior-neutral until wired:
- domain: OtaDownloadGateway port (query progress / cancel).
- data: DownloadManagerGateway (queries DownloadManager COLUMN_STATUS / bytes; remove() to cancel).
- presentation: UpdateUiState (IDLE/DOWNLOADING/VERIFYING/READY/INSTALLING/ERROR + percent,
  with pure fromDownload mapping) + UpdateViewModel (polls ~400ms while downloading; cancel)
  + UpdateViewModelFactory. Unit-tested (UpdateUiStateTest + DownloadProgressTest).

Next: modal progress dialog layout + MainActivity wiring (keep system notification).
@luisguzman-adfa luisguzman-adfa deleted the feat/ota-updater-security-redesign branch June 24, 2026 02:12
luisguzman-adfa added a commit that referenced this pull request Jun 24, 2026
- Status banner: Phase 0 done + Phase 1 core-complete; Phase 2 not started
  (~37 raw threads); Phase 3 partial (slices land but god classes grew —
  DeployFragment ~3022, MainActivity ~2384); M15 done, M8/M9 open.
- Phase 1 line: F15 A+B (#30/#32) + rootfs validation (#31) + integrity (#37) done.
- Integrity/writer entry: marked DONE via #37 (was 'next PR').
- F4 + M7: confirmed mitigated by inspection (onPause teardown + remove-before-post).
- Lint workaround: added root-cause analysis with concrete carve targets
  (addNewTerminalSession ~727 LOC; DeployFragment bind* methods) for D1/F1.

Docs only; no code change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant