@@ -25,106 +25,110 @@ final class SigningInfoProvider: ObservableObject {
2525 }
2626
2727 func fetchAll( ) async {
28- fetchSigningCertificate ( )
29- fetchEmbeddedProvision ( )
28+ await fetchEmbeddedProvisionAndCert ( )
3029 }
3130
32- // MARK: - Certificate (from code signature)
33- private func fetchSigningCertificate ( ) {
34- var code : SecCode ?
35- let status = SecCodeCopySelf ( [ ] , & code )
36- guard status == errSecSuccess , let codeUnwrapped = code else {
37- self . errorMessage = " Could not read code object "
31+ /// Reads embedded.mobileprovision, extracts provisioning Name + ExpirationDate,
32+ /// and also extracts the first DeveloperCertificates entry (DER) and reads its CN + expiry.
33+ private func fetchEmbeddedProvisionAndCert ( ) async {
34+ guard let provPath = Bundle . main . path ( forResource : " embedded " , ofType : " mobileprovision " ) else {
35+ // Not found: App Store builds and the Simulator typically won't include it.
36+ self . errorMessage = nil
3837 return
3938 }
4039
41- var signingInfoRef : CFDictionary ?
42- let infoStatus = SecCodeCopySigningInformation ( codeUnwrapped, SecCSFlags ( rawValue: kSecCSSigningInformation) , & signingInfoRef)
43- if infoStatus != errSecSuccess {
44- // Try with empty flags if the constant isn't available
45- let _ = SecCodeCopySigningInformation ( codeUnwrapped, [ ] , & signingInfoRef)
40+ do {
41+ let data = try Data ( contentsOf: URL ( fileURLWithPath: provPath) )
42+ guard let str = String ( data: data, encoding: . utf8) else {
43+ self . errorMessage = " embedded.mobileprovision decoding failed "
44+ return
45+ }
46+
47+ // Extract the plist XML segment out of the CMS envelope
48+ guard let startRange = str. range ( of: " <?xml " ) ,
49+ let endRange = str. range ( of: " </plist> " ) else {
50+ // fallback: try to find plist bytes inside the raw data
51+ if let start = data. range ( of: Data ( " <?xml " . utf8) ) ,
52+ let end = data. range ( of: Data ( " </plist> " . utf8) ) {
53+ let plistData = data [ start. lowerBound... end. upperBound]
54+ try parseProvisionPlist ( Data ( plistData) )
55+ } else {
56+ self . errorMessage = " No plist found inside embedded.mobileprovision "
57+ }
58+ return
59+ }
60+
61+ let plistString = String ( str [ startRange. lowerBound... endRange. upperBound] )
62+ if let plistData = plistString. data ( using: . utf8) {
63+ try parseProvisionPlist ( plistData)
64+ } else {
65+ self . errorMessage = " Failed to re-encode plist string "
66+ }
67+ } catch {
68+ self . errorMessage = " Failed to read embedded.mobileprovision: \( error. localizedDescription) "
69+ }
70+ }
71+
72+ private func parseProvisionPlist( _ plistData: Data ) throws {
73+ let plist = try PropertyListSerialization . propertyList ( from: plistData, options: [ ] , format: nil )
74+ guard let dict = plist as? [ String : Any ] else { return }
75+
76+ // Provisioning info
77+ if let name = dict [ " Name " ] as? String {
78+ self . provName = name
79+ }
80+ if let expiry = dict [ " ExpirationDate " ] as? Date {
81+ self . provExpiry = expiry
82+ } else if let expiryStr = dict [ " ExpirationDate " ] as? String {
83+ // attempt ISO8601 parse as a fallback
84+ let df = ISO8601DateFormatter ( )
85+ if let d = df. date ( from: expiryStr) { self . provExpiry = d }
86+ }
87+
88+ // DeveloperCertificates -> array of DER blobs (NSData)
89+ if let devCerts = dict [ " DeveloperCertificates " ] as? [ Any ] , !devCerts. isEmpty {
90+ // Try the first certificate
91+ for raw in devCerts {
92+ if let certData = raw as? Data {
93+ processCertificateDER ( certData)
94+ break
95+ } else if let nsdata = raw as? NSData {
96+ processCertificateDER ( nsdata as Data )
97+ break
98+ } else if let b64 = raw as? String , let decoded = Data ( base64Encoded: b64) {
99+ processCertificateDER ( decoded)
100+ break
101+ }
102+ }
103+ } else {
104+ // No developer certs in the profile (possible for certain distribution types)
46105 }
106+ }
47107
48- guard let signingInfo = signingInfoRef as? [ String : Any ] ,
49- let certs = signingInfo [ kSecCodeInfoCertificates as String ] as? [ SecCertificate ] ,
50- let firstCert = certs. first
51- else {
52- self . errorMessage = " No certificate in signing info "
108+ private func processCertificateDER( _ der: Data ) {
109+ guard let secCert = SecCertificateCreateWithData ( nil , der as CFData ) else {
110+ self . errorMessage = " Failed to create SecCertificate from DER "
53111 return
54112 }
55113
56- // Common name / subject summary
57- if let name = SecCertificateCopySubjectSummary ( firstCert ) as String ? {
114+ // Common name / summary (works on iOS)
115+ if let name = SecCertificateCopySubjectSummary ( secCert ) as String ? {
58116 self . certCommonName = name
59117 }
60118
61- // NotAfter (expiry)
119+ // Try to get NotAfter (expiry) using SecCertificateCopyValues (available on iOS )
62120 var valuesRef : CFDictionary ?
63121 let oids = [ kSecOIDX509V1ValidityNotAfter] as CFArray
64- if SecCertificateCopyValues ( firstCert , oids, & valuesRef) == errSecSuccess ,
65- let values = valuesRef as? [ String : Any ] ,
122+ let status = SecCertificateCopyValues ( secCert , oids, & valuesRef)
123+ if status == errSecSuccess , let values = valuesRef as? [ String : Any ] ,
66124 let notAfterEntry = values [ kSecOIDX509V1ValidityNotAfter as String ] as? [ String : Any ] ,
67125 let expiry = notAfterEntry [ kSecPropertyKeyValue as String ] as? Date {
68126 self . certExpiry = expiry
69127 } else {
70- // fallback: try to parse summary data (rare)
128+ // If SecCertificateCopyValues didn't return a Date (rare), leave nil.
71129 self . certExpiry = nil
72130 }
73131 }
74-
75- // MARK: - embedded.mobileprovision (from bundle)
76- private func fetchEmbeddedProvision( ) {
77- guard let provPath = Bundle . main. path ( forResource: " embedded " , ofType: " mobileprovision " ) else {
78- // Not found (e.g. App Store app or simulator). Keep defaults.
79- return
80- }
81-
82- do {
83- let data = try Data ( contentsOf: URL ( fileURLWithPath: provPath) )
84- guard let str = String ( data: data, encoding: . utf8) else { return }
85-
86- // The mobileprovision is a CMS envelope containing a plist. Extract plist XML segment.
87- if let startRange = str. range ( of: " <?xml " ) ,
88- let endRange = str. range ( of: " </plist> " ) {
89- let plistString = String ( str [ startRange. lowerBound... endRange. upperBound] )
90- if let plistData = plistString. data ( using: . utf8) {
91- let plist = try PropertyListSerialization . propertyList ( from: plistData, options: [ ] , format: nil )
92- if let dict = plist as? [ String : Any ] {
93- if let name = dict [ " Name " ] as? String {
94- self . provName = name
95- }
96- if let expiry = dict [ " ExpirationDate " ] as? Date {
97- self . provExpiry = expiry
98- } else if let expiryStr = dict [ " ExpirationDate " ] as? String {
99- // Some formats might return a string — try ISO8601
100- let df = ISO8601DateFormatter ( )
101- if let d = df. date ( from: expiryStr) {
102- self . provExpiry = d
103- }
104- }
105- }
106- }
107- } else {
108- // if no plist detected, try scanning bytes for plist start/end bytes
109- // (some profiles are binary or slightly different) — try a Data approach
110- if let plistRangeStart = data. range ( of: Data ( " <?xml " . utf8) ) ,
111- let plistRangeEnd = data. range ( of: Data ( " </plist> " . utf8) ) {
112- let plistData = data [ plistRangeStart. lowerBound... plistRangeEnd. upperBound]
113- let plist = try PropertyListSerialization . propertyList ( from: plistData, options: [ ] , format: nil )
114- if let dict = plist as? [ String : Any ] {
115- if let name = dict [ " Name " ] as? String {
116- self . provName = name
117- }
118- if let expiry = dict [ " ExpirationDate " ] as? Date {
119- self . provExpiry = expiry
120- }
121- }
122- }
123- }
124- } catch {
125- self . errorMessage = " Failed to read embedded.mobileprovision: \( error. localizedDescription) "
126- }
127- }
128132}
129133
130134struct AboutView : View {
@@ -212,7 +216,6 @@ struct AboutView: View {
212216 . foregroundColor ( . secondary)
213217
214218 if signingInfo. certCommonName != " Unknown " || signingInfo. certExpiry != nil || signingInfo. provName != " Unknown " || signingInfo. provExpiry != nil {
215- // Show certificate + provisioning info
216219 VStack ( spacing: 2 ) {
217220 Divider ( )
218221 HStack {
@@ -258,7 +261,6 @@ struct AboutView: View {
258261 Spacer ( )
259262 }
260263
261- // Show a match / mismatch indicator
262264 HStack {
263265 if let certExpiry = signingInfo. certExpiry, let provExpiry = signingInfo. provExpiry {
264266 if Calendar . current. compare ( certExpiry, to: provExpiry, toGranularity: . second) == . orderedSame {
@@ -271,7 +273,6 @@ struct AboutView: View {
271273 . foregroundColor ( . yellow)
272274 }
273275 } else {
274- // Can't fully compare
275276 Label ( " Comparison unavailable " , systemImage: " questionmark.circle " )
276277 . font ( . caption2)
277278 . foregroundColor ( . secondary)
0 commit comments