From c800fe7fdd8284b3e41027b1c0cd77a462ea1460 Mon Sep 17 00:00:00 2001
From: Matthias Schiffer <mschiffer@universe-factory.net>
Date: Sat, 29 Mar 2025 13:51:06 +0100
Subject: [PATCH] gluon-autoupdater: add support for HTTPS and protocol-less
 URLs

The autoupdater supports HTTPS when a ustream TLS backend is installed,
but we did not allow this in site.conf. However, just allowing HTTPS
URLs unconditionally is also a bad idea, as it might result in nodes
being unable to reach the mirror, in particular if the `tls` feature is
enabled only for some devices.

Solve this by allowing https:// URLs only if the marker file installed
by gluon-tls is found, failing the site check with an error message like
the following otherwise:

    *** All of the following alternatives have failed:
        1) site.conf error: expected autoupdater.branches.test.mirrors.1 to match pattern 'http://', but it is "https://..." (a string value)
        2) site.conf error: expected autoupdater.branches.test.mirrors.1 to use HTTPS only if the 'tls' feature is enabled, but it is "https://..." (a string value)
        3) site.conf error: expected autoupdater.branches.test.mirrors.1 to match pattern '^//', but it is "https://..." (a string value)

In addition, introduce support for protocol-less //server/path URLs,
which will use either HTTP or HTTPS depending on the availablility of
the `tls` feature. No fallback happens when `tls` is available, but the
HTTPS connection fails, preventing downgrade attack.

Based-on-patch-by: Kevin Olbrich <ko@sv01.de>
---
 docs/multidomain-site-example/site.conf       | 10 ++++++++-
 docs/site-example/site.conf                   | 10 ++++++++-
 docs/user/site.rst                            | 15 ++++++++++++-
 package/gluon-autoupdater/check_site.lua      | 21 ++++++++++++++++++-
 .../luasrc/lib/gluon/upgrade/500-autoupdater  | 18 +++++++++++++++-
 5 files changed, 69 insertions(+), 5 deletions(-)

