Swift使用原生Network框架对自签名证书进行双向认证并通信

1.为啥要写这篇文章?

之前作者触摸的都是用Objective-C语言及CocoaAsyncSocket进行Socket方面的编程,后来为了节约通信的流量,又加入了Protobuf来进行数据传输。从一开端一条Socket通道来解析一切数据(后台依据音讯重要程度区分不同的优先级音讯行列,再进行相关推送),并经过告诉的方式把事务数据抛给事务层到证券行业的两条Socket通道,一条担任Query(查询),一条担任Push(推送),Query Socket封装成Http格局的恳求并可设置恳求超时时间,Push Socket数据解析成功之后直接以告诉的方式抛给事务层。一起,在2016年直播答题比较火的时分,也用过腾讯的Mars结构开发过直播答题的App。今年也触摸了对Socket进行TLS 双向认证来保证数据传输的安全性和可靠性。

最近,作者在进行Swift开发的时分,突然想到Objective-CCocoaAsyncSocket这个结构来进行Socket开发,那么Swift有没有什么好的结构呢?在网上查找Swift Socket编程结构的时分发现了iOS原生开发结构Network,作者抱着试一试的情绪对这个结构研讨了一下,下面就来给咱们共享一下。

2.设置TLS装备并建议衔接

这里有几点:1.需要装备成TLS; 2.可选设置制止那些网络类型;3.运用ipv6协议;4.preferNoProxies要不要制止敞开代理的情况下进行Socket衔接;5.最终设置服务端的IP + Port,进行衔接。代码如下:

var myQueue = DispatchQueue.global()
let options = NWProtocolTLS.Options()
self?.params = NWParameters(tls: options)
// self.params = NWParameters.tcp
//params.prohibitedInterfaceTypes = [.wifi, .cellular]
// 运用ipv6协议
if let ipOption = self?.params.defaultProtocolStack.internetProtocol as? NWProtocolIP.Options {
    ipOption.version = .v6
}
// 制止代理
self?.params.preferNoProxies = true
self?.connection = NWConnection(host: NWEndpoint.Host("101.***.***.***"), port: NWEndpoint.Port(integerLiteral: 8902), using: self?.params ?? NWParameters())
self?.connection.start(queue: self!.myQueue)

3.TLS-验证服务端证书

由于咱们选用的是TLS双向认证,TLS双向认证分为两步:第一步是认证服务端证书,第二步是认证客户端,只要服务端和客户端都认证完结之后,才认为TLS认证通道建立起来了。

