Hans Kröner
Hans Kröner

Reputation: 61

How should Secure Transport TLS be used with BSD sockets in Swift?

I'm trying to use Secure Transport with BSD sockets using Swift. It seems like it should be simple enough, but I can't get it to work and documentation on the subject is scarce.

I've boiled my issue down to a simple "Socket" class, where I've (to the best of my knowledge) fulfilled the requirements of Secure Transport.

import Cocoa

class Socket: NSObject {

    private let hello = "Hello!"
    private var socketfd: Int32
    private var sock_addr: sockaddr

    private var sslContext: SSLContext?

    var sslWriteCallbackFunc: SSLWriteFunc {
        get {
            let ump = UnsafeMutablePointer<((SSLConnectionRef, UnsafePointer<Void>,
                UnsafeMutablePointer<Int>) -> OSStatus)>.alloc(1)

            ump.initialize(sslWriteCallback)

            return CFunctionPointer<((SSLConnectionRef, UnsafePointer<Void>,
                UnsafeMutablePointer<Int>) -> OSStatus)>(COpaquePointer(ump))
        }
    }

    var sslReadCallbackFunc: SSLReadFunc {
        get {
            let ump = UnsafeMutablePointer<((SSLConnectionRef, UnsafeMutablePointer<Void>,
                UnsafeMutablePointer<Int>) -> OSStatus)>.alloc(1)

            ump.initialize(sslReadCallback)

            return CFunctionPointer<((SSLConnectionRef, UnsafeMutablePointer<Void>,
                UnsafeMutablePointer<Int>) -> OSStatus)>(COpaquePointer(ump))
        }
    }

    init(address: String, port: UInt16) {
        socketfd = Darwin.socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)

        var addr = Darwin.sockaddr_in(sin_len: __uint8_t(sizeof(sockaddr_in)), sin_family: sa_family_t(AF_INET), sin_port: CFSwapInt16(port), sin_addr: in_addr(s_addr: inet_addr(address)), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
        sock_addr = Darwin.sockaddr(sa_len: 0, sa_family: 0, sa_data: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
        Darwin.memcpy(&sock_addr, &addr, Int(sizeof(sockaddr_in)))

        super.init()
    }

    func connect() -> Socket {
        let err = Darwin.connect(socketfd, &sock_addr, socklen_t(sizeof(sockaddr_in)))

        return self
    }

    func makeSecure() -> Socket {
        if let umc = SSLCreateContext(nil, kSSLClientSide, kSSLStreamType) {
            sslContext = umc.takeRetainedValue()

            var status = SSLSetIOFuncs(sslContext!, sslReadCallbackFunc, sslWriteCallbackFunc)
            status = SSLSetConnection(sslContext!, &socketfd)

            SSLHandshake(sslContext!)
        }

        return self
    }

    func sendHello() -> Socket {
        let bytes = [UInt8](hello.utf8)
        let data = NSData(bytes: bytes, length: bytes.count)

        let test = UnsafeMutablePointer<Int>.alloc(1)
        test.initialize(bytes.count)

        self.sslWriteCallback(&socketfd, data: data.bytes, dataLength: test)

        return self
    }

    // MARK: - SSL Callback Methods

    func sslReadCallback(connection: SSLConnectionRef,
        data: UnsafeMutablePointer<Void>,
        dataLength: UnsafeMutablePointer<Int>) -> OSStatus {

            let bytesRead = read(socketfd, data, UnsafePointer<Int>(dataLength).memory)

            return noErr
    }

    func sslWriteCallback(connection: SSLConnectionRef,
        data: UnsafePointer<Void>,
        dataLength: UnsafeMutablePointer<Int>) -> OSStatus {

            let sent = Darwin.sendto(socketfd, data, UnsafePointer<Int>(dataLength).memory, 0, &sock_addr, socklen_t(sizeof(sockaddr_in)))
            if (sent < 0) {
                let error = NSError(domain: NSPOSIXErrorDomain, code: Int(errno), userInfo: nil)
                println(error.localizedDescription)
            } else {
                println("Sent \(sent) bytes (\(hello))")
            }

            return noErr
    }
}

I've tested the non-TLS socket communication by making a simple instance:

let socket = Socket(address: "some-ip-address", port: 8080)
socket.connect().sendHello()

and running an echo server on the target machine using netcat. This works fine.

nc -l -p 8080

Trying to wrap the socket in Secure Transport's TLS (calling the makeSecure() method) crashes at the call to SSLHandshake(...) with a EXC_BAD_ADDRESS(code=2, address=...) error. Does anyone have any pointers as to what it is I'm missing here?

EDIT

I can see Console puts out:

04/06/15 09:20:48,000 kernel[0]: Data/Stack execution not permitted: TheProject[pid 29184] at virtual address 0x100602000, protections were read-write

EDIT 2

I got it working with Swift 2 in the Xcode 7 beta. See below.

Upvotes: 1

Views: 2758

Answers (3)

Brett
Brett

Reputation: 767

The answer by Hans appears to allocate memory unnecessarily. The following is a Swift 3.1 version with more error checking and URL support and grabs the common name to return (rather than actually reading or writing data).

func getCNforSSL(at url:URL, port:UInt16) -> String? {
  var socketfd = Darwin.socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)

