Wednesday, February 2, 2022

DIMM 的不同單位 (Channel, Rank, Chip, Bank, Row/Column)

引言
如果你有組裝電腦的經驗、或者正在從事記憶體相關的開發工作,相信一定會經常接觸到 DIMM (Dual Inline Memory Module) 的各種單位名詞:包括例如 Channel、Rank、Chip、Bank 等等。這篇文章,我們將會先從 Byte Addressing 說起,再解釋 SIMM 和 DIMM 的架構變化。最後,我們會詳細了解各種 DIMM 中常見的單位。

---

Byte Addressing

首先,有一個基本概念:現時大部分電腦中的記憶體地址,都以 1-byte (8-bits) 作為單位的。也就是說,每一個 byte 都會有自己的記憶體地址。這一點,跟 C Programming 中的 char array 操作方法一致:

例如:有一個 C char array:

char arr[] = { 'a', 'b', 'c', 'd' };

由於 'a' 和 'b' 都是完整的 8-bit (1-byte),所以,它們都會有自己的記憶體地址:

printf("%p\n", &arr[0]);  // example output: 0x00FC00000
printf("%p\n", &arr[1]);  // example output: 0x00FC00001
printf("%p\n", &arr[2]);  // example output: 0x00FC00002
printf("%p\n", &arr[3]);  // example output: 0x00FC00003

---

SIMM (Single Inline Memory Module)
大部分電腦及程式,都是以上述的 byte-addressing 為基礎,所以,當我們向 DRAM 查詢某個地址的資料(例如 0x00FC00001),我們只需期待記憶體能在一個 clock cycle 返回一個 byte 的內容(例如 'b')就好。初代的設計,正正是以這種方向作考慮,所以第一代的 SIMM RAM,都只能提供 8-bit 的資料頻寛。也就是說,一塊記憶體模塊,只能在一個 clock cycle 下傳輸 8-bit 的資料。

及後,人們開始注意到:電腦程式在大部分情況下,都會符合局部性原則(Principle Of Locality)。舉例來說:以上的 C 例子,開發者通常不止會用到 'a',還會經常用 loop 把 'b', 'c', 'd' 等等這些鄰近的變數都讀起來。所以,SIMM 的第二代設計,也開始容許 CPU 同一個 clock cycle 下存取多個鄰近的 bytes(也就是 Burst Read)。當時,第二代 SIMM 能提供 32-bit 的資料頻寛。也就是說,一個記憶體模塊,能在一個 clock cycle 下傳輸 32-bit (4 bytes) 的資料。

---

DIMM (Double Inline Memory Module)
由於 CPU 發展不斷加快,記憶體的密度及容量也越來越高,廠商開始覺得 32-bit 不太夠用。所以,它們又推出了 Double 版的 SIMM(也就是 DIMM),把資料頻寛翻倍成 64-bit。換句話說,現時一塊 DIMM 記憶體模塊,能在一個 clock cycle 下傳輸 64-bit (8 bytes) 的資料。

想像一下,如果一塊記憶體模組,想要在一個 clock cycle 下返回 64 bit,理論上,它就需要有 64 個儲存器同時運作。那麼,這些儲存器,到底是如何排列的呢?

---

Row / Column
簡單來說,DRAM 是由電容所組成的揮發性儲存裝置 (Volatile Memory)。每一顆電容,都可以儲存一個 bit 的資料(高電壓 = 1,低電壓 = 0),而且需要定期充電更新。而它們的排列方式,是以 Row(行)和 Column(列)進行的。

假設現在有一個 8 * 8 的矩陣,裏面放著總共 64 個電容,我們就可以用 6 個 bit(3 行,3 列,2^6 = 64)去定址它們。圖中紅色的一個電容,我們可以用 R=5 (101), C=3 (011)(也就是 101 011)表示:

