diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2020-01-14 15:28:36 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2020-01-30 14:07:35 +0100 |
| commit | 358fcf4dde4a9273977d579207da90305e1a9327 (patch) | |
| tree | b8a9b826c3a65ab9a75391da6d18a40acf5b957c | |
| parent | 5792f2a4c650487f6eaa80782091ccc1ec501106 (diff) | |
| download | mullvadvpn-358fcf4dde4a9273977d579207da90305e1a9327.tar.xz mullvadvpn-358fcf4dde4a9273977d579207da90305e1a9327.zip | |
Add build script and instructions for iOS
| -rw-r--r-- | ios/BuildInstructions.md | 175 | ||||
| -rw-r--r-- | ios/ExportOptions.plist | 23 | ||||
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 24 | ||||
| -rwxr-xr-x | ios/build.sh | 140 |
4 files changed, 352 insertions, 10 deletions
diff --git a/ios/BuildInstructions.md b/ios/BuildInstructions.md new file mode 100644 index 0000000000..7bd89d59d8 --- /dev/null +++ b/ios/BuildInstructions.md @@ -0,0 +1,175 @@ +# Create private key and Certificate Signing Request (CSR) + +## Create new private key and CSR + +OpenSSL will ask you the password for the private key, make sure to memorize it, you'll need it +later. + +``` +openssl req -new -newkey rsa:4096 \ + -outform pem -keyform pem \ + -keyout private_key.pem \ + -out cert_signing_request \ + -subj "/C=SE/O=<ORGANIZATION_NAME>/emailAddress=<YOUR_EMAIL>" +``` + +## [Alternative] Create CSR using an existing private key + +``` +openssl req -new \ + -outform pem -keyform pem \ + -key private_key.pem \ + -out cert_signing_request \ + -subj "/C=SE/O=<ORGANIZATION_NAME>/emailAddress=<YOUR_EMAIL>" +``` + +# Upload Certificate Signing Request (CSR) to Apple + +1. Go to https://developer.apple.com/account/resources/certificates/add +1. Select "Apple Distribution" option from the given list, press "Continue" +1. Select the previously created `cert_signing_request` file for upload +1. Download the provided `distribution.cer` on disk + +# Download Apple WWDR certificate + +WWDR certificate is used to verify the development and distribution certificates issued by Apple. + +``` +curl https://developer.apple.com/certificationauthority/AppleWWDRCA.cer -O +``` + +# Convert certificates to PEM format + +``` +openssl x509 -inform der -outform pem \ + -in distribution.cer \ + -out distribution.pem + +openssl x509 -inform der -outform pem \ + -in AppleWWDRCA.cer \ + -out AppleWWDRCA.pem +``` + +# Export private key and certificates + +Produce a PKCS12 container with the private key and all certificates. You will be asked to enter two +passphrases: + +1. Private key passphrase +1. Export passphrase for PKCS12 file + +You can store the produced `apple_code_signing.p12` file as a backup to be able to restore the keys +in the event of hardware failure. However you should always be able to re-create everything from +scratch. + +``` +openssl pkcs12 -export \ + -inkey private_key.pem \ + -in distribution.pem \ + -certfile AppleWWDRCA.pem \ + -out apple_code_signing.p12 \ + -name "<FRIENDLY_KEYCHAIN_NAME>" +``` + +# Import private key and certificates into Keychain + +``` +security import apple_code_signing.p12 -f pkcs12 \ + -T /usr/bin/codesign \ + -P <EXPORT_PASSPHRASE> +``` + +Note: `-T /usr/bin/codesign` instructs Keychain to suppress password prompt during code signing, +although you still have to unlock Keychain for that to have any effect. This instruction is +equivalent to choosing "Always allow" in the password prompt GUI on the first run of `codesign` +tool. + +Note: providing the export passphrase using the `-P` flag is considered unsafe. +Leave the `-P <EXPORT_PASSPHRASE>` out to enter the passphrase via GUI. + +Technically after that you can clean up all created keys and certificates since all of them are +securely stored in Keychain now. + +``` +rm distribution.{pem,cer} \ + AppleWWDRCA.{pem,cer} \ + cert_signing_request \ + apple_code_signing.p12 \ + private_key.pem +``` + +# Create iOS provisioning profiles + +We will now create the provisioning profiles listed below using the Apple developer console. + +| App ID | Provisioning Profile Name | +|-------------------------------------|---------------------------| +| net.mullvad.MullvadVPN | Mullvad VPN Release | +| net.mullvad.MullvadVPN.PacketTunnel | Packet Tunnel Release | + +Follow these steps to add each of provisioning profiles: + +1. Go to https://developer.apple.com/account/resources/profiles/add +1. Choose "App Store" under "Distribution", then hit "Continue" +1. Choose the App ID (see the table above) and hit "Continue" +1. Choose the distribution certificate that you had created after uploading the CSR + (i.e `<ORGANIZATION_NAME> (Distribution)`) +1. Type in the profile name (see the table above) and hit "Generate" +1. Download the certificate in `ios/iOS Provisioning Profiles` directory. Create the directory if it + does not exist. + + Note: you can use a different directory for storing provisioning profiles, however in that case, + make sure to provide the path to the custom location via `IOS_PROVISIONING_PROFILES_DIR` + environment variable when running `build.sh` (more on that later). + +# Setup AppStore credentials + +For the automatic uploads to work properly, make sure to provide the Apple ID and password via +environment variables: + +1. `IOS_APPLE_ID` - Your Apple ID (Email) +1. `IOS_APPLE_ID_PASSWORD` - Your Apple ID password or keychain reference (see explanation below) + + +`IOS_APPLE_ID_PASSWORD` accepts a keychain reference in form of `@keychain:<KEYCHAIN_ITEM_NAME>`. + +Use app specific password instead of the actual account password and save it to Keychain. The app +specific password can be created via AppStore Connect, and added to Keychain using the following +commands (note that `altool` will be authorized to access the saved password): + +``` +ALTOOL_BIN=$(xcrun -find altool) + +security add-generic-password \ + -a <APPLE_ID_EMAIL> \ + -w <APP_SPECIFIC_PASSWORD> \ + -s <KEYCHAIN_ITEM_NAME> \ + -T "$ALTOOL_BIN" +``` + +# Automated build and deployment + +Build script does not bump the build number, so make sure to do that manually and commit to repo: + +``` +agvtool bump +``` + +1. Run `./ios/build.sh` to build and export the app for upload to AppStore. +1. Run `./ios/build.sh --deploy` - same as above but also uploads the app to AppStore and + makes it available over TestFlight. + +# Keychain quirks + +It's possible that `codesign` will keep throwing the password prompts for Keychain, in that case try +running the following commands __after__ importing the credentials into Keychain: + +``` +security unlock-keychain <KEYCHAIN> +security set-key-partition-list -S apple-tool:,apple: -s <KEYCHAIN> +``` + +where `<KEYCHAIN>` is the name of the target Keychain where the signing credentials are stored. +This guide does not use a separate Keychain store, so use then use `login.keychain-db` then. + +Reference: https://docs.travis-ci.com/user/common-build-problems/#mac-macos-sierra-1012-code-signing-errors diff --git a/ios/ExportOptions.plist b/ios/ExportOptions.plist new file mode 100644 index 0000000000..6306c6456f --- /dev/null +++ b/ios/ExportOptions.plist @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>method</key> + <string>app-store</string> + <key>teamID</key> + <string>G7CDBEG477</string> + <key>signingStyle</key> + <string>manual</string> + <key>uploadSymbols</key> + <true/> + <key>signingCertificate</key> + <string>Apple Distribution: Mullvad VPN AB</string> + <key>provisioningProfiles</key> + <dict> + <key>net.mullvad.MullvadVPN</key> + <string>Mullvad VPN Release</string> + <key>net.mullvad.MullvadVPN.PacketTunnel</key> + <string>Packet Tunnel Release</string> + </dict> +</dict> +</plist> diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 49a7d0a18c..12aa54ffba 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -814,7 +814,8 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -875,7 +876,8 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_IDENTITY = "Apple Distribution"; + CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; @@ -905,8 +907,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = MullvadVPN/MullvadVPN.entitlements; - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = G7CDBEG477; INFOPLIST_FILE = MullvadVPN/Info.plist; @@ -918,11 +919,12 @@ MARKETING_VERSION = 2020.1; PRODUCT_BUNDLE_IDENTIFIER = net.mullvad.MullvadVPN; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; + PROVISIONING_PROFILE_SPECIFIER = "Mullvad VPN Development"; SWIFT_OBJC_BRIDGING_HEADER = "MullvadVPN/MullvadVPN-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; @@ -933,8 +935,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = MullvadVPN/MullvadVPN.entitlements; - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = G7CDBEG477; INFOPLIST_FILE = MullvadVPN/Info.plist; @@ -946,10 +947,11 @@ MARKETING_VERSION = 2020.1; PRODUCT_BUNDLE_IDENTIFIER = net.mullvad.MullvadVPN; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; + PROVISIONING_PROFILE_SPECIFIER = "Mullvad VPN Release"; SWIFT_OBJC_BRIDGING_HEADER = "MullvadVPN/MullvadVPN-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; @@ -958,7 +960,7 @@ buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = G7CDBEG477; ENABLE_BITCODE = NO; @@ -971,6 +973,7 @@ MARKETING_VERSION = 2020.1; PRODUCT_BUNDLE_IDENTIFIER = net.mullvad.MullvadVPN.PacketTunnel; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Packet Tunnel Development"; SKIP_INSTALL = YES; SWIFT_OBJC_BRIDGING_HEADER = "PacketTunnel/PacketTunnel-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -984,7 +987,7 @@ buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = G7CDBEG477; ENABLE_BITCODE = NO; @@ -997,6 +1000,7 @@ MARKETING_VERSION = 2020.1; PRODUCT_BUNDLE_IDENTIFIER = net.mullvad.MullvadVPN.PacketTunnel; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Packet Tunnel Release"; SKIP_INSTALL = YES; SWIFT_OBJC_BRIDGING_HEADER = "PacketTunnel/PacketTunnel-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/ios/build.sh b/ios/build.sh new file mode 100755 index 0000000000..52f0fa00ca --- /dev/null +++ b/ios/build.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash + +# This script is used to build and ship the iOS app +set -eu +shopt -s nullglob + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +########################################### +# Verify environment configuration +########################################### + +if [[ -z ${IOS_APPLE_ID-} ]]; then + echo "The variable IOS_APPLE_ID is not set." + exit +fi + +if [[ -z ${IOS_APPLE_ID_PASSWORD-} ]]; then + echo "The variable IOS_APPLE_ID_PASSWORD is not set." + read -sp "IOS_APPLE_ID_PASSWORD = " IOS_APPLE_ID_PASSWORD + echo "" + export IOS_APPLE_ID_PASSWORD +fi + +# Provisioning profiles directory +if [[ -z ${IOS_PROVISIONING_PROFILES_DIR-} ]]; then + IOS_PROVISIONING_PROFILES_DIR="$SCRIPT_DIR/iOS Provisioning Profiles" + + echo "The variable IOS_PROVISIONING_PROFILES_DIR is not set." + echo "Default: $IOS_PROVISIONING_PROFILES_DIR" + + export IOS_PROVISIONING_PROFILES_DIR +fi + +########################################### +# Build configuration +########################################### + +# The Xcode project name without file extension +# The folder with all sources is expected to hold the same name +PROJECT_NAME="MullvadVPN" + +# Xcode project directory +XCODE_PROJECT_DIR="$SCRIPT_DIR/$PROJECT_NAME.xcodeproj" + +# Build output directory without trailing slash +BUILD_OUTPUT_DIR="$SCRIPT_DIR/Build" + +# Xcode archive output +XCODE_ARCHIVE_DIR="$BUILD_OUTPUT_DIR/$PROJECT_NAME.xcarchive" + +# Export options file used for producing .xcarchive +EXPORT_OPTIONS_PATH="$SCRIPT_DIR/ExportOptions.plist" + +# Path to generated IPA file produced after .xcarchive export +IPA_PATH="$BUILD_OUTPUT_DIR/$PROJECT_NAME.ipa" + +# Xcodebuild intermediate files directory +DERIVED_DATA_DIR="$BUILD_OUTPUT_DIR/DerivedData" + +# System provisioning profiles directory +SYSTEM_PROVISIONING_PROFILES_DIR="$HOME/Library/MobileDevice/Provisioning Profiles" + + +########################################### +# Install provisioning profiles +########################################### + +get_mobile_provisioning_uuid() { + security cms -D -i "$1" | grep -aA1 UUID | grep -o "[-a-zA-Z0-9]\{36\}" +} + +install_mobile_provisioning() { + echo "Install system provisioning profiles into $SYSTEM_PROVISIONING_PROFILES_DIR" + + if [[ ! -d "$SYSTEM_PROVISIONING_PROFILES_DIR" ]]; then + echo "Missing system provisioning profiles directory. Creating one." + mkdir -p "$SYSTEM_PROVISIONING_PROFILES_DIR" + fi + + for mobile_provisioning_path in "$IOS_PROVISIONING_PROFILES_DIR"/*.mobileprovision; do + local profile_uuid=$(get_mobile_provisioning_uuid "$mobile_provisioning_path") + local target_path="$SYSTEM_PROVISIONING_PROFILES_DIR/$profile_uuid.mobileprovision" + + if [[ -f "$target_path" ]]; then + echo "Skip installing $mobile_provisioning_path" + else + echo "Install $mobile_provisioning_path -> $target_path" + + cp "$mobile_provisioning_path" "$target_path" + fi + done +} + +install_mobile_provisioning + + +########################################### +# Build Xcode project +########################################### + +release_build() { + xcodebuild \ + -project "$XCODE_PROJECT_DIR" \ + -scheme "$PROJECT_NAME" \ + -sdk iphoneos \ + -configuration Release \ + -derivedDataPath "$DERIVED_DATA_DIR" \ + $@ +} + +# Clean build directory +release_build clean + +# Archive project +release_build archive -archivePath "$XCODE_ARCHIVE_DIR" + +# Export IPA for distribution +xcodebuild \ + -exportArchive \ + -archivePath "$XCODE_ARCHIVE_DIR" \ + -exportOptionsPlist "$EXPORT_OPTIONS_PATH" \ + -exportPath "$BUILD_OUTPUT_DIR" + + +########################################### +# Deploy to AppStore +########################################### + +if [[ "${1:-""}" == "--deploy" ]]; then + xcrun altool \ + --upload-app --verbose \ + --type ios \ + --file "$IPA_PATH" \ + --username "$IOS_APPLE_ID" \ + --password "$IOS_APPLE_ID_PASSWORD" +else + echo "Deployment to AppStore will not be performed." + echo "Run with --deploy to upload the binary." +fi |
