Browse Source

feat(release): publish SHA-256 sums and verify them in install.sh/update.sh (#6393)

* feat(release): publish SHA-256 sums and verify them in install.sh/update.sh

The installer and updater fetched the release archive and extracted it
after checking only that the file is not empty, and the release workflow
published no checksums. TLS protects the transport, not the bytes: a
truncated or swapped asset, a bad mirror or a TLS-terminating proxy was
installed as root. #5396 added this verification for the Xray archive;
the panel's own archive was the remaining unverified download.

Publish <asset>.sha256 next to every release archive (Linux and Windows)
and verify it before extracting. A mismatch aborts the install; a missing
sidecar, which every release before this change has, only warns, so
installing older tags keeps working.

Assisted-by: Claude Code:claude-fable-5-1

* fix(install): fail closed when the checksum sidecar cannot be fetched

Review follow-up. Any curl failure on the sidecar (5xx, reset, DNS) was
treated as "no checksum published", so whoever can swap the archive
could also drop the 90-byte sidecar request and skip the check. Only a
404, which every release before the sidecar existed returns, is still
tolerated with a warning; every other outcome aborts and removes the
downloaded archive.

Assisted-by: Claude Code:claude-fable-5-1

* fix(install): restore the closing brace lost in the main merge
ilyusha 4 hours ago
parent
commit
f294e1806d
3 changed files with 68 additions and 8 deletions
  1. 16 8
      .github/workflows/release.yml
  2. 30 0
      install.sh
  3. 22 0
      update.sh

+ 16 - 8
.github/workflows/release.yml

@@ -183,13 +183,17 @@ jobs:
           cd ../..
           cd ../..
 
 
       - name: Package
       - name: Package
-        run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui
+        run: |
+          tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui
+          sha256sum x-ui-linux-${{ matrix.platform }}.tar.gz > x-ui-linux-${{ matrix.platform }}.tar.gz.sha256
 
 
       - name: Upload files to Artifacts
       - name: Upload files to Artifacts
         uses: actions/upload-artifact@v7
         uses: actions/upload-artifact@v7
         with:
         with:
           name: x-ui-linux-${{ matrix.platform }}
           name: x-ui-linux-${{ matrix.platform }}
-          path: ./x-ui-linux-${{ matrix.platform }}.tar.gz
+          path: |
+            ./x-ui-linux-${{ matrix.platform }}.tar.gz
+            ./x-ui-linux-${{ matrix.platform }}.tar.gz.sha256
 
 
       - name: Upload files to GH release
       - name: Upload files to GH release
         uses: svenstaro/upload-release-action@v2
         uses: svenstaro/upload-release-action@v2
@@ -197,8 +201,8 @@ jobs:
         with:
         with:
           repo_token: ${{ secrets.GITHUB_TOKEN }}
           repo_token: ${{ secrets.GITHUB_TOKEN }}
           tag: ${{ github.ref_name }}
           tag: ${{ github.ref_name }}
-          file: x-ui-linux-${{ matrix.platform }}.tar.gz
-          asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
+          file: x-ui-linux-${{ matrix.platform }}.tar.gz*
+          file_glob: true
           overwrite: true
           overwrite: true
           prerelease: true
           prerelease: true
 
 
@@ -316,12 +320,16 @@ jobs:
         shell: pwsh
         shell: pwsh
         run: |
         run: |
           Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip"
           Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip"
+          $hash = (Get-FileHash x-ui-windows-amd64.zip -Algorithm SHA256).Hash.ToLower()
+          [IO.File]::WriteAllText("$PWD\x-ui-windows-amd64.zip.sha256", "$hash  x-ui-windows-amd64.zip`n")
 
 
       - name: Upload files to Artifacts
       - name: Upload files to Artifacts
         uses: actions/upload-artifact@v7
         uses: actions/upload-artifact@v7
         with:
         with:
           name: x-ui-windows-amd64
           name: x-ui-windows-amd64
-          path: ./x-ui-windows-amd64.zip
+          path: |
+            ./x-ui-windows-amd64.zip
+            ./x-ui-windows-amd64.zip.sha256
 
 
       - name: Upload files to GH release
       - name: Upload files to GH release
         uses: svenstaro/upload-release-action@v2
         uses: svenstaro/upload-release-action@v2
@@ -329,8 +337,8 @@ jobs:
         with:
         with:
           repo_token: ${{ secrets.GITHUB_TOKEN }}
           repo_token: ${{ secrets.GITHUB_TOKEN }}
           tag: ${{ github.ref_name }}
           tag: ${{ github.ref_name }}
-          file: x-ui-windows-amd64.zip
-          asset_name: x-ui-windows-amd64.zip
+          file: x-ui-windows-amd64.zip*
+          file_glob: true
           overwrite: true
           overwrite: true
           prerelease: true
           prerelease: true
 
 
@@ -398,4 +406,4 @@ jobs:
               --target "${COMMIT}" --title "Dev build ${short}" --notes "${notes}"
               --target "${COMMIT}" --title "Dev build ${short}" --notes "${notes}"
           fi
           fi
 
 
-          retry gh release upload dev-latest dev-artifacts/*.tar.gz dev-artifacts/*.zip --clobber
+          retry gh release upload dev-latest dev-artifacts/*.tar.gz dev-artifacts/*.zip dev-artifacts/*.sha256 --clobber

+ 30 - 0
install.sh

@@ -1433,6 +1433,34 @@ resolve_latest_tag() {
     curl -Ls --retry 5 --retry-delay 3 --connect-timeout 15 --max-time 60 "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'
     curl -Ls --retry 5 --retry-delay 3 --connect-timeout 15 --max-time 60 "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'
 }
 }
 
 
+# Releases publish <asset>.sha256 next to each archive. A mismatch or a failed
+# sidecar download aborts the install; only a 404 (releases predating the
+# sidecar) is tolerated with a warning.
+verify_release_checksum() {
+    local url="$1" file="$2" sums="$2.sha256" code expected actual
+    rm -f "${sums}"
+    code=$(curl -sL --retry 3 --retry-delay 3 --connect-timeout 15 --max-time 60 -o "${sums}" -w '%{http_code}' "${url}.sha256")
+    if [[ "${code}" == "404" ]]; then
+        rm -f "${sums}"
+        echo -e "${yellow}No checksum published for this release, skipping verification${plain}"
+        return 0
+    fi
+    if [[ "${code}" != "200" ]]; then
+        rm -f "${sums}" "${file}"
+        echo -e "${red}Failed to download the checksum for $(basename "${file}") (HTTP ${code})${plain}"
+        exit 1
+    fi
+    expected=$(awk 'NR == 1 {print $1}' "${sums}")
+    actual=$(sha256sum "${file}" | awk '{print $1}')
+    rm -f "${sums}"
+    if [[ ! "${expected}" =~ ^[0-9a-f]{64}$ || "${expected}" != "${actual}" ]]; then
+        rm -f "${file}"
+        echo -e "${red}Checksum mismatch for $(basename "${file}"): expected ${expected:-<none>}, got ${actual}${plain}"
+        exit 1
+    fi
+    echo -e "${green}Checksum verified: ${actual}${plain}"
+}
+
 # Older tags predate some of these files (x-ui.rc arrived in v2.8.4). Serving
 # Older tags predate some of these files (x-ui.rc arrived in v2.8.4). Serving
 # main's copy against an old binary is the mismatch this pinning exists to
 # main's copy against an old binary is the mismatch this pinning exists to
 # prevent, so probe before anything is stopped or removed and refuse the tag.
 # prevent, so probe before anything is stopped or removed and refuse the tag.
@@ -1471,6 +1499,7 @@ install_x-ui() {
             echo -e "${red}Downloaded x-ui release archive is empty${plain}"
             echo -e "${red}Downloaded x-ui release archive is empty${plain}"
             exit 1
             exit 1
         fi
         fi
+        verify_release_checksum "https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz" "${xui_folder}-linux-$(arch).tar.gz"
     else
     else
         tag_version=$1
         tag_version=$1
         # The rolling dev channel ships under a fixed, non-semver tag that is
         # The rolling dev channel ships under a fixed, non-semver tag that is
@@ -1501,6 +1530,7 @@ install_x-ui() {
             echo -e "${red}Downloaded x-ui release archive is empty${plain}"
             echo -e "${red}Downloaded x-ui release archive is empty${plain}"
             exit 1
             exit 1
         fi
         fi
+        verify_release_checksum "${url}" "${xui_folder}-linux-$(arch).tar.gz"
     fi
     fi
     # x-ui.sh, x-ui.rc and the unit files must come from the same release as
     # x-ui.sh, x-ui.rc and the unit files must come from the same release as
     # the binary; only the rolling dev build tracks main.
     # the binary; only the rolling dev build tracks main.

+ 22 - 0
update.sh

@@ -1034,6 +1034,28 @@ update_x-ui() {
         rm ${xui_folder}-linux-$(arch).tar.gz -f > /dev/null 2>&1
         rm ${xui_folder}-linux-$(arch).tar.gz -f > /dev/null 2>&1
         _fail "ERROR: Downloaded x-ui release archive is empty, please be sure that your server can access GitHub"
         _fail "ERROR: Downloaded x-ui release archive is empty, please be sure that your server can access GitHub"
     fi
     fi
+    # Releases publish <asset>.sha256 next to each archive. A mismatch or a
+    # failed sidecar download aborts the update; only a 404 (releases
+    # predating the sidecar) is tolerated with a warning.
+    archive="${xui_folder}-linux-$(arch).tar.gz"
+    rm -f "${archive}.sha256"
+    sidecar_code=$(${curl_bin} -sL --retry 3 --retry-delay 3 --connect-timeout 15 --max-time 60 -o "${archive}.sha256" -w '%{http_code}' "https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz.sha256" 2> /dev/null)
+    if [[ "${sidecar_code}" == "200" ]]; then
+        expected_sha256=$(awk 'NR == 1 {print $1}' "${archive}.sha256")
+        actual_sha256=$(sha256sum "${archive}" | awk '{print $1}')
+        rm -f "${archive}.sha256"
+        if [[ ! "${expected_sha256}" =~ ^[0-9a-f]{64}$ || "${expected_sha256}" != "${actual_sha256}" ]]; then
+            rm -f "${archive}"
+            _fail "ERROR: Checksum mismatch for $(basename "${archive}"): expected ${expected_sha256:-<none>}, got ${actual_sha256}"
+        fi
+        echo -e "${green}Checksum verified: ${actual_sha256}${plain}"
+    elif [[ "${sidecar_code}" == "404" ]]; then
+        rm -f "${archive}.sha256"
+        echo -e "${yellow}No checksum published for this release, skipping verification${plain}"
+    else
+        rm -f "${archive}.sha256" "${archive}"
+        _fail "ERROR: Failed to download the checksum for x-ui-linux-$(arch).tar.gz (HTTP ${sidecar_code})"
+    fi
 
 
     if [[ -e ${xui_folder}/ ]]; then
     if [[ -e ${xui_folder}/ ]]; then
         echo -e "${green}Stopping x-ui...${plain}"
         echo -e "${green}Stopping x-ui...${plain}"