// 设置服务端证书验证的回调
sec_protocol_options_set_verify_block(options.securityProtocolOptions, { [weak self] (sec_protocol_metadata, sec_trust, sec_protocol_verify_complete) in
    // 为信任证书链设置自签名根证书
    let trust = sec_trust_copy_ref(sec_trust).takeRetainedValue()
    if let curBundle = self?.curBundle(), let url = curBundle.url(forResource: "server_cert", withExtension: "cer"),
    let data = try? Data(contentsOf: url), let cert = SecCertificateCreateWithData(nil, data as CFData) {
    if SecTrustSetAnchorCertificates(trust, [cert] as CFArray) != errSecSuccess {
      sec_protocol_verify_complete(false)
      return
    }
    }
    // 设置验证策略
    let policy = SecPolicyCreateSSL(true, nil)
    SecTrustSetPolicies(trust, policy)
    SecTrustSetAnchorCertificatesOnly(trust, true)
    // 验证证书链
    var error: CFError?
    if SecTrustEvaluateWithError(trust, &error) {
        sec_protocol_verify_complete(true)
    } else {
        sec_protocol_verify_complete(false)
        print(error!)
    }

第二行装备了TLS认证方式,第五行设置了服务端证书认证的回调。咱们这里选用的是证书链验证,其间server_cert.cert是服务端自签名的根证书,内置在客户端。这里的验证原理是,首先从本地把服务端根证书读出来,并把它设置成锚点证书。其次,设置验证策略,SecTrustSetAnchorCertificatesOnly(trust, true)这行表明,只用自己设置的证书链进行验证,不必体系的证书链。最终SecTrustEvaluateWithError(trust, &error)便是对服务端证书进行本地证书链验证并把最终结果回调出去。

由于咱们选用的是自签名证书,在进行上述这一步服务端证书校验的过程中,要事先将证书下载到手机上并进行装置,装置成功之后要去体系设置里边把”信任”开关打开,装置过Charles Https抓包证书的小伙伴应该知道这个过程。对用户来说不太现实,所以这一步咱们一般对服务端证书校验比如PublicKey、Host、Signature(经过导入OpenSSL来进行相关校验)。

4.TLS-设置要验证的客户端证书

读取本地app内p12格局的证书,这里是”client_cert.p12″,内置在bundle文件中。password为证书的暗码,客户端设置好之后,服务端会对该证书进行认证。

/// 设置客户端证书
/// - Parameter options: NWProtocolTLS.Options
func setClientCert(options: NWProtocolTLS.Options, complete: (() -> Void)?) {
    guard let curBundle = curBundle(), let certUrl = curBundle.url(forResource: "client_cert", withExtension: "p12") else {
        return
    }
    guard let certData = try? Data(contentsOf: certUrl) else {
        return
    }
    let cfCertData = certData as CFData
    let password = "******"
    let importOptions = [kSecImportExportPassphrase: password ] as NSDictionary
    var rawItems: CFArray?
    myQueue.async {
        let status = SecPKCS12Import(cfCertData as CFData, // Data from imported Identity.
                                     importOptions as CFDictionary,
                                     &rawItems)
        DispatchQueue.main.async {
            guard status == errSecSuccess, let items = rawItems, let dictionaryItems = items as? Array<Dictionary<String, Any>> else {
                return
            }
            let secIdentity: SecIdentity = dictionaryItems[0][kSecImportItemIdentity as String] as! SecIdentity
            let identity = sec_identity_create(secIdentity)
            sec_protocol_options_set_local_identity(options.securityProtocolOptions, identity!)
        complete?()
      }
    }
  }

证书校验过程中如果出现错误,可在<Security/SecBase.h>查找对应错误码的解说。

5.监听Socket衔接状况回调

self?.connection.stateUpdateHandler = { (newState) in
    switch newState {
    case .ready:
        print("state ready")
        // 当状况为ready状况时,就能够发送事务数据了
    case .cancelled:
        print("state cancel")
    case .waiting(let error):
        print("state waiting \(error)")
    case .failed(let error):
        print("state failed \(error)")
    default:
        break
    }
}

当connection处于ready状况下,才能发送事务数据。其它几种状况能够检查苹果的阐明文档。

6.经过ProtoBuf协议发送事务层数据

咱们先界说一个两边通信的协议,如下

/** Socket包包头界说 */
typedef struct {
    /** 包头+包体(2Bytes) */
    UInt16 totalLen;
    /** 包序列号(2Bytes) */
    UInt16 serialNo;
    /** 功用号(2Bytes) */
    UInt16 funcId;
    /** 扩展字段(1Byte) */
    UInt8 extension;
} PackageHeader;

一个完好的数据包由包头 + 包体组成, 包头用2个字节表明包的总长度,2个字节表明序列号(回包时对应到相关恳求),2个字节表明功用号(发送的是什么样的音讯),1个字节的扩展字段。介绍完包头的界说之后,咱们来组装一个事务层登录的数据包。

func sendLoginRequest() {
    let loginRequest = LoginRequest();
    loginRequest.userId = "12345678";
    loginRequest.password = "888888";
    let bodyData = loginRequest.data()
    if let bodyData {
        var header = PackageHeader();
        let headerLen = MemoryLayout<PackageHeader>.size
        header.totalLen = CFSwapInt16HostToBig(UInt16(headerLen + bodyData.count))
        header.serialNo = CFSwapInt16HostToBig(1)
        header.funcId = CFSwapInt16HostToBig(10001)
        header.extension = 0
        let headerData = withUnsafeBytes(of: header) { Data($0) }
        let finalData: Data? = headerData + bodyData
        self.connection.send(content: finalData, completion: .contentProcessed({ error in
            if let sendError = error {
                print(sendError)
            } else {
                print("音讯已发送,内容为: ")
            }
        }))
    }
}

在PackageHeader实例转化成Data之前,要对其间的大于1个字节的整型字段进行字节序转化,关于大端序小端序主机字节序网络字节序相关的介绍咱们可自行百度进行了解。下面列了几个咱们在组包和拆包过程中要用到几个函数:

函数 阐明
MemoryLayout<PackageHeader>.size 取结构体的巨细
CFSwapInt16HostToBig 把Int16类型的变量从主机字节序转为网络字节序
CFSwapInt32HostToBig 把Int32类型的变量从主机字节序转为网络字节序
CFSwapInt64HostToBig 把Int64类型的变量从主机字节序转为网络字节序
CFSwapInt16BigToHost 把Int16类型的变量从网络字节序转为主机字节序
CFSwapInt32BigToHost 把Int32类型的变量从网络字节序转为主机字节序
CFSwapInt64BigToHost 把Int32类型的变量从网络字节序转为主机字节序

把包头和包体数据组装成finalData,调用connection的send办法,就能够把数据发送出去,依据回调咱们能够知道音讯发送成功与否。

7. 解析收到的回包数据为ProtoBuf对应的类

receive这个函数有两个参数,minimumIncompleteLength表明最小接受的字节长度,这里因为咱们协议头的长度为7,所以这里设置为7;maximumLength表明接受的最大数据长度,这里简单设置了一下,仅做测试用。网上比较常用的做法的是,第一次把minimumIncompleteLengthmaximumLength都设置成包头的长度,在这里便是7。然后经过解析包头后得到包体的长度比如是30,然后经过receive函数设置minimumIncompleteLengthmaximumLength均为30,就能够读到一个完好的包体数据并解析成具体的类(这里是LoginResponse这个类)。

self?.connection.receive(minimumIncompleteLength: 7, maximumLength: 1024) { content, contentContext, isComplete, error in
    if let error = error {
        print(error)
        self?.connection.cancel()
        return
    }
    if let data = content {
        DispatchQueue.main.async {
            let bodyData = data.subdata(in: 7..<data.count)
            do {
                let loginResponse = try LoginResponse.parse(from: bodyData)
                print("loginresponse: \(String(describing: loginResponse))")
            } catch {
                print("error \(error)")
            }
            print("total len:\(data.count)")
        }
    }
    if isComplete {
        self?.connection.cancel()
        self?.connection = nil
    }
}