diff --git a/.github/workflows/update-merlin-sha256.yml b/.github/workflows/update-merlin-sha256.yml new file mode 100644 index 00000000..7bf939be --- /dev/null +++ b/.github/workflows/update-merlin-sha256.yml @@ -0,0 +1,121 @@ +name: Update Merlin SHA256 Checksums + +on: + schedule: + # Every 15 minutes, offset from the top of the hour to reduce scheduler congestion. + - cron: '7,22,37,52 * * * *' + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: update-merlin-sha256 + cancel-in-progress: false + +jobs: + scrape-and-commit: + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + ssh-key: ${{ secrets.DEPLOY_KEY }} + + - name: Fetch, parse, and validate SHA256 signatures + shell: bash + run: | + set -euo pipefail + + readonly SOURCE_URL='https://www.asuswrt-merlin.net/download' + readonly TARGET_FILE='merlin-sha256.txt' + readonly MIN_EXPECTED_ENTRIES=5 + + page_file="$(mktemp)" + candidate_file="$(mktemp)" + trap 'rm -f "$page_file" "$candidate_file"' EXIT + + echo "Fetching SHA256 signatures from ${SOURCE_URL}..." + curl --fail --location --silent --show-error \ + --retry 4 --retry-delay 5 --retry-connrefused \ + --connect-timeout 15 --max-time 60 \ + --user-agent 'MerlinAutoUpdate checksum mirror (+https://github.com/ExtremeFiretop/MerlinAutoUpdate-Router)' \ + --output "$page_file" \ + "$SOURCE_URL" + + # Keep the same source section MerlinAU consumes today, but write to a + # temporary candidate so a scrape/parser failure cannot destroy the + # last-known-good mirror in the repository. + sed -n '/<.*>SHA256 signatures:<\/.*>/,/<\/pre>/p' "$page_file" | \ + sed -n '/]*>/,/<\/pre>/p' | \ + sed -e 's/^.*]*>//' \ + -e 's/<[^>]*>//g' \ + -e 's/^[[:space:]]*//' \ + -e 's/[[:space:]]*$//' | \ + tr -d '\r' | \ + sed '/^[[:space:]]*$/d' > "$candidate_file" + + echo "Validating candidate checksum list..." + awk -v min_entries="$MIN_EXPECTED_ENTRIES" ' + BEGIN { + valid = 1 + count = 0 + } + { + count++ + + if (NF != 2) { + printf "Invalid field count on line %d: %s\n", NR, $0 > "/dev/stderr" + valid = 0 + next + } + + if (length($1) != 64 || $1 ~ /[^0-9A-Fa-f]/) { + printf "Invalid SHA256 on line %d: %s\n", NR, $1 > "/dev/stderr" + valid = 0 + } + + if (seen[$2]++) { + printf "Duplicate firmware filename on line %d: %s\n", NR, $2 > "/dev/stderr" + valid = 0 + } + } + END { + if (count < min_entries) { + printf "Only %d checksum entries were parsed; expected at least %d.\n", count, min_entries > "/dev/stderr" + valid = 0 + } + + if (!valid) + exit 1 + } + ' "$candidate_file" + + echo "Validated $(wc -l < "$candidate_file") checksum entries." + echo "Candidate preview:" + head -n 5 "$candidate_file" + + # Replace the working-tree copy only after the candidate has passed + # every validation check. A failed run therefore leaves the repository + # and its last-known-good checksum mirror unchanged. + mv -f "$candidate_file" "$TARGET_FILE" + + - name: Commit and push changes + shell: bash + run: | + set -euo pipefail + + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + + git add merlin-sha256.txt + + if git diff --cached --quiet; then + echo 'No checksum changes detected. Nothing to commit.' + exit 0 + fi + + git commit -m 'Automated update: refresh Merlin SHA256 checksums' + git push diff --git a/MerlinAU.sh b/MerlinAU.sh index 6237db63..6bc3fbc1 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -9,11 +9,11 @@ set -u ## Set version for each Production Release ## -readonly SCRIPT_VERSION=1.6.6 -readonly SCRIPT_VERSTAG="26081603" +readonly SCRIPT_VERSION=1.6.7 +readonly SCRIPT_VERSTAG="26082409" readonly SCRIPT_NAME="MerlinAU" ## Set to "master" for Production Releases ## -SCRIPT_BRANCH="master" +SCRIPT_BRANCH="dev" ##----------------------------------------## ## Modified by Martinski W. [2024-Jul-03] ## @@ -26,6 +26,9 @@ SCRIPT_URL_REPO="${SCRIPT_URL_BASE}/$SCRIPT_BRANCH" readonly FW_SFURL_BASE="https://sourceforge.net/projects/asuswrt-merlin/files" readonly FW_SFURL_RELEASE_SUFFIX="Release" readonly FW_GITURL_RELEASE="https://api.github.com/repos/gnuton/asuswrt-merlin.ng/releases/latest" +readonly FW_SHA256_URL="https://www.asuswrt-merlin.net/download" +# The scheduled checksum mirror is maintained on the repository's default branch. # +readonly FW_SHA256_MIRROR_URL="${SCRIPT_URL_BASE}/main/merlin-sha256.txt" ##----------------------------------------## ## Modified by Martinski W. [2024-May-31] ## @@ -5971,45 +5974,103 @@ _CopyGnutonFiles_() return 0 } -##----------------------------------------## -## Modified by Martinski W. [2025-Feb-17] ## -##----------------------------------------## +##------------------------------------------## +## Modified by ExtremeFiretop [2026-Aug-24] ## +##------------------------------------------## +##------------------------------------------## +## Modified by ExtremeFiretop [2026-Aug-24] ## +##------------------------------------------## +_GetFirmwareSHA256FromList_() +{ + local checksumList="$1" + local firmwareName="$2" + + # Return a checksum only when there is exactly one exact filename match + # and its digest is a syntactically valid SHA256 value. Using awk avoids + # treating firmware filenames as regular expressions. + printf '%s\n' "$checksumList" | awk -v firmwareName="$firmwareName" ' + $2 == firmwareName && length($1) == 64 && $1 !~ /[^0-9A-Fa-f]/ { + matchCount++ + checksum = tolower($1) + } + END { + if (matchCount == 1) + print checksum + }' +} + _CheckOnlineFirmwareSHA256_() { - # Fetch the latest SHA256 checksums from ASUSWRT-Merlin website # - checksums="$(curl -Ls --retry 4 --retry-delay 5 --retry-connrefused \ - https://www.asuswrt-merlin.net/download | - sed -n '/<.*>SHA256 signatures:<\/.*>/,/<\/pre>/p' | - sed -n '/].*>/,/<\/pre>/p' | - sed -e 's/<[^>].*>//g; s/^[[:space:]]*//; s/[[:space:]]*$//')" + local checksums="" + local dl_sig="" + local fw_name="" + local fw_sig="" + local checksumSource="" - if [ -z "$checksums" ] + if [ ! -f "$firmware_file" ] then - Say "${REDct}**ERROR**${NOct}: Could not download the firmware SHA256 signatures from the website." + Say "${REDct}**ERROR**${NOct}: Firmware image file NOT found!" _DoCleanUp_ 1 return 1 fi - if [ -f "$firmware_file" ] + fw_name="$(basename "$firmware_file")" + fw_sig="$(openssl sha256 "$firmware_file" | awk -F ' ' '{print tolower($2)}')" + + # PRIMARY: Fetch the checksum directly from the ASUSWRT-Merlin website. + checksums="$(curl -Lfs --retry 4 --retry-delay 5 --retry-connrefused \ + "$FW_SHA256_URL" 2>/dev/null | + sed -n '/<.*>SHA256 signatures:<\/.*>/,/<\/pre>/p' | + sed -n '/]*>/,/<\/pre>/p' | + sed -e 's/<[^>]*>//g; s/^[[:space:]]*//; s/[[:space:]]*$//')" + + if [ -n "$checksums" ] then - fw_sig="$(openssl sha256 "$firmware_file" | awk -F ' ' '{print $2}')" - # Extract the corresponding signature for the firmware file from the fetched checksums # - dl_sig="$(echo "$checksums" | grep "$(basename "$firmware_file")" | awk -F ' ' '{print $1}')" - if [ "$fw_sig" != "$dl_sig" ] + dl_sig="$(_GetFirmwareSHA256FromList_ "$checksums" "$fw_name")" + fi + + if [ -n "$dl_sig" ] + then + checksumSource="ASUSWRT-Merlin website" + else + Say "${YLWct}**WARNING**${NOct}: Independently published checksum could not be retrieved from the ASUSWRT-Merlin website." + + # SECONDARY: Use the repository mirror only when the official source + # did not yield a usable checksum. Never use the checksum bundled in + # the firmware archive for an online update. + checksums="$(curl -Lfs --retry 4 --retry-delay 5 --retry-connrefused \ + "$FW_SHA256_MIRROR_URL" 2>/dev/null)" + + if [ -n "$checksums" ] + then + dl_sig="$(_GetFirmwareSHA256FromList_ "$checksums" "$fw_name")" + fi + + if [ -z "$dl_sig" ] then - Say "${REDct}**ERROR**${NOct}: SHA256 signature from extracted firmware file does not match the SHA256 signature from the website." + Say "${REDct}**ERROR**${NOct}: No unique valid SHA256 signature for ${fw_name} was available from either independent online source." + Say "Online firmware update was aborted; bundled archive checksum fallback is intentionally disabled." _DoCleanUp_ 1 _SendEMailNotification_ FAILED_FW_CHECKSUM_STATUS return 1 - else - Say "SHA256 signature check for firmware image file passed successfully." - return 0 fi - else - Say "${REDct}**ERROR**${NOct}: Firmware image file NOT found!" + + checksumSource="MerlinAU GitHub checksum mirror" + Say "${YLWct}**WARNING**${NOct}: Using the MerlinAU GitHub checksum mirror for verification. (15 MINUTE DELAY!)" + fi + + # If the primary source supplied a checksum but it mismatches, this fails + # immediately. The mirror is never used to bypass a checksum mismatch. + if [ "$fw_sig" != "$dl_sig" ] + then + Say "${REDct}**ERROR**${NOct}: SHA256 signature from extracted firmware file does not match the SHA256 signature from the ${checksumSource}." _DoCleanUp_ 1 + _SendEMailNotification_ FAILED_FW_CHECKSUM_STATUS return 1 fi + + Say "SHA256 signature check for firmware image file passed successfully using the ${checksumSource}." + return 0 } ##----------------------------------------## @@ -10371,9 +10432,9 @@ _DelFWAutoUpdateHook_() fi } -##----------------------------------------## -## Modified by Martinski W. [2024-May-17] ## -##----------------------------------------## +##------------------------------------------## +## Modified by ExtremeFiretop [2026-Aug-24] ## +##------------------------------------------## _AddFWAutoUpdateHook_() { local hookScriptFile jobHookAdded=false @@ -10398,12 +10459,12 @@ _AddFWAutoUpdateHook_() if "$jobHookAdded" then Say "Cron job hook was added successfully to '$hookScriptFile' script." - else Say "Cron job hook already exists in '$hookScriptFile' script." + else DoPrintf "Cron job hook already exists in '$hookScriptFile' script.\n" fi } ##------------------------------------------## -## Modified by ExtremeFiretop [2024-Nov-18] ## +## Modified by ExtremeFiretop [2026-Aug-24] ## ##------------------------------------------## _AddScriptAutoUpdateHook_() { @@ -10429,7 +10490,7 @@ _AddScriptAutoUpdateHook_() if "$jobHookAdded" then Say "Cron job hook was added successfully to '$hookScriptFile' script." - else Say "Cron job hook already exists in '$hookScriptFile' script." + else DoPrintf "Cron job hook already exists in '$hookScriptFile' script.\n" fi } diff --git a/merlin-sha256.txt b/merlin-sha256.txt new file mode 100644 index 00000000..2eed75ce --- /dev/null +++ b/merlin-sha256.txt @@ -0,0 +1,21 @@ +a04d65e566b70a2533f1dcde9e0e1e66133438268f66165caeb973a2aaa8e065 GT-AX11000_3004_388.12_2_rog_ubi.w +24f8679af3726386321aea8ae49af2e7a560f46a8f1fca5a93e31a31d72e43d2 GT-AX11000_3004_388.12_2_ubi.w +b78b365ec8060351c5143782ad9a438f67ab39f37b454fbe06113e99a62c6c25 GT-AXE11000_3004_388.12_2_pureubi.w +c4ef1675ecd772f381c6e5a696d643db89e28f64451d140f53bb8fea62d0089d GT-AXE11000_3004_388.12_2_rog_pureubi.w +352f4a89e86f02deb89d5569db0e354c89956f28225458f8e14f4fe6bacc2123 RT-AX58U_3004_388.12_2_puresqubi.w +936e04cccc6da05b94c19c3cb9c6d8b9fbb0a6542adc7b953aea3c0c7c068903 RT-AX68U_3004_388.12_2_pureubi.w +864cb94504bd0f036f730089c779aafbf5827721af10d5533924f94d3c20982a RT-AX86U_3004_388.12_2_pureubi.w +a902d77915c609cf5ee6ac6069c52e3ab460cd4b915f0febcb269175cc842a6d RT-AX88U_3004_388.12_2_ubi.w +5dc2284ac99bcb280b8f57c98d817ae17c1ae5e60102b8cb581680894739f467 GT-AX11000_PRO_3006_102.8_4_nand_squashfs.pkgtb +9497ab9a5956da9b6c9b86a1c0172e0eef9d8e02c9531598332f0a911a92e925 GT-AX6000_3006_102.8_4_nand_squashfs.pkgtb +ea266310a61dc9018fed850e1dba38b37335740fd5e0053683a1133d69837f4a GT-AXE16000_3006_102.8_4_nand_squashfs.pkgtb +f9e896c3b46a2913e60ea2ac638b4ca624d4d239b3a5416864da95f8b4ed1873 GT-BE19000AI_3006_102.8_4_emmc_squashfs.pkgtb +11932c048a9ecc2be8de93a19af985d685bb21999091d70a1b946fbfa57b7960 GT-BE98_PRO_3006_102.8_4_nand_squashfs.pkgtb +78f296e30bde73b842f3e211b3205bdab7e9b947f1b0b40d43a248cc86badaa7 RT-AX86U_PRO_3006_102.8_4_nand_squashfs.pkgtb +8eca0813db27ef8518535cd00060475c58c7928d8878e87d34df4476dde05550 RT-AX88U_PRO_3006_102.8_4_nand_squashfs.pkgtb +566bb3a9a6331293d7a28b2dbb0676dfa846a5f11bafa3a99c848b894c087606 RT-BE58_GO_3006_102.8_4_nand_squashfs.pkgtb +7cf298f161aca6de3ad4ff3840f08eae3c11a39c067da825e444439aac67dabc RT-BE86U_3006_102.8_4_nand_squashfs.pkgtb +28954e19157261fe97adcb266591de839c202d188d485484a87089149c61dea3 RT-BE88U_3006_102.8_4_nand_squashfs.pkgtb +1fc818b504f51a378fcbd1b8acde0d1e7950d28527c364cc8c4937c9a4072fbe RT-BE92U_3006_102.8_4_nand_squashfs.pkgtb +f4483a27071d4bc074b2e42760e736a8d079a5f0fb9cab1684553de11ce5f453 RT-BE96U_3006_102.8_4_nand_squashfs.pkgtb +12b00d614e5073e78c8e87ae84c688c2d93e7afcb4d0f36d479731b1cbb9f54e XT12_3006_102.8_4_nand_squashfs.pkgtb