Skip to content
This repository was archived by the owner on Mar 7, 2026. It is now read-only.

Commit c85a086

Browse files
authored
Refactor SigningInfoProvider to streamline data fetching
Refactor SigningInfoProvider to combine fetching of provisioning and certificate information into a single method. Update error handling and parsing logic for embedded.mobileprovision.
1 parent 9014641 commit c85a086

1 file changed

Lines changed: 82 additions & 81 deletions

File tree

Sources/prosign/views/AboutView.swift

Lines changed: 82 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -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

130134
struct 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

Comments
 (0)