🥺

ESP32だってWPA2-Enterprise(EAP-TTLS)したいもん!

に公開

てっきり喫茶店みたいに SSID/PW が書いてある紙が貼ってあると思ったんだ。

この記事は2025 ZAICO アドベントカレンダーの5日目の記事です。

やあみんな!展示会場に行ったら Free WiFi が WPA2-Enterprise でウチの自慢のESP32ガジェットちゃん WPA2-Personal しか対応してないんですけど。。。
ってことよくあるよね!

ぼく:さてさてウチのESP32ちゃんをココに設置して後は SSIDとパスワードがあれば準備完了ザマスね。
ちょいとスタッフのおにぃさ〜ん!ウチのESP32ちゃん用のWiFi接続情報を頂けないザマスか?

スタッフのおにぃさん:へいお待ち!ウチはずっと WPA2-Enterprise でやらせて頂いてまさぁ!

もらった認証情報の形式↓

  • SSID (ネットワーク名): FreeWiFi_HogeHoge
  • セキュリティ: WPA2-Enterprise (802.1x)
  • EAP方式: PEAP または TTLS
  • フェーズ2認証: MSCHAPv2
  • ユーザー名 (ID): guestFugaFuga
  • パスワード: PiyoPiyo

ファッ!? (゜Д゜;).

SSIDとパスワードだけのやつ。。。じゃない!?なんか難しいやつだ。。。ダメだ詰んだ、ごめんよボクのESP32ちゃん、君は展示会場で動くことなく文鎮として飾られててくれ。。。
。。。となるのも悔しい!
ワイもエンジニアの端くれ、幸いPCとUSBケーブルは持ってきてる!ベンチでもあれば座って実装してやらぁ!
というわけで ESP32-C6 を念頭に WPA2-Enterprise の実装方式をご紹介なり〜


ちょ待てぃ そもそも WPA2-Enterprise の仕組みざっくりでも知っておきたいでござる

WPA2-Personal vs WPA2-Enterprise

普段使っている家庭用WiFi(喫茶店とかで見るやつ)は WPA2-Personal です。
SSIDとパスワードを入力すれば誰でも接続できる方式ですね。

一方、WPA2-Enterprise は企業や公共施設向けの認証方式で、より高度なセキュリティを実現します。
単一のパスワードではなく、ユーザーごとに個別の認証情報を使います。

EAP-TTLS と MSCHAPv2 の役割

WPA2-Enterpriseでは EAP(Extensible Authentication Protocol) という認証フレームワークを使います。
今回もらった認証情報では EAP-TTLS という方式が指定されてます。

EAP-TTLSは2段階で認証を行います:

  1. 外部認証(Outer Authentication)

    • OuterIdentity(匿名ID)を使用
    • サーバーとの暗号化トンネルを確立
    • 例: anonymous@example.com
  2. 内部認証(Inner Authentication)

    • 暗号化トンネルの中で実際の認証を実施
    • 今回は MSCHAPv2 という方式
    • UserNameUserPassword を使用

つまり、外側は匿名、内側で本人確認という二重構造になっています。


実装したコード(抜粋)

このコードは Arduino ESP32 Core v3.3.4(ESP-IDF v5.5.0ベース) で動作確認しています。
ESP32-C6は必然的にCore v3系を使用することになりますが。従来のESP32(無印/S3など)でCore v2.x系を使っている場合、esp_eap_client.h の代わりに esp_wpa2.hesp_wifi_sta_wpa2_ent_set_... 系APIを使用してください。

1) 必要ヘッダー

extern "C" {
#include "esp_eap_client.h"
}
#include <WiFi.h>

2) 実装ファイル

ちゅうい
このコードはサーバー証明書の検証(CA Cert)を省略しています。
WPA2-Enterpriseの場合、仕組み上 証明書検証 が事実上の必須要件です。
サーバーに「ID/パスワード」という重要情報を渡すので、渡す前に「相手が本物かどうか」を検証しなきゃいけないんですね。
今回は一時的なゲストID(使い捨て)であり、セキュリティリスクを許容できるため省略しています。

