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

Commit 9482ca8

Browse files
authored
Add CertRevokeChecker for certificate validation
1 parent 435b403 commit 9482ca8

File tree

1 file changed

+133
-0
lines changed

1 file changed

+133
-0
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// CertRevokeChecker.swift
2+
import Foundation
3+
4+
// MARK: - API Response Models
5+
struct NezushubResponse: Decodable {
6+
let success: Bool
7+
let data: NezushubData?
8+
let message: String?
9+
}
10+
11+
struct NezushubData: Decodable {
12+
let certificate: CertificateInfo
13+
let certificate_status: CertificateStatus
14+
let comparison_data: ComparisonData
15+
let entitlements: [String: AnyCodable]? // optional, not needed for UI
16+
}
17+
18+
struct CertificateInfo: Decodable {
19+
let certificate_info: CertDetails
20+
}
21+
22+
struct CertDetails: Decodable {
23+
let validity_period: ValidityPeriod
24+
}
25+
26+
struct ValidityPeriod: Decodable {
27+
let valid_to: String // ISO 8601 date string
28+
}
29+
30+
struct CertificateStatus: Decodable {
31+
let status: String // e.g. "Signed", "Revoked"
32+
let ocsp_status: String // e.g. "Good", "Revoked"
33+
}
34+
35+
struct ComparisonData: Decodable {
36+
let certificates_match: Bool
37+
}
38+
39+
// Helper to allow AnyCodable when we don't care about a field
40+
struct AnyCodable: Decodable { }
41+
42+
// MARK: - Public result used by UI
43+
enum RevocationCheckResult {
44+
case success(isSigned: Bool, expires: Date, match: Bool)
45+
case failure(Error)
46+
case networkError
47+
}
48+
49+
// MARK: - Revocation Checker
50+
final class CertRevokeChecker {
51+
static let shared = CertRevokeChecker()
52+
private init() {}
53+
54+
private let apiURL = URL(string: "https://tools.nezushub.vip/cert-ios-checker/api/")!
55+
56+
/// Perform the revocation + match check using the external API
57+
func check(p12URL: URL, provisionURL: URL, password: String) async -> RevocationCheckResult {
58+
var request = URLRequest(url: apiURL)
59+
request.httpMethod = "POST"
60+
61+
let boundary = "Boundary-\(UUID().uuidString)"
62+
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
63+
64+
var body = Data()
65+
66+
// Helper to append a file part
67+
func appendFile(_ data: Data, filename: String, fieldName: String) {
68+
body.append("--\(boundary)\r\n".data(using: .utf8)!)
69+
body.append("Content-Disposition: form-data; name=\"\(fieldName)\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!)
70+
body.append("Content-Type: application/octet-stream\r\n\r\n".data(using: .utf8)!)
71+
body.append(data)
72+
body.append("\r\n".data(using: .utf8)!)
73+
}
74+
75+
// Helper to append a text field
76+
func appendText(_ value: String, fieldName: String) {
77+
body.append("--\(boundary)\r\n".data(using: .utf8)!)
78+
body.append("Content-Disposition: form-data; name=\"\(fieldName)\"\r\n\r\n".data(using: .utf8)!)
79+
body.append(value.data(using: .utf8)!)
80+
body.append("\r\n".data(using: .utf8)!)
81+
}
82+
83+
// Read files (security-scoped for new imports)
84+
let p12Scoped = p12URL.startAccessingSecurityScopedResource()
85+
let provScoped = provisionURL.startAccessingSecurityScopedResource()
86+
defer {
87+
if p12Scoped { p12URL.stopAccessingSecurityScopedResource() }
88+
if provScoped { provisionURL.stopAccessingSecurityScopedResource() }
89+
}
90+
91+
do {
92+
let p12Data = try Data(contentsOf: p12URL)
93+
let provData = try Data(contentsOf: provisionURL)
94+
95+
appendFile(p12Data, filename: "certificate.p12", fieldName: "file")
96+
appendFile(provData, filename: "profile.mobileprovision", fieldName: "secondary_file")
97+
appendText(password, fieldName: "password")
98+
99+
body.append("--\(boundary)--\r\n".data(using: .utf8)!)
100+
request.httpBody = body
101+
102+
let (data, response) = try await URLSession.shared.data(for: request)
103+
104+
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
105+
return .networkError
106+
}
107+
108+
let decoded = try JSONDecoder().decode(NezushubResponse.self, from: data)
109+
110+
guard decoded.success, let nezData = decoded.data else {
111+
return .failure(NSError(domain: "CertRevokeChecker", code: -1, userInfo: [NSLocalizedDescriptionKey: decoded.message ?? "Unknown API error"]))
112+
}
113+
114+
// Parse expiry date
115+
let formatter = ISO8601DateFormatter()
116+
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
117+
guard let expiryDate = formatter.date(from: nezData.certificate.certificate_info.validity_period.valid_to) else {
118+
return .failure(NSError(domain: "CertRevokeChecker", code: -2, userInfo: [NSLocalizedDescriptionKey: "Could not parse expiry date"]))
119+
}
120+
121+
// Determine if it's actually signed (not revoked)
122+
let isSigned = nezData.certificate_status.status.lowercased() == "signed" &&
123+
nezData.certificate_status.ocsp_status.lowercased() == "good"
124+
125+
let match = nezData.comparison_data.certificates_match
126+
127+
return .success(isSigned: isSigned, expires: expiryDate, match: match)
128+
129+
} catch {
130+
return .failure(error)
131+
}
132+
}
133+
}

0 commit comments

Comments
 (0)