From 84e1a5177b0cd7ba67b9ba7c4d7349243aaa64eb Mon Sep 17 00:00:00 2001 From: Andrew Quenehen Date: Thu, 6 Aug 2026 08:58:22 -0300 Subject: [PATCH] Harden Switch OTA install with TLS verification and reliable NRO replace. Enable HTTPS peer checks via a romfs CA bundle, remove existing NROs before rename on sdmc, and surface launcher replace failures before writing version.txt. --- .gitignore | 1 + native/switch-ota-launcher/Makefile | 6 ++++++ native/switch-ota-launcher/README.md | 1 + native/switch-ota-launcher/src/main.c | 21 ++++++++++++++++++--- native/switch-ota-launcher/src/ota_fs.c | 3 +-- native/switch-ota-launcher/src/ota_net.c | 14 ++++++++++---- test/switch-nro-ota.spec.test.js | 7 +++++++ 7 files changed, 44 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 8f519c51..b237d83c 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ native/switch-ota-launcher/*.nacp native/switch-ota-launcher/*.elf native/switch-ota-launcher/*.map native/switch-ota-launcher/romfs/logo.png +native/switch-ota-launcher/romfs/cacert.pem # Legacy manual convenience-copy location (superseded by /dist/android/) mobile/dist/ diff --git a/native/switch-ota-launcher/Makefile b/native/switch-ota-launcher/Makefile index 45779960..6c7b463c 100644 --- a/native/switch-ota-launcher/Makefile +++ b/native/switch-ota-launcher/Makefile @@ -135,9 +135,15 @@ endif #--------------------------------------------------------------------------------- all: sync-romfs $(BUILD) +CACERT_URL := https://curl.se/ca/cacert.pem +CACERT_ROMFS := $(CURDIR)/$(ROMFS)/cacert.pem + sync-romfs: @mkdir -p $(CURDIR)/$(ROMFS) @cp -f $(LOGO_SRC) $(CURDIR)/$(ROMFS)/logo.png + @if ! curl -sfL --time-cond $(CACERT_ROMFS) -o $(CACERT_ROMFS) $(CACERT_URL); then \ + [ -f $(CACERT_ROMFS) ] || (echo "sync-romfs: failed to fetch cacert.pem" && exit 1); \ + fi $(BUILD): sync-romfs @[ -d $@ ] || mkdir -p $@ diff --git a/native/switch-ota-launcher/README.md b/native/switch-ota-launcher/README.md index ffa02cbc..010a2767 100644 --- a/native/switch-ota-launcher/README.md +++ b/native/switch-ota-launcher/README.md @@ -69,5 +69,6 @@ Manifest: `scripts/switch/ota_launcher.manifest`. - Zip extraction uses `switch-zziplib` (`ota_unzip.c`) on device. - OTA replaces launcher + game from the install zip (NACP versions stay aligned). +- HTTPS uses Mozilla CA bundle in romfs (`cacert.pem`, fetched at build time). - Sphaira HOME forwarders cache metadata until reinstalled (see docs/switch-install.md). - Release runner: `switch-dev` + (`install_devkitpro_deps.sh` **or** Docker) diff --git a/native/switch-ota-launcher/src/main.c b/native/switch-ota-launcher/src/main.c index 70cc6248..9ee2f5a1 100644 --- a/native/switch-ota-launcher/src/main.c +++ b/native/switch-ota-launcher/src/main.c @@ -185,14 +185,29 @@ static int run_update_flow(const char *install_dir) { } remove(extracted); + int have_launcher = 0; if (ota_unzip_extract_file(zip_path, LAUNCHER_MEMBER_IN_ZIP, extracted_launcher, err, sizeof(err)) == 0 || ota_unzip_extract_file(zip_path, OTA_LAUNCHER_NRO_NAME, extracted_launcher, err, sizeof(err)) == 0) { - (void)ota_fs_atomic_replace_nro(install_dir, OTA_LAUNCHER_NRO_NAME, extracted_launcher, err, - sizeof(err)); - remove(extracted_launcher); + have_launcher = 1; } + if (!have_launcher) { + remove(zip_path); + show_update_error("Could not extract", + "Update zip is missing launcher files.", err, installed); + return 0; + } + if (ota_fs_atomic_replace_nro(install_dir, OTA_LAUNCHER_NRO_NAME, extracted_launcher, err, + sizeof(err)) != 0) { + remove(extracted_launcher); + remove(zip_path); + show_update_error("Could not install", + "Game updated but launcher could not be replaced. Reinstall from the SD zip.", + err, installed); + return 0; + } + remove(extracted_launcher); char vpath[192]; snprintf(vpath, sizeof(vpath), "%s/version.txt", install_dir); diff --git a/native/switch-ota-launcher/src/ota_fs.c b/native/switch-ota-launcher/src/ota_fs.c index f2d04bc9..15d9631b 100644 --- a/native/switch-ota-launcher/src/ota_fs.c +++ b/native/switch-ota-launcher/src/ota_fs.c @@ -104,9 +104,8 @@ int ota_fs_atomic_replace_nro(const char *install_dir, const char *nro_name, if (err && err_len) snprintf(err, err_len, "copy to .part failed (%s)", nro_name); return -1; } -#ifdef _WIN32 + /* sdmc/FAT (Switch) and Windows do not replace an existing dest on rename. */ remove(dest); -#endif if (rename(part, dest) != 0) { if (err && err_len) snprintf(err, err_len, "rename .part -> %s failed", nro_name); remove(part); diff --git a/native/switch-ota-launcher/src/ota_net.c b/native/switch-ota-launcher/src/ota_net.c index 5ce38bb4..80b1510c 100644 --- a/native/switch-ota-launcher/src/ota_net.c +++ b/native/switch-ota-launcher/src/ota_net.c @@ -10,6 +10,14 @@ #endif #if defined(__SWITCH__) +#define OTA_CA_BUNDLE "romfs:/cacert.pem" + +static void ota_net_configure_tls(CURL *curl) { + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); + curl_easy_setopt(curl, CURLOPT_CAINFO, OTA_CA_BUNDLE); +} + struct mem_buf { char *data; size_t len; @@ -76,8 +84,7 @@ int ota_net_download_buffer(const char *url, long timeout_ms, char **out, size_t curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, timeout_ms); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_mem); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &mem); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + ota_net_configure_tls(curl); CURLcode rc = curl_easy_perform(curl); curl_easy_cleanup(curl); if (rc != CURLE_OK) { @@ -122,8 +129,7 @@ int ota_net_download_file(const char *url, const char *path, long timeout_ms, ch curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, timeout_ms); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_file); curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + ota_net_configure_tls(curl); if (progress) { curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, xfer_progress); diff --git a/test/switch-nro-ota.spec.test.js b/test/switch-nro-ota.spec.test.js index 1f66305e..f2688bac 100644 --- a/test/switch-nro-ota.spec.test.js +++ b/test/switch-nro-ota.spec.test.js @@ -513,7 +513,11 @@ test('AC-010: Fonte do launcher e Makefile DEVKITPRO existem @spec:AC-010', () = assert.match(otaUi, /draw_text_wrapped_centered/); assert.match(otaUi, /ota_ui_alert_error/); assert.match(read('native/switch-ota-launcher/src/ota_net.c'), /XFERINFOFUNCTION|xfer_progress/); + assert.match(read('native/switch-ota-launcher/src/ota_net.c'), /CURLOPT_SSL_VERIFYPEER,\s*1L/); + assert.match(read('native/switch-ota-launcher/src/ota_net.c'), /romfs:\/cacert\.pem/); + assert.doesNotMatch(read('native/switch-ota-launcher/src/ota_net.c'), /CURLOPT_SSL_VERIFYPEER,\s*0L/); assert.match(read('native/switch-ota-launcher/Makefile'), /^ROMFS\s*:=/m); + assert.match(read('native/switch-ota-launcher/Makefile'), /cacert\.pem/); const mk = read('native/switch-ota-launcher/Makefile'); assert.match(mk, /libnx\/switch_rules|DEVKITPRO/); @@ -526,7 +530,10 @@ test('AC-010: Fonte do launcher e Makefile DEVKITPRO existem @spec:AC-010', () = assert.match(read('native/switch-ota-launcher/src/main.c'), /ota_unzip_extract_file/); assert.match(read('native/switch-ota-launcher/src/main.c'), /GAME_MEMBER_IN_ZIP|switch\/gen1recomp\//); assert.match(read('native/switch-ota-launcher/src/main.c'), /LAUNCHER_MEMBER_IN_ZIP|ota_fs_atomic_replace_nro/); + assert.match(read('native/switch-ota-launcher/src/main.c'), /ota_fs_atomic_replace_nro\([\s\S]*\) != 0/); assert.match(read('native/switch-ota-launcher/src/ota_fs.c'), /ota_fs_atomic_replace_nro/); + assert.match(read('native/switch-ota-launcher/src/ota_fs.c'), /remove\(dest\)/); + assert.doesNotMatch(read('native/switch-ota-launcher/src/ota_fs.c'), /#ifdef _WIN32[\s\S]*remove\(dest\)/); assert.doesNotMatch(read('scripts/switch/install_devkitpro_deps.sh'), /switch-minizip/); assert.match(read('scripts/switch/install_devkitpro_deps.sh'), /switch-zziplib/); assert.match(read('scripts/switch/install_devkitpro_deps.sh'), /switch-dev/);