Get the Expiration Date of a Provisioning Profile at Run-Time

Get the EXPIRATION date of a Provisioning Profile at Run-time?

Swift version:

// Returns `nil` if it fails
private func getProvisioningProfileExpirationDateAsString() -> String? {
guard
let profilePath = Bundle.main.path(forResource: "embedded", ofType: "mobileprovision"),
let profileData = try? Data(contentsOf: URL(fileURLWithPath: profilePath)),
// Note: We use `NSString` instead of `String`, because it makes it easier working with regex, ranges, substring etc.
let profileNSString = NSString(data: profileData, encoding: String.Encoding.ascii.rawValue)
else {
print("WARNING: Could not find or read `embedded.mobileprovision`. If running on Simulator, there are no provisioning profiles.")
return nil
}

// NOTE: We have the `[\\W]*?` check to make sure that variations in number of tabs or new lines in the future does not influence the result.
guard let regex = try? NSRegularExpression(pattern: "<key>ExpirationDate</key>[\\W]*?<date>(.*?)</date>", options: []) else {
print("Warning: Could not create regex.")
return nil
}

let regExMatches = regex.matches(in: profileNSString as String, options: [], range: NSRange(location: 0, length: profileNSString.length))

// NOTE: range `0` corresponds to the full regex match, so to get the first capture group, we use range `1`
guard let rangeOfCapturedGroupForDate = regExMatches.first?.range(at: 1) else {
print("Warning: Could not find regex match or capture group.")
return nil
}

let dateAsString = profileNSString.substring(with: rangeOfCapturedGroupForDate)
return dateAsString
}

Get details of provisioning profile and certificate at runtime

I don't have access to Xcode 8 / Swift 3.2, but here is the code needed to do what you want in Swift 4. I've tested it on a couple of profiles / certs I have available to me, and it gets the information you are requesting.

Provisioning Profile

func getProvisioningProfileExpirationDate() -> Date?
{
self.getCertificateExpirationDate()

let profilePath: String? = Bundle.main.path(forResource: "embedded", ofType: "mobileprovision")
if( profilePath != nil )
{
let plistData = NSData(contentsOfFile: profilePath!)
let plistDataString = String(format: "%@", plistData!)
var plistString: String = extractPlist(fromMobileProvisionDataString:plistDataString)

let pattern = "<key>ExpirationDate</key>.*<date>(.*)</date>"
let regex = try! NSRegularExpression(pattern: pattern, options: .caseInsensitive)
let textCheckingResult : NSTextCheckingResult = regex.firstMatch(in: plistString, options: NSRegularExpression.MatchingOptions(rawValue: UInt(0)), range: NSMakeRange(0, plistString.characters.count))!
let matchRange : NSRange = textCheckingResult.range(at: 1)
let expirationDateString : String = (plistString as NSString).substring(with: matchRange)

let dateFormatter = DateFormatter()
dateFormatter.locale = Locale.current
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
print( "Profile expires: \(dateFormatter.date(from: expirationDateString)!)" )

return dateFormatter.date(from: expirationDateString)!

}

return nil
}

We need to do some manipulation, since the embedded.mobileprovision file is not readable without converting it from hex, and then pulling out just the stuff between the plist tags.

func extractPlist( fromMobileProvisionDataString:String ) -> String
{
// Remove brackets at beginning and end
var range = Range(NSMakeRange(0, 1), in: fromMobileProvisionDataString)
var plistDataString = fromMobileProvisionDataString.replacingCharacters(in:range!, with: "")
range = Range(NSMakeRange(plistDataString.count-1, 1), in: plistDataString)
plistDataString.replaceSubrange(range!, with: "")

// Remove spaces
plistDataString = plistDataString.replacingOccurrences(of: " ", with: "")

// convert hex to ascii
let profileText = hexStringtoAscii( plistDataString )

// I tried using regular expressions and normal NSString operations to get this, but it simply wouldn't work, so I went with this ugly method.
// return extractPlistText(fromProfileString:profileText)

// Remove whitespaces and new lines characters and splits into individual lines.
let profileWords = profileText.components(separatedBy: CharacterSet.newlines)

var plistString = "";
var inPlist = false;
for word in profileWords
{
if( word.contains("<plist") ) { inPlist = true }

if( inPlist ) { plistString.append(" "); plistString.append( word ) }

if (word.contains("</plist")) { inPlist = false }
}
return plistString;
}

