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+ }
0 commit comments