---
# System prepended metadata

title: AWS IoT Core-MQTT over Websocket-自定義驗證
tags: [MQTT over Websocket, AWS IoT]

---

{%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



