Managing app signing in fastlane

Tue, 5 Sep 2023 00:00:00 +0000

App signing is probably the biggest nightmare of iOS developers. Time to face your fear and master it!

If you read Advanced fastlane you now masterize configuring the tool with ease. Congratulations! However one question might still rise: how to sign your damned beautiful application with no effort?

Let’s see some of the possibilities offered by Fastlane and iOS ecosystem. Disclaimer: code signing is hell! 👿

Signing a single app

Let’s start “simple”, shall we? We’ll try to sign a single application, meaning no embedded extensions inside it (such as Widgets). In this configuration there are mainly two teams: those using fastlane match, and those using Cert and Sigh.

match

With fastlane match you can get your certificates and provisioning profiles in one command: match(). Once every thing is downloaded you can build your app.

You can also enforce the provisioning profile to sign with, making sure it uses the one you just downloaded. There are 3 ways of doing it:

1. 🛠️ By setting it in xcodeproj. Definitely not a good way

2. 🗃️ Using a xcconfig

3. 🛣️ Directly into your lane

For now we’ll go with option 3:

lane :build do
	require_envs(:MATCH_TYPE, :APP_IDENTIFIER)

	match()

	xcargs = {
      	:CODE_SIGN_STYLE => "Manual", #disable automatic signing
      	:PROVISIONING_PROFILE_SPECIFIER => lane_context[:MATCH_PROVISIONING_PROFILE_MAPPING][ENV['APP_IDENTIFIER']]
  	}

	gym(xcargs: xcargs)	 # xcargs allows us to give additional options to xcodebuild
end

Instead of xcargs you can use actions like update_code_signing_settings. I prefer xcargs because it doesn’t mutate my xcodeproj. However it also implies you did not change it manually either so that command line settings can take effect 😉

Many people like to use fastlane match because your certificates and provisioning profile can be accessed through teammates by getting stored (usually on a Git repository). While it’s useful for the private key it’s pretty much useless for provisioning profiles: they are already available on iOS dev portal.

One disadvantage about match is that you can’t renew anything automatically: neither certificate nor provisioning profiles (except in some rare cases). This could be OK if you did not have to first nuke the files you versioned on your Git repository in order to upload the new ones (that you generated manually). All in all it adds lots of additional steps for nothing.

Another big issue I faced with match: you don't own the cycle. Everything is done some way very specific and pushed directly on remote repository. If for any reason you have restricted permissions it can quickly become nightmare.

Cert + Sigh

Another solution is by using Cert and Sigh:

Cert is responsible for downloading certificate related to your app

While Sigh will download the provisioning profile

lane :build do
	cert()
	sigh()

	xcargs = {
      	:CODE_SIGN_STYLE => "Manual", #disable automatic signing
      	:PROVISIONING_PROFILE_SPECIFIER => lane_context[:SIGH_NAME]
  	}

	gym(xcargs: xcargs)
end

Cert and Sigh might not be as trendy as match but they have the advantage of downloading everything from iOS dev portal: you therefore only have one source of truth. You can even regenerate them on fly if needed. It’s definitely handy for provisioning profiles (but I would discourage doing it for certificates).

Their only downside is that you have to manage private certificate key yourself. Which is something I consider as non troublesome when you use a tool like Bitrise.

Signing an app with extensions

Up until now this was the “easy” case. But as soon as you introduce app Extensions things become tougher.

Time to get dirty!

Let’s say we have an app “com.foo.bar” with an extension “com.foo.bar.widgetExtension”. If we try the code from previous section we’ll get an error: Provisioning profile "com.foo.bar XX" has app ID "com.foo.bar", which does not match the bundle ID "com.foo.bar.widgetExtension"

Xcode is actually… trying to use our app provisioning profile to sign our extension 🤯. We indeed did not download our extension provisioning profile. To fix this let’s first try a naive approach:

lane :build do
	cert()
	sigh()
	sigh(app_identifier: "com.foo.bar.widgetExtension") # we download our extension profile

	xcargs = {
      	:CODE_SIGN_STYLE => "Manual", #disable automatic signing
      	:PROVISIONING_PROFILE_SPECIFIER => lane_context[:SIGH_NAME]
  	}

	gym(xcargs: xcargs)
