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

Commit f1fd195

Browse files
authored
Add certificates view
1 parent 84f8903 commit f1fd195

3 files changed

Lines changed: 247 additions & 0 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// certificates/certificates.swift
2+
// Put this file under certificates/ in your project
3+
4+
import Foundation
5+
import Security
6+
import CryptoKit
7+
8+
public enum CertificateCheckResult {
9+
case incorrectPassword
10+
case noMatch
11+
case success
12+
}
13+
14+
public enum CertificateError: Error {
15+
case p12ImportFailed(OSStatus)
16+
case identityExtractionFailed
17+
case certExtractionFailed
18+
case cmsDecodeFailed(OSStatus)
19+
case noCertsInProvision
20+
case publicKeyExportFailed(OSStatus)
21+
}
22+
23+
public final class CertificatesManager {
24+
/// SHA256 hex from Data
25+
private static func sha256Hex(_ d: Data) -> String {
26+
let digest = SHA256.hash(data: d)
27+
return digest.map { String(format: "%02x", $0) }.joined()
28+
}
29+
30+
/// Export public key bytes for a certificate (SecCertificate -> SecKey -> external representation)
31+
private static func publicKeyData(from cert: SecCertificate) throws -> Data {
32+
guard let secKey = SecCertificateCopyKey(cert) else {
33+
throw CertificateError.certExtractionFailed
34+
}
35+
var cfErr: Unmanaged<CFError>?
36+
guard let keyData = SecKeyCopyExternalRepresentation(secKey, &cfErr) as Data? else {
37+
if let err = cfErr?.takeRetainedValue() {
38+
throw CertificateError.publicKeyExportFailed((err as NSError).code as OSStatus)
39+
} else {
40+
throw CertificateError.publicKeyExportFailed(-1)
41+
}
42+
}
43+
return keyData
44+
}
45+
46+
/// Parse .mobileprovision CMS/PKCS#7 and return embedded SecCertificate array
47+
private static func certificatesFromMobileProvision(_ data: Data) throws -> [SecCertificate] {
48+
var decoder: CMSDecoder? = nil
49+
var status = CMSDecoderCreate(&decoder)
50+
guard status == errSecSuccess, let dec = decoder else {
51+
throw CertificateError.cmsDecodeFailed(status)
52+
}
53+
54+
// feed bytes
55+
_ = data.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> OSStatus in
56+
guard let base = ptr.baseAddress else { return errSecParam }
57+
return CMSDecoderUpdateMessage(dec, base, data.count)
58+
}
59+
60+
status = CMSDecoderFinalizeMessage(dec)
61+
guard status == errSecSuccess else {
62+
throw CertificateError.cmsDecodeFailed(status)
63+
}
64+
65+
var certsCF: CFArray? = nil
66+
status = CMSDecoderCopyAllCerts(dec, &certsCF)
67+
guard status == errSecSuccess, let certs = certsCF as? [SecCertificate], certs.count > 0 else {
68+
throw CertificateError.noCertsInProvision
69+
}
70+
71+
return certs
72+
}
73+
74+
/// Top-level check: returns result
75+
/// - Parameters:
76+
/// - p12Data: contents of .p12
77+
/// - password: p12 password
78+
/// - mobileProvisionData: contents of .mobileprovision
79+
public static func check(p12Data: Data, password: String, mobileProvisionData: Data) -> Result<CertificateCheckResult, Error> {
80+
// 1) try import .p12 (also verifies password)
81+
let options = [kSecImportExportPassphrase as String: password] as CFDictionary
82+
var itemsCF: CFArray?
83+
let importStatus = SecPKCS12Import(p12Data as CFData, options, &itemsCF)
84+
85+
if importStatus == errSecAuthFailed {
86+
return .success(.incorrectPassword)
87+
}
88+
89+
guard importStatus == errSecSuccess, let items = itemsCF as? [[String: Any]], items.count > 0 else {
90+
return .failure(CertificateError.p12ImportFailed(importStatus))
91+
}
92+
93+
guard let first = items.first,
94+
let identity = first[kSecImportItemIdentity as String] as? SecIdentity else {
95+
return .failure(CertificateError.identityExtractionFailed)
96+
}
97+
98+
// 2) extract certificate from identity
99+
var certRef: SecCertificate?
100+
let certStatus = SecIdentityCopyCertificate(identity, &certRef)
101+
guard certStatus == errSecSuccess, let p12Cert = certRef else {
102+
return .failure(CertificateError.certExtractionFailed)
103+
}
104+
105+
// 3) get public key bytes and hash
106+
do {
107+
let p12PubKeyData = try publicKeyData(from: p12Cert)
108+
let p12Hash = sha256Hex(p12PubKeyData)
109+
110+
// 4) parse mobileprovision and check embedded certs
111+
let embeddedCerts = try certificatesFromMobileProvision(mobileProvisionData)
112+
113+
for cert in embeddedCerts {
114+
do {
115+
let embPubKeyData = try publicKeyData(from: cert)
116+
let embHash = sha256Hex(embPubKeyData)
117+
if embHash == p12Hash {
118+
return .success(.success)
119+
}
120+
} catch {
121+
// ignore this cert and continue
122+
continue
123+
}
124+
}
125+
126+
// if none matched
127+
return .success(.noMatch)
128+
} catch {
129+
return .failure(error)
130+
}
131+
}
132+
}

