Skip to content

Instantly share code, notes, and snippets.

@perlmunger
Last active September 22, 2023 14:05
Show Gist options
  • Save perlmunger/8318538a02166ab4c275789a9feb8992 to your computer and use it in GitHub Desktop.
Save perlmunger/8318538a02166ab4c275789a9feb8992 to your computer and use it in GitHub Desktop.
A wrapper around SecCertificate to simplify accessing iOS app certificate properties found in the embedded.mobileprovision file inside the bundle payload. The WrapperImplementation file shows how to extract both the provisioning profile and the details of each certificate. Works on iOS 12, iOS 13, and newer
struct SecCertificateWrapper : Comparable {
var data:Data
var cert:SecCertificate
// Initialize with a data object from the "DeveloperCertificates"
// array (see WrapperImplementation.swift)
init(data:Data) {
self.cert = SecCertificateCreateWithData(nil, data as CFData)!
// Use this later for parsing the date details from the cert
self.data = data
}
// The certificate name
var name : String {
return SecCertificateCopySubjectSummary(cert)! as String
}
// Return a tuple with both notValidBefore and notValidAfter dates
var validDates : (notValidBefore:Date, notValidAfter:Date)? {
guard let decodedString = String( data: self.data, encoding: .ascii ) else { return nil }
var foundWWDRCA = false
var notValidBeforeDate = ""
var notValidAfterDate = ""
decodedString.enumerateLines { (line, _) in
if foundWWDRCA && (notValidBeforeDate.isEmpty || notValidAfterDate.isEmpty) {
let certificateData = line.prefix(13)
if notValidBeforeDate.isEmpty && !certificateData.isEmpty {
notValidBeforeDate = String(certificateData)
} else if notValidAfterDate.isEmpty && !certificateData.isEmpty {
notValidAfterDate = String(certificateData)
}
}
if line.contains("Apple Worldwide Developer Relations Certification Authority") { foundWWDRCA = true }
}
return (self.format(notValidBeforeDate), self.format(notValidAfterDate))
}
// Some convenience properties for access to the notValidBefore and notValidAfter
// dates in various formats
var notValidAfterUnixDate : Double {
return self.validDates?.notValidAfter.timeIntervalSince1970 ?? 0
}
var notValidAfterUnixDateAsString : String {
return String(self.notValidAfterUnixDate)
}
var notValidBeforeUnixDate : Double {
return self.validDates?.notValidBefore.timeIntervalSince1970 ?? 0
}
var notValidBeforeUnixDateAsString : String {
return String(self.notValidBeforeUnixDate)
}
// MARK: - Utilities
// Implement comparable by providing a less than operator
static func < (lhs: SecCertificateWrapper, rhs: SecCertificateWrapper) -> Bool {
guard let lhsDate = lhs.validDates?.notValidAfter, let rhsDate = rhs.validDates?.notValidAfter else { return true }
return lhsDate < rhsDate
}
// Create a static data formatter just for convenience
static let certificateDateFormatter : DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyMMddHHmmssZ"
return formatter
}()
// Provide a format function to help shorten the code where date
// formatting is performed
func format(_ date:String) -> Date {
return SecCertificateWrapper.certificateDateFormatter.date(from: date)!
}
}
// This implementation file shows how to extract details for the provisioning profile
// but also demonstrates how to grab the certificate info from the DeveloperCertificates
// arry found in the plist
guard let url = Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision"), let data = try? Data(contentsOf: url), let string = String(data: data, encoding: String.Encoding.isoLatin1) else {
return
}
// Extract the relevant part of the data string from the start of the opening plist tag
// to the ending one.
let scanner = Scanner(string: string)
if scanner.scanUpTo("<plist", into: nil) {
var plistString:NSString? = ""
if scanner.scanUpTo("</plist>", into: &plistString) {
plistString = (plistString! as String) + "</plist>" as NSString
if let plistData = plistString?.data(using: String.Encoding.isoLatin1.rawValue) {
// Load the property list into a dictionary
if let mobileProvision = try? PropertyListSerialization.propertyList(from: plistData, options: [], format: nil) as? [String:Any] {
// Grab all of the profile properties
let appName = mobileProvision["AppIDName"] as! String
let creationDate = mobileProvision["CreationDate"] as! Date
let expirationDate = mobileProvision["ExpirationDate"] as! Date
let teamName = mobileProvision["TeamName"] as! String
let ttl = mobileProvision["TimeToLive"] as! Int
let name = mobileProvision["Name"] as! String
// Grab the nested entitlements dictionary and relevant properties
let entitlements = mobileProvision["Entitlements"] as! [String:Any]
var taskAllow = entitlements["get-task-allow"] as? Bool ?? false
let appId = entitlements["application-identifier"] as! String
let teamId = entitlements["com.apple.developer.team-identifier"] as! String
// Iterate through the DeveloperCertificates array sorted by notValidAfter date
// (see Comparable implementation in the SecCertificateWrapper.swift file) and
// create and print SecCertificateWrapper objects
if let certData = mobileProvision["DeveloperCertificates"] as? [Data] {
for cert in certData.map( { SecCertificateWrapper(data: $0) }).sorted() {
debugPrint("Name: \(cert.name), Not Valid After Date: \(cert.notValidAfterUnixDateAsString)")
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment