以 CocoaMQTT 等套件嘗試連接 AWS IoT Core 踩坑大全

春麗 S.T.E.M.
12 min readDec 5, 2023

--

目錄
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 報文是不支援 PUBRECPUBRELPUBCOMP 的,原因在於 AWS IoT Core 不支援 QoS 2,即是準確地僅傳送一次,而 PUBRECPUBRELPUBCOMP 正好是為了準確傳送一次而有的報文類型。

而 AWS 也訂定了 MQTT 傳送的封包,可以看到這份文件中,重要的限制如下:

  1. Client ID 須小於 128 bytes
  2. 每帳每秒 MQTT 連線請求預設為每秒 500 次
  3. 每帳預設並行連線上限為 50 萬
  4. Topic 不得超過 8 層,即是 7 個 /
  5. 單一 Topic 的訂閱上限為 8 個配額
  6. Payload Size 在 128 KB
  7. MQTT Keep Alive 在 30 秒 ~ 1200 秒
  8. 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 CERTIFICATEEND CERTIFICATE 的憑證內容,然而 Private Key 卻不是 BEGIN RSA PRIVATE KEYEND 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 KEYEND ENCRYPTED PRIVATE KEY 作為開頭和結尾。

Allow Arbitrary Loads

沒轍了,這樣的結果怪罪給蘋果 ATS 似乎是個蠻方便的事,不過在這裡

已經設定為 YES 了,仍沒有辦法狀況排除。

而 AWS 的 Cognito 服務方便手機 APP 連到 IoT Core,使用 API 並設置正確,便可發送訊息到 IoT Core 與接收來自 AWS MQTT 測試用戶端傳來的資料,便不需要將手機當作 Device,設置憑證再與 AWS 連線了。下次再來講講 Cognito 如何使用吧!

這次踩坑就到這邊,感謝您的閱讀。

回目錄

繼續閱讀|回目錄

--

--

春麗 S.T.E.M.
春麗 S.T.E.M.

Written by 春麗 S.T.E.M.

Do not go gentle into that good night, Old age should burn and rave at close of day; Rage, rage, against the dying of the light.

Responses (1)