I'm trying to make an iOS client for my server. Ideally I would prefer that when establishing a tls 1.2 connection to the server, iOS will present me with the certificate it got so I can match it to an expected certificate. After tons of googling it doesn't look like this is possible. This is easy to do in android. After that I'm willing to settle for second place and have iOS accept any certificate signed by my own private CA. This way I can guarantee the server it is connected to is at least mine.
It also looks like iOS doesn't have a standard socket like in C where after you create it, you read and write on its fd or Java where you get a socket's input and output streams to do read and write like C. It looks like I need to make my own socket like class to get C/Java like read write.
import Foundation
class SSLSocket: NSObject, StreamDelegate
{
//(allow the getter to be public, only the setter is private)
private(set) var inputStream: InputStream?
private(set) var outputStream: OutputStream?
private var inputDelegate: StreamDelegate?
private var outputDelegate: StreamDelegate?
private var host: String
private var port: Int
public init(host: String, port: Int)
{
self.host = host
self.port = port
}
func connect()
{
Stream.getStreamsToHost(withName: host, port: port, inputStream: &inputStream, outputStream: &outputStream)
//iOS specific oddities (nothing similar in the android version)
inputDelegate = self
outputDelegate = self
inputStream!.delegate = inputDelegate
outputStream!.delegate = outputDelegate
inputStream!.schedule(in: RunLoop.main, forMode: RunLoopMode.defaultRunLoopMode)
outputStream!.schedule(in: RunLoop.main, forMode: RunLoopMode.defaultRunLoopMode)
//tlsv1.0+ enforcement??? (looks like no 1.2 only)
//don't do any "legitimacy checks" on the certificate or the host.
let sslSettings = //must present ssl properties in an array, not 1 by 1 in .setPropery(...
[
String(kCFStreamPropertySocketSecurityLevel): kCFStreamSocketSecurityLevelTLSv1,
String(kCFStreamSSLValidatesCertificateChain): kCFBooleanFalse
] as [String : Any]
inputStream!.setProperty(sslSettings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey)
outputStream!.setProperty(sslSettings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey)
//open the streams
inputStream!.open()
outputStream!.open()
}
func close() //better not cause timing problems because it is a bit less than instantaneous
{
inputStream!.delegate = nil
outputStream!.delegate = nil
inputStream!.close()
outputStream!.close()
inputStream!.remove(from: .main, forMode: .defaultRunLoopMode)
outputStream!.remove(from: .main, forMode: .defaultRunLoopMode)
//let the automatic reference count get rid of these
inputStream = nil
outputStream = nil
}
func stream(_ aStream: Stream, handle eventCode: Stream.Event)
{
switch eventCode
{
case Stream.Event.endEncountered:
print("socked died")
aStream.close()
aStream.remove(from: RunLoop.main, forMode: .defaultRunLoopMode)
break
case Stream.Event.hasSpaceAvailable:
print("matching presented certificate with expected")
//get the presented certificate
let sslTrustInput: SecTrust? = aStream.property(forKey: kCFStreamPropertySSLPeerTrust as Stream.PropertyKey) as! SecTrust
if(sslTrustInput == nil)
{
print("something went horribly wrong in fetching the presented certificate")
broadcastSocketResult(result: false)
return
}
if(Vars.expectedCert == nil)
{
print("probably a bug, there is no expected certificate in Vars. fail/crash in 3, 2, 1...")
broadcastSocketResult(result: false)
return
}
//set the expected certificate as the only "trusted" one
let acceptedCerts: NSMutableArray = NSMutableArray()
acceptedCerts.add(Vars.expectedCert!)
SecTrustSetAnchorCertificates(sslTrustInput!, acceptedCerts)
//check the certificate match test results
var result: SecTrustResultType = SecTrustResultType.fatalTrustFailure //must initialize with something
let err: OSStatus = SecTrustEvaluate(sslTrustInput!, &result)
if(err != errSecSuccess)
{
print("problem evaluating certificate match")
broadcastSocketResult(result: false)
return
}
if (result != SecTrustResultType.proceed)
{
print("certificate was not signed by private CA")
broadcastSocketResult(result: false)
return
}
print("socket ssl turned out ok")
broadcastSocketResult(result: true)
break
case Stream.Event.openCompleted:
print("socket is useable")
break
case Stream.Event.errorOccurred:
print("something bad happened")
broadcastSocketResult(result: false)
break;
default:
print("some other code" + String(describing: eventCode))
broadcastSocketResult(result: false)
break
}
}
private func broadcastSocketResult(result: Bool)
{
let extras = [Const.BORADCAST_SOCKET_RESULT: result]
NotificationCenter.default.post(name: NSNotification.Name(rawValue: Const.BROADCAST_SOCKET), object: extras)
}
}
The private CA's public key is obtained from a base64 encoding dump into a text box.
var certDumpValue: String? = certDump.text
if(certDumpValue == nil || certDumpValue! == "" || certDumpValue!.characters.count < 28)
{
//in android the certificate is either there or not. In iOS it could be there, or not, or incomplete.
//also no longer a file in iOS but a base64 dump
Utils.showOk(screen: self, message: "Certificate corrupted. Can't use.")
return;
}
else
{
//check it is a real certificate and not just random text or a poem
//https://stackoverflow.com/questions/28957940/remove-all-line-breaks-at-the-beginning-of-a-string-in-swift
//https://digitalleaves.com/blog/2015/10/sharing-public-keys-between-ios-and-the-rest-of-the-world/
//prepare the base64 string dump for usage
//trim off the ---start ceritificate--- and end certificate tags, get rid of the newlines
certDumpValue = certDumpValue!.replacingOccurrences(of: "\n", with: "")
let startChop = certDumpValue!.index(certDumpValue!.startIndex, offsetBy: 27)
let endChop = certDumpValue!.index(certDumpValue!.startIndex, offsetBy: certDumpValue!.characters.count-26)
certDumpValue = certDumpValue![startChop...endChop] //most complicated method of substring imaginable
//check if the string is a valid base64 encoded string
let certRaw: NSData? = NSData(base64Encoded: certDumpValue!)
if(certRaw == nil)
{
Utils.showOk(screen: self, message: "Certificate corrupted. Can't use.")
return;
}
//if it is valid base64, check if it's a real certificate
let cert = SecCertificateCreateWithData(nil, certRaw!)
if(cert == nil)
{
Utils.showOk(screen: self, message: "Certificate corrupted. Can't use.")
return;
}
Vars.expectedCert = cert
}
When I try to connect to the server I always get a recoverable trust error. I set the CA public key as trust anchor so this shouldn't happen. This strategy is largely based off this