But first things first, setting up a server using openSSL is almost simpler than a client.
The only things we need to do is calling SSL_accept after the accept POSIC accept call returns a socket for a new connection.
let result = tipAccept(onSocket: acceptSocket, timeout: timeout, addressHandler: addressHandler)
switch result {
case .closed: return .closed
case let .error(msg): return .error(message: msg)
case .timeout: return .timeout
case let .accepted(receiveSocket, clientIp):
(PS: I am using code excerpts from SecureSockets which uses SwifterSockets as a wrapper for the POSIX calls)
Of course we also need to set up an SSL_CTX (i.e. context) for the server to use. Lets do that first:
guard let context = SSL_CTX_new(TLS_server_method()) else { ... }
SSL_CTX_set_options(context, (UInt(SSL_OP_NO_SSLv2) + UInt(SSL_OP_NO_SSLv3) + UInt(SSL_OP_ALL)))
if SSL_CTX_use_certificate_file(context, encodedFileC.path, encodedFileC.encoding) != 1 {
return .error(message: ...)
}
if SSL_CTX_use_PrivateKey_file(context, encodedFileK.path, encodedFileK.encoding) != 1 {
return .error(message: "...")
}
If the server only wants to serve clients with a specific certificate, then we also have to setup those certificate paths:
var isDirectory: ObjCBool = false
if FileManager.default.fileExists(atPath: clientCertPath, isDirectory: &isDirectory) {
if isDirectory.boolValue {
if SSL_CTX_load_verify_locations(context, nil, clientCertPath) != 1 {
return .error(message: "...")
}
} else {
if SSL_CTX_load_verify_locations(context, clientCertPath, nil) != 1 {
return .error(message: "...")
}
}
}
SSL_CTX_set_verify(context, SSL_VERIFY_PEER + SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nil)
Now the context is ready for use and we can start accepting SSL connections. Keep in mind that the SSL handshake is initiated by the client.
SSL_ACCEPT_LOOP: while true {
// ===================================
// Try to establish the SSL connection
// ===================================
let result = ssl.accept()
switch result {
// On success, return the new SSL structure
case .completed:
return .accepted(ssl: ssl, socket: receiveSocket, clientIp: clientIp)
// Exit if the connection closed (i.e. there is no secure connection)
case .zeroReturn: return .closed
// Only waiting for a read or write is acceptable, everything else is an error
case .wantRead:
let selres = waitForSelect(socket: acceptSocket, timeout: timeoutTime, forRead: true, forWrite: false)
switch selres {
case .timeout: return .timeout
case .closed: return .closed
case let .error(message): return .error(message: "...")
case .ready: break
}
// Only waiting for a read or write is acceptable, everything else is an error
case .wantWrite:
let selres = waitForSelect(socket: acceptSocket, timeout: timeoutTime, forRead: false, forWrite: true)
switch selres {
case .timeout: return .timeout
case .closed: return .closed
case let .error(message): return .error(message: "...")
case .ready: break
}
// All of these are error's
case .wantConnect, .wantAccept, .wantX509Lookup, .wantAsync, .wantAsyncJob, .syscall, .ssl, .bios_errno, .errorMessage, .undocumentedSslError, .undocumentedSslFunctionResult:
return .error(message: "...")
}
}
This accept loop shows something important (that I now realise I did forget to mention in the previous parts... sorry... will update those parts later).
Any SSL_xxx call can result in multiple read/write operations on the POSIX level. Hence for the accept we also need to watch for POSIX read/write events and allow these to occur.
Other than that, accepting is easier than connecting since we don't have to verify the connection manually. The client does that.
To me, this makes sense. A server should be able to work autonomously. But a client can have user interactions where a user might want to accept a certificate that the verification process rejected.
For a server to allow an operator to accept a rejected client certificate is unworkable imo.
So far, everything is fine. The connection is accepted, a session is established and the read/write operations can proceed.
Yes, but... those pesky domains...
In the early days, each server had one domain. But it did not take long to figure out that it is much more cost effective for a server to host multiple domains. And since the domain owners may be unwilling to share a certificate, it must be possible to assign a certificate to each domain.
But the CTX can only handle a single certificate/key combination.
And thus SNI (Server Name Indication) was invented.
SNI takes a little more work. Not only from the server, also from the client. In a normal HTTP request, the client will send the domain name. Unfortunately the HTTP request is something that is done AFTER a secure connection is established. Hence that domain name cannot be used to find out which certificate is needed to start the secure connection.
Thus a client needs to tell the SSL layer (i.e. before the HTTP request is made) which domain it wants to access. Double work...
The client does so by setting up its session. In SecureSockets this is done by this function:
public func setTlsextHostname(_ name: UnsafePointer<Int8>) {
SSL_ctrl(optr, SSL_CTRL_SET_TLSEXT_HOSTNAME, Int(TLSEXT_NAMETYPE_host_name), UnsafeMutableRawPointer(mutating: name))
}
On the server side we need to do some more work.
The SSL handshake that occurs after an SSL_accept call, but before the SSL_accept is finished must have a way to allow the server application to select a certificate.
In OpenSSL 1.1.0 this is done by a callback function. When setting up the context for an accept we need to set the callback destination. Unfortunately this makes a little C-glue code necessary. In C they have implemented some trickery whereby a void(*)() ptr needs to be specified as the callback, but the callback signature itself is Int32(*)(OpaquePointer, Int32*, Void*).
I have not found a way to map those two onto each other in Swift. Hence a little C code:
void sslCtxSetTlsExtServernameCallback(SSL_CTX *ctx, int (*cb)(const SSL *ssl, int *num, void *arg), void *arg) {
SSL_CTX_set_tlsext_servername_arg(ctx, arg);
SSL_CTX_set_tlsext_servername_callback(ctx, cb);
}
As can be seen in the installation instructions of SecureSockets, I put this code in some openSSL files. That way I did not need to create any additional libraries, but simply compiled openSSL with this extension in it. I choose the ssl.h and ssl_lib.c files for this. But I think it is rather irrelevant in which files it is placed.
In the callback itself, we need to maintain a list of domain contexts. Each domain has its own context, and that context has the certificate & private key set for that domain. When the callback is activated, it hands us the session as a pointer. From that session we retrieved the asked-for domain name. And then we look for the proper domain context with the right certificate. Once we find the proper domain context, we pull a switcheroo and switch the context of the session to the context of the domain. Bam! done.
In SecureSockets code the installation of the callback and the callback itself are found in the class Ctx:
Installing the callback (calls out to the C-glue code):
sslCtxSetTlsExtServernameCallback(optr, sni_callback, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()))
And the callback itself
private let sni_callback: @convention(c) (_ ssl: OpaquePointer?, _ num: UnsafeMutablePointer<Int32>?, _ arg: UnsafeMutableRawPointer?) -> Int32 = {
(ssl_ptr, _, arg) -> Int32 in
// Get the reference to 'self'
let ourself = Unmanaged<Ctx>.fromOpaque(arg!).takeUnretainedValue()
// Get the String with the host name from the SSL session
guard let hostname = SSL_get_servername(ssl_ptr, TLSEXT_NAMETYPE_host_name) else { return SSL_TLSEXT_ERR_NOACK }
// Check if the current certificate contains the hostname
if let ctx_ptr = SSL_get_SSL_CTX(ssl_ptr) {
if let x509_ptr = SSL_CTX_get0_certificate(ctx_ptr) {
if X509_check_host(x509_ptr, hostname, 0, 0, nil) == 1 {
return SSL_TLSEXT_ERR_OK
}
}
}
// Check if there is another CXT with a certificate containing the hostname
var foundCtx: Ctx?
for testCtx in ourself.domainCtxs {
if testCtx.x509?.checkHost(hostname) ?? false {
foundCtx = testCtx
break
}
}
guard let newCtx = foundCtx else { return SSL_TLSEXT_ERR_NOACK }
// Set the new CTX to the current SSL session
if SSL_set_SSL_CTX(ssl_ptr, newCtx.optr) == nil {
// The new ctx did not have a certificate (found by source code inspection of ssl_lib.c)
// This should be impossible since that would have caused this CTX to be rejected
return SSL_TLSEXT_ERR_NOACK
}
return SSL_TLSEXT_ERR_OK
}
}
Well, that about wraps things up.
Only one thing left to do ... for you that is... ;-) see below...
Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH
We don't get the world we wish for... we get the world we pay for.