iOS Build Certificate Import Fix (macOS Sequoia)
Problem
When running local iOS production builds on macOS Sequoia (15.x) with OpenSSL 3.x, the build fails at the [PREPARE_CREDENTIALS] step:
Distribution certificate with fingerprint A738A6955B7D91E082F0810DA4FE766FD29A2AA4 hasn't been imported successfullyThis blocks eas build --platform ios --local entirely.
Root Cause
EAS stores distribution certificates as PKCS#12 (.p12) files on the Expo server. These .p12 files are created using OpenSSL 3.x, which by default uses AES-256-CBC encryption — a modern cipher.
macOS Sequoia's Security.framework (used by Fastlane's import_certificate) cannot read this modern encryption format. When Fastlane imports the .p12 into a temporary keychain, it silently fails — no error is thrown, but the certificate identity is never added to the keychain.
Later, when security find-identity checks for the expected fingerprint, it finds nothing, and the build aborts.
Why it's silent
Fastlane's import_certificate action calls security import under the hood, which returns success (exit code 0) even when it can't parse the modern .p12 encryption. The certificate simply doesn't appear in the keychain.
Additional issue
Even after re-encoding the .p12, the certificate shows as CSSMERR_TP_NOT_TRUSTED in the temporary keychain (because the Apple CA chain isn't present). The original code used security find-identity -v (valid identities only), which filters out untrusted certs — so even a correctly imported cert was invisible.
Solution
Two patches to @expo/build-tools's keychain.js (applied in the npx cache):
Patch 1: Re-encode .p12 with legacy encryption
Before importing, re-encode the .p12 using OpenSSL's -legacy flag, which produces a file using the older 3DES encryption that macOS can read:
// In importCertificate(), before runFastlane import_certificate:
const legacyCertPath = certPath + '.legacy.p12';
const { execSync } = require('child_process');
const passIn = `pass:${certPassword || ''}`;
const passOut = `pass:${certPassword || ''}`;
// Extract to PEM (try -legacy first for reading modern p12)
try {
execSync(`openssl pkcs12 -in "${certPath}" -out "${certPath}.pem" -nodes -passin "${passIn}" -legacy`, { stdio: 'pipe' });
} catch (e1) {
execSync(`openssl pkcs12 -in "${certPath}" -out "${certPath}.pem" -nodes -passin "${passIn}"`, { stdio: 'pipe' });
}
// Re-encode as legacy p12 (3DES, readable by macOS)
execSync(`openssl pkcs12 -export -in "${certPath}.pem" -out "${legacyCertPath}" -passout "${passOut}" -legacy`, { stdio: 'pipe' });
execSync(`rm -f "${certPath}.pem"`);
// Use the re-encoded cert for import
certPath = legacyCertPath;Patch 2: Remove -v flag from find-identity
Change the findIdentitiesByTeamId method to not require the certificate to be "valid" (trusted):
// Before (fails because cert is untrusted in temp keychain):
spawn('security', ['find-identity', '-v', '-s', `(${teamId})`, this.keychainPath]);
// After (finds the cert regardless of trust status):
spawn('security', ['find-identity', '-s', `(${teamId})`, this.keychainPath]);Files Patched
Both patches must be applied to two keychain.js files in the npx cache:
~/.npm/_npx/<hash>/node_modules/@expo/build-tools/dist/ios/credentials/keychain.js— Usesthis.ctx.loggerAPI~/.npm/_npx/<hash>/node_modules/@expo/build-tools/dist/steps/utils/ios/credentials/keychain.js— Usesloggeras parameter. This is the one actually invoked during builds.
Warning: These patches live in the npx cache and will be lost if
eas-cli-local-build-pluginis reinstalled or the cache is cleared. Re-apply after EAS CLI upgrades.
Affected Environments
| Component | Version |
|---|---|
| macOS | 15.x (Sequoia) |
| OpenSSL | 3.x |
| EAS CLI | 18.5.0 |
| @expo/build-tools | (bundled with EAS CLI) |
| Xcode | 16+ |
This issue does not affect:
- EAS cloud builds (they run on Linux)
- macOS versions before Sequoia with OpenSSL 1.x
- Builds where the cert was originally created with legacy encryption
Verification
After applying the patches, the build log should show:
[PREPARE_CREDENTIALS] Certificate password length: 24, isEmpty: false
[PREPARE_CREDENTIALS] Re-encoded p12 with legacy encryption for macOS compatibility
[PREPARE_CREDENTIALS] Validating whether the distribution certificate has been imported successfullyIf you see Failed to re-encode p12, check that openssl is available on your PATH and supports the -legacy flag (openssl version should show 3.x).
Quick Test
To verify the fix before running a full build:
# Extract the p12's cert with the original password
openssl pkcs12 -in cert.p12 -out cert.pem -nodes -passin "pass:YOUR_PASSWORD" -legacy
# Re-encode with legacy (3DES) encryption
openssl pkcs12 -export -in cert.pem -out cert-legacy.p12 -passout "pass:YOUR_PASSWORD" -legacy
# Create a temp keychain
security create-keychain -p test /tmp/test-keychain.keychain
# Import the legacy p12 — should say "1 identity imported"
security import cert-legacy.p12 -k /tmp/test-keychain.keychain -P "YOUR_PASSWORD" -T /usr/bin/codesign
# Verify the identity exists (without -v)
security find-identity -s "(YOUR_TEAM_ID)" /tmp/test-keychain.keychain
# Clean up
security delete-keychain /tmp/test-keychain.keychain
rm cert.pem cert-legacy.p12TestFlight Submission Fix: Invalid URL Scheme
Submission Error
After a successful local build, eas submit to App Store Connect failed with:
The following URL schemes found in your app are not in the correct format:
[com.googleusercontent.apps.GOOGLE_IOS_CLIENT_ID_REVERSED]Apple rejected the IPA because it contained a placeholder URL scheme that was never replaced with an actual value.
Why It Happened
In app.json, the @react-native-google-signin/google-signin plugin was configured with a placeholder iosUrlScheme:
["@react-native-google-signin/google-signin", { "iosUrlScheme": "com.googleusercontent.apps.GOOGLE_IOS_CLIENT_ID_REVERSED" }]This placeholder was meant to be replaced with a real reversed Google iOS client ID, but EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID in .env was never set (empty). The placeholder string got baked into the IPA's Info.plist as a registered URL scheme, and Apple's validation correctly rejected it as malformed.
The Google Sign In button component (components/auth/google-sign-in-button.tsx) already gracefully falls back to web OAuth when the native Google Sign In module is unavailable or unconfigured, so removing the native iOS URL scheme has no functional impact.
How It Was Fixed
Removed the iosUrlScheme config object from the plugin entry in app.json:
"plugins": [
"expo-router",
"expo-font",
"expo-secure-store",
- [
- "@react-native-google-signin/google-signin",
- { "iosUrlScheme": "com.googleusercontent.apps.GOOGLE_IOS_CLIENT_ID_REVERSED" }
- ],
+ "@react-native-google-signin/google-signin",
...
]Also added ascAppId to eas.json to enable fully non-interactive submissions:
"submit": {
"production": {
"ios": {
"appleId": "shrekuu@icloud.com",
"appleTeamId": "5T8PRMGZYW",
"ascAppId": "6761551196"
}
}
}After the Fix
- Rebuilt the IPA locally (
eas build --profile production --platform ios --local) - Submitted with
eas submit --platform ios --path ./build-<timestamp>.ipa --non-interactive - Submission succeeded — binary uploaded to App Store Connect and available on TestFlight
Future: Enabling Native Google Sign In
When a real Google iOS client ID is obtained:
Set
EXPO_PUBLIC_GOOGLE_IOS_CLIENT_IDin.envRestore the plugin config in
app.jsonwith the real reversed client ID:json["@react-native-google-signin/google-signin", { "iosUrlScheme": "com.googleusercontent.apps.YOUR_REAL_CLIENT_ID" }]Rebuild and resubmit
Long-Term Fix
This should eventually be fixed upstream in @expo/build-tools. Track:
- Expo GitHub Issues for related reports
- Apple may also improve
Security.frameworkto accept modern PKCS#12 formats in a future macOS update