Sources/prostore/prostore.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ struct MainTabView: View {
2121
Text("Signer")
2222
}
2323

24+
NavigationView {
25+
CertificateView()
26+
}
27+
.navigationViewStyle(StackNavigationViewStyle())
28+
.tabItem {
29+
Image(systemName: "key")
30+
Text("Certificates")
31+
}
32+
2433
NavigationView {
2534
AboutView()
2635
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// views/CertificateView.swift
2+
// Put this file under views/ in your project
3+
4+
import SwiftUI
5+
import UniformTypeIdentifiers
6+
7+
struct CertificateView: View {
8+
@State private var p12 = FileItem()
9+
@State private var prov = FileItem()
10+
@State private var p12Password = ""
11+
@State private var isProcessing = false
12+
@State private var statusMessage = "" // will hold exactly one of: "Incorrect Password", "P12 and MOBILEPROVISION do not match", "Success!"
13+
@State private var showPickerFor: SignerView.PickerKind? = nil // reuse PickerKind from SignerView
14+
15+
var body: some View {
16+
Form {
17+
Section(header: Text("Inputs")) {
18+
HStack {
19+
Text("P12:")
20+
Spacer()
21+
Text(p12.name.isEmpty ? "" : p12.name).foregroundColor(.secondary)
22+
Button("Pick") {
23+
showPickerFor = .p12
24+
}
25+
}
26+
HStack {
27+
Text("MobileProvision:")
28+
Spacer()
29+
Text(prov.name.isEmpty ? "" : prov.name).foregroundColor(.secondary)
30+
Button("Pick") {
31+
showPickerFor = .prov
32+
}
33+
}
34+
SecureField("P12 Password", text: $p12Password)
35+
}
36+
37+
Section {
38+
Button(action: checkStatus) {
39+
HStack {
40+
Spacer()
41+
Text("Check Status").bold()
42+
Spacer()
43+
}
44+
}
45+
.disabled(isProcessing || p12.url == nil || prov.url == nil)
46+
}
47+
48+
Section(header: Text("Result")) {
49+
Text(statusMessage).foregroundColor(.primary)
50+
}
51+
}
52+
.navigationTitle("Certificates")
53+
.sheet(item: $showPickerFor, onDismiss: nil) { kind in
54+
DocumentPicker(kind: kind, onPick: { url in
55+
switch kind {
56+
case .ipa: break // not used here
57+
case .p12: p12.url = url
58+
case .prov: prov.url = url
59+
}
60+
})
61+
}
62+
}
63+
64+
private func checkStatus() {
65+
guard let p12URL = p12.url, let provURL = prov.url else {
66+
statusMessage = "P12 and MOBILEPROVISION do not match"
67+
return
68+
}
69+
70+
isProcessing = true
71+
statusMessage = "Checking..."
72+
73+
DispatchQueue.global(qos: .userInitiated).async {
74+
do {
75+
let p12Data = try Data(contentsOf: p12URL)
76+
let provData = try Data(contentsOf: provURL)
77+
78+
let result = CertificatesManager.check(p12Data: p12Data, password: p12Password, mobileProvisionData: provData)
79+
80+
DispatchQueue.main.async {
81+
isProcessing = false
82+
switch result {
83+
case .success(.incorrectPassword):
84+
statusMessage = "Incorrect Password" // EXACT text requested
85+
case .success(.noMatch):
86+
statusMessage = "P12 and MOBILEPROVISION do not match" // EXACT text requested
87+
case .success(.success):
88+
statusMessage = "Success!" // EXACT text requested
89+
case .failure(let err):
90+
// If there was an unexpected error, surface a no-match (safe) or show error (dev)
91+
// We'll show no-match so user gets one of the three expected messages; but log the error.
92+
print("Certificates check failed: \(err)")
93+
statusMessage = "P12 and MOBILEPROVISION do not match"
94+
}
95+
}
96+
} catch {
97+
DispatchQueue.main.async {
98+
isProcessing = false
99+
// If the password is wrong we already catch that above. Reading files failed -> show no-match
100+
print("File read error: \(error)")
101+
statusMessage = "P12 and MOBILEPROVISION do not match"
102+
}
103+
}
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)