Wire Apple KMS release signing
This commit is contained in:
parent
ed73d7d16a
commit
126af6b5cf
17 changed files with 1913 additions and 16 deletions
|
|
@ -72,9 +72,12 @@ env:
|
||||||
BURROW_APPLE_BUNDLE_ID: ${{ vars.BURROW_APPLE_BUNDLE_ID || 'net.burrow.app' }}
|
BURROW_APPLE_BUNDLE_ID: ${{ vars.BURROW_APPLE_BUNDLE_ID || 'net.burrow.app' }}
|
||||||
BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID: ${{ vars.BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID || 'net.burrow.app.network' }}
|
BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID: ${{ vars.BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID || 'net.burrow.app.network' }}
|
||||||
BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID: ${{ vars.BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID || '3G42677598' }}
|
BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID: ${{ vars.BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID || '3G42677598' }}
|
||||||
|
BURROW_DEVELOPER_ID_CERTIFICATE_ID: ${{ vars.BURROW_DEVELOPER_ID_CERTIFICATE_ID || '9JKN6HXBHC' }}
|
||||||
BURROW_APPLE_CREATE_MISSING_BUNDLE_IDS: ${{ vars.BURROW_APPLE_CREATE_MISSING_BUNDLE_IDS || 'false' }}
|
BURROW_APPLE_CREATE_MISSING_BUNDLE_IDS: ${{ vars.BURROW_APPLE_CREATE_MISSING_BUNDLE_IDS || 'false' }}
|
||||||
BURROW_APPLE_ENABLE_CAPABILITIES: ${{ vars.BURROW_APPLE_ENABLE_CAPABILITIES || 'false' }}
|
BURROW_APPLE_ENABLE_CAPABILITIES: ${{ vars.BURROW_APPLE_ENABLE_CAPABILITIES || 'false' }}
|
||||||
BURROW_SPARKLE_SIGN_WITH_KMS: ${{ vars.BURROW_SPARKLE_SIGN_WITH_KMS || 'false' }}
|
BURROW_APPLE_SIGNING_MODE: ${{ vars.BURROW_APPLE_SIGNING_MODE || 'google-kms-staged' }}
|
||||||
|
BURROW_NOTARIZE_MACOS: ${{ vars.BURROW_NOTARIZE_MACOS || 'true' }}
|
||||||
|
BURROW_SPARKLE_SIGN_WITH_KMS: ${{ vars.BURROW_SPARKLE_SIGN_WITH_KMS || 'true' }}
|
||||||
BURROW_SPARKLE_KMS_KEY: ${{ vars.BURROW_SPARKLE_KMS_KEY || 'sparkle-ed25519' }}
|
BURROW_SPARKLE_KMS_KEY: ${{ vars.BURROW_SPARKLE_KMS_KEY || 'sparkle-ed25519' }}
|
||||||
BURROW_SPARKLE_KMS_VERSION: ${{ vars.BURROW_SPARKLE_KMS_VERSION || '1' }}
|
BURROW_SPARKLE_KMS_VERSION: ${{ vars.BURROW_SPARKLE_KMS_VERSION || '1' }}
|
||||||
BURROW_SPARKLE_FEED_URL: ${{ vars.BURROW_SPARKLE_FEED_URL || 'https://releases.burrow.net/sparkle/appcast.xml' }}
|
BURROW_SPARKLE_FEED_URL: ${{ vars.BURROW_SPARKLE_FEED_URL || 'https://releases.burrow.net/sparkle/appcast.xml' }}
|
||||||
|
|
@ -228,6 +231,29 @@ jobs:
|
||||||
xcrun --sdk iphoneos --show-sdk-path
|
xcrun --sdk iphoneos --show-sdk-path
|
||||||
xcrun --sdk iphonesimulator --show-sdk-path
|
xcrun --sdk iphonesimulator --show-sdk-path
|
||||||
|
|
||||||
|
- name: Prepare Rust Apple Targets
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PLATFORM: ${{ matrix.platform }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if ! command -v rustup >/dev/null 2>&1; then
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
fi
|
||||||
|
case "$PLATFORM" in
|
||||||
|
ios)
|
||||||
|
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios
|
||||||
|
;;
|
||||||
|
macos)
|
||||||
|
rustup target add aarch64-apple-darwin x86_64-apple-darwin
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "::error ::unsupported Apple platform ${PLATFORM}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
- name: Bootstrap Runner Environment
|
- name: Bootstrap Runner Environment
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -291,6 +317,7 @@ jobs:
|
||||||
BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
|
BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
|
||||||
PLATFORM: ${{ matrix.platform }}
|
PLATFORM: ${{ matrix.platform }}
|
||||||
UPLOAD_APP_STORE: ${{ github.event.inputs.upload_app_store || 'false' }}
|
UPLOAD_APP_STORE: ${{ github.event.inputs.upload_app_store || 'false' }}
|
||||||
|
UPLOAD_SPARKLE: ${{ github.event.inputs.upload_sparkle || 'true' }}
|
||||||
SPARKLE_CHANNEL: build-${{ needs.prepare.outputs.build_number }}
|
SPARKLE_CHANNEL: build-${{ needs.prepare.outputs.build_number }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
@ -298,6 +325,9 @@ jobs:
|
||||||
if [[ "$UPLOAD_APP_STORE" == "true" && "$PLATFORM" == "ios" ]]; then
|
if [[ "$UPLOAD_APP_STORE" == "true" && "$PLATFORM" == "ios" ]]; then
|
||||||
require_signing=true
|
require_signing=true
|
||||||
fi
|
fi
|
||||||
|
if [[ "$UPLOAD_SPARKLE" == "true" && "$PLATFORM" == "macos" ]]; then
|
||||||
|
require_signing=true
|
||||||
|
fi
|
||||||
|
|
||||||
bazel_args=()
|
bazel_args=()
|
||||||
if [[ -n "${BURROW_BAZEL_NSCCACHE_BAZELRC:-}" && -f "${BURROW_BAZEL_NSCCACHE_BAZELRC:-}" ]]; then
|
if [[ -n "${BURROW_BAZEL_NSCCACHE_BAZELRC:-}" && -f "${BURROW_BAZEL_NSCCACHE_BAZELRC:-}" ]]; then
|
||||||
|
|
@ -312,10 +342,12 @@ jobs:
|
||||||
--action_env=BUILD_NUMBER="$BUILD_NUMBER" \
|
--action_env=BUILD_NUMBER="$BUILD_NUMBER" \
|
||||||
--action_env=BURROW_RELEASE_OUT="$PWD/dist/builds/$BUILD_NUMBER" \
|
--action_env=BURROW_RELEASE_OUT="$PWD/dist/builds/$BUILD_NUMBER" \
|
||||||
--action_env=BURROW_APPLE_SIGNING_READY="${BURROW_APPLE_SIGNING_READY:-0}" \
|
--action_env=BURROW_APPLE_SIGNING_READY="${BURROW_APPLE_SIGNING_READY:-0}" \
|
||||||
|
--action_env=BURROW_APPLE_SIGNING_MODE="${BURROW_APPLE_SIGNING_MODE:-keychain}" \
|
||||||
--action_env=BURROW_APPLE_REQUIRE_SIGNING="$require_signing" \
|
--action_env=BURROW_APPLE_REQUIRE_SIGNING="$require_signing" \
|
||||||
--action_env=BURROW_APPLE_BUNDLE_ID="${BURROW_APPLE_BUNDLE_ID:-}" \
|
--action_env=BURROW_APPLE_BUNDLE_ID="${BURROW_APPLE_BUNDLE_ID:-}" \
|
||||||
--action_env=BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID="${BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID:-}" \
|
--action_env=BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID="${BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID:-}" \
|
||||||
--action_env=BURROW_APPLE_KEYCHAIN_PATH="${BURROW_APPLE_KEYCHAIN_PATH:-}" \
|
--action_env=BURROW_APPLE_KEYCHAIN_PATH="${BURROW_APPLE_KEYCHAIN_PATH:-}" \
|
||||||
|
--action_env=BURROW_NOTARIZE_MACOS="${BURROW_NOTARIZE_MACOS:-false}" \
|
||||||
--action_env=BURROW_SPARKLE_SIGN_WITH_KMS="${BURROW_SPARKLE_SIGN_WITH_KMS:-false}" \
|
--action_env=BURROW_SPARKLE_SIGN_WITH_KMS="${BURROW_SPARKLE_SIGN_WITH_KMS:-false}" \
|
||||||
--action_env=BURROW_SPARKLE_KMS_KEY="${BURROW_SPARKLE_KMS_KEY:-sparkle-ed25519}" \
|
--action_env=BURROW_SPARKLE_KMS_KEY="${BURROW_SPARKLE_KMS_KEY:-sparkle-ed25519}" \
|
||||||
--action_env=BURROW_SPARKLE_KMS_VERSION="${BURROW_SPARKLE_KMS_VERSION:-1}" \
|
--action_env=BURROW_SPARKLE_KMS_VERSION="${BURROW_SPARKLE_KMS_VERSION:-1}" \
|
||||||
|
|
|
||||||
|
|
@ -62,10 +62,83 @@ env:
|
||||||
BURROW_NIX_CACHE_URL: ${{ vars.BURROW_NIX_CACHE_URL || 'https://nix.burrow.net/burrow' }}
|
BURROW_NIX_CACHE_URL: ${{ vars.BURROW_NIX_CACHE_URL || 'https://nix.burrow.net/burrow' }}
|
||||||
BURROW_NIX_CACHE_PUBLIC_KEY: ${{ vars.BURROW_NIX_CACHE_PUBLIC_KEY }}
|
BURROW_NIX_CACHE_PUBLIC_KEY: ${{ vars.BURROW_NIX_CACHE_PUBLIC_KEY }}
|
||||||
BURROW_APPLE_BUNDLE_ID: ${{ vars.BURROW_APPLE_BUNDLE_ID || 'net.burrow.app' }}
|
BURROW_APPLE_BUNDLE_ID: ${{ vars.BURROW_APPLE_BUNDLE_ID || 'net.burrow.app' }}
|
||||||
|
BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID: ${{ vars.BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID || 'net.burrow.app.network' }}
|
||||||
|
BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID: ${{ vars.BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID || '3G42677598' }}
|
||||||
|
BURROW_DEVELOPER_ID_CERTIFICATE_ID: ${{ vars.BURROW_DEVELOPER_ID_CERTIFICATE_ID || '9JKN6HXBHC' }}
|
||||||
|
BURROW_APPLE_SIGNING_MODE: ${{ vars.BURROW_APPLE_SIGNING_MODE || 'google-kms-staged' }}
|
||||||
|
BURROW_NOTARIZE_MACOS: ${{ vars.BURROW_NOTARIZE_MACOS || 'true' }}
|
||||||
|
BURROW_SPARKLE_SIGN_WITH_KMS: ${{ vars.BURROW_SPARKLE_SIGN_WITH_KMS || 'true' }}
|
||||||
|
BURROW_SPARKLE_KMS_KEY: ${{ vars.BURROW_SPARKLE_KMS_KEY || 'sparkle-ed25519' }}
|
||||||
|
BURROW_SPARKLE_KMS_VERSION: ${{ vars.BURROW_SPARKLE_KMS_VERSION || '1' }}
|
||||||
|
BURROW_KMS_LOCATION: ${{ vars.BURROW_KMS_LOCATION || 'global' }}
|
||||||
|
BURROW_KMS_KEYRING: ${{ vars.BURROW_KMS_KEYRING || 'burrow-identity' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
sign-apple-kms:
|
||||||
|
name: Sign (Apple KMS)
|
||||||
|
if: ${{ github.event.inputs.upload_app_store == 'true' || github.event.inputs.upload_sparkle == 'true' }}
|
||||||
|
runs-on: namespace-profile-linux-medium
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: https://code.forgejo.org/actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ github.token }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Ensure Nix
|
||||||
|
shell: bash
|
||||||
|
run: bash Scripts/ci/ensure-nix.sh
|
||||||
|
|
||||||
|
- name: Configure Age
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
AGE_FORGE_SSH_KEY: ${{ secrets.AGE_FORGE_SSH_KEY }}
|
||||||
|
run: Scripts/resolve-age-identity.sh >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Decrypt Release Secrets
|
||||||
|
uses: ./.forgejo/actions/decrypt-release-secrets
|
||||||
|
|
||||||
|
- name: Validate App Store Credentials
|
||||||
|
shell: bash
|
||||||
|
run: Scripts/ci/check-release-config.sh app-store
|
||||||
|
|
||||||
|
- name: Download Staged Apple Artifacts
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
BUILD_NUMBER: ${{ github.event.inputs.build_number }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
nix develop .#ci -c Scripts/ci/google-wif-auth.sh
|
||||||
|
mkdir -p "dist/builds/${BUILD_NUMBER}/apple"
|
||||||
|
nix develop .#ci -c gcloud storage rsync --recursive "gs://${BURROW_RELEASE_GCS_BUCKET}/builds/${BUILD_NUMBER}/apple" "dist/builds/${BUILD_NUMBER}/apple"
|
||||||
|
find "dist/builds/${BUILD_NUMBER}/apple" -type f -print | sort
|
||||||
|
|
||||||
|
- name: Sync Apple Provisioning Profiles
|
||||||
|
shell: bash
|
||||||
|
run: nix develop .#ci -c Scripts/ci/sync-apple-provisioning-profiles.sh all
|
||||||
|
|
||||||
|
- name: Sign Apple Artifacts With KMS
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
BUILD_NUMBER: ${{ github.event.inputs.build_number }}
|
||||||
|
SPARKLE_CHANNEL_INPUT: ${{ github.event.inputs.sparkle_channel }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
channel="${SPARKLE_CHANNEL_INPUT:-}"
|
||||||
|
if [[ -z "$channel" ]]; then
|
||||||
|
channel="build-${BUILD_NUMBER}"
|
||||||
|
fi
|
||||||
|
SPARKLE_CHANNEL="$channel" nix develop .#ci -c Scripts/apple/sign-release-artifacts-kms.sh all
|
||||||
|
|
||||||
|
- name: Upload Signed Apple Artifacts
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
BUILD_NUMBER: ${{ github.event.inputs.build_number }}
|
||||||
|
run: nix develop .#ci -c Scripts/ci/upload-release-storage.sh
|
||||||
|
|
||||||
upload-app-store:
|
upload-app-store:
|
||||||
name: Upload (App Store Connect)
|
name: Upload (App Store Connect)
|
||||||
|
needs: sign-apple-kms
|
||||||
if: ${{ github.event.inputs.upload_app_store == 'true' }}
|
if: ${{ github.event.inputs.upload_app_store == 'true' }}
|
||||||
runs-on: namespace-profile-macos-large
|
runs-on: namespace-profile-macos-large
|
||||||
steps:
|
steps:
|
||||||
|
|
@ -120,7 +193,11 @@ jobs:
|
||||||
cp "$key_path" "$altool_key_dir/AuthKey_${ASC_API_KEY_ID}.p8"
|
cp "$key_path" "$altool_key_dir/AuthKey_${ASC_API_KEY_ID}.p8"
|
||||||
chmod 600 "$key_path" "$altool_key_dir/AuthKey_${ASC_API_KEY_ID}.p8"
|
chmod 600 "$key_path" "$altool_key_dir/AuthKey_${ASC_API_KEY_ID}.p8"
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
artifacts=(publish/apple/*.ipa publish/apple/*.pkg)
|
artifacts=()
|
||||||
|
for artifact in publish/apple/*.ipa publish/apple/*.pkg; do
|
||||||
|
[[ "$artifact" == *.unsigned.ipa ]] && continue
|
||||||
|
artifacts+=("$artifact")
|
||||||
|
done
|
||||||
if (( ${#artifacts[@]} == 0 )); then
|
if (( ${#artifacts[@]} == 0 )); then
|
||||||
echo "::error ::No Apple upload artifacts found for build ${{ github.event.inputs.build_number }}."
|
echo "::error ::No Apple upload artifacts found for build ${{ github.event.inputs.build_number }}."
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -219,6 +296,7 @@ jobs:
|
||||||
|
|
||||||
upload-sparkle:
|
upload-sparkle:
|
||||||
name: Upload (Sparkle)
|
name: Upload (Sparkle)
|
||||||
|
needs: sign-apple-kms
|
||||||
if: ${{ github.event.inputs.upload_sparkle == 'true' }}
|
if: ${{ github.event.inputs.upload_sparkle == 'true' }}
|
||||||
runs-on: namespace-profile-linux-medium
|
runs-on: namespace-profile-linux-medium
|
||||||
steps:
|
steps:
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,27 @@ for ARCH in "${BURROW_ARCHS[@]}"; do
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if [[ -x "$(command -v rustup)" ]]; then
|
||||||
|
INSTALLED_TARGETS="$(rustup target list --installed)"
|
||||||
|
MISSING_TARGETS=()
|
||||||
|
for TARGET in "${RUST_TARGETS[@]}"; do
|
||||||
|
if ! grep -qx "$TARGET" <<< "$INSTALLED_TARGETS"; then
|
||||||
|
MISSING_TARGETS+=("$TARGET")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if (( ${#MISSING_TARGETS[@]} > 0 )); then
|
||||||
|
rustup target add "${MISSING_TARGETS[@]}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
SYSROOT="$(rustc --print sysroot)"
|
||||||
|
for TARGET in "${RUST_TARGETS[@]}"; do
|
||||||
|
if [[ ! -d "${SYSROOT}/lib/rustlib/${TARGET}" ]]; then
|
||||||
|
echo "error: Rust target ${TARGET} is not installed and rustup is unavailable" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
# Pass all RUST_TARGETS in a single invocation
|
# Pass all RUST_TARGETS in a single invocation
|
||||||
CARGO_ARGS=()
|
CARGO_ARGS=()
|
||||||
for TARGET in "${RUST_TARGETS[@]}"; do
|
for TARGET in "${RUST_TARGETS[@]}"; do
|
||||||
|
|
|
||||||
53
Scripts/apple/profile-entitlements.py
Executable file
53
Scripts/apple/profile-entitlements.py
Executable file
|
|
@ -0,0 +1,53 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Extract signing entitlements from an Apple provisioning profile."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import plistlib
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def decode_profile(path: Path) -> dict:
|
||||||
|
commands = [
|
||||||
|
["security", "cms", "-D", "-i", str(path)],
|
||||||
|
["openssl", "smime", "-inform", "DER", "-verify", "-noverify", "-in", str(path)],
|
||||||
|
["openssl", "cms", "-inform", "DER", "-verify", "-noverify", "-in", str(path)],
|
||||||
|
]
|
||||||
|
errors: list[str] = []
|
||||||
|
for command in commands:
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(
|
||||||
|
command,
|
||||||
|
check=False,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
continue
|
||||||
|
if proc.returncode == 0 and proc.stdout:
|
||||||
|
return plistlib.loads(proc.stdout)
|
||||||
|
errors.append(f"{command[0]} exited {proc.returncode}: {proc.stderr.decode(errors='replace').strip()}")
|
||||||
|
raise RuntimeError(f"unable to decode provisioning profile {path}: {'; '.join(errors)}")
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("profile", type=Path)
|
||||||
|
parser.add_argument("--output", "-o", type=Path, required=True)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
profile = decode_profile(args.profile)
|
||||||
|
entitlements = profile.get("Entitlements")
|
||||||
|
if not isinstance(entitlements, dict):
|
||||||
|
raise RuntimeError(f"profile has no Entitlements dictionary: {args.profile}")
|
||||||
|
|
||||||
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
args.output.write_bytes(plistlib.dumps(entitlements, fmt=plistlib.FMT_XML))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
258
Scripts/apple/sign-release-artifacts-kms.sh
Executable file
258
Scripts/apple/sign-release-artifacts-kms.sh
Executable file
|
|
@ -0,0 +1,258 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$repo_root"
|
||||||
|
|
||||||
|
platform="${1:-all}"
|
||||||
|
build_number="${BUILD_NUMBER:-${RELEASE_BUILD_NUMBER:-${RELEASE_REF:-manual}}}"
|
||||||
|
out_root="${BURROW_RELEASE_OUT:-${repo_root}/dist/builds/${build_number}}"
|
||||||
|
apple_root="${out_root}/apple"
|
||||||
|
bundle_id="${BURROW_APPLE_BUNDLE_ID:-net.burrow.app}"
|
||||||
|
network_extension_bundle_id="${BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID:-${bundle_id}.network}"
|
||||||
|
notarize_macos="${BURROW_NOTARIZE_MACOS:-false}"
|
||||||
|
sparkle_sign_with_kms="${BURROW_SPARKLE_SIGN_WITH_KMS:-true}"
|
||||||
|
|
||||||
|
truthy() {
|
||||||
|
case "${1:-}" in
|
||||||
|
1|true|TRUE|True|yes|YES|Yes|y|Y|on|ON|On) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
sha256_file() {
|
||||||
|
local file="$1"
|
||||||
|
if command -v shasum >/dev/null 2>&1; then
|
||||||
|
shasum -a 256 "$file" > "${file}.sha256"
|
||||||
|
else
|
||||||
|
sha256sum "$file" > "${file}.sha256"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
safe_profile_slug() {
|
||||||
|
python3 - "$1" <<'PY'
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
print(re.sub(r"[^A-Za-z0-9]", "", sys.argv[1]).lower())
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
profile_for() {
|
||||||
|
local identifier="$1"
|
||||||
|
local suffix="$2"
|
||||||
|
local slug path
|
||||||
|
slug="$(safe_profile_slug "$identifier")"
|
||||||
|
for path in \
|
||||||
|
".forgejo/actions/export/${slug}${suffix}.provisionprofile" \
|
||||||
|
".forgejo/actions/export/${slug}${suffix}.mobileprovision" \
|
||||||
|
"Apple/Profiles/${slug}${suffix}.provisionprofile" \
|
||||||
|
"Apple/Profiles/${slug}${suffix}.mobileprovision"; do
|
||||||
|
if [[ -s "$path" ]]; then
|
||||||
|
printf '%s\n' "$path"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_tool() {
|
||||||
|
if ! command -v "$1" >/dev/null 2>&1; then
|
||||||
|
echo "error: missing required tool: $1" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_profile_entitlements() {
|
||||||
|
local profile="$1"
|
||||||
|
local output="$2"
|
||||||
|
Scripts/apple/profile-entitlements.py "$profile" --output "$output"
|
||||||
|
}
|
||||||
|
|
||||||
|
sign_bundle() {
|
||||||
|
local family="$1"
|
||||||
|
local bundle_path="$2"
|
||||||
|
local entitlements="$3"
|
||||||
|
local runtime_flag=()
|
||||||
|
if [[ "$family" == "developer-id" ]]; then
|
||||||
|
runtime_flag=(--runtime)
|
||||||
|
fi
|
||||||
|
Scripts/apple/sign-with-google-kms.sh \
|
||||||
|
--family "$family" \
|
||||||
|
--path "$bundle_path" \
|
||||||
|
--entitlements "$entitlements" \
|
||||||
|
"${runtime_flag[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_notary_api_key_json() {
|
||||||
|
local out="$1"
|
||||||
|
if [[ -z "${ASC_API_KEY_ID:-}" || -z "${ASC_API_ISSUER_ID:-}" || -z "${ASC_API_KEY_PATH:-}" ]]; then
|
||||||
|
echo "macOS notarization requires ASC_API_KEY_ID, ASC_API_ISSUER_ID, and ASC_API_KEY_PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
rcodesign encode-app-store-connect-api-key \
|
||||||
|
"$ASC_API_ISSUER_ID" \
|
||||||
|
"$ASC_API_KEY_ID" \
|
||||||
|
"$ASC_API_KEY_PATH" \
|
||||||
|
"$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
sign_ios() {
|
||||||
|
local unsigned_ipa app_profile ext_profile work_dir app ext app_entitlements ext_entitlements output_ipa
|
||||||
|
unsigned_ipa="${apple_root}/Burrow-iOS-${build_number}.unsigned.ipa"
|
||||||
|
if [[ ! -s "$unsigned_ipa" ]]; then
|
||||||
|
echo "error: missing staged unsigned iOS IPA: $unsigned_ipa" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
app_profile="$(profile_for "$bundle_id" "appstore")" || {
|
||||||
|
echo "error: missing iOS App Store provisioning profile for ${bundle_id}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
ext_profile="$(profile_for "$network_extension_bundle_id" "appstore")" || {
|
||||||
|
echo "error: missing iOS App Store provisioning profile for ${network_extension_bundle_id}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/burrow-ios-kms-sign.XXXXXX")"
|
||||||
|
trap 'rm -rf "${work_dir:-}"' RETURN
|
||||||
|
unzip -q "$unsigned_ipa" -d "$work_dir"
|
||||||
|
app="${work_dir}/Payload/Burrow.app"
|
||||||
|
ext="${app}/PlugIns/BurrowNetworkExtension.appex"
|
||||||
|
if [[ ! -d "$app" || ! -d "$ext" ]]; then
|
||||||
|
echo "error: unsigned IPA is missing Burrow.app or BurrowNetworkExtension.appex" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp "$app_profile" "${app}/embedded.mobileprovision"
|
||||||
|
cp "$ext_profile" "${ext}/embedded.mobileprovision"
|
||||||
|
app_entitlements="${work_dir}/app-entitlements.plist"
|
||||||
|
ext_entitlements="${work_dir}/extension-entitlements.plist"
|
||||||
|
extract_profile_entitlements "$app_profile" "$app_entitlements"
|
||||||
|
extract_profile_entitlements "$ext_profile" "$ext_entitlements"
|
||||||
|
|
||||||
|
rm -rf "${app}/_CodeSignature" "${ext}/_CodeSignature"
|
||||||
|
sign_bundle ios "$ext" "$ext_entitlements"
|
||||||
|
sign_bundle ios "$app" "$app_entitlements"
|
||||||
|
|
||||||
|
output_ipa="${apple_root}/Burrow-iOS-${build_number}.ipa"
|
||||||
|
rm -f "$output_ipa"
|
||||||
|
(cd "$work_dir" && zip -qry "$output_ipa" Payload)
|
||||||
|
sha256_file "$output_ipa"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_sparkle_appcast() {
|
||||||
|
local artifact="$1"
|
||||||
|
local channel sparkle_dir length pubdate url
|
||||||
|
channel="${SPARKLE_CHANNEL:-build-${build_number}}"
|
||||||
|
sparkle_dir="${out_root}/sparkle/${channel}"
|
||||||
|
mkdir -p "$sparkle_dir"
|
||||||
|
cp "$artifact" "$sparkle_dir/"
|
||||||
|
length="$(wc -c < "$artifact" | tr -d ' ')"
|
||||||
|
pubdate="$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S +0000')"
|
||||||
|
url="${SPARKLE_DOWNLOAD_PREFIX:-https://releases.burrow.net/sparkle}/${channel}/$(basename "$artifact")"
|
||||||
|
cat > "${sparkle_dir}/appcast.xml" <<EOF
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
|
||||||
|
<channel>
|
||||||
|
<title>Burrow ${channel}</title>
|
||||||
|
<item>
|
||||||
|
<title>Burrow ${build_number}</title>
|
||||||
|
<pubDate>${pubdate}</pubDate>
|
||||||
|
<sparkle:version>${build_number}</sparkle:version>
|
||||||
|
<sparkle:shortVersionString>0.1</sparkle:shortVersionString>
|
||||||
|
<enclosure url="${url}" sparkle:version="${build_number}" length="${length}" type="application/octet-stream" />
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
|
EOF
|
||||||
|
if truthy "$sparkle_sign_with_kms"; then
|
||||||
|
Scripts/sparkle/sign-appcast-kms.py \
|
||||||
|
--appcast "${sparkle_dir}/appcast.xml" \
|
||||||
|
--artifact-dir "$sparkle_dir"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
sign_macos() {
|
||||||
|
local unsigned_zip app_profile ext_profile work_dir app ext app_entitlements ext_entitlements output_zip notary_zip key_json
|
||||||
|
unsigned_zip="${apple_root}/Burrow-macOS-${build_number}.unsigned.zip"
|
||||||
|
if [[ ! -s "$unsigned_zip" ]]; then
|
||||||
|
echo "error: missing staged unsigned macOS ZIP: $unsigned_zip" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
app_profile="$(profile_for "$bundle_id" "developerid")" || {
|
||||||
|
echo "error: missing macOS Developer ID provisioning profile for ${bundle_id}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
ext_profile="$(profile_for "$network_extension_bundle_id" "developerid")" || {
|
||||||
|
echo "error: missing macOS Developer ID provisioning profile for ${network_extension_bundle_id}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/burrow-macos-kms-sign.XXXXXX")"
|
||||||
|
trap 'rm -rf "${work_dir:-}"' RETURN
|
||||||
|
unzip -q "$unsigned_zip" -d "$work_dir"
|
||||||
|
app="$(find "$work_dir" -maxdepth 2 -type d -name 'Burrow.app' -print | head -n 1)"
|
||||||
|
if [[ -z "$app" || ! -d "$app" ]]; then
|
||||||
|
echo "error: unsigned macOS ZIP does not contain Burrow.app" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ext="${app}/Contents/PlugIns/BurrowNetworkExtension.appex"
|
||||||
|
cp "$app_profile" "${app}/Contents/embedded.provisionprofile"
|
||||||
|
app_entitlements="${work_dir}/app-entitlements.plist"
|
||||||
|
extract_profile_entitlements "$app_profile" "$app_entitlements"
|
||||||
|
|
||||||
|
if [[ -d "$ext" ]]; then
|
||||||
|
cp "$ext_profile" "${ext}/Contents/embedded.provisionprofile"
|
||||||
|
ext_entitlements="${work_dir}/extension-entitlements.plist"
|
||||||
|
extract_profile_entitlements "$ext_profile" "$ext_entitlements"
|
||||||
|
rm -rf "${ext}/Contents/_CodeSignature"
|
||||||
|
sign_bundle developer-id "$ext" "$ext_entitlements"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "${app}/Contents/_CodeSignature"
|
||||||
|
sign_bundle developer-id "$app" "$app_entitlements"
|
||||||
|
|
||||||
|
if truthy "$notarize_macos"; then
|
||||||
|
key_json="${work_dir}/notary-api-key.json"
|
||||||
|
notary_zip="${work_dir}/Burrow.notary.zip"
|
||||||
|
write_notary_api_key_json "$key_json"
|
||||||
|
(cd "$(dirname "$app")" && zip -qry "$notary_zip" "$(basename "$app")")
|
||||||
|
rcodesign notary-submit --api-key-file "$key_json" --wait "$notary_zip"
|
||||||
|
rcodesign staple "$app"
|
||||||
|
fi
|
||||||
|
|
||||||
|
output_zip="${apple_root}/Burrow-macOS-${build_number}.zip"
|
||||||
|
rm -f "$output_zip"
|
||||||
|
(cd "$(dirname "$app")" && zip -qry "$output_zip" "$(basename "$app")")
|
||||||
|
sha256_file "$output_zip"
|
||||||
|
write_sparkle_appcast "$output_zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
require_tool python3
|
||||||
|
require_tool unzip
|
||||||
|
require_tool zip
|
||||||
|
require_tool rcodesign
|
||||||
|
require_tool gcloud
|
||||||
|
mkdir -p "$apple_root"
|
||||||
|
|
||||||
|
case "$platform" in
|
||||||
|
all)
|
||||||
|
sign_ios
|
||||||
|
sign_macos
|
||||||
|
;;
|
||||||
|
ios)
|
||||||
|
sign_ios
|
||||||
|
;;
|
||||||
|
macos)
|
||||||
|
sign_macos
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "unknown Apple KMS signing platform: $platform" >&2
|
||||||
|
exit 64
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
find "$out_root" -type f -print | sort
|
||||||
263
Scripts/apple/sign-with-google-kms.sh
Executable file
263
Scripts/apple/sign-with-google-kms.sh
Executable file
|
|
@ -0,0 +1,263 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$repo_root"
|
||||||
|
|
||||||
|
family=""
|
||||||
|
path=""
|
||||||
|
entitlements=""
|
||||||
|
certificate=""
|
||||||
|
kms_key=""
|
||||||
|
kms_version=""
|
||||||
|
runtime=false
|
||||||
|
timestamp_url=""
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: Scripts/apple/sign-with-google-kms.sh --family ios|developer-id --path <bundle-or-dmg> [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--entitlements <plist> XML entitlements for bundle signing
|
||||||
|
--certificate <cer> Apple public certificate matching the KMS key
|
||||||
|
--kms-key <name> Google KMS crypto key name/resource
|
||||||
|
--kms-version <version> Google KMS crypto key version
|
||||||
|
--runtime Set hardened-runtime code-signature flags
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--family)
|
||||||
|
family="${2:?missing value for --family}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--path)
|
||||||
|
path="${2:?missing value for --path}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--entitlements)
|
||||||
|
entitlements="${2:?missing value for --entitlements}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--certificate)
|
||||||
|
certificate="${2:?missing value for --certificate}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--kms-key)
|
||||||
|
kms_key="${2:?missing value for --kms-key}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--kms-version)
|
||||||
|
kms_version="${2:?missing value for --kms-version}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--runtime)
|
||||||
|
runtime=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "error: unknown argument $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 64
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
case "$family" in
|
||||||
|
ios)
|
||||||
|
certificate="${certificate:-Apple/Certificates/ios-distribution-3G42677598.cer}"
|
||||||
|
kms_key="${kms_key:-${BURROW_IOS_DISTRIBUTION_KMS_KEY:-apple-ios-distribution}}"
|
||||||
|
kms_version="${kms_version:-${BURROW_IOS_DISTRIBUTION_KMS_VERSION:-1}}"
|
||||||
|
timestamp_url="${BURROW_IOS_CODESIGN_TIMESTAMP_URL:-none}"
|
||||||
|
;;
|
||||||
|
developer-id)
|
||||||
|
certificate="${certificate:-Apple/Certificates/developer-id-application-9JKN6HXBHC.cer}"
|
||||||
|
kms_key="${kms_key:-${BURROW_DEVELOPER_ID_KMS_KEY:-apple-developer-id-application}}"
|
||||||
|
kms_version="${kms_version:-${BURROW_DEVELOPER_ID_KMS_VERSION:-1}}"
|
||||||
|
timestamp_url="${BURROW_DEVELOPER_ID_TIMESTAMP_URL:-${BURROW_APPLE_TIMESTAMP_URL:-}}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "error: --family must be ios or developer-id" >&2
|
||||||
|
exit 64
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ -z "$path" ]]; then
|
||||||
|
echo "error: --path is required" >&2
|
||||||
|
exit 64
|
||||||
|
fi
|
||||||
|
if [[ "$path" != /* ]]; then
|
||||||
|
path="$repo_root/$path"
|
||||||
|
fi
|
||||||
|
if [[ "$certificate" != /* ]]; then
|
||||||
|
certificate="$repo_root/$certificate"
|
||||||
|
fi
|
||||||
|
if [[ -n "$entitlements" && "$entitlements" != /* ]]; then
|
||||||
|
entitlements="$repo_root/$entitlements"
|
||||||
|
fi
|
||||||
|
if [[ ! -e "$path" ]]; then
|
||||||
|
echo "error: signing target does not exist: $path" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! -s "$certificate" ]]; then
|
||||||
|
echo "error: Apple certificate is missing or empty: $certificate" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ -n "$entitlements" && ! -s "$entitlements" ]]; then
|
||||||
|
echo "error: entitlements file is missing or empty: $entitlements" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_tool() {
|
||||||
|
if ! command -v "$1" >/dev/null 2>&1; then
|
||||||
|
echo "error: missing required tool: $1" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_module() {
|
||||||
|
local candidate
|
||||||
|
for candidate in \
|
||||||
|
"${BURROW_APPLE_PKCS11_MODULE:-}" \
|
||||||
|
"${BURROW_GCP_KMS_PKCS11_MODULE:-}" \
|
||||||
|
"${BURROW_PKCS11_MODULE:-}" \
|
||||||
|
"$(command -v google-kms-pkcs11-module >/dev/null 2>&1 && google-kms-pkcs11-module || true)" \
|
||||||
|
/nix/store/*-google-kms-pkcs11-*/lib/libkmsp11.so \
|
||||||
|
/nix/store/*-google-kms-pkcs11-*/lib/libkmsp11.dylib; do
|
||||||
|
if [[ -n "$candidate" && -f "$candidate" ]]; then
|
||||||
|
printf '%s\n' "$candidate"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cert_public_key() {
|
||||||
|
local cert_file="$1"
|
||||||
|
local out="$2"
|
||||||
|
if ! openssl x509 -inform DER -in "$cert_file" -pubkey -noout >"$out" 2>/dev/null; then
|
||||||
|
openssl x509 -in "$cert_file" -pubkey -noout >"$out"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
public_fingerprint() {
|
||||||
|
openssl pkey -pubin -in "$1" -outform DER \
|
||||||
|
| openssl dgst -sha256 -binary \
|
||||||
|
| od -An -tx1 \
|
||||||
|
| tr -d ' \n'
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_kms_parts() {
|
||||||
|
local resource="$1"
|
||||||
|
local key_ring="${BURROW_GCP_KMS_KEY_RING:-${GOOGLE_KMS_KEY_RING:-projects/${GOOGLE_CLOUD_PROJECT:-project-88c23ce9-918a-470a-b33}/locations/${BURROW_KMS_LOCATION:-global}/keyRings/${BURROW_KMS_KEYRING:-burrow-identity}}}"
|
||||||
|
|
||||||
|
if [[ "$resource" =~ ^projects/([^/]+)/locations/([^/]+)/keyRings/([^/]+)/cryptoKeys/([^/]+)/cryptoKeyVersions/([^/]+)$ ]]; then
|
||||||
|
printf '%s\n%s\n%s\n%s\n%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}" "${BASH_REMATCH[4]}" "${BASH_REMATCH[5]}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ "$resource" =~ ^projects/([^/]+)/locations/([^/]+)/keyRings/([^/]+)/cryptoKeys/([^/]+)$ ]]; then
|
||||||
|
printf '%s\n%s\n%s\n%s\n%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}" "${BASH_REMATCH[4]}" "$kms_version"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ ! "$key_ring" =~ ^projects/([^/]+)/locations/([^/]+)/keyRings/([^/]+)$ ]]; then
|
||||||
|
echo "error: invalid KMS key ring resource: $key_ring" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf '%s\n%s\n%s\n%s\n%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}" "$resource" "$kms_version"
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_certificate_matches_kms() {
|
||||||
|
local work_dir="$1"
|
||||||
|
local cert_public="$work_dir/apple-public.pem"
|
||||||
|
local kms_public="$work_dir/kms-public.pem"
|
||||||
|
local project location keyring key version expected actual
|
||||||
|
kms_info="$(resolve_kms_parts "$kms_key")"
|
||||||
|
project="$(printf '%s\n' "$kms_info" | sed -n '1p')"
|
||||||
|
location="$(printf '%s\n' "$kms_info" | sed -n '2p')"
|
||||||
|
keyring="$(printf '%s\n' "$kms_info" | sed -n '3p')"
|
||||||
|
key="$(printf '%s\n' "$kms_info" | sed -n '4p')"
|
||||||
|
version="$(printf '%s\n' "$kms_info" | sed -n '5p')"
|
||||||
|
if [[ -z "$version" ]]; then
|
||||||
|
echo "error: KMS key version is required for Apple release signing" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cert_public_key "$certificate" "$cert_public"
|
||||||
|
expected="$(public_fingerprint "$cert_public")"
|
||||||
|
gcloud kms keys versions get-public-key "$version" \
|
||||||
|
--project="$project" \
|
||||||
|
--location="$location" \
|
||||||
|
--keyring="$keyring" \
|
||||||
|
--key="$key" \
|
||||||
|
--output-file="$kms_public" >/dev/null
|
||||||
|
actual="$(public_fingerprint "$kms_public")"
|
||||||
|
if [[ "$actual" != "$expected" ]]; then
|
||||||
|
echo "error: Apple certificate public key does not match Google KMS key" >&2
|
||||||
|
echo "family=$family" >&2
|
||||||
|
echo "kms_key=$key" >&2
|
||||||
|
echo "kms_version=$version" >&2
|
||||||
|
echo "certificate_spki_sha256=$expected" >&2
|
||||||
|
echo "kms_spki_sha256=$actual" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
require_tool rcodesign
|
||||||
|
require_tool openssl
|
||||||
|
require_tool od
|
||||||
|
require_tool gcloud
|
||||||
|
|
||||||
|
if ! rcodesign sign --help 2>&1 | grep -Fq -- "--pkcs11-library"; then
|
||||||
|
echo "error: rcodesign does not include PKCS#11 signing support" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
module="$(resolve_module || true)"
|
||||||
|
if [[ -z "$module" ]]; then
|
||||||
|
echo "error: unable to locate Google KMS PKCS#11 module" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/burrow-apple-kms-sign.XXXXXX")"
|
||||||
|
trap 'rm -rf "$work_dir"' EXIT
|
||||||
|
|
||||||
|
eval "$(Scripts/release/google-kms-pkcs11-materialize.sh --emit-env)"
|
||||||
|
verify_certificate_matches_kms "$work_dir"
|
||||||
|
|
||||||
|
real_openssl="$(command -v openssl)"
|
||||||
|
export BURROW_REAL_OPENSSL="${BURROW_REAL_OPENSSL:-$real_openssl}"
|
||||||
|
export BURROW_OPENSSL="$repo_root/Scripts/release/google-kms-openssl-dgst-shim.sh"
|
||||||
|
export BURROW_APPLE_PKCS11_BACKEND=gcp-kms
|
||||||
|
export BURROW_APPLE_PKCS11_SIGN_WITH_OPENSSL_PROVIDER=true
|
||||||
|
export BURROW_APPLE_PKCS11_SIGN_WITH_GCLOUD=true
|
||||||
|
export BURROW_GCP_KMS_ACTIVE_CRYPTO_KEY="$kms_key"
|
||||||
|
export BURROW_GCP_KMS_ACTIVE_CRYPTO_KEY_VERSION="$kms_version"
|
||||||
|
|
||||||
|
pin="${BURROW_APPLE_PKCS11_PIN:-${BURROW_GCP_KMS_PKCS11_PIN:-${BURROW_PKCS11_PIN:-ignored}}}"
|
||||||
|
token_label="${BURROW_APPLE_PKCS11_TOKEN_LABEL:-${BURROW_GCP_KMS_PKCS11_TOKEN_LABEL:-${BURROW_PKCS11_TOKEN_LABEL:-burrow-release}}}"
|
||||||
|
|
||||||
|
args=(
|
||||||
|
sign
|
||||||
|
--pkcs11-library "$module"
|
||||||
|
--pkcs11-certificate-file "$certificate"
|
||||||
|
--pkcs11-token-label "$token_label"
|
||||||
|
--pkcs11-key-label "$kms_key"
|
||||||
|
--pkcs11-pin "$pin"
|
||||||
|
)
|
||||||
|
if [[ -n "$timestamp_url" ]]; then
|
||||||
|
args+=(--timestamp-url "$timestamp_url")
|
||||||
|
fi
|
||||||
|
if [[ -n "$entitlements" ]]; then
|
||||||
|
args+=(--entitlements-xml-file "$entitlements")
|
||||||
|
fi
|
||||||
|
if [[ "$runtime" == "true" ]]; then
|
||||||
|
args+=(--code-signature-flags runtime)
|
||||||
|
fi
|
||||||
|
args+=("$path")
|
||||||
|
|
||||||
|
rcodesign "${args[@]}"
|
||||||
|
|
@ -261,7 +261,7 @@ async function main() {
|
||||||
const issuerId = args["issuer-id"];
|
const issuerId = args["issuer-id"];
|
||||||
const keyFile = args["key-file"];
|
const keyFile = args["key-file"];
|
||||||
const outDir = args["out-dir"] ?? ".forgejo/actions/export";
|
const outDir = args["out-dir"] ?? ".forgejo/actions/export";
|
||||||
const appId = args["app-id"] ?? "com.hackclub.burrow";
|
const appId = args["app-id"] ?? "net.burrow.app";
|
||||||
const networkId = args["network-id"] ?? `${appId}.network`;
|
const networkId = args["network-id"] ?? `${appId}.network`;
|
||||||
const iosCertificateId = args["ios-certificate-id"] ?? process.env.BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID;
|
const iosCertificateId = args["ios-certificate-id"] ?? process.env.BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID;
|
||||||
const macosCertificateId = args["macos-certificate-id"] ?? process.env.BURROW_DEVELOPER_ID_CERTIFICATE_ID;
|
const macosCertificateId = args["macos-certificate-id"] ?? process.env.BURROW_DEVELOPER_ID_CERTIFICATE_ID;
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,15 @@ derived_data="${BURROW_DERIVED_DATA_PATH:-${repo_root}/.derived-data/apple}"
|
||||||
source_packages="${BURROW_SWIFTPM_CACHE_PATH:-${XDG_CACHE_HOME:-${HOME}/.cache}/burrow/swiftpm}"
|
source_packages="${BURROW_SWIFTPM_CACHE_PATH:-${XDG_CACHE_HOME:-${HOME}/.cache}/burrow/swiftpm}"
|
||||||
project="Apple/Burrow.xcodeproj"
|
project="Apple/Burrow.xcodeproj"
|
||||||
scheme="${BURROW_APPLE_SCHEME:-App}"
|
scheme="${BURROW_APPLE_SCHEME:-App}"
|
||||||
bundle_id="${BURROW_APPLE_BUNDLE_ID:-com.hackclub.burrow}"
|
bundle_id="${BURROW_APPLE_BUNDLE_ID:-net.burrow.app}"
|
||||||
network_extension_bundle_id="${BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID:-${bundle_id}.network}"
|
network_extension_bundle_id="${BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID:-${bundle_id}.network}"
|
||||||
signing_ready="${BURROW_APPLE_SIGNING_READY:-0}"
|
signing_ready="${BURROW_APPLE_SIGNING_READY:-0}"
|
||||||
|
signing_mode="${BURROW_APPLE_SIGNING_MODE:-keychain}"
|
||||||
require_signing="${BURROW_APPLE_REQUIRE_SIGNING:-false}"
|
require_signing="${BURROW_APPLE_REQUIRE_SIGNING:-false}"
|
||||||
sparkle_feed_url="${BURROW_SPARKLE_FEED_URL:-https://releases.burrow.net/sparkle/appcast.xml}"
|
sparkle_feed_url="${BURROW_SPARKLE_FEED_URL:-https://releases.burrow.net/sparkle/appcast.xml}"
|
||||||
sparkle_public_ed_key="${BURROW_SPARKLE_PUBLIC_ED_KEY:-Myv9ZNZT6YGKMtMezh52ra4WqaeEKc4VlvVU0evhJeI=}"
|
sparkle_public_ed_key="${BURROW_SPARKLE_PUBLIC_ED_KEY:-Myv9ZNZT6YGKMtMezh52ra4WqaeEKc4VlvVU0evhJeI=}"
|
||||||
sparkle_sign_with_kms="${BURROW_SPARKLE_SIGN_WITH_KMS:-false}"
|
sparkle_sign_with_kms="${BURROW_SPARKLE_SIGN_WITH_KMS:-false}"
|
||||||
|
notarize_macos="${BURROW_NOTARIZE_MACOS:-false}"
|
||||||
|
|
||||||
mkdir -p "$apple_root" "$archives_root" "$derived_data" "$source_packages"
|
mkdir -p "$apple_root" "$archives_root" "$derived_data" "$source_packages"
|
||||||
|
|
||||||
|
|
@ -39,6 +41,74 @@ sha256_file() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
safe_profile_slug() {
|
||||||
|
python3 - "$1" <<'PY'
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
print(re.sub(r"[^A-Za-z0-9]", "", sys.argv[1]).lower())
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
profile_for() {
|
||||||
|
local identifier="$1"
|
||||||
|
local suffix="$2"
|
||||||
|
local slug path
|
||||||
|
slug="$(safe_profile_slug "$identifier")"
|
||||||
|
for path in \
|
||||||
|
".forgejo/actions/export/${slug}${suffix}.provisionprofile" \
|
||||||
|
".forgejo/actions/export/${slug}${suffix}.mobileprovision" \
|
||||||
|
"Apple/Profiles/${slug}${suffix}.provisionprofile" \
|
||||||
|
"Apple/Profiles/${slug}${suffix}.mobileprovision"; do
|
||||||
|
if [[ -s "$path" ]]; then
|
||||||
|
printf '%s\n' "$path"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_entitlements_from_profile() {
|
||||||
|
local profile="$1"
|
||||||
|
local output="$2"
|
||||||
|
Scripts/apple/profile-entitlements.py "$profile" --output "$output"
|
||||||
|
}
|
||||||
|
|
||||||
|
apple_signing_available() {
|
||||||
|
if [[ "$signing_mode" == "google-kms" || "$signing_mode" == "google-kms-staged" || "$signing_mode" == "kms" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
[[ "$signing_ready" == "1" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
kms_sign_bundle() {
|
||||||
|
local family="$1"
|
||||||
|
local bundle_path="$2"
|
||||||
|
local entitlements="$3"
|
||||||
|
local runtime_flag=()
|
||||||
|
if [[ "$family" == "developer-id" ]]; then
|
||||||
|
runtime_flag=(--runtime)
|
||||||
|
fi
|
||||||
|
Scripts/apple/sign-with-google-kms.sh \
|
||||||
|
--family "$family" \
|
||||||
|
--path "$bundle_path" \
|
||||||
|
--entitlements "$entitlements" \
|
||||||
|
"${runtime_flag[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
kms_sign_file() {
|
||||||
|
local family="$1"
|
||||||
|
local file_path="$2"
|
||||||
|
local runtime_flag=()
|
||||||
|
if [[ "$family" == "developer-id" ]]; then
|
||||||
|
runtime_flag=(--runtime)
|
||||||
|
fi
|
||||||
|
Scripts/apple/sign-with-google-kms.sh \
|
||||||
|
--family "$family" \
|
||||||
|
--path "$file_path" \
|
||||||
|
"${runtime_flag[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
write_version() {
|
write_version() {
|
||||||
mkdir -p Apple/Configuration
|
mkdir -p Apple/Configuration
|
||||||
printf 'CURRENT_PROJECT_VERSION = %s\n' "$build_number" > Apple/Configuration/Version.xcconfig
|
printf 'CURRENT_PROJECT_VERSION = %s\n' "$build_number" > Apple/Configuration/Version.xcconfig
|
||||||
|
|
@ -93,7 +163,7 @@ unsigned_note() {
|
||||||
ensure_signing_or_note() {
|
ensure_signing_or_note() {
|
||||||
local platform_name="$1"
|
local platform_name="$1"
|
||||||
local artifact_name="$2"
|
local artifact_name="$2"
|
||||||
if [[ "$signing_ready" == "1" ]]; then
|
if apple_signing_available; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
if truthy "$require_signing"; then
|
if truthy "$require_signing"; then
|
||||||
|
|
@ -151,6 +221,14 @@ build_ios_release() {
|
||||||
build_ios_unsigned
|
build_ios_unsigned
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
if [[ "$signing_mode" == "google-kms-staged" ]]; then
|
||||||
|
build_ios_kms_staged
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ "$signing_mode" == "google-kms" || "$signing_mode" == "kms" ]]; then
|
||||||
|
build_ios_kms_release
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
archive_path="${archives_root}/Burrow-iOS-${build_number}.xcarchive"
|
archive_path="${archives_root}/Burrow-iOS-${build_number}.xcarchive"
|
||||||
export_dir="${apple_root}/ios-export"
|
export_dir="${apple_root}/ios-export"
|
||||||
|
|
@ -181,6 +259,91 @@ build_ios_release() {
|
||||||
sha256_file "${apple_root}/Burrow-iOS-${build_number}.ipa"
|
sha256_file "${apple_root}/Burrow-iOS-${build_number}.ipa"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
build_ios_kms_staged() {
|
||||||
|
local product_dir source_app stage_dir ipa
|
||||||
|
xcodebuild build \
|
||||||
|
"${xcode_common_args[@]}" \
|
||||||
|
-destination "generic/platform=iOS" \
|
||||||
|
ARCHS=arm64 \
|
||||||
|
ONLY_ACTIVE_ARCH=YES \
|
||||||
|
SKIP_INSTALL=NO \
|
||||||
|
CODE_SIGNING_ALLOWED=NO \
|
||||||
|
CODE_SIGNING_REQUIRED=NO \
|
||||||
|
CODE_SIGN_IDENTITY=
|
||||||
|
|
||||||
|
product_dir="${derived_data}/Build/Products/Release-iphoneos"
|
||||||
|
source_app="${product_dir}/Burrow.app"
|
||||||
|
if [[ ! -d "$source_app" ]]; then
|
||||||
|
echo "iOS build completed but did not produce Burrow.app" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
stage_dir="${apple_root}/ios-unsigned/Payload"
|
||||||
|
rm -rf "$stage_dir"
|
||||||
|
mkdir -p "$stage_dir"
|
||||||
|
ditto "$source_app" "${stage_dir}/Burrow.app"
|
||||||
|
ipa="${apple_root}/Burrow-iOS-${build_number}.unsigned.ipa"
|
||||||
|
rm -f "$ipa"
|
||||||
|
(cd "${apple_root}/ios-unsigned" && zip -qry "$ipa" Payload)
|
||||||
|
sha256_file "$ipa"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_ios_kms_release() {
|
||||||
|
local product_dir source_app stage_dir app ext app_profile ext_profile app_entitlements ext_entitlements ipa
|
||||||
|
xcodebuild build \
|
||||||
|
"${xcode_common_args[@]}" \
|
||||||
|
-destination "generic/platform=iOS" \
|
||||||
|
ARCHS=arm64 \
|
||||||
|
ONLY_ACTIVE_ARCH=YES \
|
||||||
|
SKIP_INSTALL=NO \
|
||||||
|
CODE_SIGNING_ALLOWED=NO \
|
||||||
|
CODE_SIGNING_REQUIRED=NO \
|
||||||
|
CODE_SIGN_IDENTITY=
|
||||||
|
|
||||||
|
product_dir="${derived_data}/Build/Products/Release-iphoneos"
|
||||||
|
source_app="${product_dir}/Burrow.app"
|
||||||
|
if [[ ! -d "$source_app" ]]; then
|
||||||
|
echo "iOS build completed but did not produce Burrow.app" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
stage_dir="${apple_root}/ios-kms/Payload"
|
||||||
|
rm -rf "$stage_dir"
|
||||||
|
mkdir -p "$stage_dir"
|
||||||
|
ditto "$source_app" "${stage_dir}/Burrow.app"
|
||||||
|
app="${stage_dir}/Burrow.app"
|
||||||
|
ext="${app}/PlugIns/BurrowNetworkExtension.appex"
|
||||||
|
if [[ ! -d "$ext" ]]; then
|
||||||
|
echo "iOS app is missing BurrowNetworkExtension.appex" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
app_profile="$(profile_for "$bundle_id" "appstore")" || {
|
||||||
|
echo "missing iOS App Store provisioning profile for ${bundle_id}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
ext_profile="$(profile_for "$network_extension_bundle_id" "appstore")" || {
|
||||||
|
echo "missing iOS App Store provisioning profile for ${network_extension_bundle_id}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
cp "$app_profile" "${app}/embedded.mobileprovision"
|
||||||
|
cp "$ext_profile" "${ext}/embedded.mobileprovision"
|
||||||
|
|
||||||
|
app_entitlements="${apple_root}/ios-kms/app-entitlements.plist"
|
||||||
|
ext_entitlements="${apple_root}/ios-kms/extension-entitlements.plist"
|
||||||
|
generate_entitlements_from_profile "$app_profile" "$app_entitlements"
|
||||||
|
generate_entitlements_from_profile "$ext_profile" "$ext_entitlements"
|
||||||
|
|
||||||
|
rm -rf "${app}/_CodeSignature" "${ext}/_CodeSignature"
|
||||||
|
kms_sign_bundle ios "$ext" "$ext_entitlements"
|
||||||
|
kms_sign_bundle ios "$app" "$app_entitlements"
|
||||||
|
|
||||||
|
ipa="${apple_root}/Burrow-iOS-${build_number}.ipa"
|
||||||
|
rm -f "$ipa"
|
||||||
|
(cd "${apple_root}/ios-kms" && zip -qry "$ipa" Payload)
|
||||||
|
sha256_file "$ipa"
|
||||||
|
}
|
||||||
|
|
||||||
build_macos_unsigned() {
|
build_macos_unsigned() {
|
||||||
local product_dir zip_path
|
local product_dir zip_path
|
||||||
xcodebuild build \
|
xcodebuild build \
|
||||||
|
|
@ -203,6 +366,14 @@ build_macos_release() {
|
||||||
build_macos_unsigned
|
build_macos_unsigned
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
if [[ "$signing_mode" == "google-kms-staged" ]]; then
|
||||||
|
build_macos_kms_staged
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ "$signing_mode" == "google-kms" || "$signing_mode" == "kms" ]]; then
|
||||||
|
build_macos_kms_release
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
archive_path="${archives_root}/Burrow-macOS-${build_number}.xcarchive"
|
archive_path="${archives_root}/Burrow-macOS-${build_number}.xcarchive"
|
||||||
export_dir="${apple_root}/macos-export"
|
export_dir="${apple_root}/macos-export"
|
||||||
|
|
@ -264,6 +435,135 @@ EOF
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
build_macos_kms_staged() {
|
||||||
|
local product_dir source_app zip_path
|
||||||
|
xcodebuild build \
|
||||||
|
"${xcode_common_args[@]}" \
|
||||||
|
-destination "platform=macOS" \
|
||||||
|
CODE_SIGNING_ALLOWED=NO \
|
||||||
|
CODE_SIGNING_REQUIRED=NO \
|
||||||
|
CODE_SIGN_IDENTITY=
|
||||||
|
|
||||||
|
product_dir="${derived_data}/Build/Products/Release"
|
||||||
|
source_app="${product_dir}/Burrow.app"
|
||||||
|
if [[ ! -d "$source_app" ]]; then
|
||||||
|
echo "macOS build completed but did not produce Burrow.app" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
zip_path="${apple_root}/Burrow-macOS-${build_number}.unsigned.zip"
|
||||||
|
rm -f "$zip_path"
|
||||||
|
ditto -c -k --keepParent "$source_app" "$zip_path"
|
||||||
|
sha256_file "$zip_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_sparkle_appcast() {
|
||||||
|
local dmg="$1"
|
||||||
|
local channel sparkle_dir length pubdate url
|
||||||
|
channel="${SPARKLE_CHANNEL:-build-${build_number}}"
|
||||||
|
sparkle_dir="${out_root}/sparkle/${channel}"
|
||||||
|
mkdir -p "$sparkle_dir"
|
||||||
|
cp "$dmg" "$sparkle_dir/"
|
||||||
|
length="$(wc -c < "$dmg" | tr -d ' ')"
|
||||||
|
pubdate="$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S +0000')"
|
||||||
|
url="${SPARKLE_DOWNLOAD_PREFIX:-https://releases.burrow.net/sparkle}/${channel}/$(basename "$dmg")"
|
||||||
|
cat > "${sparkle_dir}/appcast.xml" <<EOF
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
|
||||||
|
<channel>
|
||||||
|
<title>Burrow ${channel}</title>
|
||||||
|
<item>
|
||||||
|
<title>Burrow ${build_number}</title>
|
||||||
|
<pubDate>${pubdate}</pubDate>
|
||||||
|
<sparkle:version>${build_number}</sparkle:version>
|
||||||
|
<sparkle:shortVersionString>0.1</sparkle:shortVersionString>
|
||||||
|
<enclosure url="${url}" sparkle:version="${build_number}" length="${length}" type="application/octet-stream" />
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if truthy "$sparkle_sign_with_kms"; then
|
||||||
|
Scripts/sparkle/sign-appcast-kms.py \
|
||||||
|
--appcast "${sparkle_dir}/appcast.xml" \
|
||||||
|
--artifact-dir "$sparkle_dir"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
notarize_dmg_if_requested() {
|
||||||
|
local dmg="$1"
|
||||||
|
local key_json
|
||||||
|
if ! truthy "$notarize_macos"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ -z "${ASC_API_KEY_ID:-}" || -z "${ASC_API_ISSUER_ID:-}" || -z "${ASC_API_KEY_PATH:-}" ]]; then
|
||||||
|
echo "macOS notarization requires ASC_API_KEY_ID, ASC_API_ISSUER_ID, and ASC_API_KEY_PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
key_json="${apple_root}/notary-api-key.json"
|
||||||
|
rcodesign encode-app-store-connect-api-key \
|
||||||
|
"$ASC_API_ISSUER_ID" \
|
||||||
|
"$ASC_API_KEY_ID" \
|
||||||
|
"$ASC_API_KEY_PATH" \
|
||||||
|
"$key_json"
|
||||||
|
rcodesign notary-submit --api-key-file "$key_json" --wait --staple "$dmg"
|
||||||
|
rm -f "$key_json"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_macos_kms_release() {
|
||||||
|
local product_dir source_app app ext app_profile ext_profile app_entitlements ext_entitlements dmg
|
||||||
|
xcodebuild build \
|
||||||
|
"${xcode_common_args[@]}" \
|
||||||
|
-destination "platform=macOS" \
|
||||||
|
CODE_SIGNING_ALLOWED=NO \
|
||||||
|
CODE_SIGNING_REQUIRED=NO \
|
||||||
|
CODE_SIGN_IDENTITY=
|
||||||
|
|
||||||
|
product_dir="${derived_data}/Build/Products/Release"
|
||||||
|
source_app="${product_dir}/Burrow.app"
|
||||||
|
if [[ ! -d "$source_app" ]]; then
|
||||||
|
echo "macOS build completed but did not produce Burrow.app" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
app="${apple_root}/macos-kms/Burrow.app"
|
||||||
|
rm -rf "$app"
|
||||||
|
mkdir -p "$(dirname "$app")"
|
||||||
|
ditto "$source_app" "$app"
|
||||||
|
ext="${app}/Contents/PlugIns/BurrowNetworkExtension.appex"
|
||||||
|
|
||||||
|
app_profile="$(profile_for "$bundle_id" "developerid")" || {
|
||||||
|
echo "missing macOS Developer ID provisioning profile for ${bundle_id}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
cp "$app_profile" "${app}/Contents/embedded.provisionprofile"
|
||||||
|
app_entitlements="${apple_root}/macos-kms/app-entitlements.plist"
|
||||||
|
generate_entitlements_from_profile "$app_profile" "$app_entitlements"
|
||||||
|
|
||||||
|
if [[ -d "$ext" ]]; then
|
||||||
|
ext_profile="$(profile_for "$network_extension_bundle_id" "developerid")" || {
|
||||||
|
echo "missing macOS Developer ID provisioning profile for ${network_extension_bundle_id}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
cp "$ext_profile" "${ext}/Contents/embedded.provisionprofile"
|
||||||
|
ext_entitlements="${apple_root}/macos-kms/extension-entitlements.plist"
|
||||||
|
generate_entitlements_from_profile "$ext_profile" "$ext_entitlements"
|
||||||
|
rm -rf "${ext}/Contents/_CodeSignature"
|
||||||
|
kms_sign_bundle developer-id "$ext" "$ext_entitlements"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "${app}/Contents/_CodeSignature"
|
||||||
|
kms_sign_bundle developer-id "$app" "$app_entitlements"
|
||||||
|
|
||||||
|
dmg="${apple_root}/Burrow-macOS-${build_number}.dmg"
|
||||||
|
rm -f "$dmg"
|
||||||
|
hdiutil create -volname "Burrow ${build_number}" -srcfolder "$app" -ov -format UDZO "$dmg"
|
||||||
|
kms_sign_file developer-id "$dmg"
|
||||||
|
notarize_dmg_if_requested "$dmg"
|
||||||
|
sha256_file "$dmg"
|
||||||
|
write_sparkle_appcast "$dmg"
|
||||||
|
}
|
||||||
|
|
||||||
require_xcodebuild
|
require_xcodebuild
|
||||||
write_version
|
write_version
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ cd "$repo_root"
|
||||||
|
|
||||||
platform="${1:-all}"
|
platform="${1:-all}"
|
||||||
out_dir="${BURROW_APPLE_PROFILES_OUT_DIR:-.forgejo/actions/export}"
|
out_dir="${BURROW_APPLE_PROFILES_OUT_DIR:-.forgejo/actions/export}"
|
||||||
app_id="${BURROW_APPLE_BUNDLE_ID:-com.hackclub.burrow}"
|
app_id="${BURROW_APPLE_BUNDLE_ID:-net.burrow.app}"
|
||||||
network_id="${BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID:-${app_id}.network}"
|
network_id="${BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID:-${app_id}.network}"
|
||||||
replace_certificate_mismatch="${BURROW_APPLE_REPLACE_CERTIFICATE_MISMATCH:-false}"
|
replace_certificate_mismatch="${BURROW_APPLE_REPLACE_CERTIFICATE_MISMATCH:-false}"
|
||||||
if [[ -n "${BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID:-}" || -n "${BURROW_DEVELOPER_ID_CERTIFICATE_ID:-}" ]]; then
|
if [[ -n "${BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID:-}" || -n "${BURROW_DEVELOPER_ID_CERTIFICATE_ID:-}" ]]; then
|
||||||
|
|
|
||||||
197
Scripts/release/google-kms-openssl-dgst-shim.sh
Executable file
197
Scripts/release/google-kms-openssl-dgst-shim.sh
Executable file
|
|
@ -0,0 +1,197 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
real_openssl="${BURROW_REAL_OPENSSL:-openssl}"
|
||||||
|
gcloud_bin="${BURROW_GCLOUD_BIN:-gcloud}"
|
||||||
|
original_args=("$@")
|
||||||
|
|
||||||
|
exec_real_openssl() {
|
||||||
|
exec "$real_openssl" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
truthy() {
|
||||||
|
case "${1:-}" in
|
||||||
|
1|true|TRUE|True|yes|YES|Yes|y|Y|on|ON|On) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_pkcs11_key_name() {
|
||||||
|
local uri="$1"
|
||||||
|
local value
|
||||||
|
value="${uri#*;object=}"
|
||||||
|
if [[ "$value" != "$uri" ]]; then
|
||||||
|
printf '%s\n' "${value%%;*}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
value="${uri#*;id=}"
|
||||||
|
if [[ "$value" != "$uri" ]]; then
|
||||||
|
printf '%s\n' "${value%%;*}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
select_single_enabled_version() {
|
||||||
|
local project_id="$1"
|
||||||
|
local kms_location="$2"
|
||||||
|
local kms_keyring_name="$3"
|
||||||
|
local crypto_key_name="$4"
|
||||||
|
local versions=()
|
||||||
|
local version
|
||||||
|
|
||||||
|
while IFS= read -r version; do
|
||||||
|
[[ -n "$version" ]] && versions+=("$version")
|
||||||
|
done < <(
|
||||||
|
"$gcloud_bin" kms keys versions list \
|
||||||
|
--project="$project_id" \
|
||||||
|
--location="$kms_location" \
|
||||||
|
--keyring="$kms_keyring_name" \
|
||||||
|
--key="$crypto_key_name" \
|
||||||
|
--filter=state=ENABLED \
|
||||||
|
--format='value(name)'
|
||||||
|
)
|
||||||
|
if [[ "${#versions[@]}" -ne 1 || -z "${versions[0]:-}" ]]; then
|
||||||
|
echo "error: Google KMS crypto key ${crypto_key_name} has ${#versions[@]} ENABLED versions; set BURROW_GCP_KMS_ACTIVE_CRYPTO_KEY_VERSION" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
printf '%s\n' "${versions[0]##*/}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${1:-}" != "dgst" ]]; then
|
||||||
|
exec_real_openssl "${original_args[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! truthy "${BURROW_APPLE_PKCS11_SIGN_WITH_GCLOUD:-true}"; then
|
||||||
|
exec_real_openssl "${original_args[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
shift
|
||||||
|
digest_algorithm=""
|
||||||
|
signature_file=""
|
||||||
|
input_file=""
|
||||||
|
sign_uri=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-sha256)
|
||||||
|
digest_algorithm="sha256"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-sign)
|
||||||
|
[[ $# -ge 2 ]] || {
|
||||||
|
echo "error: openssl dgst shim received -sign without a key URI" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
sign_uri="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-out)
|
||||||
|
[[ $# -ge 2 ]] || {
|
||||||
|
echo "error: openssl dgst shim received -out without a path" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
signature_file="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-provider|-passin)
|
||||||
|
[[ $# -ge 2 ]] || {
|
||||||
|
echo "error: openssl dgst shim received $1 without a value" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
exec_real_openssl "${original_args[@]}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
input_file="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$digest_algorithm" != "sha256" || "$sign_uri" != pkcs11:* ]]; then
|
||||||
|
exec_real_openssl "${original_args[@]}"
|
||||||
|
fi
|
||||||
|
if [[ -z "$signature_file" || -z "$input_file" ]]; then
|
||||||
|
echo "error: openssl dgst shim requires -out and an input file" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$input_file" ]]; then
|
||||||
|
echo "error: openssl dgst shim input file does not exist: $input_file" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
command -v "$gcloud_bin" >/dev/null 2>&1 || {
|
||||||
|
echo "error: gcloud is required for Google KMS direct signing; set BURROW_GCLOUD_BIN" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
command -v python3 >/dev/null 2>&1 || {
|
||||||
|
echo "error: python3 is required to decode Google KMS signatures" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
crypto_key="${BURROW_GCP_KMS_ACTIVE_CRYPTO_KEY:-}"
|
||||||
|
if [[ -z "$crypto_key" ]]; then
|
||||||
|
crypto_key="$(parse_pkcs11_key_name "$sign_uri" || true)"
|
||||||
|
fi
|
||||||
|
if [[ -z "$crypto_key" ]]; then
|
||||||
|
echo "error: BURROW_GCP_KMS_ACTIVE_CRYPTO_KEY or a pkcs11 object selector is required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
key_version="${BURROW_GCP_KMS_ACTIVE_CRYPTO_KEY_VERSION:-}"
|
||||||
|
key_ring="${BURROW_GCP_KMS_KEY_RING:-${GOOGLE_KMS_KEY_RING:-projects/${GOOGLE_CLOUD_PROJECT:-project-88c23ce9-918a-470a-b33}/locations/${BURROW_KMS_LOCATION:-global}/keyRings/${BURROW_KMS_KEYRING:-burrow-identity}}}"
|
||||||
|
project_id="${GOOGLE_CLOUD_PROJECT:-project-88c23ce9-918a-470a-b33}"
|
||||||
|
kms_location=""
|
||||||
|
kms_keyring_name=""
|
||||||
|
crypto_key_name=""
|
||||||
|
|
||||||
|
if [[ "$crypto_key" =~ ^projects/([^/]+)/locations/([^/]+)/keyRings/([^/]+)/cryptoKeys/([^/]+)/cryptoKeyVersions/([^/]+)$ ]]; then
|
||||||
|
project_id="${BASH_REMATCH[1]}"
|
||||||
|
kms_location="${BASH_REMATCH[2]}"
|
||||||
|
kms_keyring_name="${BASH_REMATCH[3]}"
|
||||||
|
crypto_key_name="${BASH_REMATCH[4]}"
|
||||||
|
key_version="${BASH_REMATCH[5]}"
|
||||||
|
elif [[ "$crypto_key" =~ ^projects/([^/]+)/locations/([^/]+)/keyRings/([^/]+)/cryptoKeys/([^/]+)$ ]]; then
|
||||||
|
project_id="${BASH_REMATCH[1]}"
|
||||||
|
kms_location="${BASH_REMATCH[2]}"
|
||||||
|
kms_keyring_name="${BASH_REMATCH[3]}"
|
||||||
|
crypto_key_name="${BASH_REMATCH[4]}"
|
||||||
|
else
|
||||||
|
if [[ ! "$key_ring" =~ ^projects/([^/]+)/locations/([^/]+)/keyRings/([^/]+)$ ]]; then
|
||||||
|
echo "error: BURROW_GCP_KMS_KEY_RING must be projects/<project>/locations/<location>/keyRings/<name>, got: ${key_ring}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
project_id="${BASH_REMATCH[1]}"
|
||||||
|
kms_location="${BASH_REMATCH[2]}"
|
||||||
|
kms_keyring_name="${BASH_REMATCH[3]}"
|
||||||
|
crypto_key_name="$crypto_key"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$key_version" ]]; then
|
||||||
|
key_version="$(select_single_enabled_version "$project_id" "$kms_location" "$kms_keyring_name" "$crypto_key_name")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
encoded_signature_file="$(mktemp "${TMPDIR:-/tmp}/burrow-google-kms-signature.XXXXXX")"
|
||||||
|
trap 'rm -f "$encoded_signature_file"' EXIT
|
||||||
|
"$gcloud_bin" kms asymmetric-sign \
|
||||||
|
--project="$project_id" \
|
||||||
|
--location="$kms_location" \
|
||||||
|
--keyring="$kms_keyring_name" \
|
||||||
|
--key="$crypto_key_name" \
|
||||||
|
--version="$key_version" \
|
||||||
|
--digest-algorithm="$digest_algorithm" \
|
||||||
|
--input-file="$input_file" \
|
||||||
|
--signature-file="$encoded_signature_file" \
|
||||||
|
--quiet >/dev/null
|
||||||
|
|
||||||
|
python3 - "$encoded_signature_file" "$signature_file" <<'PY'
|
||||||
|
import base64
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
encoded_path = pathlib.Path(sys.argv[1])
|
||||||
|
signature_path = pathlib.Path(sys.argv[2])
|
||||||
|
signature_path.write_bytes(base64.b64decode(encoded_path.read_text(encoding="utf-8").strip()))
|
||||||
|
PY
|
||||||
93
Scripts/release/google-kms-pkcs11-materialize.sh
Executable file
93
Scripts/release/google-kms-pkcs11-materialize.sh
Executable file
|
|
@ -0,0 +1,93 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
emit_env=false
|
||||||
|
ROOT="$(
|
||||||
|
git rev-parse --show-toplevel 2>/dev/null || {
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")/../.."
|
||||||
|
pwd
|
||||||
|
}
|
||||||
|
)"
|
||||||
|
runtime_dir="${BURROW_GCP_KMS_PKCS11_RUNTIME_DIR:-${RUNNER_TEMP:-${TMPDIR:-/tmp}}/burrow-google-kms-pkcs11-runtime}"
|
||||||
|
config_file="${BURROW_GCP_KMS_PKCS11_CONFIG:-${KMS_PKCS11_CONFIG:-}}"
|
||||||
|
config_yaml="${BURROW_GCP_KMS_PKCS11_CONFIG_YAML:-}"
|
||||||
|
key_ring="${BURROW_GCP_KMS_KEY_RING:-${GOOGLE_KMS_KEY_RING:-projects/${GOOGLE_CLOUD_PROJECT:-project-88c23ce9-918a-470a-b33}/locations/${BURROW_KMS_LOCATION:-global}/keyRings/${BURROW_KMS_KEYRING:-burrow-identity}}}"
|
||||||
|
token_label="${BURROW_GCP_KMS_PKCS11_TOKEN_LABEL:-${BURROW_PKCS11_TOKEN_LABEL:-burrow-release}}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: Scripts/release/google-kms-pkcs11-materialize.sh [--emit-env]
|
||||||
|
|
||||||
|
Materializes optional Google KMS PKCS#11 config YAML into a runner-local file
|
||||||
|
and emits environment variables consumed by the Apple release signer.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--emit-env)
|
||||||
|
emit_env=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "error: unknown argument $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
write_config_file() {
|
||||||
|
local path="$1"
|
||||||
|
local value="$2"
|
||||||
|
mkdir -p "$(dirname "$path")"
|
||||||
|
printf '%s\n' "$value" >"$path"
|
||||||
|
chmod 600 "$path"
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_config_path() {
|
||||||
|
local path="$1"
|
||||||
|
if [[ -z "$path" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ "$path" != /* ]]; then
|
||||||
|
path="$ROOT/$path"
|
||||||
|
fi
|
||||||
|
printf '%s\n' "$path"
|
||||||
|
}
|
||||||
|
|
||||||
|
env_lines=()
|
||||||
|
if [[ -z "$config_file" && -n "$config_yaml" ]]; then
|
||||||
|
config_file="$runtime_dir/pkcs11-config.yaml"
|
||||||
|
write_config_file "$config_file" "$config_yaml"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$config_file" && -n "$key_ring" ]]; then
|
||||||
|
config_file="$runtime_dir/pkcs11-config.yaml"
|
||||||
|
write_config_file "$config_file" "---
|
||||||
|
tokens:
|
||||||
|
- key_ring: \"${key_ring}\"
|
||||||
|
label: \"${token_label}\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$config_file" ]]; then
|
||||||
|
config_file="$(resolve_config_path "$config_file")"
|
||||||
|
if [[ ! -s "$config_file" ]]; then
|
||||||
|
echo "error: Google KMS PKCS#11 config is missing or empty: $config_file" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
env_lines+=("BURROW_GCP_KMS_PKCS11_CONFIG=$config_file")
|
||||||
|
env_lines+=("KMS_PKCS11_CONFIG=$config_file")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${#env_lines[@]}" -gt 0 && -n "${GITHUB_ENV:-}" ]]; then
|
||||||
|
printf '%s\n' "${env_lines[@]}" >>"$GITHUB_ENV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$emit_env" == "true" && "${#env_lines[@]}" -gt 0 ]]; then
|
||||||
|
printf 'export %q\n' "${env_lines[@]}"
|
||||||
|
fi
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
bundle_id="${BURROW_UI_TEST_APP_BUNDLE_ID:-com.hackclub.burrow}"
|
bundle_id="${BURROW_UI_TEST_APP_BUNDLE_ID:-net.burrow.app}"
|
||||||
simulator_name="${BURROW_UI_TEST_SIMULATOR_NAME:-iPhone 17 Pro}"
|
simulator_name="${BURROW_UI_TEST_SIMULATOR_NAME:-iPhone 17 Pro}"
|
||||||
simulator_os="${BURROW_UI_TEST_SIMULATOR_OS:-26.4}"
|
simulator_os="${BURROW_UI_TEST_SIMULATOR_OS:-26.4}"
|
||||||
simulator_id="${BURROW_UI_TEST_SIMULATOR_ID:-}"
|
simulator_id="${BURROW_UI_TEST_SIMULATOR_ID:-}"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
bundle_id="${BURROW_UI_TEST_APP_BUNDLE_ID:-com.hackclub.burrow}"
|
bundle_id="${BURROW_UI_TEST_APP_BUNDLE_ID:-net.burrow.app}"
|
||||||
smoke_root="${BURROW_TAILNET_SMOKE_ROOT:-/tmp/burrow-tailnet-connectivity}"
|
smoke_root="${BURROW_TAILNET_SMOKE_ROOT:-/tmp/burrow-tailnet-connectivity}"
|
||||||
socket_path="${smoke_root}/burrow.sock"
|
socket_path="${smoke_root}/burrow.sock"
|
||||||
db_path="${smoke_root}/burrow.db"
|
db_path="${smoke_root}/burrow.db"
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ def kms_sign(args: argparse.Namespace, payload: Path) -> bytes:
|
||||||
if args.gcloud_project:
|
if args.gcloud_project:
|
||||||
command.extend(["--project", args.gcloud_project])
|
command.extend(["--project", args.gcloud_project])
|
||||||
run(command)
|
run(command)
|
||||||
return signature_path.read_bytes()
|
return base64.b64decode(signature_path.read_text(encoding="utf-8").strip())
|
||||||
|
|
||||||
|
|
||||||
def enclosure_path(enclosure: ET.Element, artifact_dir: Path) -> Path:
|
def enclosure_path(enclosure: ET.Element, artifact_dir: Path) -> Path:
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ The same change also makes the forge itself the release authority: release tags
|
||||||
- `//bazel/apple:release_macos_stamp`
|
- `//bazel/apple:release_macos_stamp`
|
||||||
These targets wrap the existing Xcode project and give the workflow one stable entrypoint that shares the same Bazel/Nix cache setup while Burrow decides whether deeper `rules_apple` targets are worth the migration.
|
These targets wrap the existing Xcode project and give the workflow one stable entrypoint that shares the same Bazel/Nix cache setup while Burrow decides whether deeper `rules_apple` targets are worth the migration.
|
||||||
- Use Namespace cache setup before Bazel/Nix work. Prefer the Namespace Bazel remote cache when `nsc` is authenticated and fall back to a local disk/repository cache when remote setup is unavailable.
|
- Use Namespace cache setup before Bazel/Nix work. Prefer the Namespace Bazel remote cache when `nsc` is authenticated and fall back to a local disk/repository cache when remote setup is unavailable.
|
||||||
- Produce unsigned Apple validation artifacts when signing is absent, but require signing for an App Store requested iOS release. Uploadable artifacts are produced only when signing assets are installed and provisioning profiles can be synced from App Store Connect credentials.
|
- Produce unsigned Apple validation artifacts when signing is absent, but require signing for an App Store requested iOS release and for a Sparkle tester release. Uploadable artifacts are produced only after provisioning profiles can be synced from App Store Connect credentials and the relevant Apple certificate is proven to match the selected Google KMS key.
|
||||||
- Resolve release credentials through age/agenix before signed Apple lanes run. Forgejo secrets should carry only the runner age identity fallback and the Authentik WIF client secret until OpenBao can mint the runner token directly. App Store Connect keys and signing certificates live as sealed files under `secrets/`.
|
- Resolve release credentials through age/agenix before signed Apple lanes run. Forgejo secrets should carry only the runner age identity fallback and the Authentik WIF client secret until OpenBao can mint the runner token directly. App Store Connect keys and signing certificates live as sealed files under `secrets/`.
|
||||||
- Persist the forge runner SSH key as `/var/lib/forgejo-runner-agent/age_keystore` and export `BURROW_RUNNER_AGE_IDENTITY_PATH` so jobs can resolve agenix identities without embedding long-lived material in every workflow.
|
- Persist the forge runner SSH key as `/var/lib/forgejo-runner-agent/age_keystore` and export `BURROW_RUNNER_AGE_IDENTITY_PATH` so jobs can resolve agenix identities without embedding long-lived material in every workflow.
|
||||||
- Add `Scripts/forgejo-dispatch.sh` and `Scripts/forgejo-dispatch-via-host.sh`:
|
- Add `Scripts/forgejo-dispatch.sh` and `Scripts/forgejo-dispatch-via-host.sh`:
|
||||||
|
|
@ -58,7 +58,7 @@ The same change also makes the forge itself the release authority: release tags
|
||||||
- Keep the canonical Forgejo repo as an ordinary source repository, not a mirror. The host config disables new mirror creation and removes mirror/push-mirror rows for the canonical Burrow repository at activation time.
|
- Keep the canonical Forgejo repo as an ordinary source repository, not a mirror. The host config disables new mirror creation and removes mirror/push-mirror rows for the canonical Burrow repository at activation time.
|
||||||
- Add `.forgejo/workflows/infra-opentofu.yml` and `Scripts/grafana-tofu.sh` for Grafana API-managed folders and dashboards. NixOS owns the Grafana service, SSO, reverse proxy, datasources, Prometheus, OpenTelemetry collector, Jaeger, secrets, and boot-time configuration; OpenTofu owns API-created dashboard objects.
|
- Add `.forgejo/workflows/infra-opentofu.yml` and `Scripts/grafana-tofu.sh` for Grafana API-managed folders and dashboards. NixOS owns the Grafana service, SSO, reverse proxy, datasources, Prometheus, OpenTelemetry collector, Jaeger, secrets, and boot-time configuration; OpenTofu owns API-created dashboard objects.
|
||||||
- Sync provisioning profiles from App Store Connect in CI using the decrypted API key:
|
- Sync provisioning profiles from App Store Connect in CI using the decrypted API key:
|
||||||
- iOS App Store profiles for `com.hackclub.burrow` and `com.hackclub.burrow.network`.
|
- iOS App Store profiles for `net.burrow.app` and `net.burrow.app.network`.
|
||||||
- macOS Developer ID profiles for the same bundle identifiers.
|
- macOS Developer ID profiles for the same bundle identifiers.
|
||||||
Provisioning profile `.age` files are not part of the normal release path.
|
Provisioning profile `.age` files are not part of the normal release path.
|
||||||
- Add a non-exportable Google KMS RSA-2048/SHA-256 key named
|
- Add a non-exportable Google KMS RSA-2048/SHA-256 key named
|
||||||
|
|
@ -85,6 +85,16 @@ The same change also makes the forge itself the release authority: release tags
|
||||||
sync must verify App Store profiles reference that certificate. Stale profiles
|
sync must verify App Store profiles reference that certificate. Stale profiles
|
||||||
with the same name may be deleted and recreated so CI does not silently sign
|
with the same name may be deleted and recreated so CI does not silently sign
|
||||||
against an older distribution identity.
|
against an older distribution identity.
|
||||||
|
- Apple release signing uses a staged KMS flow because Google publishes the
|
||||||
|
Cloud KMS PKCS#11 library for Linux release runners, not macOS runners:
|
||||||
|
- macOS Namespace builders compile unsigned iOS device and macOS app bundles
|
||||||
|
with the existing Xcode project.
|
||||||
|
- Linux release runners download those staged artifacts, authenticate to
|
||||||
|
Google through Authentik WIF, verify the Apple `.cer` public key matches the
|
||||||
|
intended Google KMS key version, embed the synced provisioning profiles, and
|
||||||
|
sign the app/extension bundles with the patched `rcodesign` PKCS#11 path.
|
||||||
|
- The signed iOS IPA is uploaded to App Store Connect, and the signed macOS
|
||||||
|
ZIP is used as the Sparkle tester artifact.
|
||||||
- Add a non-exportable Google KMS key named `sparkle-ed25519` for Sparkle
|
- Add a non-exportable Google KMS key named `sparkle-ed25519` for Sparkle
|
||||||
appcast signatures. It uses `EC_SIGN_ED25519` with software protection level
|
appcast signatures. It uses `EC_SIGN_ED25519` with software protection level
|
||||||
because Google KMS rejects Ed25519 for HSM protection. macOS builds embed
|
because Google KMS rejects Ed25519 for HSM protection. macOS builds embed
|
||||||
|
|
@ -97,7 +107,7 @@ The same change also makes the forge itself the release authority: release tags
|
||||||
- Treat Sparkle as a macOS public-channel upload lane:
|
- Treat Sparkle as a macOS public-channel upload lane:
|
||||||
- Generate staged appcast content from the signed macOS artifact when present.
|
- Generate staged appcast content from the signed macOS artifact when present.
|
||||||
- Publish `sparkle/<channel>/`, `sparkle/appcast.xml`, and `sparkle/default/` pointers from `publish-store-uploads`.
|
- Publish `sparkle/<channel>/`, `sparkle/appcast.xml`, and `sparkle/default/` pointers from `publish-store-uploads`.
|
||||||
- Full Sparkle client integration remains a follow-up because the current Apple app does not embed Sparkle.
|
- The macOS app embeds Sparkle and consumes the `https://releases.burrow.net/sparkle/appcast.xml` feed.
|
||||||
- Add `infra/releases` as the OpenTofu boundary for the GCS release and package repository backup buckets. Garage is the required S3-compatible primary distribution target after host bootstrap. `infra/identity` remains the boundary for Google KMS signing keys, the Authentik WIF pool/provider, and the runner service account.
|
- Add `infra/releases` as the OpenTofu boundary for the GCS release and package repository backup buckets. Garage is the required S3-compatible primary distribution target after host bootstrap. `infra/identity` remains the boundary for Google KMS signing keys, the Authentik WIF pool/provider, and the runner service account.
|
||||||
|
|
||||||
## Security and Operational Considerations
|
## Security and Operational Considerations
|
||||||
|
|
@ -110,8 +120,13 @@ The same change also makes the forge itself the release authority: release tags
|
||||||
- rclone is not part of the release path. Multi-cloud failover should be added through provider-specific or S3-compatible wrappers with explicit credentials and evidence.
|
- rclone is not part of the release path. Multi-cloud failover should be added through provider-specific or S3-compatible wrappers with explicit credentials and evidence.
|
||||||
- Required upload credentials are checked explicitly after `./.forgejo/actions/decrypt-release-secrets` materializes them from age:
|
- Required upload credentials are checked explicitly after `./.forgejo/actions/decrypt-release-secrets` materializes them from age:
|
||||||
- `secrets/apple/appstore-connect.env.age`: `ASC_API_KEY_ID`, `ASC_API_ISSUER_ID`, and base64-encoded `.p8` key material.
|
- `secrets/apple/appstore-connect.env.age`: `ASC_API_KEY_ID`, `ASC_API_ISSUER_ID`, and base64-encoded `.p8` key material.
|
||||||
- `secrets/apple/distribution-signing.env.age`: base64-encoded distribution `.p12` plus its password.
|
- KMS-backed Apple signing must not use `.p12` private-key material. The
|
||||||
- `secrets/apple/sparkle.env.age`: base64-encoded Sparkle EdDSA private key.
|
legacy `secrets/apple/distribution-signing.env.age` path remains accepted
|
||||||
|
only for emergency keychain signing and should not be used for the normal
|
||||||
|
tester release lane.
|
||||||
|
- Sparkle appcasts are signed with Google KMS by default; the legacy
|
||||||
|
`secrets/apple/sparkle.env.age` path remains accepted only for emergency
|
||||||
|
local EdDSA signing.
|
||||||
- future Microsoft Partner Center credentials
|
- future Microsoft Partner Center credentials
|
||||||
- The temporary CI keychain password is always the distribution certificate password; there is no separate keychain-password release secret.
|
- The temporary CI keychain password is always the distribution certificate password; there is no separate keychain-password release secret.
|
||||||
- KMS-backed Apple certificates do not satisfy the existing Xcode/keychain
|
- KMS-backed Apple certificates do not satisfy the existing Xcode/keychain
|
||||||
|
|
|
||||||
69
flake.nix
69
flake.nix
|
|
@ -51,6 +51,64 @@
|
||||||
else
|
else
|
||||||
pkgs.bazel-buildtools;
|
pkgs.bazel-buildtools;
|
||||||
optionalPackage = name: lib.optional (lib.hasAttr name pkgs) (lib.getAttr name pkgs);
|
optionalPackage = name: lib.optional (lib.hasAttr name pkgs) (lib.getAttr name pkgs);
|
||||||
|
googleKmsPkcs11Package =
|
||||||
|
if pkgs.stdenv.hostPlatform.isx86_64 && pkgs.stdenv.isLinux then
|
||||||
|
let
|
||||||
|
version = "1.9";
|
||||||
|
in
|
||||||
|
pkgs.stdenv.mkDerivation {
|
||||||
|
pname = "google-kms-pkcs11";
|
||||||
|
inherit version;
|
||||||
|
src = pkgs.fetchurl {
|
||||||
|
url = "https://github.com/GoogleCloudPlatform/kms-integrations/releases/download/pkcs11-v${version}/libkmsp11-${version}-linux-amd64.tar.gz";
|
||||||
|
sha256 = "sha256-U5f5ZQc81dHixD5RSPEC8xp8g1kC+X60HHycDxuHakA=";
|
||||||
|
};
|
||||||
|
nativeBuildInputs = [ pkgs.autoPatchelfHook ];
|
||||||
|
buildInputs = [ pkgs.stdenv.cc.cc.lib ];
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
install -Dm755 libkmsp11.so "$out/lib/libkmsp11.so"
|
||||||
|
install -Dm644 kmsp11.h "$out/include/kmsp11.h"
|
||||||
|
install -Dm644 LICENSE "$out/share/licenses/google-kms-pkcs11/LICENSE"
|
||||||
|
mkdir -p "$out/bin"
|
||||||
|
cat > "$out/bin/google-kms-pkcs11-module" <<EOF
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
printf '%s\n' "$out/lib/libkmsp11.so"
|
||||||
|
EOF
|
||||||
|
chmod 755 "$out/bin/google-kms-pkcs11-module"
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
meta = with lib; {
|
||||||
|
description = "Google Cloud KMS PKCS#11 library";
|
||||||
|
homepage = "https://github.com/GoogleCloudPlatform/kms-integrations";
|
||||||
|
license = licenses.asl20;
|
||||||
|
platforms = [ "x86_64-linux" ];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
null;
|
||||||
|
rcodesignPkcs11Package =
|
||||||
|
if pkgs ? rcodesign then
|
||||||
|
pkgs.rcodesign.overrideAttrs (old: {
|
||||||
|
pname = "rcodesign-pkcs11";
|
||||||
|
patches = (old.patches or [ ]) ++ [
|
||||||
|
./patches/rcodesign-pkcs11-tool.patch
|
||||||
|
];
|
||||||
|
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ pkgs.makeWrapper ];
|
||||||
|
postInstall = (old.postInstall or "") + ''
|
||||||
|
wrapProgram "$out/bin/rcodesign" \
|
||||||
|
--prefix PATH : ${lib.makeBinPath [ pkgs.opensc pkgs.openssl ]} \
|
||||||
|
--set-default BURROW_OPENSSL_MODULES ${pkgs.pkcs11-provider}/lib/ossl-modules
|
||||||
|
'';
|
||||||
|
passthru = (old.passthru or { }) // {
|
||||||
|
isBurrowPkcs11Fork = true;
|
||||||
|
};
|
||||||
|
meta = (old.meta or { }) // {
|
||||||
|
description = "rcodesign with Burrow PKCS#11 signing support";
|
||||||
|
};
|
||||||
|
})
|
||||||
|
else
|
||||||
|
null;
|
||||||
commonPackages = with pkgs; [
|
commonPackages = with pkgs; [
|
||||||
cargo
|
cargo
|
||||||
rustc
|
rustc
|
||||||
|
|
@ -69,12 +127,17 @@
|
||||||
jq
|
jq
|
||||||
nodejs_20
|
nodejs_20
|
||||||
python3
|
python3
|
||||||
|
openssl
|
||||||
rsync
|
rsync
|
||||||
zip
|
zip
|
||||||
unzip
|
unzip
|
||||||
] ++ optionalPackage "google-cloud-sdk"
|
] ++ optionalPackage "google-cloud-sdk"
|
||||||
++ optionalPackage "awscli2"
|
++ optionalPackage "awscli2"
|
||||||
++ optionalPackage "attic-client";
|
++ optionalPackage "attic-client"
|
||||||
|
++ optionalPackage "opensc"
|
||||||
|
++ optionalPackage "pkcs11-provider"
|
||||||
|
++ lib.optional (googleKmsPkcs11Package != null) googleKmsPkcs11Package
|
||||||
|
++ lib.optional (rcodesignPkcs11Package != null) rcodesignPkcs11Package;
|
||||||
linuxRepositoryPackages = lib.optionals pkgs.stdenv.isLinux (
|
linuxRepositoryPackages = lib.optionals pkgs.stdenv.isLinux (
|
||||||
(with pkgs; [
|
(with pkgs; [
|
||||||
dpkg
|
dpkg
|
||||||
|
|
@ -273,7 +336,9 @@
|
||||||
forgejo-nsc-dispatcher = forgejoNscDispatcher;
|
forgejo-nsc-dispatcher = forgejoNscDispatcher;
|
||||||
forgejo-nsc-autoscaler = forgejoNscAutoscaler;
|
forgejo-nsc-autoscaler = forgejoNscAutoscaler;
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs (nscPkg != null) { nsc = nscPkg; };
|
// lib.optionalAttrs (nscPkg != null) { nsc = nscPkg; }
|
||||||
|
// lib.optionalAttrs (googleKmsPkcs11Package != null) { google-kms-pkcs11 = googleKmsPkcs11Package; }
|
||||||
|
// lib.optionalAttrs (rcodesignPkcs11Package != null) { rcodesign-pkcs11 = rcodesignPkcs11Package; };
|
||||||
}))
|
}))
|
||||||
// {
|
// {
|
||||||
nixosModules.burrow-forge = import ./nixos/modules/burrow-forge.nix;
|
nixosModules.burrow-forge = import ./nixos/modules/burrow-forge.nix;
|
||||||
|
|
|
||||||
522
patches/rcodesign-pkcs11-tool.patch
Normal file
522
patches/rcodesign-pkcs11-tool.patch
Normal file
|
|
@ -0,0 +1,522 @@
|
||||||
|
--- a/apple-codesign/src/cli/certificate_source.rs
|
||||||
|
+++ b/apple-codesign/src/cli/certificate_source.rs
|
||||||
|
@@ -16,0 +17 @@
|
||||||
|
+ bytes::Bytes,
|
||||||
|
@@ -19,0 +21 @@
|
||||||
|
+ signature::Signer,
|
||||||
|
@@ -21,2 +23,10 @@
|
||||||
|
- std::path::PathBuf,
|
||||||
|
- x509_certificate::CapturedX509Certificate,
|
||||||
|
+ std::{
|
||||||
|
+ path::PathBuf,
|
||||||
|
+ process::Command,
|
||||||
|
+ sync::atomic::{AtomicU64, Ordering},
|
||||||
|
+ },
|
||||||
|
+ x509_certificate::{
|
||||||
|
+ CapturedX509Certificate, KeyAlgorithm, Sign, Signature, SignatureAlgorithm,
|
||||||
|
+ X509CertificateError,
|
||||||
|
+ },
|
||||||
|
+ zeroize::Zeroizing,
|
||||||
|
@@ -36,0 +47,2 @@
|
||||||
|
+static PKCS11_SIGN_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||||
|
+
|
||||||
|
@@ -204,0 +214,262 @@
|
||||||
|
+#[derive(Clone, Debug)]
|
||||||
|
+struct Pkcs11PrivateKey {
|
||||||
|
+ tool: String,
|
||||||
|
+ module: PathBuf,
|
||||||
|
+ slot_id: Option<String>,
|
||||||
|
+ token_label: Option<String>,
|
||||||
|
+ key_id: Option<String>,
|
||||||
|
+ key_label: Option<String>,
|
||||||
|
+ pin: String,
|
||||||
|
+ cert: CapturedX509Certificate,
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+impl signature::Signer<Signature> for Pkcs11PrivateKey {
|
||||||
|
+ fn try_sign(&self, message: &[u8]) -> Result<Signature, signature::Error> {
|
||||||
|
+ match self.cert.key_algorithm() {
|
||||||
|
+ Some(KeyAlgorithm::Rsa) => {}
|
||||||
|
+ Some(algorithm) => {
|
||||||
|
+ return Err(signature::Error::from_source(format!(
|
||||||
|
+ "PKCS#11 signing currently supports RSA certificates, not {algorithm:?}"
|
||||||
|
+ )));
|
||||||
|
+ }
|
||||||
|
+ None => {
|
||||||
|
+ return Err(signature::Error::from_source(
|
||||||
|
+ "failed to resolve PKCS#11 certificate key algorithm",
|
||||||
|
+ ));
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ let sequence = PKCS11_SIGN_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||||
|
+ let base = std::env::temp_dir().join(format!(
|
||||||
|
+ "rcodesign-pkcs11-{}-{sequence}",
|
||||||
|
+ std::process::id()
|
||||||
|
+ ));
|
||||||
|
+ let input_path = base.with_extension("in");
|
||||||
|
+ let output_path = base.with_extension("sig");
|
||||||
|
+ let openssl_config_path = base.with_extension("openssl.cnf");
|
||||||
|
+ let provider_debug_path = base.with_extension("pkcs11-provider.log");
|
||||||
|
+
|
||||||
|
+ std::fs::write(&input_path, message).map_err(signature::Error::from_source)?;
|
||||||
|
+
|
||||||
|
+ let provider_override =
|
||||||
|
+ std::env::var("BURROW_APPLE_PKCS11_SIGN_WITH_OPENSSL_PROVIDER")
|
||||||
|
+ .or_else(|_| std::env::var("BURROW_PKCS11_SIGN_WITH_OPENSSL_PROVIDER"))
|
||||||
|
+ .ok()
|
||||||
|
+ .and_then(|value| {
|
||||||
|
+ if value.trim().is_empty() {
|
||||||
|
+ None
|
||||||
|
+ } else {
|
||||||
|
+ Some(matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES" | "on" | "ON"))
|
||||||
|
+ }
|
||||||
|
+ });
|
||||||
|
+ let use_openssl_provider = provider_override.unwrap_or_else(|| {
|
||||||
|
+ std::env::var("BURROW_APPLE_PKCS11_BACKEND")
|
||||||
|
+ .or_else(|_| std::env::var("BURROW_PKCS11_BACKEND"))
|
||||||
|
+ .map(|value| value == "gcp-kms")
|
||||||
|
+ .unwrap_or(false)
|
||||||
|
+ || self.module.to_string_lossy().contains("kmsp11")
|
||||||
|
+ || std::env::var("BURROW_GCP_KMS_PKCS11_CONFIG").is_ok()
|
||||||
|
+ || std::env::var("KMS_PKCS11_CONFIG").is_ok()
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
+ if use_openssl_provider {
|
||||||
|
+ let token_label = self.token_label.as_ref().ok_or_else(|| {
|
||||||
|
+ signature::Error::from_source(
|
||||||
|
+ "OpenSSL PKCS#11 provider signing requires --pkcs11-token-label",
|
||||||
|
+ )
|
||||||
|
+ })?;
|
||||||
|
+ let mut key_uri = format!("pkcs11:token={token_label}");
|
||||||
|
+ if let Some(label) = &self.key_label {
|
||||||
|
+ key_uri.push_str(";object=");
|
||||||
|
+ key_uri.push_str(label);
|
||||||
|
+ } else if let Some(id) = &self.key_id {
|
||||||
|
+ key_uri.push_str(";id=");
|
||||||
|
+ key_uri.push_str(id);
|
||||||
|
+ } else {
|
||||||
|
+ return Err(signature::Error::from_source(
|
||||||
|
+ "OpenSSL PKCS#11 provider signing requires --pkcs11-key-label or --pkcs11-key-id",
|
||||||
|
+ ));
|
||||||
|
+ }
|
||||||
|
+ key_uri.push_str(";type=private");
|
||||||
|
+
|
||||||
|
+ let pin_path = base.with_extension("pin");
|
||||||
|
+ std::fs::write(&pin_path, &self.pin).map_err(signature::Error::from_source)?;
|
||||||
|
+
|
||||||
|
+ let module_path = self.module.to_string_lossy();
|
||||||
|
+ let kms_config = std::env::var("KMS_PKCS11_CONFIG")
|
||||||
|
+ .or_else(|_| std::env::var("BURROW_GCP_KMS_PKCS11_CONFIG"))
|
||||||
|
+ .ok();
|
||||||
|
+ let needs_kms_config = module_path.contains("kmsp11") || kms_config.is_some();
|
||||||
|
+ if needs_kms_config {
|
||||||
|
+ let config_path = kms_config.as_ref().ok_or_else(|| {
|
||||||
|
+ signature::Error::from_source(
|
||||||
|
+ "OpenSSL PKCS#11 provider signing with Google KMS requires KMS_PKCS11_CONFIG",
|
||||||
|
+ )
|
||||||
|
+ })?;
|
||||||
|
+ let metadata = std::fs::metadata(config_path).map_err(|err| {
|
||||||
|
+ signature::Error::from_source(format!(
|
||||||
|
+ "OpenSSL PKCS#11 provider signing could not read KMS_PKCS11_CONFIG at {config_path}: {err}"
|
||||||
|
+ ))
|
||||||
|
+ })?;
|
||||||
|
+ if !metadata.is_file() || metadata.len() == 0 {
|
||||||
|
+ return Err(signature::Error::from_source(format!(
|
||||||
|
+ "OpenSSL PKCS#11 provider signing requires a non-empty KMS_PKCS11_CONFIG file at {config_path}"
|
||||||
|
+ )));
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ let openssl_modules = std::env::var("BURROW_OPENSSL_MODULES")
|
||||||
|
+ .or_else(|_| std::env::var("OPENSSL_MODULES"))
|
||||||
|
+ .ok()
|
||||||
|
+ .filter(|value| !value.trim().is_empty());
|
||||||
|
+ if let Some(modules) = &openssl_modules {
|
||||||
|
+ let provider_extension = if cfg!(target_os = "macos") {
|
||||||
|
+ "dylib"
|
||||||
|
+ } else {
|
||||||
|
+ "so"
|
||||||
|
+ };
|
||||||
|
+ let provider_module = PathBuf::from(modules)
|
||||||
|
+ .join(format!("pkcs11.{provider_extension}"));
|
||||||
|
+ if provider_module.is_file() {
|
||||||
|
+ let config = format!(
|
||||||
|
+ "HOME = .\nopenssl_conf = openssl_init\n\n[openssl_init]\nproviders = provider_sect\n\n[provider_sect]\ndefault = default_sect\npkcs11 = pkcs11_sect\n\n[default_sect]\nactivate = 1\n\n[pkcs11_sect]\nmodule = {}\npkcs11-module-path = {}\npkcs11-module-token-pin = file:{}\npkcs11-module-cache-pins = cache\npkcs11-module-quirks = no-operation-state no-allowed-mechanisms\nactivate = 1\n",
|
||||||
|
+ provider_module.display(),
|
||||||
|
+ self.module.display(),
|
||||||
|
+ pin_path.display(),
|
||||||
|
+ );
|
||||||
|
+ std::fs::write(&openssl_config_path, config)
|
||||||
|
+ .map_err(signature::Error::from_source)?;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ let openssl = std::env::var("BURROW_OPENSSL").unwrap_or_else(|_| "openssl".into());
|
||||||
|
+ let mut command = Command::new(openssl);
|
||||||
|
+ command
|
||||||
|
+ .env("PKCS11_PROVIDER_MODULE", &self.module)
|
||||||
|
+ .arg("dgst")
|
||||||
|
+ .arg("-provider")
|
||||||
|
+ .arg("pkcs11")
|
||||||
|
+ .arg("-provider")
|
||||||
|
+ .arg("default")
|
||||||
|
+ .arg("-sha256")
|
||||||
|
+ .arg("-sign")
|
||||||
|
+ .arg(&key_uri)
|
||||||
|
+ .arg("-passin")
|
||||||
|
+ .arg(format!("file:{}", pin_path.display()))
|
||||||
|
+ .arg("-out")
|
||||||
|
+ .arg(&output_path)
|
||||||
|
+ .arg(&input_path);
|
||||||
|
+ if let Some(config_path) = kms_config {
|
||||||
|
+ command
|
||||||
|
+ .env("KMS_PKCS11_CONFIG", &config_path)
|
||||||
|
+ .env("BURROW_GCP_KMS_PKCS11_CONFIG", &config_path);
|
||||||
|
+ }
|
||||||
|
+ if let Ok(credentials_path) = std::env::var("GOOGLE_APPLICATION_CREDENTIALS") {
|
||||||
|
+ command.env("GOOGLE_APPLICATION_CREDENTIALS", credentials_path);
|
||||||
|
+ }
|
||||||
|
+ if let Some(modules) = openssl_modules {
|
||||||
|
+ command.env("OPENSSL_MODULES", modules);
|
||||||
|
+ }
|
||||||
|
+ if openssl_config_path.is_file() {
|
||||||
|
+ command.env("OPENSSL_CONF", &openssl_config_path);
|
||||||
|
+ }
|
||||||
|
+ if let Ok(debug) = std::env::var("BURROW_PKCS11_PROVIDER_DEBUG")
|
||||||
|
+ .or_else(|_| std::env::var("PKCS11_PROVIDER_DEBUG"))
|
||||||
|
+ {
|
||||||
|
+ command.env("PKCS11_PROVIDER_DEBUG", debug);
|
||||||
|
+ } else {
|
||||||
|
+ command.env(
|
||||||
|
+ "PKCS11_PROVIDER_DEBUG",
|
||||||
|
+ format!("file:{},level:1", provider_debug_path.display()),
|
||||||
|
+ );
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ let output = command.output().map_err(signature::Error::from_source)?;
|
||||||
|
+ let _ = std::fs::remove_file(&input_path);
|
||||||
|
+ let _ = std::fs::remove_file(&pin_path);
|
||||||
|
+ let _ = std::fs::remove_file(&openssl_config_path);
|
||||||
|
+
|
||||||
|
+ if !output.status.success() {
|
||||||
|
+ let _ = std::fs::remove_file(&output_path);
|
||||||
|
+ let provider_debug = std::fs::read_to_string(&provider_debug_path)
|
||||||
|
+ .ok()
|
||||||
|
+ .filter(|value| !value.trim().is_empty())
|
||||||
|
+ .map(|value| {
|
||||||
|
+ let tail = value
|
||||||
|
+ .lines()
|
||||||
|
+ .rev()
|
||||||
|
+ .take(80)
|
||||||
|
+ .collect::<Vec<_>>()
|
||||||
|
+ .into_iter()
|
||||||
|
+ .rev()
|
||||||
|
+ .collect::<Vec<_>>()
|
||||||
|
+ .join("\n");
|
||||||
|
+ format!("\nPKCS#11 provider debug tail:\n{tail}\n")
|
||||||
|
+ })
|
||||||
|
+ .unwrap_or_default();
|
||||||
|
+ let _ = std::fs::remove_file(&provider_debug_path);
|
||||||
|
+ return Err(signature::Error::from_source(format!(
|
||||||
|
+ "OpenSSL PKCS#11 provider signing failed with status {}: {}{}{}",
|
||||||
|
+ output.status,
|
||||||
|
+ String::from_utf8_lossy(&output.stdout),
|
||||||
|
+ String::from_utf8_lossy(&output.stderr),
|
||||||
|
+ provider_debug
|
||||||
|
+ )));
|
||||||
|
+ }
|
||||||
|
+ let _ = std::fs::remove_file(&provider_debug_path);
|
||||||
|
+ } else {
|
||||||
|
+ let mut command = Command::new(&self.tool);
|
||||||
|
+ command
|
||||||
|
+ .arg("--module")
|
||||||
|
+ .arg(&self.module)
|
||||||
|
+ .arg("--login")
|
||||||
|
+ .arg("--pin")
|
||||||
|
+ .arg(&self.pin)
|
||||||
|
+ .arg("--sign")
|
||||||
|
+ .arg("--mechanism")
|
||||||
|
+ .arg("SHA256-RSA-PKCS")
|
||||||
|
+ .arg("--input-file")
|
||||||
|
+ .arg(&input_path)
|
||||||
|
+ .arg("--output-file")
|
||||||
|
+ .arg(&output_path);
|
||||||
|
+
|
||||||
|
+ if let Some(slot_id) = &self.slot_id {
|
||||||
|
+ command.arg("--slot").arg(slot_id);
|
||||||
|
+ }
|
||||||
|
+ if let Some(token_label) = &self.token_label {
|
||||||
|
+ command.arg("--token-label").arg(token_label);
|
||||||
|
+ }
|
||||||
|
+ if let Some(id) = &self.key_id {
|
||||||
|
+ command.arg("--id").arg(id);
|
||||||
|
+ }
|
||||||
|
+ if let Some(label) = &self.key_label {
|
||||||
|
+ command.arg("--label").arg(label);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ let output = command.output().map_err(signature::Error::from_source)?;
|
||||||
|
+
|
||||||
|
+ let _ = std::fs::remove_file(&input_path);
|
||||||
|
+
|
||||||
|
+ if !output.status.success() {
|
||||||
|
+ let _ = std::fs::remove_file(&output_path);
|
||||||
|
+ return Err(signature::Error::from_source(format!(
|
||||||
|
+ "pkcs11-tool signing failed with status {}: {}{}",
|
||||||
|
+ output.status,
|
||||||
|
+ String::from_utf8_lossy(&output.stdout),
|
||||||
|
+ String::from_utf8_lossy(&output.stderr)
|
||||||
|
+ )));
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ let signature = std::fs::read(&output_path).map_err(signature::Error::from_source)?;
|
||||||
|
+ let _ = std::fs::remove_file(&output_path);
|
||||||
|
+
|
||||||
|
+ if signature.is_empty() {
|
||||||
|
+ return Err(signature::Error::from_source(
|
||||||
|
+ "pkcs11-tool produced an empty signature",
|
||||||
|
+ ));
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ Ok(signature.into())
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
@@ -204,0 +384,214 @@
|
||||||
|
+impl Sign for Pkcs11PrivateKey {
|
||||||
|
+ fn sign(&self, message: &[u8]) -> Result<(Vec<u8>, SignatureAlgorithm), X509CertificateError> {
|
||||||
|
+ let algorithm = self.signature_algorithm()?;
|
||||||
|
+
|
||||||
|
+ Ok((self.try_sign(message)?.into(), algorithm))
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ fn key_algorithm(&self) -> Option<KeyAlgorithm> {
|
||||||
|
+ self.cert.key_algorithm()
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ fn public_key_data(&self) -> Bytes {
|
||||||
|
+ self.cert.public_key_data()
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ fn signature_algorithm(&self) -> Result<SignatureAlgorithm, X509CertificateError> {
|
||||||
|
+ match self.cert.key_algorithm() {
|
||||||
|
+ Some(KeyAlgorithm::Rsa) => Ok(SignatureAlgorithm::RsaSha256),
|
||||||
|
+ Some(algorithm) => Err(X509CertificateError::UnknownSignatureAlgorithm(format!(
|
||||||
|
+ "PKCS#11 signing currently supports RSA certificates, not {algorithm:?}"
|
||||||
|
+ ))),
|
||||||
|
+ None => Err(X509CertificateError::UnknownSignatureAlgorithm(
|
||||||
|
+ "failed to resolve PKCS#11 certificate key algorithm".into(),
|
||||||
|
+ )),
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ fn private_key_data(&self) -> Option<Zeroizing<Vec<u8>>> {
|
||||||
|
+ None
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ fn rsa_primes(
|
||||||
|
+ &self,
|
||||||
|
+ ) -> Result<Option<(Zeroizing<Vec<u8>>, Zeroizing<Vec<u8>>)>, X509CertificateError> {
|
||||||
|
+ Ok(None)
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+impl x509_certificate::KeyInfoSigner for Pkcs11PrivateKey {}
|
||||||
|
+
|
||||||
|
+impl PrivateKey for Pkcs11PrivateKey {
|
||||||
|
+ fn as_key_info_signer(&self) -> &dyn x509_certificate::KeyInfoSigner {
|
||||||
|
+ self
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ fn to_public_key_peer_decrypt(
|
||||||
|
+ &self,
|
||||||
|
+ ) -> Result<
|
||||||
|
+ Box<dyn crate::remote_signing::session_negotiation::PublicKeyPeerDecrypt>,
|
||||||
|
+ AppleCodesignError,
|
||||||
|
+ > {
|
||||||
|
+ Err(AppleCodesignError::CliGeneralError(
|
||||||
|
+ "PKCS#11 keys cannot decrypt remote-signing peer setup data".into(),
|
||||||
|
+ ))
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ fn finish(&self) -> Result<(), AppleCodesignError> {
|
||||||
|
+ Ok(())
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+#[derive(Args, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
+#[serde(deny_unknown_fields)]
|
||||||
|
+pub struct Pkcs11SigningKey {
|
||||||
|
+ /// Path to pkcs11-tool
|
||||||
|
+ #[arg(id = "pkcs11_tool", long = "pkcs11-tool", default_value = "pkcs11-tool", value_name = "PATH")]
|
||||||
|
+ pub tool: String,
|
||||||
|
+
|
||||||
|
+ /// Path to the PKCS#11 module to use for signing
|
||||||
|
+ #[arg(
|
||||||
|
+ long = "pkcs11-library",
|
||||||
|
+ alias = "pkcs11-module",
|
||||||
|
+ id = "pkcs11_library",
|
||||||
|
+ value_name = "PATH"
|
||||||
|
+ )]
|
||||||
|
+ pub module: Option<PathBuf>,
|
||||||
|
+
|
||||||
|
+ /// PKCS#11 slot id containing the signing key
|
||||||
|
+ #[arg(id = "pkcs11_slot_id", long = "pkcs11-slot-id", value_name = "SLOT")]
|
||||||
|
+ pub slot_id: Option<String>,
|
||||||
|
+
|
||||||
|
+ /// PKCS#11 token label containing the signing key
|
||||||
|
+ #[arg(id = "pkcs11_token_label", long = "pkcs11-token-label", value_name = "LABEL")]
|
||||||
|
+ pub token_label: Option<String>,
|
||||||
|
+
|
||||||
|
+ /// PKCS#11 private-key object id, usually hex
|
||||||
|
+ #[arg(id = "pkcs11_key_id", long = "pkcs11-key-id", value_name = "ID")]
|
||||||
|
+ pub key_id: Option<String>,
|
||||||
|
+
|
||||||
|
+ /// PKCS#11 private-key object label
|
||||||
|
+ #[arg(id = "pkcs11_key_label", long = "pkcs11-key-label", value_name = "LABEL")]
|
||||||
|
+ pub key_label: Option<String>,
|
||||||
|
+
|
||||||
|
+ /// PIN used to unlock the PKCS#11 token
|
||||||
|
+ #[arg(id = "pkcs11_pin", long = "pkcs11-pin", group = "pkcs11-pin-source", value_name = "PIN")]
|
||||||
|
+ pub pin: Option<String>,
|
||||||
|
+
|
||||||
|
+ /// File containing the PIN used to unlock the PKCS#11 token
|
||||||
|
+ #[arg(id = "pkcs11_pin_file", long = "pkcs11-pin-file", group = "pkcs11-pin-source", value_name = "PATH")]
|
||||||
|
+ pub pin_path: Option<PathBuf>,
|
||||||
|
+
|
||||||
|
+ /// Environment variable holding the PIN used to unlock the PKCS#11 token
|
||||||
|
+ #[arg(id = "pkcs11_pin_env", long = "pkcs11-pin-env", group = "pkcs11-pin-source", value_name = "ENV")]
|
||||||
|
+ #[serde(skip)]
|
||||||
|
+ pub pin_env: Option<String>,
|
||||||
|
+
|
||||||
|
+ /// DER certificate file corresponding to the PKCS#11 private key
|
||||||
|
+ #[arg(
|
||||||
|
+ long = "pkcs11-certificate-file",
|
||||||
|
+ alias = "pkcs11-certificate-der-file",
|
||||||
|
+ id = "pkcs11_certificate_file",
|
||||||
|
+ value_name = "PATH"
|
||||||
|
+ )]
|
||||||
|
+ pub certificate_path: Option<PathBuf>,
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+impl Pkcs11SigningKey {
|
||||||
|
+ pub(crate) fn configured(&self) -> bool {
|
||||||
|
+ self.module.is_some()
|
||||||
|
+ || self.slot_id.is_some()
|
||||||
|
+ || self.token_label.is_some()
|
||||||
|
+ || self.key_id.is_some()
|
||||||
|
+ || self.key_label.is_some()
|
||||||
|
+ || self.pin.is_some()
|
||||||
|
+ || self.pin_path.is_some()
|
||||||
|
+ || self.pin_env.is_some()
|
||||||
|
+ || self.certificate_path.is_some()
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ fn not_configured(&self) -> bool {
|
||||||
|
+ !self.configured()
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ fn resolve_pin(&self) -> Result<String, AppleCodesignError> {
|
||||||
|
+ if let Some(pin) = &self.pin {
|
||||||
|
+ Ok(pin.clone())
|
||||||
|
+ } else if let Some(path) = &self.pin_path {
|
||||||
|
+ Ok(std::fs::read_to_string(path)?.trim().to_string())
|
||||||
|
+ } else if let Some(env) = &self.pin_env {
|
||||||
|
+ std::env::var(env).map_err(|_| {
|
||||||
|
+ AppleCodesignError::CliGeneralError(format!(
|
||||||
|
+ "failed reading PKCS#11 PIN from {env} environment variable"
|
||||||
|
+ ))
|
||||||
|
+ })
|
||||||
|
+ } else {
|
||||||
|
+ Err(AppleCodesignError::CliGeneralError(
|
||||||
|
+ "--pkcs11-pin, --pkcs11-pin-file, or --pkcs11-pin-env is required".into(),
|
||||||
|
+ ))
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+impl KeySource for Pkcs11SigningKey {
|
||||||
|
+ fn resolve_certificates(&self) -> Result<SigningCertificates, AppleCodesignError> {
|
||||||
|
+ if !self.configured() {
|
||||||
|
+ return Ok(Default::default());
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ let module = self.module.clone().ok_or_else(|| {
|
||||||
|
+ AppleCodesignError::CliGeneralError("--pkcs11-library is required".into())
|
||||||
|
+ })?;
|
||||||
|
+ if self.slot_id.is_none() && self.token_label.is_none() {
|
||||||
|
+ return Err(AppleCodesignError::CliGeneralError(
|
||||||
|
+ "--pkcs11-slot-id or --pkcs11-token-label is required".into(),
|
||||||
|
+ ));
|
||||||
|
+ }
|
||||||
|
+ if self.key_id.is_none() && self.key_label.is_none() {
|
||||||
|
+ return Err(AppleCodesignError::CliGeneralError(
|
||||||
|
+ "--pkcs11-key-id or --pkcs11-key-label is required".into(),
|
||||||
|
+ ));
|
||||||
|
+ }
|
||||||
|
+ let certificate_path = self.certificate_path.clone().ok_or_else(|| {
|
||||||
|
+ AppleCodesignError::CliGeneralError("--pkcs11-certificate-file is required".into())
|
||||||
|
+ })?;
|
||||||
|
+ let pin = self.resolve_pin()?;
|
||||||
|
+
|
||||||
|
+ warn!(
|
||||||
|
+ "reading PKCS#11 certificate data from {}",
|
||||||
|
+ certificate_path.display()
|
||||||
|
+ );
|
||||||
|
+ let cert = CapturedX509Certificate::from_der(std::fs::read(certificate_path)?)?;
|
||||||
|
+
|
||||||
|
+ match cert.key_algorithm() {
|
||||||
|
+ Some(KeyAlgorithm::Rsa) => {}
|
||||||
|
+ Some(algorithm) => {
|
||||||
|
+ return Err(AppleCodesignError::CliGeneralError(format!(
|
||||||
|
+ "PKCS#11 signing currently supports RSA certificates, not {algorithm:?}"
|
||||||
|
+ )));
|
||||||
|
+ }
|
||||||
|
+ None => {
|
||||||
|
+ return Err(AppleCodesignError::CliGeneralError(
|
||||||
|
+ "failed to resolve PKCS#11 certificate key algorithm".into(),
|
||||||
|
+ ));
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ let signer = Pkcs11PrivateKey {
|
||||||
|
+ tool: self.tool.clone(),
|
||||||
|
+ module,
|
||||||
|
+ slot_id: self.slot_id.clone(),
|
||||||
|
+ token_label: self.token_label.clone(),
|
||||||
|
+ key_id: self.key_id.clone(),
|
||||||
|
+ key_label: self.key_label.clone(),
|
||||||
|
+ pin,
|
||||||
|
+ cert: cert.clone(),
|
||||||
|
+ };
|
||||||
|
+
|
||||||
|
+ Ok(SigningCertificates {
|
||||||
|
+ keys: vec![Box::new(signer)],
|
||||||
|
+ certs: vec![cert],
|
||||||
|
+ })
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
@@ -643,0 +1037,8 @@
|
||||||
|
+ rename = "pkcs11",
|
||||||
|
+ skip_serializing_if = "Pkcs11SigningKey::not_configured"
|
||||||
|
+ )]
|
||||||
|
+ pub pkcs11_key: Pkcs11SigningKey,
|
||||||
|
+
|
||||||
|
+ #[command(flatten)]
|
||||||
|
+ #[serde(
|
||||||
|
+ default,
|
||||||
|
@@ -680,0 +1082,2 @@
|
||||||
|
+ res.push(&self.pkcs11_key as &dyn KeySource);
|
||||||
|
+
|
||||||
|
--- a/apple-codesign/src/cli/mod.rs
|
||||||
|
+++ b/apple-codesign/src/cli/mod.rs
|
||||||
|
@@ -1529 +1532,6 @@
|
||||||
|
- let certs = c.signer.resolve_certificates(true)?;
|
||||||
|
+ let signer = if self.certificate.pkcs11_key.configured() {
|
||||||
|
+ &self.certificate
|
||||||
|
+ } else {
|
||||||
|
+ &c.signer
|
||||||
|
+ };
|
||||||
|
+ let certs = signer.resolve_certificates(true)?;
|
||||||
Loading…
Add table
Add a link
Reference in a new issue