Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions .github/actions/configure-signing/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Configure code signing from AWS Secrets Manager
# Uses role + secret name from builds.yml (per Mobile Signer Roles & Secrets doc)
name: 'Configure Signing'
description: 'Assume AWS role and fetch signing certificates from Secrets Manager'

inputs:
aws-role-to-assume:
description: 'The AWS IAM role to assume'
required: true
aws-region:
description: 'The AWS region where the secret is stored'
required: true
default: 'us-east-2'
platform:
description: 'Platform (android or ios)'
required: true
aws-secret-name:
description: 'AWS Secrets Manager secret name (e.g. metamask-mobile-main-uat-signer)'
required: true
android-keystore-path:
description: 'Target path in android/keystores/ (e.g. internalRelease.keystore). Required for Android.'
required: false

runs:
using: 'composite'
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ inputs.aws-role-to-assume }}
aws-region: ${{ inputs.aws-region }}

- name: Fetch secret and export as environment variables
shell: bash
env:
AWS_REGION: ${{ inputs.aws-region }}
AWS_SECRET_NAME: ${{ inputs.aws-secret-name }}
run: |
echo "🔐 Fetching secret from Secrets Manager..."
secret_json=$(aws secretsmanager get-secret-value \
--region "$AWS_REGION" \
--secret-id "$AWS_SECRET_NAME" \
--query SecretString \
--output text)

keys=$(echo "$secret_json" | jq -r 'keys[]')
for key in $keys; do
value=$(echo "$secret_json" | jq -r --arg k "$key" '.[$k]')
echo "::add-mask::$value"
echo "$key=$(printf '%s' "$value")" >> "$GITHUB_ENV"
echo "✅ Set secret for key: $key"
done
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated AWS secret fetching logic across workflows

Medium Severity

The "Fetch secret and export as environment variables" step (lines 33-52) is nearly identical to existing code in .github/workflows/push-eas-update.yml (lines 384-400). Both implementations use the same AWS Secrets Manager CLI call, jq parsing pattern, masking, and export logic. The new action should be reused by push-eas-update.yml to consolidate this duplicated code.

Fix in Cursor Fix in Web

Comment thread
cursor[bot] marked this conversation as resolved.

- name: Configure Android Signing Certificates
if: inputs.platform == 'android'
shell: bash
env:
ANDROID_KEYSTORE_TARGET: ${{ inputs.android-keystore-path }}
run: |
echo "📦 Configuring Android keystore..."
if [[ -z "$ANDROID_KEYSTORE" ]]; then
echo "⚠️ ANDROID_KEYSTORE is not set. Skipping keystore decoding."
exit 1
fi

# When copying to target, always decode to temp first to avoid "same file" error
# (secret may set ANDROID_KEYSTORE_PATH to the target path)
if [[ -n "$ANDROID_KEYSTORE_TARGET" ]]; then
KEYSTORE_PATH="/tmp/android.keystore"
else
KEYSTORE_PATH="${ANDROID_KEYSTORE_PATH:-/tmp/android.keystore}"
fi
echo "$ANDROID_KEYSTORE" | base64 --decode > "$KEYSTORE_PATH"
echo "✅ Android keystore decoded to $KEYSTORE_PATH"

if [[ -n "$ANDROID_KEYSTORE_TARGET" ]]; then
mkdir -p android/keystores
cp "$KEYSTORE_PATH" "android/keystores/$ANDROID_KEYSTORE_TARGET"
echo "✅ Android keystore copied to android/keystores/$ANDROID_KEYSTORE_TARGET"
fi

- name: Configure iOS Signing Certificates
if: inputs.platform == 'ios'
shell: bash
run: |
echo "📦 Configuring iOS code signing..."

CERT_PATH="$RUNNER_TEMP/build_certificate.p12"
PROFILE_PATH="$RUNNER_TEMP/build_pp.mobileprovision"
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
CERT_PW="${IOS_SIGNING_KEYSTORE_PASSWORD}"

