2

I want to fetch and display the details (such as expiry date and registered company) of my provisioning profile and distribution certificate in my app. I have already tried this but it doesn't work correctly in my app. It give nil for profilePath initially itself.

I am using swift 2.3 and Xcode 8.2.1. I have tried to mix and match that code into my app as I could not convert it to swift entirely (got stuck at sscanf method). Any help is appreciated.

rmaddy
  • 314,917
  • 42
  • 532
  • 579
Skywalker
  • 1,590
  • 1
  • 18
  • 36
  • When you get nil for profilepath, are you running on a physical device? If you are running in the simulator, there is no embedded.mobileprovision file, so you will get nil in the simulator. – wottle Mar 12 '18 at 19:54
  • @wottle Yes. I was running on simulator. Will try that. But I also need the other details and I could use the help. Thanks – Skywalker Mar 13 '18 at 01:02
  • What other details? Getting the expiration date of the embedded profile should be possible, but in certain situations may give you a false idea of when the app will no longer launch (e.g. the embedded profile that came with the app expires tomorrow, but a newer profile was installed on the device that doesn't expire for 6 months). In other words, the embedded provisioning profile may not be the only one iOS looks at when determining whether an app should run. Getting details of the cert can be more tricky - you'll likely need to use the openssl library to decode the cert. – wottle Mar 13 '18 at 13:53
  • @wottle Other details I require are expiry date and registered company of the certificate used. – Skywalker Mar 14 '18 at 03:01

1 Answers1

6

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.

wottle
  • 13,095
  • 4
  • 27
  • 68
  • Take a look at the certificate description that I print out to the console. It might give you what you need for the registered company. To get the certificate expiration, you should be able to use the openssl library to get that. I've given you the code to get the cert, you would just need to do something like this: https://stackoverflow.com/a/8903088/3708242 But I think extracting the cert from the embedded provisioning profile was the hard part. – wottle Mar 14 '18 at 13:53
  • I have some code working that I think gets the cert details, but it's in Objective-C at the moment. It is also a pretty fragile implementation, so I can't guarantee it will work in the future if Apple changes how they encode the certificate info. I'll look to convert it to swift and post it in an update. – wottle Mar 14 '18 at 17:26