▲ 在這個例子中,如果我們想要取得 101011 (紅色格)的內容,Row Address Decoder 會先開啟第六行,將內容複製到 Sense Amplifier 之中,其後,Column Selector 就會把第四列的內容挑出,並輸出至 Input/Output Buffer 中。這裡,我們就能得到 1-bit 的資料。

---

Bank
文章開始的時候,我們提到:由於大部分電腦均採用 Byte Addressing,所以每一個地址,都應該要指向 8-bits 的內容,而非 1-bit。所以,記憶體應該要儲存 8 bits,而非只有 1 bit。要解決這個問題,最簡單的做法,就是把同一個矩陣複製八份。這樣,一個地址,就能同時回傳 8 個 bits。這種排列方法,我們稱為一個 Bank:

▲ 在這個例子中,如果我們向八個矩陣都發出相同的 Row/Column 指令,我們便能同時得到 8 個 bit,也就是一個 byte 的內容。

---

Chip
Chip 就是指一塊黑色的 memory chip。通常,基於效能等種種原因,一個 memory chip 內會有多於一個 bank,而每個 Bank 均可以完全獨立運作。例如下圖的 memory chip 所示,它一共有 8 個 banks。這樣,當用戶正在存取 Bank 0 的時候,Bank 1-7 就可以忙其他事情(例如預先存取 Prefetch、更新電容等等):

▲ 在這個例子中,我們可以用 3 個 bits 去表示不同 Bank。所以,如果我們想要第 Bank 0 的 Row 1, Column 2, 我們可以把地址寫成 Bank(000), Row(001), Column(010),也就是 000 001 010。

---

Rank
從以上例子所見,每一顆 memory chip 都能在同一個 cycle 中返回 8 bits(1 byte)。但是,由於 DIMM 能支援在同一個 cycle 內返回 64 bits(8 bytes)的資料,如果我們只採用一顆 memory chip,理論上,我們就只能用到 8 bit / 64 bit = 12.5% 的頻寛,十分浪費。有見及此,DIMM 會把八顆 8-bit 的 memory chip 並聯在一起(或者四顆 16-bit 的 memory chip,視乎情況),並以 Burst Mode 等方法,令八顆 memory chip 能同時進行讀寫。這種排列,則稱為一個 Rank:

 

普遍而言,一塊單邊 DIMM 會有 8 顆 memory chip。如果每一粒 memory chip 的頻寬均為 8-bit,那麼 8 顆同時運作,就有 64 bits,就代表一個 Rank,這種就稱為 single rank。

有些 DIMM 的 memory chip 為 16 bit,那麼四顆就已經 64 bit (16 bits * 4 = 64 bits)。這種情況下,擁有八顆就稱為 dual rank。還有一種雙面 DIMM,計算方法也大同小異。

---

Channel
Memory Controller 的數量,決定了 memory channel 的數量。大部分情況下,一個 CPU 可以有一個或多個 memory controller,而每一個 memory controller 則只能有一個 channel。每一個 channel 也只能同時讀寫同一個 Rank 的 memory chip。

例如有一個 CPU,它只有一個 memory controller,而該 memory controller 也只支援一個 channel。如果該主機插上了兩條 single rank 的 DIMM,它也只能同時對其中一條 DIMM 進行讀寫。

又例如有一個 CPU,它有兩個 memory controller,所以它能同時用到兩個 channel(dual channel)。如果該主機插上了兩條 single rank 的 DIMM,它就能同時對兩條 DIMM 進行讀寫。

理論上,如果 CPU 的 memory controller 支援兩個 channel,CPU 就能在一個 clock cycle 讀取 64 bits * 2 = 128 bits 的資料。但如果只有一個 channel,它就只能在一個 clock cycle 讀取 64 bits 的資料。所以,同一個資料量,在理想的情況下,dual channel 能比 single channel 節省一半的存取時間。