echo "$IOS_SIGNING_KEYSTORE" | base64 --decode > "$CERT_PATH"
echo "$IOS_SIGNING_PROFILE" | base64 --decode > "$PROFILE_PATH"
echo "✅ Decoded .p12 and provisioning profile"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iOS signing lacks validation unlike Android path

Medium Severity

The iOS signing step directly uses IOS_SIGNING_KEYSTORE_PASSWORD, IOS_SIGNING_KEYSTORE, and IOS_SIGNING_PROFILE without validating they are set, unlike the Android step which explicitly checks ANDROID_KEYSTORE and exits with a clear error message (lines 61-64). If these iOS environment variables are missing from the AWS secret, the step will create empty/invalid certificate files and fail later during security import with a cryptic error like "could not decode the blob" instead of a clear "secret not configured" message.

Fix in Cursor Fix in Web


security create-keychain -p "$CERT_PW" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$CERT_PW" "$KEYCHAIN_PATH"

echo "🔐 Importing certificate..."
if ! security import "$CERT_PATH" -P "$CERT_PW" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"; then
echo "❌ Failed to import certificate."
exit 1
fi
echo "✅ Certificate imported"

security set-key-partition-list -S apple-tool:,apple: -k "$CERT_PW" "$KEYCHAIN_PATH" 2>/dev/null || true

mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp "$PROFILE_PATH" ~/Library/MobileDevice/Provisioning\ Profiles/
echo "✅ Installed provisioning profile"