func hexStringtoAscii(_ hexString : String) -> String {

let pattern = "(0x)?([0-9a-f]{2})"
let regex = try! NSRegularExpression(pattern: pattern, options: .caseInsensitive)
let nsString = hexString as NSString
let matches = regex.matches(in: hexString, options: [], range: NSMakeRange(0, nsString.length))
let characters = matches.map {
Character(UnicodeScalar(UInt32(nsString.substring(with: $0.range(at: 2)), radix: 16)!)!)
}
return String(characters)
}

I've verified this works to pull out the expiration date from the embedded.mobileprovision file on a physical device. It would be trivial to pull out other elements from the profile plist data.

Certificate:

To get the certificate information, I was able to get it to work using the following:

func getCertificateExpirationDate() -> Date?
{

let profilePath: String? = Bundle.main.path(forResource: "embedded", ofType: "mobileprovision")
if( profilePath != nil )
{
let plistData = NSData(contentsOfFile: profilePath!)
let plistDataString = String(format: "%@", plistData!)
var plistString: String = extractPlist(fromMobileProvisionDataString:plistDataString)

// Trying to extract thecert information aswell, but haven't gotten it to work.

let certPattern = "<key>DeveloperCertificates</key>\\s*<array>\\s*<data>([^<]*)</data>"
let certRegex = try! NSRegularExpression(pattern: certPattern, options: .caseInsensitive)
let certCheckingResult : NSTextCheckingResult = certRegex.firstMatch(in: plistString, options: NSRegularExpression.MatchingOptions(rawValue: UInt(0)), range: NSMakeRange(0, plistString.characters.count))!
let certMatchRange : NSRange = certCheckingResult.range(at: 1)
let certDataString : String = (plistString as NSString).substring(with: certMatchRange)

let decodedData = Data(base64Encoded: certDataString, options: [])

let decodedString = String( data: decodedData!, encoding: .ascii )

let cfData = decodedData as! CFData
let certificate: SecCertificate = SecCertificateCreateWithData(nil, cfData)!
var description: CFString = SecCertificateCopySubjectSummary(certificate)!
print( "Certificate name: \(description)")

let certDate = self.extractCertExpirationDate(fromDecodedCertDataString: decodedString!)
print( "Certificate expires: \(certDate)")

let certOrg = self.extractCertOrg(fromDecodedCertDataString: decodedString!)
print( "Certificate organization: \(certOrg)")

return certDate
}
return nil
}

func extractCertExpirationDate( fromDecodedCertDataString: String ) -> Date
{
// Remove new lines characters and split into individual lines.
let certWords = fromDecodedCertDataString.components(separatedBy: CharacterSet.newlines)

var foundWWDRCA = false;
var certStartDate = ""
var certEndDate = ""
var certOrg = ""

for word in certWords
{
if( foundWWDRCA && (certStartDate.isEmpty || certEndDate.isEmpty))
{
var certData = word.prefix(13)
if( certStartDate.isEmpty && !certData.isEmpty )
{
certStartDate = String( certData );
}
else if( certEndDate.isEmpty && !certData.isEmpty )
{
certEndDate = String( certData );
}
}
if( word.contains("Apple Worldwide Developer Relations Certification Authority") ) { foundWWDRCA = true }
}

let dateFormatter = DateFormatter()
dateFormatter.locale = Locale.current
dateFormatter.dateFormat = "yyMMddHHmmssZ"
return dateFormatter.date(from: certEndDate)!
}

