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

Commit 8ad9304

Browse files
authored
DEV UPDATE: Add generateCert and test install
1 parent 470b238 commit 8ad9304

4 files changed

Lines changed: 293 additions & 1 deletion

File tree

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// GenerateCert.swift
2+
import Foundation
3+
import OpenSSL
4+
5+
enum CertGenError: Error {
6+
case keyGenerationFailed(String)
7+
case x509CreationFailed(String)
8+
case writeFailed(String)
9+
case sanCreationFailed(String)
10+
}
11+
12+
final class Logger {
13+
static let shared = Logger()
14+
private let logFile: URL
15+
private let queue = DispatchQueue(label: "LoggerQueue")
16+
17+
private init() {
18+
let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
19+
logFile = docs.appendingPathComponent("log.txt")
20+
try? "".write(to: logFile, atomically: true, encoding: .utf8)
21+
}
22+
23+
func log(_ message: String) {
24+
let timestamp = ISO8601DateFormatter().string(from: Date())
25+
let fullMsg = "[\(timestamp)] \(message)\n"
26+
print(fullMsg, terminator: "")
27+
queue.async {
28+
if let data = fullMsg.data(using: .utf8) {
29+
if FileManager.default.fileExists(atPath: self.logFile.path) {
30+
if let handle = try? FileHandle(forWritingTo: self.logFile) {
31+
handle.seekToEndOfFile()
32+
handle.write(data)
33+
handle.closeFile()
34+
}
35+
} else {
36+
try? data.write(to: self.logFile)
37+
}
38+
}
39+
}
40+
}
41+
42+
func logError(_ error: Error) {
43+
log("ERROR: \(error)")
44+
}
45+
}
46+
47+
public final class GenerateCert {
48+
49+
public static func createAndSaveCerts(caCN: String = "My Local CA",
50+
serverCN: String = "127.0.0.1",
51+
rsaBits: Int32 = 2048,
52+
daysValid: Int32 = 36500) async throws -> [URL] {
53+
Logger.shared.log("Initializing OpenSSL...")
54+
_ = OpenSSL_add_all_algorithms()
55+
ERR_load_crypto_strings()
56+
57+
Logger.shared.log("Generating CA key...")
58+
guard let caPkey = try generateRSAKey(bits: rsaBits) else { throw CertGenError.keyGenerationFailed("CA key generation failed") }
59+
Logger.shared.log("CA key generated.")
60+
61+
Logger.shared.log("Creating self-signed CA certificate...")
62+
guard let caX509 = try createSelfSignedCertificate(pkey: caPkey, commonName: caCN, days: daysValid, isCA: true) else {
63+
throw CertGenError.x509CreationFailed("CA certificate creation failed")
64+
}
65+
Logger.shared.log("CA certificate created.")
66+
67+
Logger.shared.log("Generating server key...")
68+
guard let serverPkey = try generateRSAKey(bits: rsaBits) else { throw CertGenError.keyGenerationFailed("Server key generation failed") }
69+
Logger.shared.log("Server key generated.")
70+
71+
Logger.shared.log("Creating server certificate signed by CA...")
72+
guard let serverX509 = try createCertificateSignedByCA(serverPKey: serverPkey, caPkey: caPkey, caX509: caX509, commonName: serverCN, days: daysValid) else {
73+
throw CertGenError.x509CreationFailed("Server certificate creation failed")
74+
}
75+
Logger.shared.log("Server certificate created.")
76+
77+
let docs = try documentsDirectory()
78+
let rootCertURL = docs.appendingPathComponent("rootCA.pem")
79+
let rootKeyURL = docs.appendingPathComponent("rootCA.key.pem")
80+
let serverKeyURL = docs.appendingPathComponent("localhost.key.pem")
81+
let serverCertURL = docs.appendingPathComponent("localhost.crt.pem")
82+
83+
Logger.shared.log("Writing CA key to \(rootKeyURL.path)")
84+
try writePrivateKeyPEM(pkey: caPkey, to: rootKeyURL.path)
85+
Logger.shared.log("Writing CA cert to \(rootCertURL.path)")
86+
try writeX509PEM(x509: caX509, to: rootCertURL.path)
87+
88+
Logger.shared.log("Writing server key to \(serverKeyURL.path)")
89+
try writePrivateKeyPEM(pkey: serverPkey, to: serverKeyURL.path)
90+
Logger.shared.log("Writing server cert to \(serverCertURL.path)")
91+
try writeX509PEM(x509: serverX509, to: serverCertURL.path)
92+
93+
Logger.shared.log("Certificate generation completed successfully.")
94+
95+
EVP_PKEY_free(caPkey)
96+
X509_free(caX509)
97+
EVP_PKEY_free(serverPkey)
98+
X509_free(serverX509)
99+
100+
return [rootCertURL, rootKeyURL, serverKeyURL, serverCertURL]
101+
}
102+
103+
// MARK: - Helpers
104+
105+
private static func documentsDirectory() throws -> URL {
106+
let fm = FileManager.default
107+
guard let url = fm.urls(for: .documentDirectory, in: .userDomainMask).first else {
108+
throw CertGenError.writeFailed("Documents directory not found")
109+
}
110+
return url
111+
}
112+
113+
private static func generateRSAKey(bits: Int32) throws -> UnsafeMutablePointer<EVP_PKEY>? {
114+
guard let rsa = RSA_new() else { throw CertGenError.keyGenerationFailed("RSA_new failed") }
115+
guard let bn = BN_new() else { RSA_free(rsa); throw CertGenError.keyGenerationFailed("BN_new failed") }
116+
if BN_set_word(bn, UInt(65537)) != 1 { BN_free(bn); RSA_free(rsa); throw CertGenError.keyGenerationFailed("BN_set_word failed") }
117+
if RSA_generate_key_ex(rsa, bits, bn, nil) != 1 { BN_free(bn); RSA_free(rsa); throw CertGenError.keyGenerationFailed("RSA_generate_key_ex failed") }
118+
BN_free(bn)
119+
guard let pkey = EVP_PKEY_new() else { RSA_free(rsa); throw CertGenError.keyGenerationFailed("EVP_PKEY_new failed") }
120+
if EVP_PKEY_assign_RSA(pkey, rsa) != 1 { EVP_PKEY_free(pkey); RSA_free(rsa); throw CertGenError.keyGenerationFailed("EVP_PKEY_assign_RSA failed") }
121+
return pkey
122+
}
123+
124+
private static func createSelfSignedCertificate(pkey: UnsafeMutablePointer<EVP_PKEY>?,
125+
commonName: String,
126+
days: Int32,
127+
isCA: Bool) throws -> UnsafeMutablePointer<X509>? {
128+
guard let x509 = X509_new() else { throw CertGenError.x509CreationFailed("X509_new failed") }
129+
X509_set_version(x509, 2)
130+
if let serial = ASN1_INTEGER_new() { ASN1_INTEGER_set(serial, 1); X509_set_serialNumber(x509, serial); ASN1_INTEGER_free(serial) }
131+
X509_gmtime_adj(X509_get_notBefore(x509), 0)
132+
X509_gmtime_adj(X509_get_notAfter(x509), Int64(days) * 24 * 3600)
133+
X509_set_pubkey(x509, pkey)
134+
guard let name = X509_get_subject_name(x509) else { X509_free(x509); throw CertGenError.x509CreationFailed("X509_get_subject_name nil") }
135+
_ = addNameEntry(name: name, field: "C", value: "AU")
136+
_ = addNameEntry(name: name, field: "ST", value: "NSW")
137+
_ = addNameEntry(name: name, field: "L", value: "Sydney")
138+
_ = addNameEntry(name: name, field: "O", value: "MyCompany")
139+
_ = addNameEntry(name: name, field: "OU", value: "Dev")
140+
_ = addNameEntry(name: name, field: "CN", value: commonName)
141+
X509_set_issuer_name(x509, name)
142+
if isCA {
143+
if let ext = X509V3_EXT_conf_nid(nil, nil, NID_basic_constraints, "CA:TRUE") { X509_add_ext(x509, ext, -1); X509_EXTENSION_free(ext) }
144+
if let ext2 = X509V3_EXT_conf_nid(nil, nil, NID_key_usage, "keyCertSign,cRLSign") { X509_add_ext(x509, ext2, -1); X509_EXTENSION_free(ext2) }
145+
}
146+
if X509_sign(x509, pkey, EVP_sha256()) == 0 { X509_free(x509); throw CertGenError.x509CreationFailed("X509_sign failed") }
147+
return x509
148+
}
149+
150+
private static func createCertificateSignedByCA(serverPKey: UnsafeMutablePointer<EVP_PKEY>?,
151+
caPkey: UnsafeMutablePointer<EVP_PKEY>?,
152+
caX509: UnsafeMutablePointer<X509>?,
153+
commonName: String,
154+
days: Int32) throws -> UnsafeMutablePointer<X509>? {
155+
guard let cert = X509_new() else { throw CertGenError.x509CreationFailed("X509_new failed") }
156+
X509_set_version(cert, 2)
157+
if let serial = ASN1_INTEGER_new() { ASN1_INTEGER_set(serial, Int(time(nil) & 0xffffffff)); X509_set_serialNumber(cert, serial); ASN1_INTEGER_free(serial) }
158+
X509_gmtime_adj(X509_get_notBefore(cert), 0)
159+
X509_gmtime_adj(X509_get_notAfter(cert), Int64(days) * 24 * 3600)
160+
X509_set_pubkey(cert, serverPKey)
161+
guard let subj = X509_get_subject_name(cert) else { X509_free(cert); throw CertGenError.x509CreationFailed("X509_get_subject_name nil") }
162+
_ = addNameEntry(name: subj, field: "C", value: "AU")
163+
_ = addNameEntry(name: subj, field: "ST", value: "NSW")
164+
_ = addNameEntry(name: subj, field: "L", value: "Sydney")
165+
_ = addNameEntry(name: subj, field: "O", value: "MyCompany")
166+
_ = addNameEntry(name: subj, field: "OU", value: "Dev")
167+
_ = addNameEntry(name: subj, field: "CN", value: commonName)
168+
if let ca = caX509 { if let caSubject = X509_get_subject_name(ca) { X509_set_issuer_name(cert, caSubject) } }
169+
do { try addSubjectAltName_IP(cert: cert, ipString: "127.0.0.1") } catch { Logger.shared.log("Warning: SAN add failed: \(error)") }
170+
if let ext_bc = X509V3_EXT_conf_nid(nil, nil, NID_basic_constraints, "CA:FALSE") { X509_add_ext(cert, ext_bc, -1); X509_EXTENSION_free(ext_bc) }
171+
if let ext_ku = X509V3_EXT_conf_nid(nil, nil, NID_key_usage, "digitalSignature,keyEncipherment") { X509_add_ext(cert, ext_ku, -1); X509_EXTENSION_free(ext_ku) }
172+
guard let caKey = caPkey else { X509_free(cert); throw CertGenError.x509CreationFailed("CA private key missing") }
173+
if X509_sign(cert, caKey, EVP_sha256()) == 0 { X509_free(cert); throw CertGenError.x509CreationFailed("X509_sign with CA key failed") }
174+
return cert
175+
}
176+
177+
@discardableResult private static func addNameEntry(name: UnsafeMutablePointer<X509_NAME>?, field: String, value: String) -> Int32 {
178+
guard let name = name else { return 0 }
179+
return X509_NAME_add_entry_by_txt(name, field, MBSTRING_ASC, value.withCString { UnsafeRawPointer($0) }, -1, -1, 0)
180+
}
181+
182+
private static func addSubjectAltName_IP(cert: UnsafeMutablePointer<X509>?, ipString: String) throws {
183+
guard let cert = cert else { throw CertGenError.sanCreationFailed("cert nil") }
184+
var ipaddr = in_addr()
185+
guard inet_pton(AF_INET, ipString, &ipaddr) == 1 else { throw CertGenError.sanCreationFailed("inet_pton failed") }
186+
guard let gen = GENERAL_NAME_new() else { throw CertGenError.sanCreationFailed("GENERAL_NAME_new failed") }
187+
guard let ipOctet = ASN1_OCTET_STRING_new() else { GENERAL_NAME_free(gen); throw CertGenError.sanCreationFailed("ASN1_OCTET_STRING_new failed") }
188+
var rawIP = ipaddr.s_addr
189+
withUnsafePointer(to: &rawIP) { ptr in
190+
let p = UnsafeRawPointer(ptr)
191+
ASN1_OCTET_STRING_set(ipOctet, p.assumingMemoryBound(to: UInt8.self), 4)
192+
}
193+
GENERAL_NAME_set0_value(gen, GEN_IPADD, ipOctet)
194+
guard let stack = sk_GENERAL_NAME_new_null() else { GENERAL_NAME_free(gen); throw CertGenError.sanCreationFailed("sk_GENERAL_NAME_new_null failed") }
195+
sk_GENERAL_NAME_push(stack, gen)
196+
if X509_add1_ext_i2d(cert, NID_subject_alt_name, stack, 0, X509V3_ADD_REPLACE) != 1 {
197+
sk_GENERAL_NAME_pop_free(stack, { GENERAL_NAME_free(UnsafeMutablePointer(mutating: $0)) })
198+
throw CertGenError.sanCreationFailed("X509_add1_ext_i2d failed")
199+
}
200+
sk_GENERAL_NAME_pop_free(stack, { GENERAL_NAME_free(UnsafeMutablePointer(mutating: $0)) })
201+
}
202+
203+
private static func writePrivateKeyPEM(pkey: UnsafeMutablePointer<EVP_PKEY>?, to path: String) throws {
204+
guard let pkey = pkey else { throw CertGenError.writeFailed("pkey nil") }
205+
guard let bio = BIO_new_file(path, "w") else { throw CertGenError.writeFailed("BIO_new_file failed for \(path)") }
206+
defer { BIO_free_all(bio) }
207+
if PEM_write_bio_PrivateKey(bio, pkey, nil, nil, 0, nil, nil) != 1 { throw CertGenError.writeFailed("PEM_write_bio_PrivateKey failed for \(path)") }
208+
}
209+
210+
private static func writeX509PEM(x509: UnsafeMutablePointer<X509>?, to path: String) throws {
211+
guard let x509 = x509 else { throw CertGenError.writeFailed("x509 nil") }
212+
guard let bio = BIO_new_file(path, "w") else { throw CertGenError.writeFailed("BIO_new_file failed for \(path)") }
213+
defer { BIO_free_all(bio) }
214+
if PEM_write_bio_X509(bio, x509) != 1 { throw CertGenError.writeFailed("PEM_write_bio_X509 failed for \(path)") }
215+
}
216+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// CertTestView.swift
2+
import SwiftUI
3+
4+
struct CertTestView: View {
5+
@State private var logs: String = "Logs will appear here...\n"
6+
@State private var generating = false
7+
8+
var body: some View {
9+
VStack(spacing: 20) {
10+
ScrollView {
11+
Text(logs)
12+
.frame(maxWidth: .infinity, alignment: .leading)
13+
.padding()
14+
.background(Color.black.opacity(0.8))
15+
.foregroundColor(.green)
16+
.cornerRadius(12)
17+
}
18+
19+
Button(action: generateCerts) {
20+
HStack {
21+
if generating { ProgressView() }
22+
Text(generating ? "Generating..." : "Generate Certificates")
23+
.bold()
24+
}
25+
.padding()
26+
.background(Color.blue)
27+
.foregroundColor(.white)
28+
.cornerRadius(10)
29+
}
30+
}
31+
.padding()
32+
.onAppear { loadLog() }
33+
}
34+
35+
private func generateCerts() {
36+
generating = true
37+
Task {
38+
do {
39+
_ = try await GenerateCert.createAndSaveCerts()
40+
appendLog("✅ Certificates generated successfully.")
41+
loadLog()
42+
} catch {
43+
appendLog("❌ Error: \(error)")
44+
}
45+
generating = false
46+
}
47+
}
48+
49+
private func loadLog() {
50+
let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
51+
let logFile = docs.appendingPathComponent("log.txt")
52+
if let content = try? String(contentsOf: logFile) {
53+
logs = content
54+
}
55+
}
56+
57+
private func appendLog(_ message: String) {
58+
logs += message + "\n"
59+
}
60+
}
61+
62+
struct CertTestView_Previews: PreviewProvider {
63+
static var previews: some View {
64+
CertTestView()
65+
}
66+
}

Sources/prostore/prostore.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ struct MainSidebarView: View {
2424
NavigationLink(value: SidebarItem.updater) {
2525
Label("Updater", systemImage: "square.and.arrow.down")
2626
}
27+
NavigationLink(value: SidebarItem.test) {
28+
Label("Test Install", systemImage: "checkmark.seal")
29+
}
2730
NavigationLink(value: SidebarItem.about) {
2831
Label("About", systemImage: "info.circle")
2932
}
@@ -59,6 +62,12 @@ struct MainSidebarView: View {
5962
.navigationTitle("About")
6063
.navigationBarTitleDisplayMode(.large)
6164
}
65+
case .about:
66+
NavigationStack {
67+
CertTestView()
68+
.navigationTitle("Test Install")
69+
.navigationBarTitleDisplayMode(.large)
70+
}
6271
case .updater:
6372
NavigationStack {
6473
UpdaterView()
@@ -74,6 +83,7 @@ struct MainSidebarView: View {
7483

7584
enum SidebarItem: Hashable {
7685
case updater
86+
case test
7787
case certificates
7888
case apps
7989
case about

project.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ targets:
3131
CFBundleDisplayName: "ProStore"
3232
CFBundleName: "prostore"
3333
CFBundleVersion: "33"
34-
CFBundleShortVersionString: "0.10.1"
34+
CFBundleShortVersionString: "0.11.0d"
3535
UILaunchStoryboardName: "LaunchScreen"
3636
NSPrincipalClass: "UIApplication"
3737
NSAppTransportSecurity:

0 commit comments

Comments
 (0)