bool connectEnterpriseWiFi(const char* ssid,
                          const char* username,
                          const char* password,
                          const char* identity) {

    // 1. Wi-Fi切断と初期化
    WiFi.disconnect(true);
    delay(100);
    WiFi.mode(WIFI_STA);
    delay(100);

    // 2. EAP Identity(外部ID)を設定
    if (identity && strlen(identity) > 0) {
        esp_eap_client_set_identity((uint8_t*)identity, strlen(identity));
    } else {
        // 空の場合は空文字列を設定
        esp_eap_client_set_identity((uint8_t*)"", 0);
    }

    // 3. ユーザー名とパスワードを設定
    esp_eap_client_set_username((uint8_t*)username, strlen(username));
    esp_eap_client_set_password((uint8_t*)password, strlen(password));

    // 4. Phase2認証方式をMSCHAPv2に設定
    esp_eap_client_set_ttls_phase2_method(ESP_EAP_TTLS_PHASE2_MSCHAPV2);

    // 5. WPA2 Enterpriseを有効化
    esp_wifi_sta_enterprise_enable();

    // 6. WiFi.begin()の前に少し待機
    delay(100);

    // 7. 接続開始
    WiFi.begin(ssid);

    // 8. 接続待機
    uint32_t startTime = millis();
    while (WiFi.status() != WL_CONNECTED) {
        if (millis() - startTime > 30000) {
            Serial.printf("接続タイムアウト (Status: %d)\n", WiFi.status());
            esp_wifi_sta_enterprise_disable();
            return false;
        }
        delay(500);
        Serial.print(".");
    }

    // 9. 接続成功
    Serial.println("\n接続成功!");
    Serial.printf("IP Address: %s\n", WiFi.localIP().toString().c_str());
    return true;
}

3) 使用例

void setup() {
    Serial.begin(115200);
    delay(3000);

    // Enterprise WiFi接続
    const char* ssid = "Enterprise_WiFi";
    const char* identity = "anonymous@example.com";
    const char* username = "user123@example.com";
    const char* password = "secret123";

    if (connectEnterpriseWiFi(ssid, username, password, identity)) {
        Serial.println("WiFi接続完了");
    } else {
        Serial.println("WiFi接続失敗");
    }
}

void loop() {
    // メイン処理
}

コードの説明

WiFi初期化

WiFi.disconnect(true);
delay(100);
WiFi.mode(WIFI_STA);
delay(100);

既存のWiFi接続を切断し、STAモードに設定します。

EAP Identity(外部ID)の設定

if (identity && strlen(identity) > 0) {
    esp_eap_client_set_identity((uint8_t*)identity, strlen(identity));
} else {
    esp_eap_client_set_identity((uint8_t*)"", 0);
}

mobileconfigの OuterIdentity に対応する外部IDを設定します。

ユーザー名とパスワードの設定

esp_eap_client_set_username((uint8_t*)username, strlen(username));
esp_eap_client_set_password((uint8_t*)password, strlen(password));

mobileconfigの UserNameUserPassword をそのまま設定します。

Phase2認証方式の設定

esp_eap_client_set_ttls_phase2_method(ESP_EAP_TTLS_PHASE2_MSCHAPV2);

mobileconfigの TTLSInnerAuthentication: MSCHAPv2 に対応する設定です。
この設定を忘れると接続できません。

WPA2 Enterpriseの有効化

esp_wifi_sta_enterprise_enable();

ESP32をWPA2-Enterpriseモードに切り替えます。

接続開始

delay(100);
WiFi.begin(ssid);

WiFi.begin() にはSSIDのみを渡します。パスワードは渡しません。
EAP設定で指定した認証情報が使用されます。

接続待機とタイムアウト処理

uint32_t startTime = millis();
while (WiFi.status() != WL_CONNECTED) {
    if (millis() - startTime > 30000) {
        Serial.printf("接続タイムアウト (Status: %d)\n", WiFi.status());
        esp_wifi_sta_enterprise_disable();
        return false;
    }
    delay(500);
    Serial.print(".");
}

30秒のタイムアウトを設けて接続を待機します。
タイムアウト時はEnterprise WiFiを無効化してから終了してます。