當然,現實情況下,資料擺放的位置不一、程式設計造成的瓶頸等,都會令實際速度有所折扣。所以,現實中的 dual channel 並沒有想像中那麼快。

▲ 從以上圖片可見,一個 CPU 可以支援兩個(或以上)的 channel,而每一個 channel 可以支援多個 DIMM 模塊。不過,在同一個 channel 下,只有一個 rank 的 memory chips 能被同時讀取。

---

近年發展
隨著近年 DDR4 / DDR5 等高速記憶體開始普及,廠商也積極引入更多新的技術,以便進行更多的並行運算(parallelism)和更低的延遲(例如 Group Memory Banks 等等)。最經典的例子則是 DDR (Double Data Rate):DDR 相比起 SDR (Single Data Rate),會同時用上 reference clock 的 rising edge 和 falling edge 進行讀寫。但整體而言,記憶體的基本架構仍然不變。

---

參考

[1] Random Access Memory - https://www.youtube.com/playlist?list=PLTd6ceoshpreE_xQfQ-akUMU1sEtthFdB

[2] 圖解RAM結構與原理,系統記憶體的Channel、Chip與Bank - https://www.techbang.com/posts/18381-from-the-channel-to-address-computer-main-memory-structures-to-understand 


Thursday, January 13, 2022

C++ Reference Collapsing (引用折叠) 及 Forwarding Reference (轉發引用)

1. 引言
在 C++ 98 初推出的時候,C++ 並沒有 Reference of Reference 的概念 (&&),所以,開發者不但不能直接使用 &&,即使用了 typedef 將 reference type 包裝好,然後再加上 &,編譯器都一律會報錯。

後來,C++11 終於開放了對 Reference of Reference (&&) 的支援。不過由於 C++11 也同時引進了 Move Semantics (也是 &&) 的寫法,所以,Reference of Reference 就會跟 Move Semantics 有所衝突。

以下是一個例子,開發者想做的是:func(k) 要跑的,是 lvalue 的版本的 func()(第一個,單 & 的版本),而非 rvalue 的版本( &&,也就是 move semantics 的版本):

#include <iostream>

typedef int& INTR;

template<class T>
void func(T& i) {
       std::cout << __PRETTY_FUNCTION__ << std::endl;
}

template<class T>
void func(T&& i) {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
}

int main() {
        int i   = 123;
        INTR j  = i;    // int&  j = i
        INTR& k = j;    // int&& k = j

        func(k);        // k is lvalue (should run T(&i))
        func(123);      // 123 is rvalue (should run T(&&i))
        return 0;
}

它的輸出應為:

void func(T&) [with T = int]
void func(T&&) [with T = int]

這裏看起來,明明 k就應該要是 int &&,但編譯器居然知道它不是指 rvalue,而是指 reference of reference。當中的原因是, C++ 11 新增了 Reference Collapsing (引用折叠) 的規則,確保編譯器在在大部分情況下(也就是使用了 typedef / decltype / template 的情況下), 都能辨認出 Reference of Reference 仍然是 "reference",而非 "rvalue" 。 

---

2. Reference Collapsing 四個規則
引用折叠有四個主要的編譯規則(其實有更多,不過不太重要,在此不述):

  • A& & becomes A&
  • A& && becomes A&
  • A&& & becomes A&
  • A&& && becomes A&&

反正重點是,編譯器會在碰到兩次 && 的時候,它才會當它是指 rvalue。其他情況的話,它會把 Reference of Reference (&&) 都降級成 Reference (&)。

---

3. Universal Reference / Forwarding Reference (轉發引用)
既然 && 會在編譯的時候,自動按情況降級成 &,那麼我們在寫 function template 的時候,是否可以寫少一個,然後讓編譯器自行將 function 變成兩個 (&& 和 &) 的版本呢?答案是可以的。

先看以下例子:

#include <iostream>

template<class T>
void function(T&& t) {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
}

