以 CocoaMQTT 等套件嘗試連接 AWS IoT Core 踩坑大全
目錄
1. 前情提要
2. CocoaMQT 與 AWS IoT Core
2-1 kCFStreamErrorDomainSSL Code=-9806
2-2 MGCDAsyncSocketErrorDomain Code=8 “Error in SSLSetCertificate”
3. P.12
3-1 更改 openssl 指令碼
3-2 Allow Arbitrary Loads
⦿ 前情提要
⦿ CocoaMQT 與 AWS IoT Core
⦿ kCFStreamErrorDomainSSL Code=-9806
⦿ MGCDAsyncSocketErrorDomain Code=8 “Error in SSLSetCertificate”
⦿ P.12
⦿ 更改 openssl 指令碼
⦿ Allow Arbitrary Loads
前情提要
在之前的文章中,使用 ESP32 透過 PubSubClient 設置 AWS 相關憑證(CA X.509、Device X.509、Private Key)後,便能透過 MQTTs(8883 port)來與 AWS IoT Core 做溝通。
在 IoT 的相關文章中,也側重於 AWS IoT Core 的連接,其中最折騰人的便是 SSL/TLS
,即是憑證的獲取與使用,以 IoT 的架構中,雲端負責的是消息的轉發,也就是手機 APP 不與設備直連,而是透過對 MQTT topic 傳送訊息到雲端後,雲端也下發一個訊息到設備的 MQTT topic,藉此實現了遠端控制設備。
ESP32 須透過相關憑證方能與伺服器建立安全性的溝通,然而在手機 APP 的部份並不是如此,我們不將手機當作一個設備,因為它多是以控制、顯示當前設備狀態為目標,為此,會由伺服器提供的 API 來讓手機與之做溝通,以 AWS 為例,AWS 提供了一個 Cognito 服務,讓手機 APP 能夠與 AWS IoT Core 溝通。
如果今天不把手機當成控制器,或是顯示螢幕,而是把它當作 Device
呢?下面就來踩踩坑。
繼續閱讀|回目錄
CocoaMQTT 與 AWS IoT Core
在這個常用的 iOS 套件中,區分了 MQTT 3.1.1 與 MQTT 5 的連接方式,我們看到 AWS 的文件,裡面也說明了 AWS 支援兩種版本的 MQTT 連接,但它也註明了 IoT Core 與 MQTT 的差異,可以看到這份文件。
像是文件中說到,在 AWS 的 MQTT3.1.1 與 MQTT5 的 MQTT 報文是不支援 PUBREC
、PUBREL
、PUBCOMP
的,原因在於 AWS IoT Core 不支援 QoS 2,即是準確地僅傳送一次,而 PUBREC
、PUBREL
、PUBCOMP
正好是為了準確傳送一次而有的報文類型。
而 AWS 也訂定了 MQTT 傳送的封包,可以看到這份文件中,重要的限制如下:
- Client ID 須小於
128
bytes - 每帳每秒 MQTT 連線請求預設為
每秒 500 次
- 每帳預設並行連線上限為
50 萬
- Topic 不得超過
8 層
,即是7 個 /
- 單一 Topic 的訂閱上限為
8 個
配額 - Payload Size 在
128 KB
內 - MQTT Keep Alive 在
30 秒 ~ 1200 秒
- WebSocket 保持連線為
86400 秒
,即一天
回到 CocoaMQTT,MQTT Client 必須包含 Client ID、MQTT 端點、Port,如果是非安全性連線(沒有 SSL/TLS
),Client 就可以呼叫 connect()
這個 Function 了,不過要記得非安全性連線的 Port 為 1883,安全性連線則為 8883,程式碼如下:
let clientID = "CocoaMQTT--" + String(ProcessInfo().processIdentifier)
mqtt = CocoaMQTT(clientID: clientID,
host: defaultHost,
port: 1883)
mqtt!.logLevel = .debug
mqtt!.username = ""
mqtt!.password = ""
mqtt!.willMessage = CocoaMQTTMessage(topic: "/will",
string: "dieout")
mqtt!.keepAlive = 60
mqtt!.delegate = self
而在安全性連線下,我們要加入三個東西,即是 CA Certificate、Device Certificate 跟 Private Key,也是踩坑地獄的到來,首先,將 Client 設定為 mqtt!.enableSSL = true
—— 加入 SSL 的連線。
kCFStreamErrorDomainSSL Code=-9806
雖然 enableSSL 設為 true
,按下連線後並沒有真的透過憑證
建立連線,所以會在 Debug Area 出現如下錯誤:
[TRACE] [withError]: Optional(Error Domain=kCFStreamErrorDomainSSL Code=-9806 "(null)" UserInfo={NSLocalizedRecoverySuggestion=Error code definition can be found in Apple's SecureTransport.h})
這是由於 SSL/TLS 交握過程出錯,而如果使用 mqtt5!.allowUntrustCACertificate = true
這段程式碼,代表你要設置自己的憑證,這時 Debug Area 會多一段訊息:[TRACE] [didReceive]: trust: <SecTrustRef: 0x6000034441e0>,這表示要接受 X.509 證書的使用。
MGCDAsyncSocketErrorDomain Code=8 “Error in SSLSetCertificate”
在 CocoaMQTT 中想要使用 X.509,我們這樣設置:
if let caCertPath = Bundle.main.path(forResource: "AmazonRootCA", ofType: "pem"),
let caCert = try? Data(contentsOf: URL(fileURLWithPath: caCertPath)),
let certPath = Bundle.main.path(forResource: "certificate", ofType: "pem"),
let cert = try? Data(contentsOf: URL(fileURLWithPath: certPath)),
let pKeyPath = Bundle.main.path(forResource: "private", ofType: "pem"),
let pKey = try? Data(contentsOf: URL(fileURLWithPath: pKeyPath)) {
let certificates: [AnyObject] = [
cert as AnyObject,
pKey as AnyObject,
caCert as AnyObject
]
var sslSettings: [String: NSObject] = [:]
sslSettings[kCFStreamSSLCertificates as String] = certificates as NSObject
mqtt5!.sslSettings = sslSettings
}
在 MQTT Client 的 sslSettings 跟 CocoaMQTTSocket 的 sslSettings 是 [String: NSObject] 類型,以 kCFStreamSSLCertificates
作為 key,value 即是憑證
陣列。
這段程式碼是將你拖進
專案裡的憑證檔案,以 Data 讀出來,放到憑證陣列中,最後再放到 sslSettings 裡。然而,不論是 allowUntrustCACertificate 與否,最後都得到下面這樣的錯誤訊息:
[TRACE] [withError]: Optional(Error Domain=MGCDAsyncSocketErrorDomain Code=8 “Error in SSLSetCertificate” UserInfo={NSLocalizedDescription=Error in SSLSetCertificate})
SSL 仍然錯誤,這很可能是兩張 X.509(CA Certificate、Device Certificate)跟 Private Key 是 pem 檔,如果直接用 Data 讀出造成的錯誤,到這邊,即便處理過 X.509 再試著放入 sslSettings 裡仍沒成功。
繼續閱讀|回目錄
P.12
憑證相關的檔案,除了 PEM、DER,還有一種 P.12,如果 PEM 不能直接讀取,在 CocoaMQTT 中,直接操作 P.12 試試看,我們在 Command Line Tool 操作,將剛才的 PEM 包成一包 P.12,鍵入如下:
openssl pkcs12 -export -out client-keycert.p12 -inkey privateKey.pem -in clientCert.pem -certfile caCert.pem
openssl pkcs12 -export -out client-keycert.p12
-inkey privateKey.pem
-in clientCert.pem
-certfile caCert.pem
使用 openssl 工具,採用 pkcs12 演算法,在此種情況下要包出 client-keycert.p12,需要三個檔案,分別是 Private Key、Device Certificate、CA Certificate。
接著必須給 P.12 密碼,解開時需輸入此密碼,包完 P.12 後,我們也可鍵入 openssl pkcs12 -info -in path_to_file.p12
試試看,PEM 檔案,我們可以用文字編輯器(TextEditor)開啟,然而若用文字編輯器打開 P.12 則會看到一堆亂碼。
單一憑證的封裝,在鍵入上面的命令,我們可以看到 Base64 編碼的由 BEGIN CERTIFICATE
到 END CERTIFICATE
的憑證內容,然而 Private Key 卻不是 BEGIN RSA PRIVATE KEY
、END RSA PRIVATE KEY
了,而是如下:
這也已經跟原本 PEM 形式的 Private Key 長得不一樣了。
不過我們仍然需試試看,使用 func getClientCertFromP12File(certName: String, certPassword: String) -> CFArray?
這個 Function 取出 CFArray,它便是裝在陣列裡的憑證們。
程式碼如下:
let clientCertArray =
getClientCertFromP12File(certName: "keyAndcerts",
certPassword: "3345678")
var sslSettings: [String: NSObject] = [:]
sslSettings[kCFStreamSSLCertificates as String] = clientCertArray
mqtt5!.sslSettings = sslSettings
接著嘗試連線,出現錯誤訊息如下:
ERROR: SecPKCS12Import returned errSecAuthFailed. Incorrect password?
[TRACE] [didStateChangeTo]: new state: connecting
[TRACE] [didReceive]: trust: <SecTrustRef: 0x600003d50280>
[TRACE] [withError]: Optional(Error Domain=kCFStreamErrorDomainSSL Code=-9806 "(null)" UserInfo={NSLocalizedRecoverySuggestion=Error code definition can be found in Apple's SecureTransport.h})
[TRACE] [didStateChangeTo]: new state: disconnected
首先在 SecPKCS12Import 時就已經出現錯誤警告了,接著又出現前段的 -9806,其實這些錯誤通稱 SSL/TLS
交握錯誤,很顯然仍是憑證的問題。
更改 openssl 指令碼
我們假定是 openssl 在包成 P.12 的時候,改動了 Private Key 所以造成交握錯誤,那輸入下列指令:
openssl pkcs12 -export -out client-keycert.p12
-inkey privateKey.pem
-in clientCert.pem
-certfile caCert.pem
-nodes
多了一個nodes
,即是 “No DES”,不使用 DES 加密 Private Key,此時可以輸入另外一個指令,只單純看 Private Key 的變化。
openssl pkcs12 -in client-keycert.p12
-nocerts -out extracted-key.pem
只提取 PEM,但很不幸地,仍然是 BEGIN ENCRYPTED PRIVATE KEY
與 END ENCRYPTED PRIVATE KEY
作為開頭和結尾。