end

Still not compiling! Now we get… Provisioning profile "com.foo.bar.widgetExtension YY" has app ID "com.foo.bar.widgetExtension", which does not match the bundle ID "com.foo.bar". What? That’s the exact opposite of previous error!

It actually makes sense. When using lane_context[:SIGH_NAME] we’re referring to the last downloaded profile… which is the one for our extension!

With match the error would be the same as the first one as we explicitly use our app identifier.

Let’s try something else: let’s not enforce provisioning profile and use automatic signing:

lane :build do
	cert()
	sigh()
	sigh(app_identifier: "com.foo.bar.widgetExtension") # we download our extension profile

	xcargs = {
      	:CODE_SIGN_STYLE => "Automatic"
  	}

	gym(xcargs: xcargs)
end

And this time… 🥁… it works! 🎉 So while enforcing profile for a single target is OK, it can become troublesome when having multiple ones.

However we now have no way of making sure the app was signed with the just recently downloaded provisioning profile. We can workaround this by adding a verification step at the very end:

lane :build do
	require_envs(:APP_IDENTIFIER)

	cert()
	sigh()

	app_provisioning_uuid = ENV['SIGH_UUID']

	sigh(app_identifier: "com.foo.bar.widgetExtension") # we download our extension profile

	xcargs = {
      	:CODE_SIGN_STYLE => "Automatic"
  	}

	gym(xcargs: xcargs)
	verify_build(bundle_identifier: ENV['APP_IDENTIFIER'], provisioning_uuid: app_provisioning_uuid)
end

Let Xcode do the heavy job

Some time ago Xcode introduced a new option: allowProvisioningUpdates. This option is supposed to let Xcode download (or generate) certificates and provisioning profiles. Using it we can get rid of cert, sigh… or match!

lane :build do
	require_envs(:APP_IDENTIFIER)

	xcargs = "-allowProvisioningUpdates"
  	xcargs += " -authenticationKeyID #{ENV['APP_STORE_CONNECT_API_KEY_KEY_ID']}"
  	xcargs += " -authenticationKeyIssuerID #{ENV['APP_STORE_CONNECT_API_KEY_ISSUER_ID']}"
  	xcargs += " -authenticationKeyPath '#{ENV['APP_STORE_CONNECT_API_KEY_KEY_FILEPATH']}'"
	xcargs += " CODE_SIGN_STYLE=Automatic"

	gym(xcargs: xcargs)
end

Note the usage of authentication*** parameters: this is needed to give Xcode an API token in order for allowProvisioningUpdates to work.

Also note that authenticationKeyPath expects an absolute path so you might want to check it:

key_file_path = Pathname.new(ENV['APP_STORE_CONNECT_API_KEY_KEY_FILEPATH'])

UI.user_error "APP_STORE_CONNECT_API_KEY_KEY_FILEPATH must be absolute" if key_file_path.relative?

xcargs += " -authenticationKeyPath #{key_file_path}"

If you run it then you should see… your app built and archived! 🎉

Getting “There are no accounts registered with Xcode” on CI? You probably wrote -authenticationKeyID=XX instead of -authenticationKeyID XX

Automatic or Manual?

We saw signing using both automatic and manual.

Automatic has the big advantage of requiring less configuration. But you obviously have less control about which provisioning profile will be used by Xcode.

Manual is the opposite: you have to clearly declare which provisioning profile to use. However it can be hard to configure when having multiple targets. In this scenario you won’t be able to rely much on fastlane and will have no choice but to configure provisioning profiles per target inside Xcode, using Build Settings or xcconfig files:

# TARGET A.xcconfig
PROVISIONING_PROFILE_SPECIFIER = myProfile

# TARGET B.xcconfig
PROVISIONING_PROFILE_SPECIFIER = anotherProfile

Code signing is still something hard to get correctly especially when having extensions. But things got easier with tools like match or Cert + Sigh. Or even with Xcode, now handling signing for you right from your command line! 👏

In any case best thing is to configure signing as less as possible and let those tools handle all the rest.