int main() {
        int i = 123;
        function(i);
        function(996);
        function(std::move(i));
        return 0;
}

它的執行結果為:

void function(T&&) [with T = int&]
void function(T&&) [with T = int]
void function(T&&) [with T = int]

首先,i 是 lvalue (int &),所以 T = int& = T&&& = T & ,
其餘的 996 和 std::move(i) 則因為本身是 rvalue(int &&),所以直接 T = int。

---

4. Perfect Forwarding 完美轉發
有一個情況要注意:如果在 template function 中,我們想要把變數傳到下一個 function 的話, 正常來講,我們可能會這樣做:

template<class T>
void function(T&& t) {
        subFunction(t);
}

然後,我們會期待傳入 subFunction(t) 的 t 應該是 T&& / rvalue,但其實並不一定 ...

以下有一個例子:

#include <iostream>

void subFunction(int&& i) {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
}

void subFunction(int& i) {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
}

template<class T>
void function(T&& t) {
        std::cout << "Value: " << t << std::endl;
        std::cout << __PRETTY_FUNCTION__ << std::endl;
        subFunction(t);
        subFunction(std::forward<T>(t));
        subFunction(static_cast<T&&>(t));
        subFunction((T&&)(t));
}

int main() {
        int i = 123;
        function(i);
        function(996);
        function(std::move(i));
        return 0;
}

它的輸出為:

Value: 123
void function(T&&) [with T = int&]
void subFunction(int&)
void subFunction(int&)
void subFunction(int&)
void subFunction(int&)

Value: 996
void function(T&&) [with T = int]
void subFunction(int&)
void subFunction(int&&)
void subFunction(int&&)
void subFunction(int&&)

Value: 123
void function(T&&) [with T = int]
void subFunction(int&)
void subFunction(int&&)
void subFunction(int&&)
void subFunction(int&&)

由結果可見,如果只用  subFunction(t),編譯器會認為 t 是一個 lvalue。
在 function 的 scope 之中,t 的確是一個變數,是 T&& 的 reference (T&& & = T&)。

如果要解決這個問題,把 && 正確地傳到 subFunction,最簡單的方法,就是把 t 的型態轉強制換成 T&&。如此,傳入來的 t 就能完全不經處理地傳到 subFunction。在解 subFunction 的時候,編譯器會找回最初的 i / 996 / std::move(i),再決定執行那一個版本的 subFunction。

而 std::forward<T>,其實就是 static_cast<T&&> 的語法糖而已。

---

小結
C++11 為了讓 Reference of Reference 變得簡單,並加上對 Move Semantics 的支援,同時令編譯器做到最大程度的向後兼容(不加新 keyword + 舊程式碼可跑),似乎把事情變得更複雜了:它使用了 Reference Collapsing 將不同情況的 && 分開。但同時,這種做法也導致 template function 如果用了 &&,很大機會要在 function call 時,對 variables 加上 static_cast,才能正常地運作 。

但是,我們只要無腦採用 universal reference (兩個 &&)加上 std::forward<T>,基本上就能解決 99% 的問題。向好的方向去想:使用 move semantics 加上 universal reference,不但能加快執行時間,或許更能節省程式碼行數,加快 debug 效率。

Saturday, October 16, 2021

Device Provisioning Protocol (DPP) / Wi-Fi Easy Connect 分析

DPP / Wi-Fi Easy Connect 簡介

日常生活中,我們經常都需要連接不同的 Wi-Fi 熱點。例如:當我們去到咖啡店的時候,通常牆身都會寫上了 Wi-Fi 的 SSID 和密碼。或者當去到朋友家的時候,我們也會問朋友拿 Wi-Fi 的密碼。不過,以這種方法連接 Wi-Fi,其實會帶來兩個潛在的資安風險:「主動攻擊」和「被動攻擊」。

