Apple,Articles,Deployment,iOS,Management,Security June 5, 2012 at 12:00 pm

Re-Signing iOS apps

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:

t:Payload tperfitt$ codesign -dvvv AngryBirdsiPad.app/
Executable=/private/tmp/Payload/AngryBirdsiPad.app/AngryBirdsiPad
Identifier=com.chillingo.angrybirdsipad
Format=bundle with Mach-O thin (armv7)
CodeDirectory v=20100 size=19077 flags=0x0(none) hashes=945+5 location=embedded
Hash type=sha1 size=20
CDHash=ade2c9b4f6cf7fe7c844d2ca482156cd558b59dc
Signature size=3582
Authority=Apple iPhone OS Application Signing
Authority=Apple iPhone Certification Authority
Authority=Apple Root CA
Signed Time=Aug 24, 2011 9:41:26 PM
Info.plist entries=29
Sealed Resources rules=5 files=712
Internal requirements count=2 size=320
)

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:

/usr/bin/codesign --force --sign 929ddfe9869f90aebf6062ae80fbfb3e "--resource-rules=/Users/tperfitt/Library/Developer/Xcode/DerivedData/Cert-aqprpgslvfpcxxdbzmswbhpkweqh/Build/Products/Debug-iphoneos/SSL Detective.app/ResourceRules.plist" --entitlements "/Users/tperfitt/Library/Developer/Xcode/DerivedData/Cert-aqprpgslvfpcxxdbzmswbhpkweqh/Build/Intermediates/Cert.build/Debug-iphoneos/SSL Detective.build/SSL Detective.xcent" "/Users/tperfitt/Library/Developer/Xcode/DerivedData/Cert-aqprpgslvfpcxxdbzmswbhpkweqh/Build/Products/Debug-iphoneos/SSL Detective.app"

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:

find-identity -v -p codesigning
3) AC9A9D1DD1C311BEFCB81D69060104A89934AE75 "iPhone Developer: Timothy Perfitt (XYZZYXYZZY)"
4) C4ED92CD0D3E9062E678A64FC5471B0A5009E844 "iPhone Distribution: EXAMPLE Software, Inc."
5) 986848C3D7302D881EAC158298FDC1728A4AD336 "3rd Party Mac Developer Application: EXAMPLE Software, Inc."
6) 4F74F28D731385733D761416DDC02774A6816AC1 "Developer ID Application: EXAMPLE Software, Inc."
7) F8E869B7AB277981741F21CF599365CE12507917 "Mac Developer: Timothy Perfitt (XYZZYXYZZY)"

(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

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.

More Posts - Website

Follow Me:
Twitter

Leave a reply

You must be logged in to post a comment.