概述
詳細
需求:
並不是每個SSL/TLS站點都能得到一個全球公認的證書,很多時候需要自行生成自簽名證書做為根證書。有了自簽名根證書還需要手動地用它去驗證服務端證書。
概要:
1.生成自簽名概證書,服務端證書
2.做一個使用服務端證書的SSL/TLS的服務
3.做一個使用自簽名證書訪問服務的客戶端
結構:

效果:

初始界面 發送接收后
我們開始吧!
-
證書生成:
先構建目錄:
1.mkdir certs
2.cd certs
3.unzip store.zip
操作之后,目錄如下:
生成自簽名根證書,在centos上依次執行以下命令:
1.私鑰:openssl genrsa -out ca.key 1024
2.公鑰:openssl rsa -in ca.key -pubout -out ca.pem
3.證書:openssl req -new -x509 -days 365 -key ca.key -out ca.crt
執行完成在當前目錄下產生以下文件:

其中ca.pem就是生成的自簽名根證書。
生成服務端證書,在centos上依次執行如下命令:
1.私鑰:openssl genrsa -out server.key 1024
2.公鑰:openssl rsa -in server.key -pubout -out server.pem
3.請求:openssl req -new -nodes -key server.key -out server.csr
4.簽證:openssl ca -in server.csr -out server.crt -cert ca.crt -keyfile ca.key -config store/openssl.cnf
執行完成后當前目錄如下所示:

其中server.pem,server.key分別是服務端的證書和私鑰。因為iOS需der格式的證書,我們把根證書ca.pem轉換一下。
5.轉換:openssl x509 -outform der -in ca.crt -out ca.der
-
服務端程序:
好了,所需證書都已生成。其中服務端需要server.crt, server.key,需客戶端需要ca.der。下面我們先做一個非常簡單的服務端,用來配合客戶端的連接測試。在centos上創建文件server.go,內容如下:
package main
import (
"log"
"io"
"net"
"crypto/tls"
)
func main() {
crt, err := tls.LoadX509KeyPair("server.crt", "server.key")
if err != nil {
log.Fatal(err)
}
conf := &tls.Config{Certificates: []tls.Certificate{crt}}
listener, err := tls.Listen("tcp", ":8080", conf)
if err != nil {
log.Fatal(err)
}
defer listener.Close()
for {
conn, err := listener.Accept()
if err != nil {
log.Fatal(err)
}
process(conn)
conn.Close()
}
}
func process(conn net.Conn) {
rdata := make([]byte, 2048)
rlen, err := conn.Read(rdata)
if err != nil && err != io.EOF {
log.Println(err)
return
}
_, err = conn.Write(rdata[:rlen])
if err != nil {
log.Println(err)
return
}
}
程序很簡單,創建支持tls的服務程序,接收到發送過來的內容,再原樣返回出去。
編譯:go build server.go
注意:server.crt, server.key與編譯出來的server放在同一目錄下。然后,執行程序,等待連接到來。
執行:./server
-
客戶端程序:
創建一個iOS工程, 然后把ca.der拖到工程下面:
![]()

注意:添加ca.der時,一定要選上Add to targets選項。

Main.storyboard里添加一個Label和一個Button即可,我們畢竟只是演示tls如何工作,沒必要搞那么花哨。

在ViewController里添加上如下代碼:
import UIKit
import Network
class ViewController: UIViewController {
@IBOutlet weak var messageLabel: UILabel!
let queue = DispatchQueue(label: "myqueue")
var conn: NWConnection!
override func viewDidLoad() {
super.viewDidLoad()
messageLabel.layer.borderWidth = 1
}
@IBAction func start(_ sender: Any) {
let host = NWEndpoint.Host("10.21.16.202")
let port = NWEndpoint.Port(integerLiteral: 8080)
let options = NWProtocolTLS.Options()
sec_protocol_options_set_verify_block(options.securityProtocolOptions, { (sec_protocol_metadata, sec_trust, sec_protocol_verify_complete) in
// 為信任證書鏈設置自簽名根證書
let trust = sec_trust_copy_ref(sec_trust).takeRetainedValue()
if let url = Bundle.main.url(forResource: "ca", withExtension: "der"),
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, "myserver" as CFString)
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!)
}
}, queue)
conn = NWConnection(host: host, port: port, using: NWParameters(tls: options))
conn.start(queue: queue)
let messge = "hello"
conn.send(content: messge.data(using: .utf8)!, completion: .contentProcessed({ (error) in
if let error = error {
print(error)
self.conn.cancel()
} else {
print("消息已發送:\(messge)")
}
}))
conn.receive(minimumIncompleteLength: 1, maximumLength: 1024) { (data, context, isComplete, error) in
if let error = error {
print(error)
self.conn.cancel()
return
}
if let data = data {
DispatchQueue.main.async {
self.messageLabel.text = String(data: data, encoding: .utf8)!
}
print("消息已收到:\(String(data: data, encoding: .utf8)!)")
}
if isComplete {
self.conn.cancel()
self.conn = nil
}
}
}
}
NWConnection需要一個NWParameters類型的選項,當NWConnection建立連接以及收發數據的時候會使用這些選項調整連接的行為。系統默認一個選項是NWParameters.tls,然后這個選項在tls連接建立時驗證服務端證書的時候使用的是iOS系統里預置的要證書,這並不滿足我們的需求。
我們必須找到一個地方能定制化雙方握手時的證書驗證形為。我們可以通過配置NWProtocolTLS.Options.SecurityProtocolOptions添加一個驗證回調塊來達成這個需求。原型如下:
typedef void (^sec_protocol_verify_t)(sec_protocol_metadata_t metadata, sec_trust_t trust_ref, sec_protocol_verify_complete_t complete); API_AVAILABLE(macos(10.14), ios(12.0), watchos(5.0), tvos(12.0)) void sec_protocol_options_set_verify_block(sec_protocol_options_t options, sec_protocol_verify_t verify_block, dispatch_queue_t verify_block_queue);
回調塊的參數metadata,可以從中遍歷出對端的證書列表。參數trust_ref,可以從中遍歷出信任證書列表(對端的證書列表和對端證書鏈對應的根證書),當然我們自簽名根證書不在iOS系統中,系統不會自動為我們添加上,需要我們手動添加。
通過SecCertificateCreateWithData()我們從ca.der生成要證書對象,然后通過SecTrustSetAnchorCeritificates()把它添加信任證書鏈表中。接着我們通過SecPolicyCreateSSL()生成一個驗證策略,其中"myserver"是服務端證書對應的名字,可以查看服務端證書得到,這里也即限制服務端證書的CN必須為myserver,否則驗證失敗。SecTrustSetPolicies()為信任證書鏈添加驗證策略,SecTrunstSetAnchorCeritificatesOnly()只信任我們自已添加的根證書來驗證服務端證書。
SecTrustEvaluateWithError()來最終驗證服務端證書,如若有錯,通過打印error知道具體的錯誤原因。驗證的成功否是失敗都要通過參數complete回調來告知NWConnection以繼續后續的握手操作。
連接建立之后就可以自由的收發消息了。
最后項目結構介紹:
源碼目錄如下:

其中server.go是服務端的代碼, learn.zip是客戶端的代碼 store.zip是生成證書的時一些配置文件。
