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:
- 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).
- 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:
- Need to figure out the big ass number to identify the developer ID.
- 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.
- 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")
Recent Comments