security default-keychain -s "$KEYCHAIN_PATH"
echo "✅ Default keychain set"
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
18 changes: 18 additions & 0 deletions .github/builds.README.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,24 @@ export const selectPerpsEnabledFlag = createSelector(

---

## Signing Configuration (AWS Secrets Manager)

Builds that produce signed device binaries (not simulator/debug) use AWS Secrets Manager for code signing. The `signing` section in `builds.yml` maps each build to an AWS role and secret.

| Build type | AWS Role | AWS Secret | Android keystore |
| -------------------------------------- | --------------------------- | --------------------------------- | ------------------------ |
| main-prod | metamask-mobile-prod-signer | metamask-mobile-main-prod-signer | release.keystore |
| main-rc | metamask-mobile-rc-signer | metamask-mobile-main-rc-signer | rc.keystore |
| main-test, main-e2e, main-exp, qa-prod | metamask-mobile-uat-signer | metamask-mobile-main-uat-signer | internalRelease.keystore |
| flask-prod | metamask-mobile-prod-signer | metamask-mobile-flask-prod-signer | flaskRelease.keystore |
| flask-test, flask-e2e | metamask-mobile-uat-signer | metamask-mobile-flask-uat-signer | flask-uat.keystore |

**Dev builds** (main-dev, flask-dev, qa-dev) omit `signing` — they use simulator/debug and require no certificates.

**AWS secret structure** (per build.gradle expectations): The secret must contain keys such as `ANDROID_KEYSTORE` (base64), `BITRISEIO_ANDROID_QA_KEYSTORE_PASSWORD`, `BITRISEIO_ANDROID_QA_KEYSTORE_ALIAS`, `BITRISEIO_ANDROID_QA_KEYSTORE_PRIVATE_KEY_PASSWORD` for UAT builds; adjust for prod/rc/flask variants.

---

## YAML Anchors

`builds.yml` uses YAML anchors to avoid repetition. The pattern is: **single anchor with production defaults, override in specific builds as needed**.
Expand Down
36 changes: 35 additions & 1 deletion .github/builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
# Structure:
# - env: sets environment variables directly (servers, build config, etc.)
# - secrets: maps env var names to GitHub Secret names
# - signing: (optional) AWS role + secret for code signing (omit for dev/simulator builds)
# - code_fencing: features to include via code fencing
# - remote_feature_flags: build-time defaults for LaunchDarkly flags (seeded into RemoteFeatureFlagController)
#
# GitHub Environment determines actual secret values - builds.yml just maps the names.
# Signing: AWS Secrets Manager (Account 363762752069) - see Mobile Signer Roles & Secrets doc.

# =============================================================================
# YAML Anchors (reusable config blocks)
Expand Down Expand Up @@ -74,6 +76,29 @@ _secrets: &secrets
# Card/Baanx
MM_CARD_BAANX_API_CLIENT_KEY: "MM_CARD_BAANX_API_CLIENT_KEY"

# Signing config (AWS Secrets Manager) - omit for dev/simulator builds
# android_keystore_path: filename in android/keystores/ (build.gradle expects fixed paths)
_signing_prod: &signing_prod
aws_role: "arn:aws:iam::363762752069:role/metamask-mobile-prod-signer"
aws_secret: "metamask-mobile-main-prod-signer"
android_keystore_path: "release.keystore"
_signing_rc: &signing_rc
aws_role: "arn:aws:iam::363762752069:role/metamask-mobile-rc-signer"
aws_secret: "metamask-mobile-main-rc-signer"
android_keystore_path: "rc.keystore"
_signing_uat: &signing_uat
aws_role: "arn:aws:iam::363762752069:role/metamask-mobile-uat-signer"
aws_secret: "metamask-mobile-main-uat-signer"
android_keystore_path: "internalRelease.keystore"
_signing_flask_prod: &signing_flask_prod
aws_role: "arn:aws:iam::363762752069:role/metamask-mobile-prod-signer"
aws_secret: "metamask-mobile-flask-prod-signer"
android_keystore_path: "flaskRelease.keystore"
_signing_flask_uat: &signing_flask_uat
aws_role: "arn:aws:iam::363762752069:role/metamask-mobile-uat-signer"
aws_secret: "metamask-mobile-flask-uat-signer"
android_keystore_path: "flask-uat.keystore"

# Main code fencing features
_code_fencing_main: &code_fencing_main
- preinstalled-snaps
Expand Down Expand Up @@ -132,6 +157,7 @@ builds:
# Production release (App Store)
main-prod:
github_environment: build-production
signing: *signing_prod
env:
METAMASK_ENVIRONMENT: "production"
METAMASK_BUILD_TYPE: "main"
Expand All @@ -147,6 +173,7 @@ builds:
# Release candidate
main-rc:
github_environment: build-rc
signing: *signing_rc
env:
METAMASK_ENVIRONMENT: "rc"
METAMASK_BUILD_TYPE: "main"
Expand All @@ -161,6 +188,7 @@ builds:
# Test builds (QA)
main-test:
github_environment: build-test
signing: *signing_uat
env:
METAMASK_ENVIRONMENT: "test"
METAMASK_BUILD_TYPE: "main"
Expand All @@ -182,6 +210,7 @@ builds:
# E2E test builds (no Sentry)
main-e2e:
github_environment: build-e2e
signing: *signing_uat
env:
METAMASK_ENVIRONMENT: "e2e"
METAMASK_BUILD_TYPE: "main"
Expand All @@ -202,6 +231,7 @@ builds:
# Experimental builds
main-exp:
github_environment: build-exp
signing: *signing_uat
env:
METAMASK_ENVIRONMENT: "exp"
METAMASK_BUILD_TYPE: "main"
Expand Down Expand Up @@ -272,6 +302,7 @@ builds:
# Flask production release
flask-prod:
github_environment: build-production
signing: *signing_flask_prod
env:
METAMASK_ENVIRONMENT: "production"
METAMASK_BUILD_TYPE: "flask"
Expand All @@ -285,7 +316,8 @@ builds:

# Flask test builds (QA)
flask-test:
github_environment: build-test
github_environment: build-flask-uat
signing: *signing_flask_uat
env:
METAMASK_ENVIRONMENT: "test"
METAMASK_BUILD_TYPE: "flask"
Expand All @@ -306,6 +338,7 @@ builds:
# Flask E2E test builds (no Sentry)
flask-e2e:
github_environment: build-e2e
signing: *signing_flask_uat
env:
METAMASK_ENVIRONMENT: "e2e"
METAMASK_BUILD_TYPE: "flask"
Expand Down Expand Up @@ -365,6 +398,7 @@ builds:
# QA production build
qa-prod:
github_environment: build-qa
signing: *signing_uat
env:
METAMASK_ENVIRONMENT: "production"
METAMASK_BUILD_TYPE: "qa"
Expand Down
24 changes: 16 additions & 8 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ jobs:
outputs:
github_environment: ${{ steps.config.outputs.github_environment }}
secrets_json: ${{ steps.config.outputs.secrets_json }}
signing_aws_role: ${{ steps.config.outputs.signing_aws_role }}
signing_aws_secret: ${{ steps.config.outputs.signing_aws_secret }}
signing_android_keystore_path: ${{ steps.config.outputs.signing_android_keystore_path }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
Expand All @@ -61,6 +64,10 @@ jobs:
const build = config.builds['${{ inputs.build_name }}'];
fs.appendFileSync(process.env.GITHUB_OUTPUT, 'github_environment=' + build.github_environment + '\n');
fs.appendFileSync(process.env.GITHUB_OUTPUT, 'secrets_json=' + JSON.stringify(build.secrets || {}) + '\n');
const signing = build.signing;
fs.appendFileSync(process.env.GITHUB_OUTPUT, 'signing_aws_role=' + (signing ? signing.aws_role || '' : '') + '\n');
fs.appendFileSync(process.env.GITHUB_OUTPUT, 'signing_aws_secret=' + (signing ? signing.aws_secret || '' : '') + '\n');
fs.appendFileSync(process.env.GITHUB_OUTPUT, 'signing_android_keystore_path=' + (signing && signing.android_keystore_path ? signing.android_keystore_path : '') + '\n');
"

# Build
Expand All @@ -70,7 +77,7 @@ jobs:
matrix:
platform: ${{ inputs.platform == 'both' && fromJSON('["android", "ios"]') || fromJSON(format('["{0}"]', inputs.platform)) }}
# Android: Cirrus lg (large) runner for 8GB Gradle heap; iOS: GitHub-hosted macOS
runs-on: ${{ matrix.platform == 'ios' && 'macos-latest' || 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-lg' }}
runs-on: ${{ matrix.platform == 'ios' && 'ghcr.io/cirruslabs/macos-runner:sequoia-xl' || 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-lg' }}
environment: ${{ needs.prepare.outputs.github_environment }}
env:
# So bundle exec (in yarn pod:install) finds ios/Gemfile when running from repo root
Expand Down Expand Up @@ -155,15 +162,16 @@ jobs:
java-version: '17'
distribution: 'temurin'

# Android qa signing (exp-test-e2e): skip for Expo dev builds (main-dev, flask-dev, qa-dev use debug keystore)
- name: Configure Android signing certificates
if: matrix.platform == 'android' && inputs.build_name != 'main-dev' && inputs.build_name != 'flask-dev' && inputs.build_name != 'qa-dev'
uses: MetaMask/github-tools/.github/actions/configure-keystore@0259e8a920318b02a8860e178d79796eaa08de02
# Signing: uses role + secret from builds.yml (skip for dev builds - main-dev, flask-dev, qa-dev use debug/simulator)
- name: Configure signing certificates
if: needs.prepare.outputs.signing_aws_role != ''
uses: ./.github/actions/configure-signing
with:
aws-role-to-assume: 'arn:aws:iam::363762752069:role/metamask-mobile-build-signer-qa'
aws-role-to-assume: ${{ needs.prepare.outputs.signing_aws_role }}
aws-region: 'us-east-2'
platform: 'android'
target: ${{ startsWith(inputs.build_name, 'flask') && 'flask' || 'qa' }}
platform: ${{ matrix.platform }}
aws-secret-name: ${{ needs.prepare.outputs.signing_aws_secret }}
android-keystore-path: ${{ needs.prepare.outputs.signing_android_keystore_path }}

- name: Setup project dependencies with retry
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2
Expand Down
Loading