1- // certificates .swift
1+ // CertificatesManager .swift
22import Foundation
33import Security
44import CryptoKit
5+ import Combine
56
67public enum CertificateCheckResult {
78 case incorrectPassword
@@ -19,19 +20,27 @@ public enum CertificateError: Error {
1920 case unknown
2021}
2122
22- public final class CertificatesManager {
23- // SHA256 hex from Data
24- static func sha256Hex( _ d: Data ) -> String {
23+ /// CertificatesManager handles cert extraction/checking and exposes the currently selected identity.
24+ /// Replace `SecIdentity` with your own wrapper type if you use a custom model.
25+ public final class CertificatesManager : ObservableObject {
26+ public static let shared = CertificatesManager ( )
27+ private init ( ) { }
28+
29+ // The currently selected certificate identity (nil if none chosen).
30+ @Published public var selectedCertificate : SecIdentity ? = nil
31+
32+ // MARK: - Utility: SHA256 hex
33+ public static func sha256Hex( _ d: Data ) -> String {
2534 let digest = SHA256 . hash ( data: d)
2635 return digest. map { String ( format: " %02x " , $0) } . joined ( )
2736 }
28-
29- // Export public key bytes for a certificate ( SecCertificate -> SecKey -> external representation)
37+
38+ // MARK: - Export public key bytes for a SecCertificate
3039 private static func publicKeyData( from cert: SecCertificate ) throws -> Data {
3140 guard let secKey = SecCertificateCopyKey ( cert) else {
3241 throw CertificateError . certExtractionFailed
3342 }
34-
43+
3544 var cfErr : Unmanaged < CFError > ?
3645 guard let keyData = SecKeyCopyExternalRepresentation ( secKey, & cfErr) as Data ? else {
3746 if let cfError = cfErr? . takeRetainedValue ( ) {
@@ -41,28 +50,27 @@ public final class CertificatesManager {
4150 throw CertificateError . publicKeyExportFailed ( - 1 )
4251 }
4352 }
44-
53+
4554 return keyData
4655 }
47-
48- // Extract the <plist>...</plist> portion from a .mobileprovision (PKCS7) blob,
49- // parse it to a dictionary and return SecCertificate objects from DeveloperCertificates.
56+
57+ // MARK: - Extract certificates array from mobileprovision (PKCS7) blob
5058 private static func certificatesFromMobileProvision( _ data: Data ) throws -> [ SecCertificate ] {
5159 let startTag = Data ( " <plist " . utf8)
5260 let endTag = Data ( " </plist> " . utf8)
53-
61+
5462 guard let startRange = data. range ( of: startTag) ,
5563 let endRange = data. range ( of: endTag) else {
5664 throw CertificateError . plistExtractionFailed
5765 }
58-
66+
5967 let plistData = data [ startRange. lowerBound..< endRange. upperBound]
6068 let parsed = try PropertyListSerialization . propertyList ( from: Data ( plistData) , options: [ ] , format: nil )
61-
69+
6270 guard let dict = parsed as? [ String : Any ] else {
6371 throw CertificateError . plistExtractionFailed
6472 }
65-
73+
6674 var resultCerts : [ SecCertificate ] = [ ]
6775 if let devArray = dict [ " DeveloperCertificates " ] as? [ Any ] {
6876 for item in devArray {
@@ -78,17 +86,17 @@ public final class CertificatesManager {
7886 }
7987 }
8088 }
81-
89+
8290 if resultCerts. isEmpty {
8391 throw CertificateError . noCertsInProvision
8492 }
85-
93+
8694 return resultCerts
8795 }
88-
89- /// Get the certificate's display name (subject summary)
90- public static func getCertificateName( mobileProvisionData: Data ) -> String ? {
91- // Extract the <plist>...</plist> block from the mobileprovision (PKCS7) blob
96+
97+ // MARK: - Readable display name from mobileprovision
98+ public func getCertificateName( mobileProvisionData: Data ) -> String ? {
99+ // Extract the <plist>...</plist> block
92100 let startTag = Data ( " <plist " . utf8)
93101 let endTag = Data ( " </plist> " . utf8)
94102 guard let startRange = mobileProvisionData. range ( of: startTag) ,
@@ -99,76 +107,80 @@ public final class CertificatesManager {
99107 let plistDataSlice = mobileProvisionData [ startRange. lowerBound..< endRange. upperBound]
100108 let plistData = Data ( plistDataSlice)
101109
102- // Parse plist into a dictionary
103110 guard let parsed = try ? PropertyListSerialization . propertyList ( from: plistData, options: [ ] , format: nil ) ,
104111 let dict = parsed as? [ String : Any ] else {
105112 return nil
106113 }
107114
108- // Prefer TeamName if present
109115 if let teamName = dict [ " TeamName " ] as? String , !teamName. isEmpty {
110116 return teamName
111117 }
112-
113- // Fallback to Name (string)
114118 if let name = dict [ " Name " ] as? String , !name. isEmpty {
115119 return name
116120 }
117-
118121 return nil
119122 }
120-
121- /// Top-level check: returns result
123+
124+ // MARK: - Top-level check: verify p12 matches one of the embedded certs in mobileprovision
125+ /// Returns .success(.success) if match, .success(.noMatch) if no match, or .failure(Error)
122126 public static func check( p12Data: Data , password: String , mobileProvisionData: Data ) -> Result < CertificateCheckResult , Error > {
123127 let options = [ kSecImportExportPassphrase as String : password] as CFDictionary
124128 var itemsCF : CFArray ?
125-
129+
126130 let importStatus = SecPKCS12Import ( p12Data as CFData , options, & itemsCF)
127-
131+
128132 if importStatus == errSecAuthFailed {
129133 return . success( . incorrectPassword)
130134 }
131-
135+
132136 guard importStatus == errSecSuccess, let items = itemsCF as? [ [ String : Any ] ] , items. count > 0 else {
133137 return . failure( CertificateError . p12ImportFailed ( importStatus) )
134138 }
135-
139+
136140 guard let first = items. first else {
137141 return . failure( CertificateError . identityExtractionFailed)
138142 }
139-
140- let identity = first [ kSecImportItemIdentity as String ] as! SecIdentity
141-
143+
144+ // kSecImportItemIdentity should be present
145+ guard let identityAny = first [ kSecImportItemIdentity as String ] else {
146+ return . failure( CertificateError . identityExtractionFailed)
147+ }
148+
149+ // Attempt to cast to SecIdentity
150+ guard let identity = identityAny as? SecIdentity else {
151+ return . failure( CertificateError . identityExtractionFailed)
152+ }
153+
142154 var certRef : SecCertificate ?
143155 let certStatus = SecIdentityCopyCertificate ( identity, & certRef)
144-
156+
145157 guard certStatus == errSecSuccess, let p12Cert = certRef else {
146158 return . failure( CertificateError . certExtractionFailed)
147159 }
148-
160+
149161 do {
150162 let p12PubKeyData = try publicKeyData ( from: p12Cert)
151163 let p12Hash = sha256Hex ( p12PubKeyData)
152-
164+
153165 let embeddedCerts = try certificatesFromMobileProvision ( mobileProvisionData)
154-
166+
155167 for cert in embeddedCerts {
156168 do {
157169 let embPubKeyData = try publicKeyData ( from: cert)
158170 let embHash = sha256Hex ( embPubKeyData)
159-
171+
160172 if embHash == p12Hash {
161173 return . success( . success)
162174 }
163175 } catch {
176+ // continue checking other embedded certs
164177 continue
165178 }
166179 }
167-
180+
168181 return . success( . noMatch)
169182 } catch {
170183 return . failure( error)
171184 }
172185 }
173-
174186}
0 commit comments