「主動攻擊」是指,有一些黑客,他們會主動去製造一些假的 Wi-Fi AP 基站:只要它們發出同樣的 SSID,設上同樣的密碼,用家就有可能會不小心連上了這些假 AP。只要訊號夠強,速度夠快,大部分用家根本不會發現。這樣,黑客就能發動中間人攻擊 (man-in-the-middle attacks),去盜取用家所有的訊息。

「被動攻擊」則指,黑客們只負責竊聽 (eavesdrop) 在大氣電波中的數據,並不主動發動攻擊。由於密碼都寫在牆上了,所以只要基站 (AP) 與使用者 (STA) 雙方,都沒有用上足夠安全的加密協議(例如只使用了 WEP / WPA / WPA2)的話,黑客就可以輕易將內容解密。

DPP / Wi-Fi Easy Connect 協議的出現,正正是為了解決以上的情況而設計的。簡單來說,在主動攻擊方面,DPP 透過公私鑰的交換,確保用戶只會連到真正的 AP。而在被動攻擊方面,DPP 使用了迪菲-赫爾曼密鑰交換 (Elliptic Curve Diffie-Hellman Key Exchange) 的方法,建立出獨一無二的 session key。所以,即使竊聽者擁有密碼,他們也沒辦法輕易推斷出 session key,繼而也無法解密訊息。  

這篇文章,我只會簡介 DPP 的不同步驟 (steps) 、角色 (roles) 和在 hostap 的實作。關於 Public Private Key Infrastructure (PKI)、Diffie-Hellman 的原理和每個步驟的細節,之後會再寫另一篇解釋。 


DPP 五大步驟

簡單來說,如果只考慮 AP 與 STA 之間的協議,DPP 有以下五大步驟:

  • Bootstrapping (起步):這個步驟是 DPP 的第一步。它指使用頻外驗證 (Out-of-band Authentication) 的方法去交換公鑰。例如將公鑰印成 QR code 貼在牆上、印在機身,或者使用藍芽或 NFC 這類近距離的方法。

  • DPP Authentication(DPP 驗證):在 bootstrapping 期間成功交換的公鑰,將會用作 HMAC 的參數,繼而產生三條不同的 session keys (k1, k2 以及 ke)。這個步驟主要用作確保雙方均在開機的狀態(又稱為 Aliveness),而且產生出來的 ke 是獨一無二的。

  • DPP Configuration(DPP 設定佈置):當 authentication 產生了 ke 後,configuration 會用這個 ke (配以 AES-SIV)來加密 Wi-Fi AP 的登入憑證 (credentials / configuration object)。例如,它會包含 SSID, WPA3/WPA2 密碼,或者是 DPP Connector 公私鑰。(如果是 DPP Connector 的話,Connector 入面還包含了 groupId 和 netRole 這些資訊,而且該 connector 會以 ECDSA 簽名,所以任何接收到 connector 的人,都不能擅自改動 connector 的內容。)

  • DPP Network Introduction / Peer Discovery(DPP 設備探索):這個步驟只適用於有收到 DPP Connector 的情況(如果是 WPA2/3,它就會離開 DPP,走傳統的連線方法)。簡單而言,不同裝置之間會在這步交換 connector,去查核對方的 groupId 和 netRole。如果對方的 netRole 正確,就會繼續產生 Pairwise Master Key (PMK)。例如,如果 AP 擁有一個 groupId="Group1" 和 netRole="ap" 的 connector,而 STA 則擁有一個 groupId="Group1" 和 netRole="sta" 的 connector 的話,因為它有相同的 groupId,而且一個是 ap,另一個是 sta,所以雙方就會同意繼續連線,並且用 connector 中的 netaccesskey 進行 ECDH 及 HKDF,產生 PMK。

  • DPP Network Access / Association(DPP 網絡接入):最後一步,就是使用在 peer discovery 中產生的 PMK 接入 Wi-Fi AP。這個步驟大致上跟 WPA2 的 PSK 相似,只是把 PSK 換成了 PMK。所以,STA 會以 Open 認證方式連上 AP,再跑 EAPOL (4-way handshake),從而由 PMK 產生一條新的 PTK。在產生過程中,使用者亦可在 DPP KDE 中表示對 PFS (Perfect Forward Secrecy) 的支援,它就會在接入的過程中,使用 AP 在 RSN IE 中廣播的 Z 公鑰,以加強 PTK 的獨特性。

 

