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

Commit 40c0bd5

Browse files
authored
Enhance AboutView with signing info display
Added signing information provider to display certificate and provisioning profile details in AboutView.
1 parent bf0ee1b commit 40c0bd5

1 file changed

Lines changed: 216 additions & 4 deletions

File tree

Sources/prosign/views/AboutView.swift

Lines changed: 216 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// AboutView.swift
22
import SwiftUI
3+
import Security
34

45
struct Credit: Identifiable {
56
var id = UUID()
@@ -9,7 +10,126 @@ struct Credit: Identifiable {
910
var avatarURL: URL
1011
}
1112

13+
@MainActor
14+
final class SigningInfoProvider: ObservableObject {
15+
@Published var certCommonName: String = "Unknown"
16+
@Published var certExpiry: Date? = nil
17+
18+
@Published var provName: String = "Unknown"
19+
@Published var provExpiry: Date? = nil
20+
21+
@Published var errorMessage: String? = nil
22+
23+
init() {
24+
Task { await fetchAll() }
25+
}
26+
27+
func fetchAll() async {
28+
fetchSigningCertificate()
29+
fetchEmbeddedProvision()
30+
}
31+
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"
38+
return
39+
}
40+
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)
46+
}
47+
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"
53+
return
54+
}
55+
56+
// Common name / subject summary
57+
if let name = SecCertificateCopySubjectSummary(firstCert) as String? {
58+
self.certCommonName = name
59+
}
60+
61+
// NotAfter (expiry)
62+
var valuesRef: CFDictionary?
63+
let oids = [kSecOIDX509V1ValidityNotAfter] as CFArray
64+
if SecCertificateCopyValues(firstCert, oids, &valuesRef) == errSecSuccess,
65+
let values = valuesRef as? [String: Any],
66+
let notAfterEntry = values[kSecOIDX509V1ValidityNotAfter as String] as? [String: Any],
67+
let expiry = notAfterEntry[kSecPropertyKeyValue as String] as? Date {
68+
self.certExpiry = expiry
69+
} else {
70+
// fallback: try to parse summary data (rare)
71+
self.certExpiry = nil
72+
}
73+
}
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+
}
128+
}
129+
12130
struct AboutView: View {
131+
@StateObject private var signingInfo = SigningInfoProvider()
132+
13133
private let credits: [Credit] = [
14134
Credit(
15135
name: "SuperGamer474",
@@ -45,6 +165,17 @@ struct AboutView: View {
45165
Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0"
46166
}
47167

168+
private var buildString: String {
169+
Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "1"
170+
}
171+
172+
private var dateFormatter: DateFormatter {
173+
let df = DateFormatter()
174+
df.dateStyle = .medium
175+
df.timeStyle = .short
176+
return df
177+
}
178+
48179
var body: some View {
49180
NavigationStack {
50181
List {
@@ -75,9 +206,82 @@ struct AboutView: View {
75206
.font(.title2)
76207
.fontWeight(.semibold)
77208

78-
Text("Version \(versionString)")
79-
.font(.footnote)
80-
.foregroundColor(.secondary)
209+
VStack(spacing: 2) {
210+
Text("Version \(versionString) (\(buildString))")
211+
.font(.footnote)
212+
.foregroundColor(.secondary)
213+
214+
if signingInfo.certCommonName != "Unknown" || signingInfo.certExpiry != nil || signingInfo.provName != "Unknown" || signingInfo.provExpiry != nil {
215+
// Show certificate + provisioning info
216+
VStack(spacing: 2) {
217+
Divider()
218+
HStack {
219+
VStack(alignment: .leading, spacing: 4) {
220+
Text("Signing certificate")
221+
.font(.caption)
222+
.foregroundColor(.secondary)
223+
Text(signingInfo.certCommonName)
224+
.font(.caption2)
225+
.lineLimit(1)
226+
if let certExpiry = signingInfo.certExpiry {
227+
Text("Expires: \(dateFormatter.string(from: certExpiry))")
228+
.font(.caption2)
229+
.foregroundColor(.secondary)
230+
} else {
231+
Text("Expires: Unknown")
232+
.font(.caption2)
233+
.foregroundColor(.secondary)
234+
}
235+
}
236+
Spacer()
237+
}
238+
.padding(.top, 6)
239+
240+
HStack {
241+
VStack(alignment: .leading, spacing: 4) {
242+
Text("Provisioning profile")
243+
.font(.caption)
244+
.foregroundColor(.secondary)
245+
Text(signingInfo.provName)
246+
.font(.caption2)
247+
.lineLimit(1)
248+
if let provExpiry = signingInfo.provExpiry {
249+
Text("Expires: \(dateFormatter.string(from: provExpiry))")
250+
.font(.caption2)
251+
.foregroundColor(.secondary)
252+
} else {
253+
Text("Expires: Unknown")
254+
.font(.caption2)
255+
.foregroundColor(.secondary)
256+
}
257+
}
258+
Spacer()
259+
}
260+
261+
// Show a match / mismatch indicator
262+
HStack {
263+
if let certExpiry = signingInfo.certExpiry, let provExpiry = signingInfo.provExpiry {
264+
if Calendar.current.compare(certExpiry, to: provExpiry, toGranularity: .second) == .orderedSame {
265+
Label("Cert and provision match ✅", systemImage: "checkmark.shield")
266+
.font(.caption2)
267+
.foregroundColor(.green)
268+
} else {
269+
Label("Cert / provision dates differ ⚠️", systemImage: "exclamationmark.triangle")
270+
.font(.caption2)
271+
.foregroundColor(.yellow)
272+
}
273+
} else {
274+
// Can't fully compare
275+
Label("Comparison unavailable", systemImage: "questionmark.circle")
276+
.font(.caption2)
277+
.foregroundColor(.secondary)
278+
}
279+
Spacer()
280+
}
281+
.padding(.top, 6)
282+
}
283+
}
284+
}
81285
}
82286
.frame(maxWidth: .infinity)
83287
.padding(.vertical, 20)
@@ -88,8 +292,17 @@ struct AboutView: View {
88292
CreditRow(credit: c)
89293
}
90294
}
295+
296+
if let err = signingInfo.errorMessage {
297+
Section(header: Text("Debug")) {
298+
Text(err)
299+
.font(.footnote)
300+
.foregroundColor(.red)
301+
}
302+
}
91303
}
92304
.listStyle(InsetGroupedListStyle())
305+
.navigationTitle("About")
93306
}
94307
}
95308
}
@@ -144,5 +357,4 @@ struct AboutView_Previews: PreviewProvider {
144357
static var previews: some View {
145358
AboutView()
146359
}
147-
148360
}

0 commit comments

Comments
 (0)