func extractCertOrg( fromDecodedCertDataString: String ) -> String
{
// Remove new lines characters and split into individual lines.
let certWords = fromDecodedCertDataString.components(separatedBy: CharacterSet.newlines)

var foundWWDRCA = false;
var certStartDate = ""
var certEndDate = ""
var certOrg = ""

for word in certWords
{
if( foundWWDRCA && (certStartDate.isEmpty || certEndDate.isEmpty))
{
var certData = word.prefix(13)
if( certStartDate.isEmpty && !certData.isEmpty )
{
certStartDate = String( certData );
}
else if( certEndDate.isEmpty && !certData.isEmpty )
{
certEndDate = String( certData );
}
}
else if( foundWWDRCA && word.contains("\u{17}") && certOrg.isEmpty)
{
var orgString = word.suffix(word.count-1)
certOrg = String( orgString.prefix(orgString.count - 1))
}

if( word.contains("Apple Worldwide Developer Relations Certification Authority") ) { foundWWDRCA = true }
}
return certOrg
}

Note that this only checks the provisioning profile / cert that is bundled with the app when installed. It will not check other, potentially valid, profiles on the device. So even if the embedded profile has expired, there is a chance the app could still run, if there are other mechanisms for installing profiles on the device that are used (device management, installing another app with newer wildcard provisioning profile, etc. ). However, if the certificate used to sign the app has expired, it will not run, even if a newer provisioning profile exists on the device.

For the certificate information, I still think the safest way would be to use the openssl library to decript the DER encoded x509 certificate, but the parsing I was able to do after base64 decoding the certificate data seems to pull the information you need.

List all the Provisional Profiles on the device at Runtime and get their EXPIRY dates

You won't be able to access the other provisioning profiles installed on the device because of iOS sandboxing. You will only get the oldPP because that is the only .mobileprovision file in the applications bundle. I do not believe Apple will ever let apps query for PPs on the device, as this could be used to get information about other apps installed on the device.

I would recommend simply repackaging your enterprise app each time the PP is expiring, so that the embedded.mobileprovision file is always the latest. Then you can use the notification feature (I have a way to get the pp expiration as well as the certificate expiration here) to inform the user that they need to install a new version of the app. Even if there are no code changes, the ipa will at least have the bew embedded.mobileprovision file in it.

And if you use MDM, rather than pushing the new provisioning profile to the device, you can now push the new ipa, with new embedded.mobileprovision file to the devices. If you are asking them to update their provisioning profile, it's probably just as simple to provide them a link to the .ipa file.

Unfortunately, it's unlikely that Apple will allow developers toquery other PPs installed on the iOS device.

How long does a provisioning profile last for testing?

Till the provisioning profile expires...You can check out when they expire in the provisioning portal, or if it is installed on the device, under settings->general->profiles

iOS Provisioning profile expiring after 6 days

I'm afraid it's not possible to have an app installed 'forever' on your device if it's not installed via the App Store, it will need 're-provisioning' occasionally in order to still be valid.

The reason your app expires after 6 days is that this is the time limit on applications deployed to devices using Apple's free developer program. There are also a few other limitations relating to capabilities enabled via entitlements.

The paid developer program increases this time limit to 1 year. Other distribution methods (such as Enterprise distribution) also have similar time limits.

How to get a longer provisioning profile?

You'll need to upgrade to a paid developer account. With a paid account, you can create a certificate that will have a 3 year expiration, and provisioning profile that will have a 1 year expiration date. So basically it will get you a build that will last for up to a year ("up to" because unless you create the provisioning profile on the day you do the build, it will be less than 1 year).

Expired Provisioning-profiles update

So, unfortunately, none of the existing deployed apps will run now, so you will not be able to use the self-updating logic in the app to correct this. This is up to the developers to keep track of expiring profiles and certificates and ensure they get updated as needed.

All of the information below assumes you are using an Enterprise Distribution Profile to build these iOS apps.