DPP 的四大角色

在官方的文檔中,有四個角色 (Initiator / Responder / Configurator/ Enrollee) 經常出現混淆。
但其實,分類方法十分簡單:

DPP Authentication 中有兩個角色:

  • Initiator(發起者):它接收由 Responder 發出的 DPP URI,然後發出 Authentication Request。 (如果啟用了雙向驗證的話,則兩者互換 DPP URI)
  • Responder(回應者):它接收 Authentication Request,並以 Authentication Response 回應 Initiator 的請求。 

DPP Configuration 中有兩個角色:

  • Configurator(佈局者):它儲存了所有憑證 (credentials) 相關的資訊,也會在別人發出 Configuration Request 時,以 Configuration Response 回應它的請求。它也有一條 Certificate Signing Key (又稱 csign key), 用作簽發 DPP connector 之用。
  • Enrollee(登記者):所有希望接收憑證 (credentials) 的人都是 Enrollee。它們會發出 Configuration Request,等待 Configurator 的回應。

 

這四個角色,可以隨著不同的情況完全自由配搭,例如:

  • AP (Initiator + Configurator) 配搭 STA (Responder + Enrollee)
    • AP 通過掃瞄 STA 的 QR code,令 STA 可以連上 AP
    • 這種情況,AP 是路由器的 App,STA 則是 IoT 設備

  • STA (Initiator + Configurator) 配搭 AP (Responder + Enrollee)
    • STA 掃瞄 AP 的 QR code,然後設置路由器的 WPA2/3 密碼
    • 比較少見,但理論上可行

  • STA (Initiator + Configurator) 配搭 STA (Responder + Enrollee)
  • AP (Responder + Configurator) 配搭 STA (Initiator + Enrollee)
    • AP 產生一個 QR code 貼在牆上,其他 STA 一掃就能連上。
    • AP 是路由器,STA 是手機

  • STA (Responder + Configurator) 配搭 STA (Initiator + Enrollee) 
    • 兩部手機之間互相分享 WPA2/WPA3 密碼
    • Android STA 已部分支援這個功能 (Initiator + Enrollee)

反正就是,只要你想到的,就可以做到,協議中並沒有限制角色配搭的可能性。

 

在 HostAP 中實作

現時,hostap 2.9 只支援大部分的 DPP 1.0,以及少部分的 DPP 2.0。
而在最新的 commit 中(截稿前是這個),大部分 DPP 2.0 的實作已經做好。

以下的 hostap 指令例子,是以 AP (Initiator + Configurator) + STA (Responder + Enrollee) 為基礎的。你也可以把它想像成這個 use case :

  • hostapd 是路由器
  • hostapd_cli 是路由器的手機界面
  • wpa_supplicant 和 wpa_cli 是沒有按鈕,只顯示著 QR code 畫面的 IoT 裝置

 

Step 1: STA - 放以下的 config (wpa_dpp.conf):

ctrl_interface=DIR=/var/run/wpa_supplicant
ctrl_interface_group=0
update_config=1
pmf=2
dpp_config_processing=2

 

Step 2. STA - 跑以下的命令,開啟 wpa_supplicant:

./wpa_supplicant -d -Dnl80211 -iwlp1s0 -cwpa_dpp.conf

 

Step 3. STA - 跑以下的命令,產生一條新的 DPP URI QR code,然後等待連線:

./wpa_cli dpp_bootstrap_gen type=qrcode chan=81/1
dpp_bootstrap_get_uri 1
dpp_listen 2412