diff --git a/docs/multidomain-site-example/site.conf b/docs/multidomain-site-example/site.conf
index fc5298f7a..88f0f9822 100644
--- a/docs/multidomain-site-example/site.conf
+++ b/docs/multidomain-site-example/site.conf
@@ -39,7 +39,15 @@
     branches = {
       stable = {
         name = 'stable',
-        mirrors = {'http://update.example.org/stable/sysupgrade'},
+        mirrors = {
+          'http://1.updates.example.org/stable/sysupgrade',
+
+          -- Requires the tls feature in image-customization.lua
+          -- 'https://2.updates.example.org/stable/sysupgrade',
+
+          -- Uses http or https depending on the tls feature in image-customization.lua
+          '//3.updates.example.org/stable/sysupgrade',
+        },
         good_signatures = 2,
         pubkeys = {
           'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', -- Alice
diff --git a/docs/site-example/site.conf b/docs/site-example/site.conf
index 0c037e9d6..3e529c302 100644
--- a/docs/site-example/site.conf
+++ b/docs/site-example/site.conf
@@ -174,7 +174,15 @@
         name = 'stable',
 
         -- List of mirrors to fetch images from. IPv6 required!
-        mirrors = {'http://1.updates.services.ffhl/stable/sysupgrade'},
+        mirrors = {
+          'http://1.updates.example.org/stable/sysupgrade',
+
+          -- Requires the tls feature in image-customization.lua
+          -- 'https://2.updates.example.org/stable/sysupgrade',
+
+          -- Uses http or https depending on the tls feature in image-customization.lua
+          '//3.updates.example.org/stable/sysupgrade',
+        },
 
         -- Number of good signatures required.
         -- Have multiple maintainers sign your build and only
diff --git a/docs/user/site.rst b/docs/user/site.rst
index da432dde9..60fc14457 100644
--- a/docs/user/site.rst
+++ b/docs/user/site.rst
@@ -467,7 +467,10 @@ autoupdater \: package
           name = 'stable',
           mirrors = {
             'http://[fdca:ffee:babe:1::fec1]/firmware/stable/sysupgrade/',
-            'http://autoupdate.alpha-centauri.freifunk.net/firmware/stable/sysupgrade/',
+            -- Requires the tls feature in image-customization.lua
+            'https://autoupdate.alpha-centauri.freifunk.net/firmware/stable/sysupgrade/',
+            -- Uses http or https depending on the tls feature in image-customization.lua
+            '//autoupdate2.alpha-centauri.freifunk.net/firmware/stable/sysupgrade/',
           },
           -- Number of good signatures required
           good_signatures = 2,
@@ -482,6 +485,16 @@ autoupdater \: package
   All configured mirrors must be reachable from the nodes via IPv6. If you don't want to set an IPv6 address
   explicitly, but use a hostname (which is recommended), see also the :ref:`FAQ <faq-dns>`.
 
+  HTTPS URLs can be used if the **tls** feature is enabled in **image-customization.lua**.
+
+  Use protocol-less ``//server/path`` URLs to use HTTPS if the **tls** feature is available,
+  but fall back to HTTP otherwise. The server **must** allow HTTPS connections and provide
+  a valid certificate in this case; the autoupdater will not fall back to HTTP if the **tls**
+  feature is enabled, but the HTTPS connection fails.
+
+  Note that the validity period of TLS certificates is checked as well, so care must be taken
+  to provide working NTP servers in addition to the update mirrors when using HTTPS.
+
 .. _user-site-config_mode:
 
 config_mode \: optional
diff --git a/package/gluon-autoupdater/check_site.lua b/package/gluon-autoupdater/check_site.lua
index aaf1763c3..aa9ca6044 100644
--- a/package/gluon-autoupdater/check_site.lua
+++ b/package/gluon-autoupdater/check_site.lua
@@ -1,8 +1,27 @@
+local has_tls = (function()
+	local f = io.open((os.getenv('IPKG_INSTROOT') or '') .. '/lib/gluon/features/tls')
+	if f then
+		f:close()
+		return true
+	end
+	return false
+end)()
+
 local branches = table_keys(need_table({'autoupdater', 'branches'}, function(branch)
 	need_alphanumeric_key(branch)
 
 	need_string(in_site(extend(branch, {'name'})))
-	need_string_array_match(extend(branch, {'mirrors'}), '^http://')
+	need_array(extend(branch, {'mirrors'}), function(mirror)
+		alternatives(function()
+			need_string_match(mirror, 'http://')
+		end, function()
+			need_string_match(mirror, 'https://')
+			need(mirror, function() return has_tls end, nil,
+				"use HTTPS only if the 'tls' feature is enabled")
+		end, function()
+			need_string_match(mirror, '^//')
+		end)
+	end)
 
 	local pubkeys = need_string_array_match(in_site(extend(branch, {'pubkeys'})), '^%x+$')
 	need_number(in_site(extend(branch, {'good_signatures'})))
diff --git a/package/gluon-autoupdater/luasrc/lib/gluon/upgrade/500-autoupdater b/package/gluon-autoupdater/luasrc/lib/gluon/upgrade/500-autoupdater
index 351c8e04e..f5e6ce78d 100755
--- a/package/gluon-autoupdater/luasrc/lib/gluon/upgrade/500-autoupdater
+++ b/package/gluon-autoupdater/luasrc/lib/gluon/upgrade/500-autoupdater
@@ -4,14 +4,30 @@ local site = require 'gluon.site'
 local uci = require('simple-uci').cursor()
 local unistd = require 'posix.unistd'
 
+local has_tls = unistd.access('/lib/gluon/features/tls') ~= nil
+local default_scheme = has_tls and 'https:' or 'http:'
 
 local min_branch
 
+local function mirror_urls(mirrors)
+	local ret = {}
+
+	for _, mirror in ipairs(mirrors) do
+		if string.match(mirror, '^//') ~= nil then
+			table.insert(ret, default_scheme .. mirror)
+		else
+			table.insert(ret, mirror)
+		end
+	end
+
+	return ret
+end
+
 for name, config in pairs(site.autoupdater.branches()) do
 	uci:delete('autoupdater', name)
 	uci:section('autoupdater', 'branch', name, {
 		name = config.name,
-		mirror = config.mirrors,
+		mirror = mirror_urls(config.mirrors),
 		good_signatures = config.good_signatures,
 		pubkey = config.pubkeys,
 	})
-- 
GitLab