You should note is that there are two things that can expire: the provisioning profile and the certificate.

Expiring Provisioning Profile:

Typically the provisioning profile expiring is easier to deal with, as you only need to get a new profile on the device. Technically, doing a new build with a new provisioning profile will do this, but there are other ways. For example, if these are managed, company devices, you can typically use the MDM software to push a new profile to the devices, without requiring a new .ipa (app binary) to be installed on the device). Also, if you use wildcard app ids in your provisioning profile, installing another device with a newer provisioning profile will also work (although this is a bit unorthodox). Long story, short: You need to get the new profile on the device. At this point, that is likely through you informing users they need to go re-download a new version of the app.

Expiring Certificate

If the certificate used to code sign the application is expiring, you will need to generate a new binary with the new certificate. There are ways to resign an existing ipa, but if you have the source code, it is easier to just re-build with the new certificate. The good news is that the certificate only expires every 3 years for an enterprise distribution certificate (vs. every 1 year for the provisioning profile). So this is not needed as often. But this will certainly require you to re-create a new binary signed with the new certificate.

Preventing This From Recurring

If you rely on the app to check for updates and self update, you need to make sure a new version gets published well enough in advance that users will launch the app in the time between the new version being released, and the profile or cert expiration. This length of time depends on your app. If it's a corporate app that people use daily, you can probably get by with 2 or 3 weeks (for people who are out of the office). If it's seldom used, I would consider deploying a new version, with a new provisioning profile at least 3-6 months in advance of the old one expiring. This takes planning and reminders to ensure you don't miss the timing window.

Also of note, if you are using automatic code signing, you lose some control over when a new profile is generated and used, as well as the certificates. That's why I recommend for enterprise apps to use manual code signing settings to allow you to be very explicit with which provisioning profiles are used, as well as the cert. Also, delete all older profiles from the Mac when doing a build to ensure you are using the right profile (you can have many profiles on the Mac for the same application at any given time. You can find them here: https://stackoverflow.com/a/45642752/3708242). It's risky to assume Xcode will pick the most recent one.

Enterprise Distribution Provisioning Profile Expiration

So generating a new provisioning profile will not invalidate any of the apps out there on devices. Simply generate the new provisioning profile, build a new version of the app with the new provisioning profile, and just make sure all your users / testers update to the new version of the app.

Alternatively, you could generate the provisioning profile and then distribute the profile to all the devices through MDM (if you're using an MDM solution) or by email (not a great experience). Basically the app will continue to run as long as the new provisioning profile gets on the device before the old one expires, whether that's through MDM, manually, or by installing a new version of the app with the provisioning profile in the .app payload. Or if your users download any app with the new provisioning profile, assuming that provisioning profile is set up with a wildcard app ID, that will also correct it (see information about that here: https://stackoverflow.com/a/29121777/3708242).

Basically, you need to do something before the provisioning profile expires (the sooner the better) and get that new provisioning profiles on the device (through one of the options above).

Having test app provisioning profile not expire

You cannot set provisioning profiles to never expire. Development provisioning profiles will expire after a set amount of time. For a free developer account with Apple, your profiles will be set to expire after 30 days. If you have a paid developer account, the profiles will last for a year from when it is created. Note that a new profile isn't created every time you build, so you have to keep an eye on the profile expiration date and generate a new one when you get to 10 months or so.

Apple will not let you go longer than this, as it would allow / encourage other distribution mechanisms. If developers could build an app and distribute it to other peoples' devices, and those apps could run indefinitely, someone would quickly develop a 3rd party app store and Apple would lose control of its ecosystem.

FYI - If a provisioning profile expires, the app will launch briefly, then shut down when iOS realizes that there is a code signing problem (also happens if your certificate has been revoked or has expired). You can check for sure by plugging the device into your Mac and monitoring the device console when you attempt to launch the app. You will likely see a code signing error in the logs.



Related Topics



Leave a reply



Submit