From d482498b232043fce452f962ce96df66bf72a864 Mon Sep 17 00:00:00 2001 From: hashbk Date: Fri, 19 Jun 2026 02:12:37 +0000 Subject: [PATCH 1/2] feat: support custom client config signing with self-owned Ed25519 key Replace the hardcoded official public key with a self-generated one, and add CI workflow steps to sign custom.txt from Action Secrets. - Generate new Ed25519 key pair for custom.txt verification - Replace public key in src/common.rs - Add res/sign_custom.py script for signing config with Ed25519 - Add CI workflow steps to sign and place custom.txt for all platforms (Windows Flutter/Sciter, macOS, Linux Flutter/Sciter) - Include custom.txt in deb, rpm, and arch packages when present Required GitHub Secrets: - CUSTOM_CLIENT_PRIVATE_KEY: Base64-encoded 64-byte Ed25519 private key - CUSTOM_CLIENT_CONFIG: JSON string of custom client configuration --- .github/workflows/flutter-build.yml | 70 +++++++++++++++++++++++ build.py | 8 +++ res/PKGBUILD | 4 ++ res/rpm-flutter-suse.spec | 4 ++ res/rpm-flutter.spec | 4 ++ res/sign_custom.py | 89 +++++++++++++++++++++++++++++ src/common.rs | 2 +- 7 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 res/sign_custom.py diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 17709d5acc8..df3ab002538 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -46,6 +46,8 @@ env: MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}" SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}-2" + CUSTOM_CLIENT_PRIVATE_KEY: "${{ secrets.CUSTOM_CLIENT_PRIVATE_KEY }}" + CUSTOM_CLIENT_CONFIG: "${{ secrets.CUSTOM_CLIENT_CONFIG }}" jobs: generate-bridge: @@ -163,12 +165,27 @@ jobs: head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true shell: bash + - name: Sign and generate custom.txt + if: env.CUSTOM_CLIENT_PRIVATE_KEY != null && env.CUSTOM_CLIENT_CONFIG != null + shell: bash + run: | + pip3 install cryptography + python3 res/sign_custom.py \ + --private-key-b64 "${{ secrets.CUSTOM_CLIENT_PRIVATE_KEY }}" \ + --config "${{ secrets.CUSTOM_CLIENT_CONFIG }}" \ + --output custom.txt + - name: Build rustdesk run: | # Windows: build RustDesk python3 .\build.py --portable --hwcodec --flutter --vram --skip-portable-pack mv ./flutter/build/windows/x64/runner/Release ./rustdesk + # Place custom.txt alongside the executable + if [ -f custom.txt ]; then + cp custom.txt ./rustdesk/custom.txt + fi + # Download usbmmidd_v2.zip and extract it to ./rustdesk Invoke-WebRequest -Uri https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip -OutFile usbmmidd_v2.zip Expand-Archive usbmmidd_v2.zip -DestinationPath . @@ -356,6 +373,16 @@ jobs: head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true shell: bash + - name: Sign and generate custom.txt + if: env.CUSTOM_CLIENT_PRIVATE_KEY != null && env.CUSTOM_CLIENT_CONFIG != null + shell: bash + run: | + pip3 install cryptography + python3 res/sign_custom.py \ + --private-key-b64 "${{ secrets.CUSTOM_CLIENT_PRIVATE_KEY }}" \ + --config "${{ secrets.CUSTOM_CLIENT_CONFIG }}" \ + --output custom.txt + - name: Build rustdesk id: build shell: bash @@ -367,6 +394,10 @@ jobs: cargo build --locked --features inline,vram,hwcodec --release --bins mkdir -p ./Release mv ./target/release/rustdesk.exe ./Release/rustdesk.exe + # Place custom.txt alongside the executable + if [ -f custom.txt ]; then + cp custom.txt ./Release/custom.txt + fi curl -LJ -o ./Release/sciter.dll https://github.com/c-smile/sciter-sdk/raw/master/bin.win/x32/sciter.dll echo "output_folder=./Release" >> $GITHUB_OUTPUT curl -LJ -o ./usbmmidd_v2.zip https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip @@ -711,6 +742,16 @@ jobs: cargo -V rustc -V + - name: Sign and generate custom.txt + if: env.CUSTOM_CLIENT_PRIVATE_KEY != null && env.CUSTOM_CLIENT_CONFIG != null + shell: bash + run: | + pip3 install cryptography + python3 res/sign_custom.py \ + --private-key-b64 "${{ secrets.CUSTOM_CLIENT_PRIVATE_KEY }}" \ + --config "${{ secrets.CUSTOM_CLIENT_CONFIG }}" \ + --output custom.txt + - name: Build rustdesk run: | if [ "${{ matrix.job.target }}" = "aarch64-apple-darwin" ]; then @@ -722,6 +763,11 @@ jobs: fi ./build.py --flutter --hwcodec --unix-file-copy-paste ${{ matrix.job.extra-build-args }} + # Place custom.txt in macOS app Resources directory + if [ -f custom.txt ]; then + cp custom.txt ./flutter/build/macos/Build/Products/Release/RustDesk.app/Contents/Resources/custom.txt + fi + - name: create unsigned dmg if: env.UPLOAD_ARTIFACT == 'true' run: | @@ -1410,6 +1456,16 @@ jobs: name: bridge-artifact path: ./ + - name: Sign and generate custom.txt + if: env.CUSTOM_CLIENT_PRIVATE_KEY != null && env.CUSTOM_CLIENT_CONFIG != null + shell: bash + run: | + pip3 install cryptography + python3 res/sign_custom.py \ + --private-key-b64 "${{ secrets.CUSTOM_CLIENT_PRIVATE_KEY }}" \ + --config "${{ secrets.CUSTOM_CLIENT_CONFIG }}" \ + --output custom.txt + - uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 # no release tag; commit 2026-05-26 name: Build rustdesk id: vcpkg @@ -1694,6 +1750,16 @@ jobs: RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}') echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> $GITHUB_ENV + - name: Sign and generate custom.txt + if: env.CUSTOM_CLIENT_PRIVATE_KEY != null && env.CUSTOM_CLIENT_CONFIG != null + shell: bash + run: | + pip3 install cryptography + python3 res/sign_custom.py \ + --private-key-b64 "${{ secrets.CUSTOM_CLIENT_PRIVATE_KEY }}" \ + --config "${{ secrets.CUSTOM_CLIENT_CONFIG }}" \ + --output custom.txt + - uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 # no release tag; commit 2026-05-26 name: Build rustdesk sciter binary for ${{ matrix.job.arch }} id: vcpkg @@ -1826,6 +1892,10 @@ jobs: # make debian package mkdir -p ./Release mv ./target/release/rustdesk ./Release/rustdesk + # Place custom.txt alongside the executable + if [ -f custom.txt ]; then + cp custom.txt ./Release/custom.txt + fi wget -O ./Release/libsciter-gtk.so https://github.com/c-smile/sciter-sdk/raw/master/bin.lnx/${{ matrix.job.sciter_arch }}/libsciter-gtk.so export DEB_ARCH=${{ matrix.job.deb_arch }} ./build.py --package ./Release diff --git a/build.py b/build.py index 894a7808d26..eabb93cacd5 100755 --- a/build.py +++ b/build.py @@ -352,6 +352,10 @@ def build_flutter_deb(version, features): system2( "echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") + # Include custom.txt if present + if os.path.isfile('../custom.txt'): + system2('cp ../custom.txt tmpdeb/usr/share/rustdesk/custom.txt') + system2('mkdir -p tmpdeb/DEBIAN') generate_control_file(version) system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/') @@ -389,6 +393,10 @@ def build_deb_from_folder(version, binary_folder): system2( "echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") + # Include custom.txt if present + if os.path.isfile('../custom.txt'): + system2('cp ../custom.txt tmpdeb/usr/share/rustdesk/custom.txt') + system2('mkdir -p tmpdeb/DEBIAN') generate_control_file(version) system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/') diff --git a/res/PKGBUILD b/res/PKGBUILD index 92b6ffd0b1b..df374967aec 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -32,4 +32,8 @@ package() { install -Dm 644 $HBB/res/rustdesk-link.desktop -t "${pkgdir}/usr/share/rustdesk/files" install -Dm 644 $HBB/res/128x128@2x.png "${pkgdir}/usr/share/icons/hicolor/256x256/apps/rustdesk.png" install -Dm 644 $HBB/res/scalable.svg "${pkgdir}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg" + # Include custom.txt if present + if [ -f "${HBB}/custom.txt" ]; then + install -Dm 644 "${HBB}/custom.txt" -t "${pkgdir}/usr/share/rustdesk" + fi } diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index f72e9dca878..962ebb4c824 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -31,6 +31,10 @@ install -Dm 644 $HBB/res/rustdesk.desktop -t "%{buildroot}/usr/share/rustdesk/fi install -Dm 644 $HBB/res/rustdesk-link.desktop -t "%{buildroot}/usr/share/rustdesk/files" install -Dm 644 $HBB/res/128x128@2x.png "%{buildroot}/usr/share/icons/hicolor/256x256/apps/rustdesk.png" install -Dm 644 $HBB/res/scalable.svg "%{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg" +# Include custom.txt if present +if [ -f "${HBB}/custom.txt" ]; then + install -Dm 644 "${HBB}/custom.txt" -t "%{buildroot}/usr/share/rustdesk" +fi %files /usr/share/rustdesk/* diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 0e7d53ec7a2..2f9a8945cf4 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -31,6 +31,10 @@ install -Dm 644 $HBB/res/rustdesk.desktop -t "%{buildroot}/usr/share/rustdesk/fi install -Dm 644 $HBB/res/rustdesk-link.desktop -t "%{buildroot}/usr/share/rustdesk/files" install -Dm 644 $HBB/res/128x128@2x.png "%{buildroot}/usr/share/icons/hicolor/256x256/apps/rustdesk.png" install -Dm 644 $HBB/res/scalable.svg "%{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg" +# Include custom.txt if present +if [ -f "${HBB}/custom.txt" ]; then + install -Dm 644 "${HBB}/custom.txt" -t "%{buildroot}/usr/share/rustdesk" +fi %files /usr/share/rustdesk/* diff --git a/res/sign_custom.py b/res/sign_custom.py new file mode 100644 index 00000000000..79af7d67254 --- /dev/null +++ b/res/sign_custom.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Sign custom client config and generate custom.txt. + +Usage: + python3 sign_custom.py --private-key-b64 --config --output + +The private key should be a 64-byte Ed25519 key (seed + public_key) encoded in Base64, +compatible with sodiumoxide's sign::SecretKey format. + +The config should be a JSON string containing the custom client configuration. + +The output will be a Base64-encoded string of (signature + message), +compatible with sodiumoxide's sign::verify() format. +""" + +import argparse +import base64 +import json +import sys + +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from cryptography.hazmat.primitives import serialization + + +def sign_config(private_key_b64: str, config_json: str) -> str: + """Sign the config JSON with the given Ed25519 private key. + + Args: + private_key_b64: Base64-encoded 64-byte private key (seed + public_key, sodiumoxide format) + config_json: JSON string of the custom client config + + Returns: + Base64-encoded signed data (signature + message) + """ + sk_bytes = base64.b64decode(private_key_b64) + if len(sk_bytes) != 64: + print(f"Error: Private key must be 64 bytes, got {len(sk_bytes)}", file=sys.stderr) + sys.exit(1) + + # sodiumoxide format: first 32 bytes = seed, last 32 bytes = public key + seed = sk_bytes[:32] + private_key = Ed25519PrivateKey.from_private_bytes(seed) + + # Sign the message + message = config_json.encode("utf-8") + signature = private_key.sign(message) + + # sodiumoxide sign format: signature (64 bytes) + message + signed_data = signature + message + + return base64.b64encode(signed_data).decode("ascii") + + +def main(): + parser = argparse.ArgumentParser(description="Sign custom client config") + parser.add_argument( + "--private-key-b64", + required=True, + help="Base64-encoded 64-byte Ed25519 private key (sodiumoxide format)", + ) + parser.add_argument( + "--config", + required=True, + help="JSON string of the custom client config", + ) + parser.add_argument( + "--output", + required=True, + help="Output path for the signed custom.txt file", + ) + args = parser.parse_args() + + # Validate JSON + try: + json.loads(args.config) + except json.JSONDecodeError as e: + print(f"Error: Invalid JSON config: {e}", file=sys.stderr) + sys.exit(1) + + signed = sign_config(args.private_key_b64, args.config) + + with open(args.output, "w") as f: + f.write(signed) + + print(f"Signed custom.txt written to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/src/common.rs b/src/common.rs index 69e3ec3045d..05732f3d518 100644 --- a/src/common.rs +++ b/src/common.rs @@ -2184,7 +2184,7 @@ pub fn read_custom_client(config: &str) { log::error!("Failed to decode custom client config"); return; }; - const KEY: &str = "5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM="; + const KEY: &str = "rgvzRBBtKBZnTT6iL2zhpyKxrtyC0iSnZXIUshDcNs4="; let Some(pk) = get_rs_pk(KEY) else { log::error!("Failed to parse public key of custom client"); return; From afaa03df1fdc1e8bc7245093f349aefc8d61f39d Mon Sep 17 00:00:00 2001 From: hashbk Date: Fri, 19 Jun 2026 03:13:16 +0000 Subject: [PATCH 2/2] fix: use PowerShell syntax for custom.txt check in Windows Flutter build The Windows Flutter build step runs on PowerShell by default, bash `if [ -f ]` syntax causes a parser error. Switch to `if (Test-Path)` which is native PowerShell. --- .github/workflows/flutter-build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index df3ab002538..0c59468ebd5 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -182,9 +182,9 @@ jobs: mv ./flutter/build/windows/x64/runner/Release ./rustdesk # Place custom.txt alongside the executable - if [ -f custom.txt ]; then - cp custom.txt ./rustdesk/custom.txt - fi + if (Test-Path custom.txt) { + Copy-Item custom.txt ./rustdesk/custom.txt + } # Download usbmmidd_v2.zip and extract it to ./rustdesk Invoke-WebRequest -Uri https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip -OutFile usbmmidd_v2.zip