Skip to content

[Bug] "unable to load private key" with "bad base64 decode" during iOS CI with OpenSSLΒ #366

@sadnyani-4

Description

@sadnyani-4

I am trying to set up a CI/CD workflow for my Flutter iOS app on a macos-latest runner. The workflow consistently fails at the Setup iOS Credentials step with the error unable to load private key followed by PEM routines:PEM_read_bio_ex:bad base64 decode.

I have confirmed the following troubleshooting steps:

  • Workflow Syntax: The openssl command is correctly formatted with -passin and -passout flags.
  • Secret Content: The IOS_BUILD_PRIVATE_KEY_BASE64 secret has been regenerated multiple times to ensure it is a clean, single-line string of base64-encoded data, without any extra metadata.
  • Key Type: I have tried both -----BEGIN RSA PRIVATE KEY----- and -----BEGIN PRIVATE KEY----- headers.

The error message bad base64 decode strongly suggests that the decoded data itself is not a valid base64 stream, even though it was generated from a valid PEM key. This points to a potential incompatibility between the base64 encoding/decoding process or the OpenSSL version on the runner.

Workflow File (flutter_ios.yml):

name: Flutter iOS CI
on:
  push:
    branches:
      - main
jobs:
  build_ios:
    name: Build iOS App
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with:
          channel: "stable"

      - name: Clean Flutter Project
        run: flutter clean

      - run: flutter pub get

      - name: Setup iOS Credentials
        env:
          BUILD_PRIVATE_KEY_BASE64: ${{ secrets.IOS_BUILD_PRIVATE_KEY_BASE64 }}
          BUILD_CERTIFICATE_BASE64_RAW: ${{ secrets.IOS_BUILD_CERTIFICATE_BASE64_RAW }}
          BUILD_CERTIFICATE_PASSWORD: ${{ secrets.IOS_BUILD_CERTIFICATE_PASSWORD }}
          PROVISIONING_PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
        run: |
          # Create and unlock a temporary keychain
          KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
          security create-keychain -p "" "$KEYCHAIN_PATH"
          security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
          security default-keychain -s "$KEYCHAIN_PATH"
          security unlock-keychain -p "" "$KEYCHAIN_PATH"
          
          # Decode and install the provisioning profile
          PROVISIONING_PROFILE_PATH="$HOME/Library/MobileDevice/Provisioning Profiles/RoomEase_CICD_Ad_Hoc.mobileprovision"
          mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
          echo "$PROVISIONING_PROFILE_BASE64" | base64 --decode > "$PROVISIONING_PROFILE_PATH"
          
          # Set locale to C to avoid 'Illegal byte sequence'
          export LC_ALL=C
          PRIVATE_KEY_CONTENT=$(echo "$BUILD_PRIVATE_KEY_BASE64" | base64 -d | tr -d '[:cntrl:]' | tr -d ' ')
          
          # CRITICAL CHANGE: Use generic PRIVATE KEY headers
          printf "%s\n" "-----BEGIN PRIVATE KEY-----" > private.key
          printf "%s" "$PRIVATE_KEY_CONTENT" >> private.key
          printf "\n%s\n" "-----END PRIVATE KEY-----" >> private.key
          
          # Reconstruct the certificate file using a direct pipe to openssl
          echo "$BUILD_CERTIFICATE_BASE64_RAW" | openssl base64 -d -A | openssl x509 -inform DER -outform PEM -out certificate.cer
          
          # Use the macOS OpenSSL to combine them into a p12 file
          openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.cer -passin pass:"$BUILD_CERTIFICATE_PASSWORD" -passout pass:"$BUILD_CERTIFICATE_PASSWORD"
          
          # Import the newly created p12 file into the keychain
          security import certificate.p12 -k "$KEYCHAIN_PATH" -P "$BUILD_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
          
          # Clean up temporary files
          rm private.key certificate.cer certificate.p12
          
          # Verify that the certificate was imported correctly
          echo "Verifying certificates in keychain..."
          security find-identity -p codesigning -v "$KEYCHAIN_PATH"

      - name: Final Fix - Project Configuration
        run: |
          # Correct the signing identity in the project configuration
          sed -i '' -E 's/"iPhone Developer"/"Apple Distribution: Sohan Lal Khazan Chand (DHG8JNTHZL)"/g' ios/Runner.xcodeproj/project.pbxproj

          # Set the correct provisioning profile specifier
          sed -i '' -E 's/PROVISIONING_PROFILE_SPECIFIER = RoomEase Ad Hoc Testing New;/PROVISIONING_PROFILE_SPECIFIER = "RoomEase_CICD_Ad_Hoc";/' ios/Runner.xcodeproj/project.pbxproj

          echo "Verification: Printing code signing identity after fix."
          grep -C 3 'CODE_SIGN_IDENTITY' ios/Runner.xcodeproj/project.pbxproj

      - name: Build and Sign the IPA
        env:
          TEAM_ID: DHG8JNTHZL
        run: flutter build ipa --release --export-options-plist ./ios/ExportOptions.plist

      - name: Upload iOS App Artifact
        if: success()
        uses: actions/upload-artifact@v4
        with:
          name: ios-release
          path: build/ios/ipa/*.ipa

Run Logs:

##[debug]Evaluating: secrets.IOS_BUILD_PRIVATE_KEY_BASE64
##[debug]Evaluating Index:
##[debug]..Evaluating secrets:
##[debug]..=> Object
##[debug]..Evaluating String:
##[debug]..=> 'IOS_BUILD_PRIVATE_KEY_BASE64'
##[debug]=> '***'
##[debug]Result: '***'
##[debug]Evaluating: secrets.IOS_BUILD_CERTIFICATE_BASE64_RAW
##[debug]Evaluating Index:
##[debug]..Evaluating secrets:
##[debug]..=> Object
##[debug]..Evaluating String:
##[debug]..=> 'IOS_BUILD_CERTIFICATE_BASE64_RAW'
##[debug]=> '***'
##[debug]Result: '***'
##[debug]Evaluating: secrets.IOS_BUILD_CERTIFICATE_PASSWORD
##[debug]Evaluating Index:
##[debug]..Evaluating secrets:
##[debug]..=> Object
##[debug]..Evaluating String:
##[debug]..=> 'IOS_BUILD_CERTIFICATE_PASSWORD'
##[debug]=> '***'
##[debug]Result: '***'
##[debug]Evaluating: secrets.IOS_PROVISIONING_PROFILE_BASE64
##[debug]Evaluating Index:
##[debug]..Evaluating secrets:
##[debug]..=> Object
##[debug]..Evaluating String:
##[debug]..=> 'IOS_PROVISIONING_PROFILE_BASE64'
##[debug]=> '***'
##[debug]Result: '***'
##[debug]Evaluating condition for step: 'Setup iOS Credentials'
##[debug]Evaluating: success()
##[debug]Evaluating success:
##[debug]=> true
##[debug]Result: true
##[debug]Starting: Setup iOS Credentials
##[debug]Loading inputs
##[debug]Loading env
Run # Create and unlock a temporary keychain
  # Create and unlock a temporary keychain
  KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
  security create-keychain -p "" "$KEYCHAIN_PATH"
  security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
  security default-keychain -s "$KEYCHAIN_PATH"
  security unlock-keychain -p "" "$KEYCHAIN_PATH"
  
  # Decode and install the provisioning profile
  PROVISIONING_PROFILE_PATH="$HOME/Library/MobileDevice/Provisioning Profiles/RoomEase_CICD_Ad_Hoc.mobileprovision"
  mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
  echo "$PROVISIONING_PROFILE_BASE64" | base64 --decode > "$PROVISIONING_PROFILE_PATH"
  
  # Set locale to C to avoid 'Illegal byte sequence'
  export LC_ALL=C
  PRIVATE_KEY_CONTENT=$(echo "$BUILD_PRIVATE_KEY_BASE64" | base64 -d | tr -d '[:cntrl:]' | tr -d ' ')
  
  # πŸš€ CRITICAL CHANGE: Use generic PRIVATE KEY headers
  printf "%s\n" "-----BEGIN PRIVATE KEY-----" > private.key
  printf "%s" "$PRIVATE_KEY_CONTENT" >> private.key
  printf "\n%s\n" "-----END PRIVATE KEY-----" >> private.key
  
  # Reconstruct the certificate file using a direct pipe to openssl
  echo "$BUILD_CERTIFICATE_BASE64_RAW" | openssl base64 -d -A | openssl x509 -inform DER -outform PEM -out certificate.cer
  
  # Use the macOS OpenSSL to combine them into a p12 file
  openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.cer -passin pass:"$BUILD_CERTIFICATE_PASSWORD" -passout pass:"$BUILD_CERTIFICATE_PASSWORD"
  
  # Import the newly created p12 file into the keychain
  security import certificate.p12 -k "$KEYCHAIN_PATH" -P "$BUILD_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
  
  # Clean up temporary files
  rm private.key certificate.cer certificate.p12
  
  # Verify that the certificate was imported correctly
  echo "Verifying certificates in keychain..."
  security find-identity -p codesigning -v "$KEYCHAIN_PATH"
  shell: /bin/bash -e {0}
  env:
    FLUTTER_ROOT: /Users/runner/hostedtoolcache/flutter/stable-3.32.8-arm64
    PUB_CACHE: /Users/runner/.pub-cache
    BUILD_PRIVATE_KEY_BASE64: ***
    BUILD_CERTIFICATE_BASE64_RAW: ***
    BUILD_CERTIFICATE_PASSWORD: ***
    PROVISIONING_PROFILE_BASE64: ***
##[debug]/bin/bash -e /Users/runner/work/_temp/75bcd883-f8d6-4bd4-8d3b-653c00d75a15.sh
unable to load private key
8606519168:error:09091064:PEM routines:PEM_read_bio_ex:bad base64 decode:crypto/pem/pem_lib.c:949:
Error: Process completed with exit code 1.
##[debug]Finishing: Setup iOS Credentials

Environment:

  • Runner: macos-latest
  • Flutter Version: stable
  • Local Machine OS (used for key generation): Windows

What I Expect:

I expect the workflow to successfully decode the IOS_BUILD_PRIVATE_KEY_BASE64 secret into a valid private.key file and proceed with the code signing process.

What Actually Happens:

The openssl pkcs12 command fails with the error unable to load private key and PEM routines:PEM_read_bio_ex:bad base64 decode, causing the job to terminate.

Potential Clue:

The base64 string was generated on a Windows machine. It's possible there is a subtle difference in the encoding/decoding that is causing a problem on the macOS runner.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions