In order for an app to run on an iOS device, it needs to be code signed. This proves to iOS that the app has been approved to run on iOS devices. This is true of any apps in the App store, ad-hoc, or enterprise apps. The App store apps add an additional level of protection, as the apps are not only cryptographically signed by Apple, but also protected by DRM. Since only Apple can apply this DRM to apps, the app needs to go through the approval process and be “blessed” by Apple.

(And yes, app store apps are signed by Apple, not the developer. The developer signs them when submitting to the store, but then Apple re-signs them with their own identity. Don’t believe me? Try this:

However, Ad Hoc and Enterprise signed apps are a different story. Code signing happens during app building in Xcode, but you don’t need access to the source code to sign an app. I won’t get into code signing details here, but cryptographically signing something has two key components:

1. You need a private key to sign something, and anyone with a public key can then verify it has not been modified (yes, I did just skip about 25 details).
2. The public key is normally included in a certificate. This certificate is signed by someone else (in this case, Apple), to vouch for the validity of certificate.

So with these two pieces of knowledge, you can take an app that is signed for Ad Hoc and re-sign with another Ad Hoc identity, or an enterprise app.

But what are the steps? Well, Xcode comes to the rescue. When building an app, Xcode gives you the command you need:

So now we see the man behind the curtain. The codesign binary does all the heavy lifting. Here are the different options from the man page:

--force: If there is any signature existing, replace them.--sign (big ass number): Tells to sign using the identity referenced by (big ass number). More on this in a bit.--resource-rules: All the resources in the bundle are used when generating the signature. You can use the resource-rules option to ignore some when calculating the signature. This usually tells code sign to not include any files that start with “dot” (I’m looking at you, Finder. And you, cvs. Hey, svn, you too), Info.plist, and ResourceRults.plist.

--entitlements: Different apps need access to different resources and iOS sandboxes apps so they only get access to resource to which they are supposed to have access. The entitlements part is added to the signature data. This option specifies the entitlements file to read the entitlements from and add to the signature. In practical terms, entitlements files usually just specify the key access group and if the app can be debugged. The entitlements in the signature must match those in the provisioning profile (generally), so we just copy the entitlements from the provisioning profile into the signature.

So we have a couple of challenges:

1. Need to figure out the big ass number to identify the developer ID.
2. The app that is being re-resigned will have a different provisioning profile, and the app ID needs to match the provisioning profile. The new provisioning profile needs to be added to the app bundle.
3. We need to automate it a bit.

Finding the developer ID is pretty easy, as the security command dumps the big ass number:

(For the record, the big ass number is actually the SHA1 fingerprint of the certificate, but that is just fancy talk).

That was easy. Now for the provisioning profile. It is in the app bundle as a file called “embedded.mobileprovision”, and we can pull out the entitlements and app id, and then update the info.plist file with the app id and sign the whole thing with the provided identity and entitlements.

So thanks to Xcode, we now know how to sign apps without recompiling. Now we need to script it up. Ruby has some awesome certificate handling modules, so we’ll use Ruby.

The following script is run like this:
resign –prov_profile_path /path/to/prov/profile –app-path /path/to/ios/app/that/ends/in/.app –developer-id “iPhone Developer: Some Name (SOMEDIGITS)”

After it is done, you’ll end up with an ipa that can be distributed that includes the new provisioning profile, app id, and is signed by the provided identity. The script requires the plist ruby modules. The whole thing is packaged up here.

require "base64"
require 'openssl'
require 'base64'
require 'cgi'
require 'stringio'
require 'ftools'
require 'getoptlong'
require 'pathname'
require File.dirname(__FILE__)+"/generator.rb"
require File.dirname(__FILE__)+"/parser.rb"
def self.sign_to_der(certPEM, privateKeyPEM, dataToSign)
cert = OpenSSL::X509::Certificate.new(certPEM)
privateKey = OpenSSL::PKey::RSA.new(privateKeyPEM)
flags = OpenSSL::PKCS7::BINARY
pkcs7 = OpenSSL::PKCS7::sign(cert, privateKey, dataToSign, nil, flags)
return pkcs7.to_der
end
# Remove the data from the signed package. Don't worry about
# checking the signature.
def self.unwrap_signed_data(signedData)
pkcs7 = OpenSSL::PKCS7.new(signedData)
store = OpenSSL::X509::Store.new
flags = OpenSSL::PKCS7::NOVERIFY
pkcs7.verify([], store, nil, flags) # Verify it so we can pull out the data
return pkcs7.data
end
def subject_from_cert(inCert)
certificate=OpenSSL::X509::Certificate.new inCert
subject=certificate.subject.to_s
subject=subject[/CN=.*?\//].sub!("CN=","").sub("\/","")
return subject
end
# go through an array of strings and see if they match identities in the keychain. Note that the
# identities much start with iPhone and be of type codesigning.
def find_matching_identities (inCertificateSubjects)
#get identities using the commmand line tool security
identities=`security find-identity -v -p codesigning`
#create an array
identities=identities.split("\n")
#identity_labels is the common name of all the matching certs.
identity_labels=[]
#loop over the certs that came in and compare to each identity from the keychain
inCertificateSubjects.each{|certSubject|
identities.each { |id|
#only use identities that start with iPhone. This could cause issues later.
if id[/iPhone.*/] then
#we have a trailing quote, so need to delete it
label=id[/iPhone.*/].delete!("\"")
#if we match, then we found an identity that should be saved and
#we add it to our array
if (label==certSubject) then
identity_labels.push certSubject
$stderr.puts "Matched #{label}"
end
end
}
}
return identity_labels
end
#dev_id is the name of the identity passed in
dev_id=nil
#prov_profile_path is the posix path to the provisioning profile
prov_profile_path=nil
#app_path is the posix path to the application bundle to sign
app_path=nil
no_copy_provisioning_profile=false
#setup the options
opts = GetoptLong.new(
[ '--prov_profile_path', '-p', GetoptLong::REQUIRED_ARGUMENT ],
[ '--app_path', '-a', GetoptLong::REQUIRED_ARGUMENT ],
[ '--developerid', '-d', GetoptLong::REQUIRED_ARGUMENT ],
[ '--no_copy_provisioning_profile', '-c', GetoptLong::NO_ARGUMENT ]
)
opts.each do |opt, arg|
case opt
when '--prov_profile_path'
prov_profile_path=arg
when '--app_path'
app_path = arg
when '--developerid'
dev_id = arg
when '--no_copy_provisioning_profile'
no_copy_provisioning_profile=true
end
end
throw "file #{prov_profile_path} does not exist" if !File.exists?(prov_profile_path)
#read in the signed provisioning profile from disk and extract plist.
#we don't care who signed it.
$stderr.puts " Reading in provisioning profile..."
signedData=File.read(prov_profile_path)
r = Plist::parse_xml(unwrap_signed_data(signedData))
#get the entitlements from the profile
app_id=r['Entitlements']['application-identifier']
#strip off vendor ID
app_id=app_id.split(".")[1,app_id.length].join "."
entitlements=r['Entitlements']
#build an array of subjects from each developer certificate
#certificateSubjects is an array that we'll store the names
certificateSubjects=[]
#get all the dev certificates. We get back a ref to a StringIO
#not a string, so we have to read them in.
certificatesArray=r['DeveloperCertificates']
#we iterate over all the certicates and save them in an array
#and also detect if a match is found. We do both because
#this tool can be called with or without a dev_id. If it is
#called without a dev_id, then all of the matching certificates are
#returned. If a dev_id is provided, we mark it as found and they use that.
#waste a bit of memory but this shortens how much code is used.
found=false
certificatesArray.each { |inCert|
curSubject=subject_from_cert(inCert.read)
certificateSubjects.push curSubject
if ((dev_id!=nil) and (dev_id==curSubject) ) then
found=true
end
}
$stderr.puts "found "+certificatesArray.count.to_s+" certificates"
#if we don't have a dev_id, then we just return the matched certificates and
#exit
if (dev_id==nil)
matchingArray=find_matching_identities(certificateSubjects)
puts matchingArray.uniq.join("\n")
exit
end
#check to make sure the app passed in really exists
throw "file #{app_path} does not exist" if !File.exists? app_path
#the plist is most likely in binary format, so we change to text
#otherwise the plist library fails
info_plist_path="#{app_path}/Info.plist"
$stderr.puts " Converting Info.plist from binary to text..."
system("plutil -convert xml1 \"#{info_plist_path}\"")
#Read in the plist file, put into an array, and then change the App ID
# this is so that the bundle ID will match the app id in the provisioning
#profile. We then save it out with help from the plist library.
$stderr.puts " Updating Info.plist with new bundle id of #{app_id}..."
file_data=File.read(info_plist_path)
info_plist=Plist::parse_xml(file_data)
info_plist['CFBundleIdentifier']=app_id
$stderr.puts " Saving updated Info.plist and Entitlements to app bundle..."
info_plist.save_plist info_plist_path
entitlements.save_plist("#{app_path}/Entitlements.plist")
#Dump the old embedded.mobileprovision and copy in the one provided
$stderr.puts " Removing the prior embedded.mobileprovision..."
File.unlink("#{app_path}/embedded.mobileprovision") if File.exists? "#{app_path}/embedded.mobileprovision"
if no_copy_provisioning_profile==false then
$stderr.puts " Moving provisioning profile into app..."
File.copy(prov_profile_path,"#{app_path}/embedded.mobileprovision")
end
#now we sign the whole she-bang using the info provided
$stderr.puts "running /usr/bin/codesign -f -s \"#{dev_id}\" --resource-rules=\"#{app_path}/ResourceRules.plist\" \"#{app_path}\""
result=system("/usr/bin/codesign -f -s \"#{dev_id}\" --resource-rules=\"#{app_path}/ResourceRules.plist\" \"#{app_path}\"")
$stderr.puts "codesigning returned #{result}"
throw "Codesigning failed" if result==false
app_folder=Pathname.new(app_path).dirname.to_s
newFolder="#{app_folder}/SignedApp"
#we add the .app into a Payload folder and then zip it
#up with the extension .ipa so it can easiy be added to iTunes
#However, we must account for the fact that a duplicate folder name
#exists. We just add an integer onto the end if we find it.
i=1
while (File.exists? newFolder)
newFolder="#{app_folder}/SignedApp"+"-"+i.to_s
i+=1
end
#create the new folder and a payload folder
Dir.mkdir(newFolder)
Dir.mkdir("#{newFolder}/Payload")
#Get the app name (without extension) and create a folder with the same name
appName=Pathname.new(app_path).basename.sub(".app","")
File.move(app_path,"#{newFolder}/Payload")
#zip it up. zip is a bit strange in that you have to actually be in the
#folder otherwise it puts the entire tree (though empty) in the zip.
system("pushd \"#{newFolder}\" && /usr/bin/zip -r \"#{appName}.ipa\" Payload")

Timothy Perfitt is currently the head of Twocanoes Software, Inc, creator of iOS and Mac apps for the IT market. Prior to Twocanoes Software, he survived the collapse of the dot com era by jumping from Coolboard.com to Apple, Inc in 2001. He worked on the initial certification training materials for Mac OS X, worked in Education Sales, and then finished his time at Apple in 2012 working with Fortune 500 customers to integrate Macs and iOS devices into complex environments. He is a returned Peace Corps volunteer, serving in the Solomon Islands as a math and science teacher from 1991 to 1993.