feat(update): verify signed update metadata (#15914)

* feat(update): verify signed update metadata

* fix(update): harden metadata publishing and app names

- share strict app-name validation across Windows and macOS
- preserve published sidecars until replacements are ready
- select macOS test features by target architecture
- update the signing dependency and URL validation coverage

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(update): prevent replacing published release assets

- block reruns after signed update metadata is published
- clarify the security rationale for app-name validation

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(update): cache TLS responses and avoid double translations

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact: reduce the diffs

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact(update): remove obsolete update paths

- remove unused FFI update branches
- delete legacy Windows and macOS update helpers
- simplify macOS daemon updates and redundant gating

Signed-off-by: fufesou <linlong1266@gmail.com>

* feat(update): secure same-identity macOS offline updates

- verify DMGs using signed metadata, size, and SHA-256
- enforce package and bundle identity plus macOS signature checks
- support custom release policies and propagate manual update failures

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(update): verify Windows installers with Authenticode

Reject unsigned or untrusted EXE/MSI updates
before elevation, clean up failed verified copies,
and fix the macOS manual updater logging import.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(update): clear stale URL on response body errors

Keep body-read failures in the common cleanup path and document the
cache-only Authenticode verification policy.

Signed-off-by: fufesou <linlong1266@gmail.com>

* refactor(update): remove OS signature sanity checks

Remove Windows Authenticode and macOS codesign/Gatekeeper verification.
Keep metadata, artifact integrity, bundle identity, and transaction checks.

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact(update): move update scripts into res/update

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(update): secure Windows MSI handoff against junction races

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(update): secure Windows EXE handoff against junction races

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(update): preserve legacy custom client app names

- allow names starting with digits or underscores
- preserve leading hyphens on Windows for compatibility
- reject leading hyphens on macOS to avoid pgrep option parsing

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact(update): remove some simple wrappers

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact(update): simple refactor

* refact(update): comments on update.scpt

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact(update): comments

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(update): create download destinations exclusively

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(macos): roll back updates when final readiness checks fail

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(msi): propagate MSI installation failures

Signed-off-by: fufesou <linlong1266@gmail.com>

* chore

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(macos): reject stale IPC sockets during updates

Require open IPC sockets in addition to launchd running state before committing the privileged update transaction. Inspect sockets with lsof to avoid triggering IPC authentication.

Add native socket regression coverage to macOS CI. Remove the unused Windows helper, combine offline metadata exports, and restore unrelated formatting changed by the PR.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(macos): allow offline updates without an installed bundle

Validate app-only updates against the running client's bundle identifier and check the destination identity when it exists. Replace source-text assertions with behavioral identity tests and run the macOS update tests in CI.

Signed-off-by: fufesou <linlong1266@gmail.com>

* ci, gate test with PUBLISH_UPDATE_METADATA == 'true'

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou authored and GitHub committed 2026-10-12 10:56:50 +08:00
1 parent c8532719c0
commit 0921a7432f
40 files changed
+5416 -916

No files matched your search

+266 -6
View File
@@ -9,6 +9,9 @@ on:
upload-tag:
type: string
default: "nightly"
publish-update-metadata:
type: boolean
default: false
# NOTE: F-Droid builder script 'flutter/build_fdroid.sh' reads environment
# variables from this workflow!
@@ -51,10 +54,48 @@ env:
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
PUBLISH_UPDATE_METADATA: "${{ inputs.publish-update-metadata }}"
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}-2"
jobs:
validate-update-metadata-keys:
runs-on: ubuntu-22.04
permissions:
contents: read
steps:
- name: Checkout source code
if: ${{ inputs.publish-update-metadata }}
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: Install Python
if: ${{ inputs.publish-update-metadata }}
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
- name: Install Python dependencies
if: ${{ inputs.publish-update-metadata }}
run: 'python -m pip install --only-binary=:all: --require-hashes -r res/update/update_metadata_requirements.txt'
- name: Test update metadata signer
if: ${{ inputs.publish-update-metadata }}
run: |
python -m unittest \
res/update/test_generate_update_metadata.py \
res/update/test_update_metadata_cli.py \
res/update/test_macos_update_scripts.py
- name: Validate update metadata keys
if: ${{ inputs.publish-update-metadata }}
env:
RUSTDESK_UPDATE_ED25519_SEED: ${{ secrets.RUSTDESK_UPDATE_ED25519_SEED }}
RUSTDESK_UPDATE_ED25519_PUBLIC_KEY: ${{ vars.RUSTDESK_UPDATE_ED25519_PUBLIC_KEY }}
run: python res/update/generate_update_metadata.py check-key
generate-sbom:
needs: [validate-update-metadata-keys]
runs-on: ubuntu-latest
permissions:
@@ -83,9 +124,11 @@ jobs:
rustdesk.sbom.json
generate-bridge:
needs: [validate-update-metadata-keys]
uses: ./.github/workflows/bridge.yml
build-RustDeskTempTopMostWindow:
needs: [validate-update-metadata-keys]
uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml
strategy:
fail-fast: false
@@ -360,6 +403,18 @@ jobs:
pip3 install requests argparse
BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./rustdesk/
- name: Verify Windows Flutter version
if: env.UPLOAD_ARTIFACT == 'true' && env.PUBLISH_UPDATE_METADATA == 'true'
shell: powershell
run: |
$reportedVersion = (& ".\rustdesk\rustdesk.exe" --version | Out-String).Trim()
if ($LASTEXITCODE -ne 0) {
throw "rustdesk --version exited with $LASTEXITCODE"
}
if ($reportedVersion -cne $env:VERSION) {
throw "RustDesk reports version '$reportedVersion', expected '$env:VERSION'"
}
- name: Build self-extracted executable
shell: bash
if: env.UPLOAD_ARTIFACT == 'true'
@@ -450,9 +505,25 @@ jobs:
run: |
BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput
- name: Upload signed Windows Flutter EXE
if: env.UPLOAD_ARTIFACT == 'true' && env.PUBLISH_UPDATE_METADATA == 'true' && env.SIGN_BASE_URL != '-2'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: windows-flutter-${{ matrix.job.arch }}-exe
path: ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.exe
if-no-files-found: error
- name: Upload signed Windows Flutter MSI
if: env.UPLOAD_ARTIFACT == 'true' && env.PUBLISH_UPDATE_METADATA == 'true' && env.SIGN_BASE_URL != '-2'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: windows-flutter-${{ matrix.job.arch }}-msi
path: ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.msi
if-no-files-found: error
- name: Publish Release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
if: env.UPLOAD_ARTIFACT == 'true'
if: env.UPLOAD_ARTIFACT == 'true' && env.PUBLISH_UPDATE_METADATA != 'true'
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -463,6 +534,7 @@ jobs:
# The fallback for the flutter version, we use Sciter for 32bit Windows.
build-for-windows-sciter:
name: ${{ matrix.job.target }} (${{ matrix.job.os }})
needs: [validate-update-metadata-keys]
runs-on: ${{ matrix.job.os }}
# Temporarily disable this action due to additional test is needed.
# if: false
@@ -595,6 +667,18 @@ jobs:
pip3 install requests argparse
BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./Release/
- name: Verify Windows Sciter version
if: env.UPLOAD_ARTIFACT == 'true' && env.PUBLISH_UPDATE_METADATA == 'true'
shell: powershell
run: |
$reportedVersion = (& ".\Release\rustdesk.exe" --version | Out-String).Trim()
if ($LASTEXITCODE -ne 0) {
throw "rustdesk --version exited with $LASTEXITCODE"
}
if ($reportedVersion -cne $env:VERSION) {
throw "RustDesk reports version '$reportedVersion', expected '$env:VERSION'"
}
- name: Build self-extracted executable
shell: bash
run: |
@@ -612,9 +696,17 @@ jobs:
run: |
BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput/
- name: Upload signed Windows Sciter EXE
if: env.UPLOAD_ARTIFACT == 'true' && env.PUBLISH_UPDATE_METADATA == 'true' && env.SIGN_BASE_URL != '-2'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: windows-sciter-x86-exe
path: ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.exe
if-no-files-found: error
- name: Publish Release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
if: env.UPLOAD_ARTIFACT == 'true'
if: env.UPLOAD_ARTIFACT == 'true' && env.PUBLISH_UPDATE_METADATA != 'true'
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -755,6 +847,7 @@ jobs:
target: x86_64-apple-darwin,
os: macos-15-intel, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
extra-build-args: "",
extra-cargo-features: "",
arch: x86_64,
vcpkg-triplet: x64-osx,
}
@@ -763,6 +856,7 @@ jobs:
os: macos-14,
# extra-build-args: "--disable-flutter-texture-render", # disable this for mac, because we see a lot of users reporting flickering both on arm and x64, and we can not confirm if texture rendering has better performance if htere is no vram, https://github.com/rustdesk/rustdesk/issues/6296
extra-build-args: "--screencapturekit",
extra-cargo-features: ",screencapturekit",
arch: aarch64,
vcpkg-triplet: arm64-osx,
}
@@ -916,9 +1010,18 @@ jobs:
sed -i -e "s/platform :osx, '.*'/platform :osx, '${MIN_MACOS_VERSION}'/" flutter/macos/Podfile
sed -i -e "s/osx_minimum_system_version = \"[0-9]*.[0-9]*\"/osx_minimum_system_version = \"${MIN_MACOS_VERSION}\"/" Cargo.toml
sed -i -e "s/MACOSX_DEPLOYMENT_TARGET = [0-9]*.[0-9]*;/MACOSX_DEPLOYMENT_TARGET = ${MIN_MACOS_VERSION};/" flutter/macos/Runner.xcodeproj/project.pbxproj
echo "MACOSX_DEPLOYMENT_TARGET=${MIN_MACOS_VERSION}" >> "$GITHUB_ENV"
fi
./build.py --flutter --hwcodec --unix-file-copy-paste ${{ matrix.job.extra-build-args }}
- name: Test verified updates
if: env.PUBLISH_UPDATE_METADATA == 'true'
run: |
python3 -m unittest res/update/test_macos_update_scripts.py
cargo test --locked --release --lib platform::macos::update_tests --features "flutter,hwcodec,unix-file-copy-paste${{ matrix.job.extra-cargo-features }}"
cargo test --locked --release --lib verified_dmg_tests --features "flutter,hwcodec,unix-file-copy-paste${{ matrix.job.extra-cargo-features }}"
cargo test --locked --release --lib update_metadata::tests --features "flutter,hwcodec,unix-file-copy-paste${{ matrix.job.extra-cargo-features }}"
- name: create unsigned dmg
if: env.UPLOAD_ARTIFACT == 'true'
run: |
@@ -951,6 +1054,13 @@ jobs:
./flutter/build/macos/Build/Products/Release/RustDesk.app \
${{ secrets.MACOS_CODESIGN_IDENTITY }} \
./flutter/macos/Runner/Release.entitlements
if [ "$PUBLISH_UPDATE_METADATA" = "true" ]; then
reported_version="$(./flutter/build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/RustDesk --version)"
if [ "$reported_version" != "$VERSION" ]; then
echo "::error::RustDesk reports version '$reported_version', expected '$VERSION'"
exit 1
fi
fi
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv
# notarize the rustdesk-${{ env.VERSION }}.dmg
@@ -958,13 +1068,26 @@ jobs:
- name: Rename rustdesk
if: env.UPLOAD_ARTIFACT == 'true'
shell: bash
env:
VERSION: ${{ env.VERSION }}
ARCH: ${{ matrix.job.arch }}
run: |
for name in rustdesk*??.dmg; do
mv "$name" "${name%%.dmg}-${{ matrix.job.arch }}.dmg"
done
if [ -f "rustdesk-${VERSION}.dmg" ]; then
mv "rustdesk-${VERSION}.dmg" "rustdesk-${VERSION}-${ARCH}.dmg"
fi
test -f "rustdesk-${VERSION}-${ARCH}.dmg"
- name: Upload signed macOS DMG
if: env.UPLOAD_ARTIFACT == 'true' && env.PUBLISH_UPDATE_METADATA == 'true' && env.MACOS_P12_BASE64 != null
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: macos-${{ matrix.job.arch }}-dmg
path: ./rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg
if-no-files-found: error
- name: Publish DMG package
if: env.UPLOAD_ARTIFACT == 'true'
if: env.UPLOAD_ARTIFACT == 'true' && env.PUBLISH_UPDATE_METADATA != 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
prerelease: true
@@ -972,6 +1095,141 @@ jobs:
files: |
rustdesk*-${{ matrix.job.arch }}.dmg
publish-signed-update-metadata:
needs: [build-for-windows-flutter, build-for-windows-sciter, build-for-macOS]
runs-on: ubuntu-22.04
if: ${{ inputs.upload-artifact && inputs.publish-update-metadata && github.repository == 'rustdesk/rustdesk' }}
concurrency:
group: signed-update-${{ inputs.upload-tag }}
cancel-in-progress: false
permissions:
# Required to inspect and publish signed release assets for the target tag.
contents: write
steps:
- name: Refuse to replace published update metadata
shell: bash
env:
GH_TOKEN: ${{ github.token }}
TAG_NAME: ${{ env.TAG_NAME }}
run: |
set -euo pipefail
releases="$(gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/releases?per_page=100")"
release_id="$(
jq -r --arg tag "$TAG_NAME" '
[.[][] | select(.tag_name == $tag)] |
if length == 0 then ""
elif length == 1 then .[0].id
else error("multiple releases found for tag")
end
' <<<"$releases"
)"
if [ -z "$release_id" ]; then
exit 0
fi
assets="$(gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets?per_page=100")"
if jq -e 'any(.[][]; .name == "rustdesk-update.json")' <<<"$assets" >/dev/null; then
echo "::error::Signed update metadata is already published for ${TAG_NAME}; use a new release tag for corrected artifacts."
exit 1
fi
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: Install Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
- name: Install Python dependencies
run: 'python -m pip install --only-binary=:all: --require-hashes -r res/update/update_metadata_requirements.txt'
- name: Download Windows artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: windows-*
path: ./artifacts/
merge-multiple: true
- name: Download macOS artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: macos-*
path: ./artifacts/
merge-multiple: true
- name: Sign update metadata
env:
RUSTDESK_UPDATE_ED25519_SEED: ${{ secrets.RUSTDESK_UPDATE_ED25519_SEED }}
run: |
python res/update/generate_update_metadata.py sign \
--artifact windows x86_64 exe "./artifacts/rustdesk-${VERSION}-x86_64.exe" \
--artifact windows x86_64 msi "./artifacts/rustdesk-${VERSION}-x86_64.msi" \
--artifact windows aarch64 exe "./artifacts/rustdesk-${VERSION}-aarch64.exe" \
--artifact windows aarch64 msi "./artifacts/rustdesk-${VERSION}-aarch64.msi" \
--artifact windows x86 exe "./artifacts/rustdesk-${VERSION}-x86-sciter.exe" \
--artifact macos aarch64 dmg "./artifacts/rustdesk-${VERSION}-aarch64.dmg" \
--artifact macos x86_64 dmg "./artifacts/rustdesk-${VERSION}-x86_64.dmg" \
--version "$VERSION" --release-id "$TAG_NAME" \
--package-id rustdesk \
--release-base-url https://github.com/rustdesk/rustdesk/releases/download \
--published-at "$(git log -1 --format=%cI)" \
--metadata-out ./rustdesk-update.json \
--signature-out ./rustdesk-update.json.sig
- name: Verify update metadata
env:
RUSTDESK_UPDATE_ED25519_PUBLIC_KEY: ${{ vars.RUSTDESK_UPDATE_ED25519_PUBLIC_KEY }}
run: |
python res/update/generate_update_metadata.py verify \
--metadata ./rustdesk-update.json \
--signature ./rustdesk-update.json.sig \
--artifact "./artifacts/rustdesk-${VERSION}-x86_64.exe" \
--artifact "./artifacts/rustdesk-${VERSION}-x86_64.msi" \
--artifact "./artifacts/rustdesk-${VERSION}-aarch64.exe" \
--artifact "./artifacts/rustdesk-${VERSION}-aarch64.msi" \
--artifact "./artifacts/rustdesk-${VERSION}-x86-sciter.exe" \
--artifact "./artifacts/rustdesk-${VERSION}-aarch64.dmg" \
--artifact "./artifacts/rustdesk-${VERSION}-x86_64.dmg" \
--version "$VERSION" --release-id "$TAG_NAME" \
--package-id rustdesk \
--release-base-url https://github.com/rustdesk/rustdesk/releases/download
- name: Upload signed update artifacts
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
prerelease: true
fail_on_unmatched_files: true
tag_name: ${{ env.TAG_NAME }}
files: |
./artifacts/rustdesk-${{ env.VERSION }}-x86_64.exe
./artifacts/rustdesk-${{ env.VERSION }}-x86_64.msi
./artifacts/rustdesk-${{ env.VERSION }}-aarch64.exe
./artifacts/rustdesk-${{ env.VERSION }}-aarch64.msi
./artifacts/rustdesk-${{ env.VERSION }}-x86-sciter.exe
./artifacts/rustdesk-${{ env.VERSION }}-aarch64.dmg
./artifacts/rustdesk-${{ env.VERSION }}-x86_64.dmg
# The release action replaces same-name assets, so keep published sidecars
# until replacements are ready.
- name: Upload signed update metadata signature
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
prerelease: true
fail_on_unmatched_files: true
tag_name: ${{ env.TAG_NAME }}
files: ./rustdesk-update.json.sig
# Clients discover the release through metadata, so publish it only after every referenced asset.
- name: Publish signed update metadata
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
prerelease: true
fail_on_unmatched_files: true
tag_name: ${{ env.TAG_NAME }}
files: ./rustdesk-update.json
publish_unsigned:
needs:
- build-for-macOS
@@ -2172,6 +2430,7 @@ jobs:
build-rustdesk-linux-sciter:
if: ${{ inputs.upload-artifact }}
needs: [validate-update-metadata-keys]
runs-on: ${{ matrix.job.on }}
name: build-rustdesk-linux-sciter ${{ matrix.job.target }}
strategy:
@@ -2552,6 +2811,7 @@ jobs:
build-rustdesk-web:
if: False
needs: [validate-update-metadata-keys]
name: build-rustdesk-web
runs-on: ubuntu-22.04
permissions: