Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
979 views
in Technique[技术] by (71.8m points)

ios - Get details of provisioning profile and certificate at runtime

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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...