將產生的 QR code 拷貝下來,在 Step 6 會用到。

 

Step 4. AP - 放以下的 config (hostapd_dpp.conf):

interface=wlp0s20f3
driver=nl80211
ctrl_interface=/var/run/hostapd
ssid=test
channel=1
wpa=2
wpa_key_mgmt=DPP
ieee80211w=1
wpa_pairwise=CCMP
rsn_pairwise=CCMP

 

Step 5. AP - 跑以下的命令,開啟 hostapd:

./hostapd -d hostapd_dpp.conf

 

Step 6.  AP - 打開 hostapd_cli,並貼上剛拷貝的 DPP URI 到以下的命令中

./hostapd_cli
dpp_qr_code <YOUR COPIED DPP URI>

 

Step 7. AP - 在 hostpad 中加一個 configurator。這個 configurator 將會用來簽發所有裝置(包括 ap 自己)的 DPP connectors :

dpp_configurator_add

 

Step 8. AP - 用 configurator 為 ap 自己,簽發一個 connector。在 Peer Discovery 中,每一個裝置都是用 connector 去交換身分資料,所以,ap 自己也需要一個 connector:

dpp_configurator_sign conf=ap-dpp ssid=74657374 configurator=1

 

Step 9. AP - 把剛剛 configurator 產生的 connector 套用到 ap 本身身上。這樣,當有 peer discovery request 的時候,ap 就可以把自己的 connector 傳出去,而且也懂得用套用上的 csign public key 驗證對方的 connector:

set dpp_connector <CONNECTOR VALUE IN TERMINAL>
set dpp_csign <CSIGN VALUE IN TERMINAL>
set dpp_netaccesskey <NETACCESSKEY VALUE IN CONSOLE>

注意這裏的 connector 是已經簽好的 base64 字串,裏面包含了 groupId, netRole, netaccesskey 的public key 和一個 ECDSA 簽名。這個 csign key 則是 public key (csign key 的 private key 是藏在 configurator 裏面的)。而最後,這個 netaccesskey 則是 private key,用作給 ap 在 peer discovery 時產生 PMK 之用。

 

Step 10. AP - 因為 AP 是 initiator 及 configurator,所以要跑以下命令,發起 DPP authentication request。這個命令也要求 configurator 在 configuration response 期間,產生一個 netrole=sta, ssid=test 的 dpp connector 回傳給對方 :

dpp_auth_init peer=1 conf=sta-dpp ssid=74657374 configurator=1

 

小結

DPP (Device Provisioning Protocol,又名 Wi-Fi Easy Connect)是近年來比較新的 Wi-Fi 驗證方法。雖然它仍然需要 out-of-band 的方法去做第一步,而且步驟繁瑣,但至少也算是提供了一個可行的做法。

它的設計邏輯,其實跟 USB Type C 也有點類似:它提供了同一樣的接口,讓兩個裝置至少溝通得上,但實際支援與否,則要看它在交換的過程中,對方的 role 與自己是否配搭。

作為 protocol designer,我認為 DPP 的 signalling messages 實在有點多,而且對 WPA2 的 backward compatibility 可能會令所有事情功虧一簣。但另一方面,正正因為它把步驟都拆分得很細,它也可以在耗用系統資源較少的情況下,及早辨認出 DoS flooding 的封包。這一點,對於未來「萬物皆互聯」的世界,似乎對系統穩定性也有幫助。

但是,如果文檔附有 formal verification 的話,我相信說服力應該會再大一點。

---

引用

[1] - Wi-Fi Easy Connect Spec 2.0 - https://www.wi-fi.org/download.php?file=/sites/default/files/private/Wi-Fi_Easy_Connect_Specification_v2.0.pdf

[2] - AOSP Wi-Fi Easy Connect - https://source.android.com/devices/tech/connect/wifi-easy-connect