  guard let ip = urlToIP(url) else {
    NSLog("Could not get IP from URL \(url)")
    return nil
  }

  let inAddr = in_addr(s_addr: inet_addr(ip))

  var addr = sockaddr_in(sin_len: __uint8_t(MemoryLayout<sockaddr_in>.size),
                         sin_family: sa_family_t(AF_INET),
                         sin_port: CFSwapInt16(port),
                         sin_addr: inAddr,
                         sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
  var sock_addr = sockaddr(sa_len: 0,
                           sa_family: 0,
                           sa_data: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
  _ = memcpy(&sock_addr, &addr, MemoryLayout<sockaddr_in>.size)

  guard connect(socketfd, &sock_addr, socklen_t(MemoryLayout<sockaddr_in>.size)) == 0 else {
    NSLog("Failed connection for \(url) port \(port) with error \(Darwin.errno)")
    return nil
  }

  defer {
    if close(socketfd) != 0 {
      NSLog("Error closing socket for \(url) port \(port) with error \(Darwin.errno)")
    }
  }

  guard let sslContext = SSLCreateContext(kCFAllocatorDefault, .clientSide, .streamType) else {
    NSLog("Could not create SSL Context for \(url) port \(port)")
    return nil
  }

  defer {
    SSLClose(sslContext)
  }

  SSLSetIOFuncs(sslContext, sslReadCallback, sslWriteCallback)
  SSLSetConnection(sslContext, &socketfd)
  SSLSetSessionOption(sslContext, .breakOnServerAuth, true)

  var secTrust:SecTrust? = nil
  var status:OSStatus = 0
  var subject:String? = nil
  repeat {
    status = SSLHandshake(sslContext)
    if status == errSSLPeerAuthCompleted {
      SSLCopyPeerTrust(sslContext, &secTrust)
      if let trust = secTrust {
        // 0 always garunteed to exist
        let cert = SecTrustGetCertificateAtIndex(trust, 0)!
        subject = SecCertificateCopySubjectSummary(cert) as String?
      }
    }
  } while status == errSSLWouldBlock

  guard status == errSSLPeerAuthCompleted else {
    NSLog("SSL Handshake Error for \(url) port \(port) OSStatus \(status)")
    return nil
  }

  return subject
}

func sslReadCallback(connection: SSLConnectionRef,
                            data: UnsafeMutableRawPointer,
                            dataLength: UnsafeMutablePointer<Int>) -> OSStatus {

  let socketfd = connection.load(as: Int32.self)

  let bytesRequested = dataLength.pointee
  let bytesRead = read(socketfd, data, UnsafePointer<Int>(dataLength).pointee)

  if (bytesRead > 0) {
    dataLength.initialize(to: bytesRead)
    if bytesRequested > bytesRead {
      return Int32(errSSLWouldBlock)
    } else {
      return noErr
    }
  } else if (bytesRead == 0) {
    dataLength.initialize(to: 0)
    return Int32(errSSLClosedGraceful)
  } else {
    dataLength.initialize(to: 0)
    switch (errno) {
    case ENOENT: return Int32(errSSLClosedGraceful)
    case EAGAIN: return Int32(errSSLWouldBlock)
    case ECONNRESET: return Int32(errSSLClosedAbort)
    default: return Int32(errSecIO)
    }
  }
}

func sslWriteCallback(connection: SSLConnectionRef,
                             data: UnsafeRawPointer,
                             dataLength: UnsafeMutablePointer<Int>) -> OSStatus {
  let socketfd = connection.load(as: Int32.self)

  let bytesToWrite = dataLength.pointee
  let bytesWritten = write(socketfd, data, UnsafePointer<Int>(dataLength).pointee)

  if (bytesWritten > 0) {
    dataLength.initialize(to: bytesWritten)
    if (bytesToWrite > bytesWritten) {
      return Int32(errSSLWouldBlock)
    } else {
      return noErr
    }
  } else if (bytesWritten == 0) {
    dataLength.initialize(to: 0)
    return Int32(errSSLClosedGraceful)
  } else {
    dataLength.initialize(to: 0)
    if (EAGAIN == errno) {
      return Int32(errSSLWouldBlock)
    } else {
      return Int32(errSecIO)
    }
  }
}

private func urlToIP(_ url:URL) -> String? {
  guard let hostname = url.host else {
    return nil
  }

  guard let host = hostname.withCString({gethostbyname($0)}) else {
    return nil
  }

  guard host.pointee.h_length > 0 else {
    return nil
  }

  var addr = in_addr()
  memcpy(&addr.s_addr, host.pointee.h_addr_list[0], Int(host.pointee.h_length))

  guard let remoteIPAsC = inet_ntoa(addr) else {
    return nil
  }

  return String.init(cString: remoteIPAsC)
}

Upvotes: 1

Hans Kr&#246;ner
Hans Kr&#246;ner

Reputation: 61

Starting with Swift 2 included with the Xcode 7 beta, Function Pointers in Swift work and have been greatly simplified. I turned my example above into this, which works:

import Foundation

func sslReadCallback(connection: SSLConnectionRef,
    data: UnsafeMutablePointer<Void>,
    var dataLength: UnsafeMutablePointer<Int>) -> OSStatus {

        let socketfd = UnsafePointer<Int32>(connection).memory

        let bytesRequested = dataLength.memory
        let bytesRead = read(socketfd, data, UnsafePointer<Int>(dataLength).memory)

        if (bytesRead > 0) {
            dataLength = UnsafeMutablePointer<Int>.alloc(1)
            dataLength.initialize(bytesRead)
            if bytesRequested > bytesRead {
                return Int32(errSSLWouldBlock)
            } else {
                return noErr
            }
        } else if (bytesRead == 0) {
            dataLength = UnsafeMutablePointer<Int>.alloc(1)
            dataLength.initialize(0)
            return Int32(errSSLClosedGraceful)
        } else {
            dataLength = UnsafeMutablePointer<Int>.alloc(1)
            dataLength.initialize(0)
            switch (errno) {
            case ENOENT: return Int32(errSSLClosedGraceful)
            case EAGAIN: return Int32(errSSLWouldBlock)
            case ECONNRESET: return Int32(errSSLClosedAbort)
            default: return Int32(errSecIO)
            }
        }
}

func sslWriteCallback(connection: SSLConnectionRef,
    data: UnsafePointer<Void>,
    var dataLength: UnsafeMutablePointer<Int>) -> OSStatus {

        let socketfd = UnsafePointer<Int32>(connection).memory

        let bytesToWrite = dataLength.memory
        let bytesWritten = write(socketfd, data, UnsafePointer<Int>(dataLength).memory)

        if (bytesWritten > 0) {
            dataLength = UnsafeMutablePointer<Int>.alloc(1)
            dataLength.initialize(bytesWritten)
            if (bytesToWrite > bytesWritten) {
                return Int32(errSSLWouldBlock)
            } else {
                return noErr
            }
        } else if (bytesWritten == 0) {
            dataLength = UnsafeMutablePointer<Int>.alloc(1)
            dataLength.initialize(0)
            return Int32(errSSLClosedGraceful)
        } else {
            dataLength = UnsafeMutablePointer<Int>.alloc(1)
            dataLength.initialize(0)
            if (EAGAIN == errno) {
                return Int32(errSSLWouldBlock)
            } else {
                return Int32(errSecIO)
            }
        }
}
var socketfd = Darwin.socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)

var addr = Darwin.sockaddr_in(sin_len: __uint8_t(sizeof(sockaddr_in)), sin_family: sa_family_t(AF_INET), sin_port: CFSwapInt16(8080), sin_addr: in_addr(s_addr: inet_addr("192.168.0.113")), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
var sock_addr = Darwin.sockaddr(sa_len: 0, sa_family: 0, sa_data: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
Darwin.memcpy(&sock_addr, &addr, Int(sizeof(sockaddr_in)))

var err = Darwin.connect(socketfd, &sock_addr, socklen_t(sizeof(sockaddr_in)))

if let umc = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType) {
    var sslContext = umc.takeRetainedValue()
    SSLSetIOFuncs(sslContext, sslReadCallback, sslWriteCallback)
    SSLSetConnection(sslContext, &socketfd)
    SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnClientAuth, Boolean(1))
    SSLHandshake(sslContext)
}

Upvotes: 2

geowar
geowar

Reputation: 4447

I asked a network guru peer your question; this was his response:

This person is SOL because Secure Transport requires that you implement C function callbacks and Swift does not currently support that .

I recommend that developer use CFSocketStream, which takes care of TLS and is easily callable from Swift. See the TLSTool sample code.

https://developer.apple.com/library/mac/samplecode/SC1236/

Upvotes: 0

Related Questions