{%hackmd xIp4k0sIS6SuPecdKUPMlw %} > [name=Alvin Yang] [color=#11ccff] [TOC] --- ## 前言 <!-- 本文件包含使用 AWS IoT Core 建立測試裝置及相關自訂身份驗證的流程、相關參數定義、使用方法 因大量的 IoT 設備憑證存放於 AWS IoT Core 需要大量成本,故尋求其他替代方案 AWS 官方文件提及 **MQTT over WebSocket** 協定的驗證方式有兩種,此文件將針對自訂身份驗證多加描述,預期可以達到自架 DB 存放自訂公私鑰,提供裝置使用 --> 這份文件介紹了如何在 AWS IoT Core 上建立測試裝置以及相關的自訂身份驗證流程、參數定義,使用方法。由於在 AWS IoT Core 上存放大量 IoT 設備憑證會帶來高額成本,因此我們的焦點在於**尋找替代解決方案**,以替換 AWS IoT Core 工作流程,**簡化成直接讓 Device 與 AWS IoT Core 驗證後,建立連線,再透過自行封裝的 Service (Credential Provider 的 Lambda) 取得相關權限,用以正常運作**,最終達到成本節省的目標。 :book: **目標**:可以動態選擇驗證方式(X.509、自訂驗證) 使<font color=#ff00>原 AWS IoT Core 的工作流程</font> <---抽換---> <font color=#55ccdd>封裝 Service 後的工作流程 **(圖-自定義授權流程)**</font> <font size="5">**原 AWS IoT Core 的工作流程**</font> ``` graphviz digraph { label="圖-AWS IoT Core 工作流程" rankdir=LR; subgraph cluster_a { label="取得限時授權" edge [color=Blue, style=dashed] node [shape = defaul]; D [label="A. Device SDK" fontcolor="Red"] CP [label="B. Credentials provider (X.509)" shape = default fontcolor="Red"] Auth [label="C. AWS IoT Authentication and Authorization Module"] STS [label="D. AWS STS"] D -> CP [ label = "1. Request" ] CP -> Auth [ label = "2. Validate request" ] Auth -> CP [ label = "3. Success / Faulure" ] CP -> STS [ label = "4. Assume Role" ] STS -> CP [ label = "5. Sucurity token"] CP -> D [ label = "6. Sucurity token"] } subgraph cluster_b { label="換證" edge [color=Blue, style=dashed] node [shape = defaul]; AWS [label="E. AWS"] IAM [label="F. AWS IAM"] D -> AWS [ label = "7. Sign request with retrieved security token"] AWS -> IAM [ label = "8. Validate Signature"] IAM -> AWS [ label = "9. Allow / Deny"] } rank="same" {D} } ``` > 引用自 (AWS IoT Core work flow) https://docs.aws.amazon.com/zh_tw/iot/latest/developerguide/authorizing-direct-aws.html --- <font size="5">**封裝 Service 後的工作流程**</font> ``` graphviz digraph { label="圖-自定義授權流程" nodesep=1 // increases the separation between nodes rankdir=UD; size="8,5" node [shape=default] D [label="Device" fontcolor=red] L [label="Authorizer Lambda function " fontcolor=red] AIC [label="AWS IoT Core" fontcolor=red] N1 [label="N1", shape=diamond,style=filled,label="",height=0,width=1] ; D -> AIC [dir="both"] AIC -> L [label="Credentials and connection data"] L -> AIC [label=" AWS IoT Core Policy"] AIC -> N1 {rank=same; L, AIC } {rank=same; D } {rank=same; N1 } // {rank=same; PC, PE } // {rank=same; Nod1 } } ``` ``` graphviz digraph { label="圖-呼叫服務流程" nodesep=1 // increases the separation between nodes rankdir=UD; size="8,5" node [shape=default] // D [label="Device" fontcolor=red] // AIC [label="AWS IoT Core" fontcolor=red] // L [label="Authorizer Lambda // function // " fontcolor=red] N1 [label="N1", shape=diamond,style=filled,label="",height=0,width=1.5] N2 [label="", shape=diamond,style=filled,label="",height=0,width=0] N3 [label="", shape=diamond,style=filled,label="",height=0,width=0] PE [label="Policy evaluted"] PC [label="Policy cached"] AVS [label="Alexa Voice Service"] ARE [label="AWS IoT Core Rules Engine"] ADS [label="AWS IoT Core Device Shadow"] AMB [label="AWS IoT Core Message Broker"] AMJ [label="AWS IoT Device Management Jobs"] Nod1 [label="", shape=diamond,style=filled,label="",height=0,width=0] ; // D -> AIC [dir="both"] // AIC -> L [label="Credentials // and // connection // data"] // L -> AIC [label="AWS // IoT // Core // Policy"] // AIC -> PE N1 -> N2 [dir="none"] N2 -> N3 [dir="none"] N3 -> PE PE -> PC [dir="both"] Nod1 -> AVS Nod1 -> ARE Nod1 -> ADS Nod1 -> AMB Nod1 -> AMJ // Nod1 -> PE [label="Allow", dir="forward",arrowhead="crow",arrowtail="normal"] PE -> Nod1 [label="Allowed", dir="back",arrowhead="crow"] // {rank=same; AIC, L } {rank=same; PC, PE, N2, N3 } // {rank=same; AVS, ARE, ADS, AMB, AMJ } // {rank=same; Nod1 } } ``` > ### AWS IoT Core 自訂身分驗證和授權工作流程 > 下方清單說明自訂身分驗證和授權工作流程中的每個步驟。 > > 1. 裝置會使用其中一個支援的裝置通訊協定,連接至客戶的 AWS IoT Core 資料端點。裝置會在要求的標頭欄位或查詢參數 (適用於 HTTP 發佈或 MQTT over WebSockets 通訊協定) 或在 MQTT CONNECT 訊息的使用者名稱和密碼欄位 (適用於 MQTT 和 MQTT over WebSockets 通訊協定) 中傳遞憑證。 > 2. AWS IoT Core 會檢查兩個條件之一: > * 傳入的請求指定授權方。 > * 接收請求的 AWS IoT Core 資料端點具有為其設定的預設授權方。 > > **如果 AWS IoT Core 以這兩種方式之一找到授權方,AWS IoT Core 會觸發與授權方相關聯的 Lambda 函數**。 > 3. (選用) 如果您已啟用字符簽署,AWS IoT Core 會在觸發 Lambda 函數之前,使用授權方中存放的**公有金鑰**來**驗證**請求簽章。如果驗證失敗,AWS IoT Core 會停止請求,而不會叫用 Lambda 函數。 > > 4. Lambda 函數會接收請求中的憑證和連線中繼資料,並做出**身分驗證決策**。 > > 5. Lambda 函數會傳回身分驗證決策的結果,以及指定連線中允許哪些動作的 AWS IoT Core 政策文件。Lambda 函數也會傳回資訊,藉由叫用 Lambda 函數來指定 AWS IoT Core 重新驗證請求中憑證的頻率。 > > 6. AWS IoT Core 會針對從 Lambda 函數接收的政策來評估連線上的活動。 > > 引用自 (了解自訂身分驗證工作流程) https://docs.aws.amazon.com/zh_tw/iot/latest/developerguide/custom-authorizer.html --- 根據 AWS 官方文件,有兩種驗證方式可用於 MQTT over WebSocket 協議。在這份文件中,我們將更詳細地探討**自訂身份驗證**,以期能夠建立自己的資料庫以儲存自訂公私鑰,並提供給裝置使用。 <table> <thead> <tr> <th colspan="100"> <div>通訊協定、身分驗證和連接埠對應</div> </th> </tr> <tr> <th>通訊協定</th> <th>支援的操作</th> <th>身分驗證</th> <th>連接埠</th> <th>ALPN 通訊協定名稱</th> </tr> </thead> <tbody> <tr> <td>MQTT over WebSocket</td> <td>發佈、訂閱</td> <td>Signature 第 4 版</td> <td>443</td> <td>N/A</td> </tr> <tr> <td><p>MQTT over WebSocket</p></td> <td>發佈、訂閱</td> <td style="color:#ff0000">自訂身分驗證</td> <td>443</td> <td>N/A</td> </tr> <tr> <td>MQTT</td> <td>發佈、訂閱</td> <td>X.509 用戶端憑證</td> <td>443<sup>†</sup></td> <td><code>x-amzn-mqtt-ca</code></td> </tr> <tr> <td colspan=50> ....以下省略</td> </tr> </tbody> </table> > 引用自 (通訊協定、連接埠映射和身分驗證)https://docs.aws.amazon.com/zh_tw/iot/latest/developerguide/protocols.html ## 會學到什麼? <!-- * ~~創建 AWS IoT 裝置~~ --> * 創建 Lambda 函數 * 創建 AWS IoT 憑證授權方、金鑰及使用 * MQTT over Websocket 額外的使用限制 * terminal 中 openssl 簡易指令使用 * 撰寫客戶端程式碼,連線至 AWS IoT Core 裝置,取得訂閱主題訊息 * 使用 AWS IoT 提供的 MQTT 測試用戶端驗證訊息接收 ## 先確定環境中有... * AWS 帳戶 * 已安裝 AWS CLI 工具 > https://docs.aws.amazon.com/zh_tw/cli/latest/userguide/getting-started-install.html * 已安裝 Openssl 工具 > https://www.openssl.org/ --- ## Step 1. 創建 Lambda 函數 <font size="4">此 Lambda 用於驗證通過後,提供相應權限的 policy 給裝置使用</font> 名稱定義為 <font color="#f00">**custom-auth-iot-core**</font> ![](https://hackmd.io/_uploads/S1WgNnVzp.png) >引用自 (內容步驟 1 有驗證成功,提供給裝置 policy 的範例程式碼)https://docs.aws.amazon.com/zh_tw/iot/latest/developerguide/custom-auth-tutorial.html --- ## Step 2. 創建憑證授權方 <font size="4">IoT 設備連線時,要有合法授權,所以需要經過該授權方驗證金鑰</font> ![](https://hackmd.io/_uploads/ryyqQnNM6.png) * 名稱定義為 <font color="#f00">**my-new-authorizer**</font> * 選擇 **作用中** ![](https://hackmd.io/_uploads/Bkxlo04fT.png) <!-- ![](https://hackmd.io/_uploads/BJs7FANzT.png) --> * Lambda 函數選擇 <font color="#f00">**custom-auth-iot-core**</font> * 選擇 **關閉** ![](https://hackmd.io/_uploads/H1iDoA4M6.png) ## Step 3. 產生驗證金鑰 > 自訂授權方需要**公有和私有金鑰**來驗證它。本節中的命令會使用 OpenSSL 工具來建立此金鑰對。 > 1. 建立私有金鑰檔案。 > ``` script > openssl genrsa -out private-key.pem 4096 > ``` > 2. 驗證您剛建立的私有金鑰檔案。 > ``` script > openssl rsa -check -in private-key.pem -noout > ``` > 如果命令未顯示任何錯誤,則私有金鑰檔案是有效的。 > > 3. 建立公有金鑰檔案。 > ``` script > openssl rsa -in private-key.pem -pubout -out public-key.pem > ``` > 4. 驗證公有金鑰檔案。 > ``` script > openssl pkey -inform PEM -pubin -in public-key.pem -noout > ``` > 如果命令未顯示任何錯誤,則公有金鑰檔案是有效的。 > > 引用自 (內容步驟 2 產生金鑰)https://docs.aws.amazon.com/zh_tw/iot/latest/developerguide/custom-auth-tutorial.html#custom-auth-tutorial-keys * 字符驗證 **啟用** * 符記金鑰名稱定義為 <font color="#f00">**tokenKeyName**</font> * 金鑰名稱可以設定兩組,驗證的時候,要特別指定與哪把鑰匙匹配,這邊為了有區別,個別定義為 * <font color="#f00">**FirstKey**</font> * <font color="#f00">**keyone**</font> * 公有金鑰 將**檔案內容 <font color="#f00">public-key.pem</font>** 貼入 ![](https://hackmd.io/_uploads/B1jchR4zp.png) --- ## MQTT 連線及驗證的方式 期望目標是基於 WebSocket 對 MQTT 建立連線,但<font color="red">**發現 WebSocket 的 Header 無法透過該 Potocol 一併傳送**</font>,只有 Http 可以這麼做,所以只能另尋驗證方法 > <font color="red">There is no method in the JavaScript WebSockets API for specifying additional headers for the client/browser to send.</font> The HTTP path ("GET /xyz") and protocol header ("Sec-WebSocket-Protocol") can be specified in the WebSocket constructor. > ..... > ..... > Basic authentication was formerly an option but this has been deprecated and modern browsers don't send the header even if it is specified. > > 引用自 https://stackoverflow.com/questions/4361173/http-headers-in-websockets-client-api 承上,WebSocket 使用 Header 驗證的方式既然無法實現,只能透過 queryString 帶資料,傳遞特定 key, value 當作驗證方法 1. 使用 username=\${username}&password=\${password} 2. <font color="#f00">(**AWS 限定**)</font> 特定 ~~header~~ key 組成 key=value 形式 <pre><code>?x-amz-customauthorizer-name=<font color="blue">${authorizer-name}</font>&x-amz-customauthorizer-signature=<font color="#f00">${token-signature}</font>&<font color="green">${token-key-name}</font>=<font color="green">${token-value}</font></code></pre> * x-amz-customauthorizer-name=<font color="#f00">**\${授權方名稱}**</font> * x-amz-customauthorizer-signature=<font color="#f00">**\${經簽署後的私鑰;經過 sha256 加密,對 private-key.pem 簽署後,轉成 base64 的內容 }**</font> 範例為 "FirstKey" ``` script echo -n "FirstKey" | openssl dgst -sha256 -sign private-key.pem | openssl base64 -A ``` > 引用自 (簽署字符)https://docs.aws.amazon.com/zh_tw/iot/latest/developerguide/custom-auth.html * <font color="#f00">**\${token-key-name符記金鑰名稱}**</font>=<font color="#f00">**\${金鑰名稱}**</font> e.g. 按上述變數帶入的結果 <pre><code>/mqtt?x-amz-customauthorizer-name=<span style="color: blue; font-weight:bold;">my-new-authorizer</span>&x-amz-customauthorizer-signature=<span style="color: red; font-weight:bold;">IVgcDgbgyN%2FKMxj6PkkDIA7aA%2FwMXo87aHRW0Qqpz9ahgm%2BpdADTfR0HfMl4b9G7A6DFKv3zQjEkplhPw2%2F2fypXe8YfhOx%2F%2FVLQTZoJucP%2B3qhXPaxyx%2B3vK79OH1raftFZKh1MrVkIhzz08w3oWot%2/CZ9DlEnLXcftcHBhjLA95WYh9t8jyqwFmlvSMgSUqdYy2Br2HTzemYZAbCPqVpIRPSp%2/S7o0cRndTMcXcvnZkSIEmSH79iwNwrJvVY14SCTfgqccLamggSUYSeY7LNiAGnxjfn6It0gIt5q7zHvqiMXPJ%2/FKFhjH5qItQ%2/3qQQAyYXLEIVY1Rcj8%2/8RXdzevG28CQa99M5i7upk8UQmc6Tm5aUySGlZtfwyooHvd9SaJfxA5mv7OIqFtLEfSQ0oYkK0xaBMyqZdguv0wWFt0%2/ZpOAh4TPOHtQCSWJWzCSSmbasNC9Eo09C6HtxOF61wIzDguOHsneJc93UpcyGwWt3J7VGjDHUQy5KQ6tJc2oorGC%2/NJITOVCg1slCnIqtLfTTk0ajZYd7QAy84%2/xw%2/4Qmv8739rGlWK5rWsMuEq4kDQGAt8Tx0CwUiHhAC7bw6t%2/E</span>&tokenKeyName=<span style="color: green; font-weight:bold;">FirstKey</span></code></pre> <!-- ![](https://hackmd.io/_uploads/SkiZwyHG6.png) --> > 引用自 (MQTT)https://docs.aws.amazon.com/zh_tw/iot/latest/developerguide/custom-auth.html --- ## 測試 ### 客戶端完整 html 程式碼 ``` javascript= <!DOCTYPE html> <html> <head> <title>測試 Ws mqtt.js</title> <!-- 導入 mqtt.js 庫,用於 WebSocket 通信 --> <script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script> </head> <body> <script> // 創建 Header 定義區 const header = { 'x-amz-customauthorizer-name': 'my-new-authorizer', 'x-amz-customauthorizer-signature': 'IVgcDgbgyN/KMxj6PkkDIA7aA/wMXo87aHRW0Qqpz9ahgm+pdADTfR0HfMl4b9G7A6DFKv3zQjEkplhPw2/2fypXe8YfhOx//VLQTZoJucP+3qhXPaxyx+3vK79OH1raftFZKh1MrVkIhzz08w3oWot/CZ9DlEnLXcftcHBhjLA95WYh9t8jyqwFmlvSMgSUqdYy2Br2HTzemYZAbCPqVpIRPSp/S7o0cRndTMcXcvnZkSIEmSH79iwNwrJvVY14SCTfgqccLamggSUYSeY7LNiAGnxjfn6It0gIt5q7zHvqiMXPJ/KMFhjH5qItQ/3qQQAyYXLEIVY1Rcj8+8RXdzevG28CQa99M5i7upk8UQmc6Tm5aUySGlZtfwyooHvd9SaJfxA5mv7OIqFtLEfSQ0oYkK0xaBMyqZdguv0wWFt0/ZpOAh4TPOHtQCSWJWzCSSmbasNC9Eo09C6HtxOF61wIzDguOHsneJc93UpcyGwWt3J7VGjDHUQy5KQ6tJc2oorGC+NJITOVCg1slCnIqtLfTTk0ajZYd7QAy84/xw+A3h4PS8SP3MrxAAOAopse7bYYzAUXr7EHO/sVOYl7ZQnN5pbojesVr0/KUr0ukXAeKO1k4HWlKf5AIbA/4Qmv8739rGlWK5rWsMuEq4kDQGAt8Tx0CwUiHhAC7bw6t+E=', 'tokenKeyName': 'FirstKey', } // 使用 URLSearchParams 將請求頭信息轉換為 URL 參數字符串 const urlParams = new URLSearchParams(header); // 創建 MQTT 客戶端並連接到指定的 AWS IoT 端點 const client = mqtt.connect({ protocol: 'wss', // clientId: 'iotconsole-54d447c0-96f4-47ed-a15a-d8e0c01c3e71', hostname: 'agy12ekcmrleo-ats.iot.us-east-1.amazonaws.com', path: `/mqtt?${urlParams.toString()}`, reconnectPeriod: 5000, // 設置重新連接的時間間隔 }); console.log('client', client); const topic = '/TOPIC' // 訂閱的主題 // 客戶端連接成功後執行的回調函數 client.on('connect', () => { console.log('connect') // 訂閱指定主題 client.subscribe(topic) }); // 處理接收到的消息 client.on('message', function (topic, payload) { console.log('message', topic, payload.toString()); }); // 客戶端連接關閉時執行的回調函數 client.on('close', () => { console.log(`連接已關閉!!`) }); </script> </body> </html> ``` ### 使用 AWS IoT Core 工具,訂閱 TOPIC 測試客戶端連線 ![](https://hackmd.io/_uploads/SyFSHZBMa.png) ### 發送訊息至特定主題 /TOPIC ![](https://hackmd.io/_uploads/S1N5SWrz6.png) ### 確認客戶端程式碼連線及主題訂閱是否符合預期 ![](https://hackmd.io/_uploads/r19prbHMa.png) --- > reference link > > * 裝置通訊協定 > https://docs.aws.amazon.com/zh_tw/iot/latest/developerguide/protocols.html > * 使用自訂身分驗證連接至 AWS IoT Core > https://docs.aws.amazon.com/zh_tw/iot/latest/developerguide/custom-auth.html > * 教學課程:建立 AWS IoT Core 的自訂授權方 > https://docs.aws.amazon.com/zh_tw/iot/latest/developerguide/custom-auth-tutorial.html > * Lambda > https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions > > * 自訂授權方 > https://us-east-1.console.aws.amazon.com/iot/home?region=us-east-1#/authorizerhub > > * 了解自訂身分驗證工作流程 > https://docs.aws.amazon.com/zh_tw/iot/latest/developerguide/custom-authorizer.html > > * MQTT 測試用戶端 > https://us-east-1.console.aws.amazon.com/iot/home?region=us-east-1#/test > Issue > > * HTTP headers in Websockets client API > https://stackoverflow.com/questions/4361173/http-headers-in-websockets-client-api > > * Support for additional HTTP headers in Websocket connection > https://github.com/mqttjs/MQTT.js/issues/856