summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2025-02-24 19:30:54 +0100
committerDavid Lönnhager <david.l@mullvad.net>2025-03-05 23:32:36 +0100
commita193f5bf70500bd1b10edf2904c986419c18748f (patch)
treef55313d61c431ca79ae61f01cdf15fcd94cdd73e
parentb5964d5b9f64089d99e29925fc5f578e7cf75b24 (diff)
downloadmullvadvpn-a193f5bf70500bd1b10edf2904c986419c18748f.tar.xz
mullvadvpn-a193f5bf70500bd1b10edf2904c986419c18748f.zip
Sign and notarize installer downloader
-rw-r--r--.github/workflows/downloader.yml4
-rwxr-xr-xinstaller-downloader/build.sh266
-rw-r--r--installer-downloader/src/cacao_impl/mod.rs2
-rw-r--r--installer-downloader/src/resource.rs2
4 files changed, 233 insertions, 41 deletions
diff --git a/.github/workflows/downloader.yml b/.github/workflows/downloader.yml
index c8c90716ae..33ce992fb7 100644
--- a/.github/workflows/downloader.yml
+++ b/.github/workflows/downloader.yml
@@ -55,7 +55,7 @@ jobs:
- name: Check file size
uses: ./.github/actions/check-file-size
with:
- artifact: "./installer-downloader/dist/MullvadDownloader.exe"
+ artifact: "./dist/Install Mullvad VPN.exe"
max_size: ${{ env.MAX_BINARY_SIZE }}
build-macos:
@@ -77,5 +77,5 @@ jobs:
- name: Check file size
uses: ./.github/actions/check-file-size
with:
- artifact: "./installer-downloader/dist/MullvadDownloader.dmg"
+ artifact: "./dist/Install Mullvad VPN.dmg"
max_size: ${{ env.MAX_BINARY_SIZE }}
diff --git a/installer-downloader/build.sh b/installer-downloader/build.sh
index a425c10ff6..56adbd6be3 100755
--- a/installer-downloader/build.sh
+++ b/installer-downloader/build.sh
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
-# This script is used to build, and optionally sign the downloader, always in release mode.
+# This script is used to build, and optionally sign, the downloader, always in release mode.
# This script performs the equivalent of the following profile:
#
@@ -25,8 +25,71 @@ source ../scripts/utils/host
source ../scripts/utils/log
CARGO_TARGET_DIR=${CARGO_TARGET_DIR:-"../target"}
-DIST_DIR="./dist"
+export CARGO_TARGET_DIR
+# Temporary build directory
+BUILD_DIR="./build"
+# Successfully built (and signed) artifacts
+DIST_DIR="../dist"
+
+BUNDLE_NAME="MullvadDownloader"
+BUNDLE_ID="net.mullvad.$BUNDLE_NAME"
+
+FILENAME="Install Mullvad VPN"
+
+rm -rf "$BUILD_DIR"
+mkdir -p "$BUILD_DIR"
+
+mkdir -p "$DIST_DIR"
+
+# Whether to sign and notarized produced binaries
+SIGN="false"
+
+# Temporary keychain to store the .p12 in.
+# This is automatically created/replaced when signing on macOS.
+SIGN_KEYCHAIN_PATH="$HOME/Library/Keychains/mv-metadata-keychain-db"
+
+# Parse arguments
+while [[ "$#" -gt 0 ]]; do
+ case $1 in
+ --sign)
+ SIGN="true"
+ ;;
+ *)
+ log_error "Unknown parameter: $1"
+ exit 1
+ ;;
+ esac
+ shift
+done
+
+# Check that we have the correct environment set for signing
+function assert_can_sign {
+ if [[ "$(uname -s)" == "Darwin" ]]; then
+ if [[ -z ${CSC_LINK-} ]]; then
+ log_error "The variable CSC_LINK is not set. It needs to point to a file containing the private key used for signing of binaries."
+ exit 1
+ fi
+ if [[ -z ${CSC_KEY_PASSWORD-} ]]; then
+ read -rsp "CSC_KEY_PASSWORD = " CSC_KEY_PASSWORD
+ echo ""
+ export CSC_KEY_PASSWORD
+ fi
+ if [[ -z ${NOTARIZE_KEYCHAIN-} || -z ${NOTARIZE_KEYCHAIN_PROFILE-} ]]; then
+ log_error "The variables NOTARIZE_KEYCHAIN and NOTARIZE_KEYCHAIN_PROFILE must be set."
+ exit 1
+ fi
+ elif [[ "$(uname -s)" == "MINGW"* ]]; then
+ if [[ -z ${CERT_HASH-} ]]; then
+ log_error "The variable CERT_HASH is not set. It needs to be set to the thumbprint of the signing certificate."
+ exit 1
+ fi
+ fi
+}
+
+# Run cargo with all appropriate flags and options
+# Arguments:
+# - (optional) target
function build_executable {
local -a target_args=()
@@ -43,16 +106,12 @@ function build_executable {
set -u
}
-function dist_windows_app {
- cp "$CARGO_TARGET_DIR/release/installer-downloader.exe" "$DIST_DIR/MullvadDownloader.exe"
-}
-
-# Combine executables on macOS
+# Combine executables on macOS. This must be run after build_executable for both x86 and arm64.
function lipo_executables {
local target_exes
target_exes=()
- rm -rf "$DIST_DIR/installer-downloader"
+ rm -rf "$BUILD_DIR/installer-downloader"
case $HOST in
x86_64-apple-darwin) target_exes=(
@@ -67,17 +126,79 @@ function lipo_executables {
;;
esac
- lipo "${target_exes[@]}" -create -output "$DIST_DIR/installer-downloader"
+ lipo "${target_exes[@]}" -create -output "$BUILD_DIR/installer-downloader"
+}
+
+# Create temporary keychain for importing $CSC_LINK
+function setup_macos_keychain {
+ log_info "Creating a temporary keychain \"$SIGN_KEYCHAIN_PATH\" for $CSC_LINK"
+
+ SIGN_KEYCHAIN_PASS=$(openssl rand -base64 64)
+ export SIGN_KEYCHAIN_PASS
+
+ delete_macos_keychain
+ trap "delete_macos_keychain" EXIT
+
+ /usr/bin/security create-keychain -p "$SIGN_KEYCHAIN_PASS" "$SIGN_KEYCHAIN_PATH"
+ /usr/bin/security unlock-keychain -p "$SIGN_KEYCHAIN_PASS" "$SIGN_KEYCHAIN_PATH"
+ /usr/bin/security set-keychain-settings "$SIGN_KEYCHAIN_PATH"
+
+ # Include keychain in the search list, or codesign won't find it
+ /usr/bin/security list-keychains -d user -s "$SIGN_KEYCHAIN_PATH"
+
+ log_info "Importing PKCS #12 to keychain"
+
+ /usr/bin/security import "$CSC_LINK" -k "$SIGN_KEYCHAIN_PATH" -P "$CSC_KEY_PASSWORD" -T /usr/bin/codesign
+
+ # Prevent password prompt when signing
+ /usr/bin/security set-key-partition-list -S "apple-tool:,apple:" -s -k "$SIGN_KEYCHAIN_PASS" "$SIGN_KEYCHAIN_PATH"
+
+ log_info "Done."
+
+ # Find identity
+ log_info "Find the identity to use"
+
+ /usr/bin/security find-identity -p codesigning
+ read -rp "Enter identity: " SIGN_KEYCHAIN_IDENTITY
+ export SIGN_KEYCHAIN_IDENTITY
+
+ # TODO: auto-detect identity
}
+function delete_macos_keychain {
+ /usr/bin/security delete-keychain "$SIGN_KEYCHAIN_PATH" || true
+ rm -f "$SIGN_KEYCHAIN_PATH"
+}
+
+# Sign an artifact.
+# - setup_macos_keychain must be called first
+# Arguments:
+# - file to sign
+function sign_macos {
+ local file="$1"
+ if [[ "$SIGN" == "false" ]]; then
+ # Ad-hoc sign app bundle
+ /usr/bin/codesign --identifier "$BUNDLE_ID" \
+ --sign - \
+ --timestamp=none --verbose=0 -o runtime \
+ "$file"
+ else
+ /usr/bin/codesign --identifier "$BUNDLE_ID" \
+ --sign "$SIGN_KEYCHAIN_IDENTITY" \
+ --keychain "$SIGN_KEYCHAIN_PATH" \
+ --verbose=0 -o runtime \
+ "$file"
+ fi
+}
+
+# Build app bundle and dmg, and optionally sign it.
+# If `$SIGN` is false, the app bundle is only ad-hoc signed.
function dist_macos_app {
- local app_path
- bundle_name="MullvadDownloader"
- bundle_id="net.mullvad.$bundle_name"
- app_path="$DIST_DIR/$bundle_name.app/"
+ local app_path="$BUILD_DIR/$FILENAME.app/"
+ local dmg_path="$BUILD_DIR/$FILENAME.dmg"
# Build app bundle
- echo "Building $app_path..."
+ log_info "Building $app_path..."
rm -rf "$app_path"
@@ -87,37 +208,108 @@ function dist_macos_app {
mkdir -p "$app_path/Contents/MacOS"
cp ./assets/Info.plist "$app_path/Contents/Info.plist"
- cp "$DIST_DIR/installer-downloader" "$app_path/Contents/MacOS/installer-downloader"
+ cp "$BUILD_DIR/installer-downloader" "$app_path/Contents/MacOS/installer-downloader"
- # Ad-hoc sign app bundle
- codesign --force --deep --identifier "$bundle_id" --sign - \
- --timestamp=none --verbose=0 -o runtime \
- "$DIST_DIR/$bundle_name.app"
+ # Sign app bundle
+ if [[ "$SIGN" != "false" ]]; then
+ setup_macos_keychain
+ fi
+ sign_macos "$app_path"
# Pack in .dmg
- echo "Creating .dmg image..."
+ log_info "Creating $dmg_path image..."
+ hdiutil create -volname "$FILENAME" -srcfolder "$app_path" -ov -format UDZO \
+ "$dmg_path"
- hdiutil create -volname "MullvadDownloader" -srcfolder "$app_path" -ov -format UDZO \
- "$DIST_DIR/MullvadDownloader.dmg"
+ # Sign .dmg
+ sign_macos "$dmg_path"
+
+ # Notarize .dmg
+ if [[ "$SIGN" != "false" ]]; then
+ notarize_mac "$dmg_path"
+ fi
- # TODO: sign image?
+ # Move to dist dir
+ log_info "Moving final artifacts to $DIST_DIR"
+ rm -rf "$DIST_DIR/$FILENAME.app/"
+ rm -rf "$DIST_DIR/$FILENAME.dmg"
+ mv "$app_path" "$DIST_DIR/"
+ mv "$dmg_path" "$DIST_DIR/"
}
-mkdir -p "$DIST_DIR"
+# Notarize and staple a file.
+# Arguments:
+# - file to sign
+function notarize_mac {
+ local file="$1"
-if [[ "$(uname -s)" == "Darwin" ]]; then
- case $HOST in
- x86_64-apple-darwin) TARGETS=("" aarch64-apple-darwin);;
- aarch64-apple-darwin) TARGETS=("" x86_64-apple-darwin);;
- esac
+ log_info "Notarizing $file"
+ xcrun notarytool submit "$file" \
+ --keychain "$NOTARIZE_KEYCHAIN" \
+ --keychain-profile "$NOTARIZE_KEYCHAIN_PROFILE" \
+ --wait
+
+ log_info "Stapling $file"
+ xcrun stapler staple "$file"
+}
+
+# Sign a file.
+# Arguments:
+# - file to sign
+function sign_win {
+ local binary=$1
+ local num_retries=3
+
+ for i in $(seq 0 ${num_retries}); do
+ log_info "Signing $binary..."
+ if signtool sign \
+ -tr http://timestamp.digicert.com -td sha256 \
+ -fd sha256 -d "Mullvad VPN installer" \
+ -du "https://github.com/mullvad/mullvadvpn-app#readme" \
+ -sha1 "$CERT_HASH" "$binary"
+ then
+ break
+ fi
- for t in "${TARGETS[@]:-"$HOST"}"; do
- build_executable "$t"
+ if [ "$i" -eq "${num_retries}" ]; then
+ return 1
+ fi
+
+ sleep 1
done
+}
+
+# Copy executable and optionally sign it.
+function dist_windows_app {
+ cp "$CARGO_TARGET_DIR/release/installer-downloader.exe" "$BUILD_DIR/$FILENAME.exe"
+ if [[ "$SIGN" != "false" ]]; then
+ sign_win "$BUILD_DIR/$FILENAME.exe"
+ fi
+ mv "$BUILD_DIR/$FILENAME.exe" "$DIST_DIR/"
+}
+
+function main {
+ if [[ "$SIGN" != "false" ]]; then
+ assert_can_sign
+ fi
+
+ if [[ "$(uname -s)" == "Darwin" ]]; then
+ case $HOST in
+ x86_64-apple-darwin) TARGETS=("" aarch64-apple-darwin);;
+ aarch64-apple-darwin) TARGETS=("" x86_64-apple-darwin);;
+ esac
+
+ for t in "${TARGETS[@]:-"$HOST"}"; do
+ build_executable "$t"
+ done
+
+ lipo_executables
+ dist_macos_app
+
+ elif [[ "$(uname -s)" == "MINGW"* ]]; then
+ build_executable
+ dist_windows_app
+ fi
+}
- lipo_executables
- dist_macos_app
-elif [[ "$(uname -s)" == "MINGW"* ]]; then
- build_executable
- dist_windows_app
-fi
+main
diff --git a/installer-downloader/src/cacao_impl/mod.rs b/installer-downloader/src/cacao_impl/mod.rs
index 3b607c3295..717223893e 100644
--- a/installer-downloader/src/cacao_impl/mod.rs
+++ b/installer-downloader/src/cacao_impl/mod.rs
@@ -8,7 +8,7 @@ mod delegate;
mod ui;
pub fn main() {
- let app = App::new("net.mullvad.downloader", AppImpl::default());
+ let app = App::new("net.mullvad.MullvadDownloader", AppImpl::default());
// Load "global" values and resources
let environment = match Environment::load() {
diff --git a/installer-downloader/src/resource.rs b/installer-downloader/src/resource.rs
index 07bfe4161e..f235333845 100644
--- a/installer-downloader/src/resource.rs
+++ b/installer-downloader/src/resource.rs
@@ -1,7 +1,7 @@
//! Shared text and other resources
/// Window title
-pub const WINDOW_TITLE: &str = "Mullvad VPN downloader";
+pub const WINDOW_TITLE: &str = "Mullvad VPN installer";
/// Window width
pub const WINDOW_WIDTH: usize = 600;
/// Window height