1+ // certificates.swift
12import Foundation
23import Security
34import CryptoKit
4- import Combine
55
66public enum CertificateCheckResult {
77 case incorrectPassword
@@ -19,152 +19,156 @@ public enum CertificateError: Error {
1919 case unknown
2020}
2121
22- public final class CertificatesManager : ObservableObject {
23- public static let shared = CertificatesManager ( )
24- private init ( ) { }
25-
26- /// Returns the currently selected SecIdentity by loading from the selected folder in UserDefaults
27- public var selectedIdentity : SecIdentity ? {
28- guard let folderName = UserDefaults . standard. string ( forKey: " selectedCertificateFolder " ) ,
29- !folderName. isEmpty else {
30- return nil
31- }
32-
33- let certDir = CertificateFileManager . shared. certificatesDirectory
34- . appendingPathComponent ( folderName)
35-
36- let p12URL = certDir. appendingPathComponent ( " certificate.p12 " )
37- let pwURL = certDir. appendingPathComponent ( " password.txt " )
38-
39- guard let p12Data = try ? Data ( contentsOf: p12URL) ,
40- let passwordRaw = try ? String ( contentsOf: pwURL, encoding: . utf8) else {
41- return nil
42- }
43-
44- let password = passwordRaw. trimmingCharacters ( in: . whitespacesAndNewlines)
45-
46- var items : CFArray ?
47- let options = [ kSecImportExportPassphrase as String : password] as CFDictionary
48-
49- let status = SecPKCS12Import ( p12Data as CFData , options, & items)
50-
51- guard status == errSecSuccess,
52- let cfItems = items as? [ [ String : Any ] ] ,
53- let identityAny = cfItems. first ? [ kSecImportItemIdentity as String ] ,
54- CFGetTypeID ( identityAny as CFTypeRef ) == SecIdentityGetTypeID ( ) else {
55- return nil
56- }
57-
58- let identity = identityAny as! SecIdentity
59- return identity
60- }
61-
62- // MARK: - SHA256 hex
63- public static func sha256Hex( _ d: Data ) -> String {
22+ public final class CertificatesManager {
23+ // SHA256 hex from Data
24+ static func sha256Hex( _ d: Data ) -> String {
6425 let digest = SHA256 . hash ( data: d)
6526 return digest. map { String ( format: " %02x " , $0) } . joined ( )
6627 }
67-
68- // MARK: - Public key data
28+
29+ // Export public key bytes for a certificate (SecCertificate -> SecKey -> external representation)
6930 private static func publicKeyData( from cert: SecCertificate ) throws -> Data {
7031 guard let secKey = SecCertificateCopyKey ( cert) else {
7132 throw CertificateError . certExtractionFailed
7233 }
73- var error : Unmanaged < CFError > ?
74- guard let data = SecKeyCopyExternalRepresentation ( secKey, & error) as Data ? else {
75- let code = error. map { OSStatus ( CFErrorGetCode ( $0. takeRetainedValue ( ) ) ) } ?? - 1
76- throw CertificateError . publicKeyExportFailed ( code)
34+
35+ var cfErr : Unmanaged < CFError > ?
36+ guard let keyData = SecKeyCopyExternalRepresentation ( secKey, & cfErr) as Data ? else {
37+ if let cfError = cfErr? . takeRetainedValue ( ) {
38+ let code = CFErrorGetCode ( cfError)
39+ throw CertificateError . publicKeyExportFailed ( OSStatus ( code) )
40+ } else {
41+ throw CertificateError . publicKeyExportFailed ( - 1 )
42+ }
7743 }
78- return data
44+
45+ return keyData
7946 }
80-
81- // MARK: - Extract certs from mobileprovision
47+
48+ // Extract the <plist>...</plist> portion from a .mobileprovision (PKCS7) blob,
49+ // parse it to a dictionary and return SecCertificate objects from DeveloperCertificates.
8250 private static func certificatesFromMobileProvision( _ data: Data ) throws -> [ SecCertificate ] {
8351 let startTag = Data ( " <plist " . utf8)
8452 let endTag = Data ( " </plist> " . utf8)
85-
53+
8654 guard let startRange = data. range ( of: startTag) ,
8755 let endRange = data. range ( of: endTag) else {
8856 throw CertificateError . plistExtractionFailed
8957 }
90-
58+
9159 let plistData = data [ startRange. lowerBound..< endRange. upperBound]
9260 let parsed = try PropertyListSerialization . propertyList ( from: Data ( plistData) , options: [ ] , format: nil )
93-
94- guard let dict = parsed as? [ String : Any ] ,
95- let devArray = dict [ " DeveloperCertificates " ] as? [ Any ] else {
96- throw CertificateError . noCertsInProvision
61+
62+ guard let dict = parsed as? [ String : Any ] else {
63+ throw CertificateError . plistExtractionFailed
9764 }
98-
99- var result : [ SecCertificate ] = [ ]
100- for item in devArray {
101- if let certData = item as? Data ,
102- let cert = SecCertificateCreateWithData ( nil , certData as CFData ) {
103- result. append ( cert)
104- } else if let base64 = item as? String ,
105- let certData = Data ( base64Encoded: base64) ,
106- let cert = SecCertificateCreateWithData ( nil , certData as CFData ) {
107- result. append ( cert)
65+
66+ var resultCerts : [ SecCertificate ] = [ ]
67+ if let devArray = dict [ " DeveloperCertificates " ] as? [ Any ] {
68+ for item in devArray {
69+ if let certData = item as? Data {
70+ if let secCert = SecCertificateCreateWithData ( nil , certData as CFData ) {
71+ resultCerts. append ( secCert)
72+ }
73+ } else if let base64String = item as? String ,
74+ let certData = Data ( base64Encoded: base64String) {
75+ if let secCert = SecCertificateCreateWithData ( nil , certData as CFData ) {
76+ resultCerts. append ( secCert)
77+ }
78+ }
10879 }
10980 }
110-
111- guard !result. isEmpty else { throw CertificateError . noCertsInProvision }
112- return result
81+
82+ if resultCerts. isEmpty {
83+ throw CertificateError . noCertsInProvision
84+ }
85+
86+ return resultCerts
11387 }
114-
115- // MARK: - Display name from provision
116- public func getCertificateName( mobileProvisionData: Data ) -> String ? {
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
11792 let startTag = Data ( " <plist " . utf8)
11893 let endTag = Data ( " </plist> " . utf8)
11994 guard let startRange = mobileProvisionData. range ( of: startTag) ,
120- let endRange = mobileProvisionData. range ( of: endTag) else { return nil }
121-
122- let plistData = mobileProvisionData [ startRange. lowerBound..< endRange. upperBound]
123- guard let parsed = try ? PropertyListSerialization . propertyList ( from: Data ( plistData) , options: [ ] , format: nil ) ,
124- let dict = parsed as? [ String : Any ] else { return nil }
125-
126- return ( dict [ " TeamName " ] as? String ) ?? ( dict [ " Name " ] as? String )
127- }
128-
129- // MARK: - Check p12 ↔ mobileprovision match
130- public static func check( p12Data: Data , password: String , mobileProvisionData: Data ) -> Result < CertificateCheckResult , Error > {
131- let options = [ kSecImportExportPassphrase as String : password] as CFDictionary
132- var items : CFArray ?
95+ let endRange = mobileProvisionData. range ( of: endTag) else {
96+ return nil
97+ }
13398
134- let status = SecPKCS12Import ( p12Data as CFData , options, & items)
99+ let plistDataSlice = mobileProvisionData [ startRange. lowerBound..< endRange. upperBound]
100+ let plistData = Data ( plistDataSlice)
135101
136- if status == errSecAuthFailed { return . success( . incorrectPassword) }
102+ // Parse plist into a dictionary
103+ guard let parsed = try ? PropertyListSerialization . propertyList ( from: plistData, options: [ ] , format: nil ) ,
104+ let dict = parsed as? [ String : Any ] else {
105+ return nil
106+ }
137107
138- guard status == errSecSuccess,
139- let itemsArray = items as? [ [ String : Any ] ] ,
140- let identityAny = itemsArray. first ? [ kSecImportItemIdentity as String ] ,
141- CFGetTypeID ( identityAny as CFTypeRef ) == SecIdentityGetTypeID ( ) else {
142- return . failure( CertificateError . p12ImportFailed ( status) )
108+ // Prefer TeamName if present
109+ if let teamName = dict [ " TeamName " ] as? String , !teamName. isEmpty {
110+ return teamName
143111 }
144112
145- let identity = identityAny as! SecIdentity
113+ // Fallback to Name (string)
114+ if let name = dict [ " Name " ] as? String , !name. isEmpty {
115+ return name
116+ }
146117
118+ return nil
119+ }
120+
121+ /// Top-level check: returns result
122+ public static func check( p12Data: Data , password: String , mobileProvisionData: Data ) -> Result < CertificateCheckResult , Error > {
123+ let options = [ kSecImportExportPassphrase as String : password] as CFDictionary
124+ var itemsCF : CFArray ?
125+
126+ let importStatus = SecPKCS12Import ( p12Data as CFData , options, & itemsCF)
127+
128+ if importStatus == errSecAuthFailed {
129+ return . success( . incorrectPassword)
130+ }
131+
132+ guard importStatus == errSecSuccess, let items = itemsCF as? [ [ String : Any ] ] , items. count > 0 else {
133+ return . failure( CertificateError . p12ImportFailed ( importStatus) )
134+ }
135+
136+ guard let first = items. first else {
137+ return . failure( CertificateError . identityExtractionFailed)
138+ }
139+
140+ let identity = first [ kSecImportItemIdentity as String ] as! SecIdentity
141+
147142 var certRef : SecCertificate ?
148- guard SecIdentityCopyCertificate ( identity, & certRef) == errSecSuccess,
149- let p12Cert = certRef else {
143+ let certStatus = SecIdentityCopyCertificate ( identity, & certRef)
144+
145+ guard certStatus == errSecSuccess, let p12Cert = certRef else {
150146 return . failure( CertificateError . certExtractionFailed)
151147 }
152-
148+
153149 do {
154- let p12KeyData = try publicKeyData ( from: p12Cert)
155- let p12Hash = sha256Hex ( p12KeyData)
156-
157- let embedded = try certificatesFromMobileProvision ( mobileProvisionData)
158-
159- for cert in embedded {
160- let keyData = try publicKeyData ( from: cert)
161- if sha256Hex ( keyData) == p12Hash {
162- return . success( . success)
150+ let p12PubKeyData = try publicKeyData ( from: p12Cert)
151+ let p12Hash = sha256Hex ( p12PubKeyData)
152+
153+ let embeddedCerts = try certificatesFromMobileProvision ( mobileProvisionData)
154+
155+ for cert in embeddedCerts {
156+ do {
157+ let embPubKeyData = try publicKeyData ( from: cert)
158+ let embHash = sha256Hex ( embPubKeyData)
159+
160+ if embHash == p12Hash {
161+ return . success( . success)
162+ }
163+ } catch {
164+ continue
163165 }
164166 }
167+
165168 return . success( . noMatch)
166169 } catch {
167170 return . failure( error)
168171 }
169172 }
173+
170174}
0 commit comments