ハマった点

WiFi.scanNetworks() を実行すると、esp_eap_client_set_* で設定したEAP情報が消えてしまうようです。
その結果、Status=6 (DISCONNECTED) が続く現象が発生しました。
なのでスキャン後にEAP設定を実行する必要があります。


なんとか WPA2-Enterprise できた ESP32ちゃん

展示会場で動く esp-c6ちゃん、かわえ、かわえ。。。
苦労を共にするとこんなにも可愛さマシマシなんですね。
WPA2-Enterprise → esp32繋げられないじゃん!ってなった時はそりたつ壁の前に尻もちつきそうでしたが、限られた時間で調べながら一歩づつ進められてよかったです。


後日談:実はプロビジョニングファイル氏が鍵を握ってた

実は会場からの配布物として、プロビジョニングファイルを提供されてました。
PCやスマホであればコレをダウンロードしてインストールすればWiFiに接続できるよ〜と言うものです。
今回は親切に認証情報を教えて貰えたけど、もしプロビジョニングファイルしか入手できなかったらどうなってたんだろ? てゆうかプロビジョニングファイルの中身、気になるよね〜

と言うわけでおもむろに開いてみる

プロビジョニングファイル(拡張子 .mobileconfig)は実質XMLファイルなので、テキストエディタで開けます。
以下のような構造になっています:

<dict>
    <key>EAPClientConfiguration</key>
    <dict>
        <key>UserName</key>
        <string>user123@example.com</string>
        <key>UserPassword</key>
        <string>password123</string>
        <key>AcceptEAPTypes</key>
        <array>
            <integer>21</integer>
        </array>
        <key>TTLSInnerAuthentication</key>
        <string>MSCHAPv2</string>
        <key>OuterIdentity</key>
        <string>anonymous@example.com</string>
    </dict>
</dict>

これは・・・!
何やら認証情報がまるっと書いてある感じですね。


UserPassword (!) えっえっ平文で書いていいの?

今回の展示会場は「共通のゲストアカウントを大量配布する」という特殊ケースなので、利便性全振りでプロビジョニングファイルに UserPassword がガッツリ記述されてます。
(なので会場の人が親切に教えてくれたんですね〜納得)

一般の会社や大学のプロビジョニングファイルでは、ここは空欄になっていてインストール時に手入力させるか、そもそもMDMで裏から流し込むので、こんな風にパスワードが拾えることはないですよ〜。


mobileconfigの項目

ここで重要なのは以下の4項目です:

キー 値の例 意味
AcceptEAPTypes 21 EAP-TTLS方式を使用
TTLSInnerAuthentication MSCHAPv2 内部認証方式
OuterIdentity anonymous@example.com 外部ID(匿名ID)
UserName / UserPassword (上記参照) 実際の認証情報

AcceptEAPTypes21 は EAP-TTLS を意味し、内部認証が MSCHAPv2 であることがわかります。

(もう一度言います)今回のケースでは大量に接続情報をバラまくので UserPassword は平文で書いてありますが、本来 UserPassword は絶対漏れちゃダメなやつです!

と言うわけで実はプロビジョニングファイルに必要な情報が全て記載されてたと言うオチでした〜
(でもプロビジョニングしか提供されてなかったら勝手に繋いじゃダメだゾ☆)


さ〜〜て! 次回の ZAICO アドベントカレンダーは〜〜〜?!

なな、なんと!我らがリーダー、田村社長(通称:タムタム)の投稿だ〜〜!!!
記事のタイルは〜?
>>>2日で作る自動在庫管理システム<<<

2日で作る?!タイトル見ただけで気になっちゃうよね〜〜〜
なんたってタムタムは倉庫現場からの叩き上げだゾ☆
在庫管理の実務プロ+2日で作る在庫システムってどんななのか気になるよね〜〜〜〜!

他にも ZAICOの猛者たちが投稿してるから2025 ZAICO アドベントカレンダー要チェケ!だよ☆


参考文献

ESP32 / Arduino Core 関連

プロトコル・規格関連

設定ファイル関連

ZAICO Developers Blog

Discussion