espressif / esp-idf

Espressif IoT Development Framework. Official development framework for Espressif SoCs.
Apache License 2.0
13.39k stars 7.25k forks source link

802.1X WPA2 Enterprise incorrectly uses PEAP instead of EAP even when no password is specified (IDFGH-12403) #13429

Open tpbedford-oomdata opened 6 months ago

tpbedford-oomdata commented 6 months ago

Answers checklist.

General issue report

I am attempting to connect to WPA2 Enterprise wifi which requires EAP-TLS. I call the ESP's enterprise wifi API similarly to the example/wifi-enterprise code when configured for EAP-TLS (i.e. not PEAP or TTLS). Yet, the Enterprise AP reports that the ESP is proposing PEAP, which it doesn't support and the authentication is rejected.

I'm using ESP-IDF 5.1 (as I'm also using ESP-ADF v2.6 and that's the latest supported ESP-IDF version)

My code:

    // reset
    esp_wifi_disconnect(); // wifi_connectAsStation loop start reset
    esp_wifi_stop(); // wifi_connectAsStation loop start reset

    // clear any previous client cert config
    esp_eap_client_clear_certificate_and_key();
    esp_wifi_sta_enterprise_disable(); // EAP

    // client and CA certs - length is strlen(cert) NOT INCLUDING any terminating NUL
    const unsigned char *client_cert = _wifi_clientCert.cert;
    int client_cert_len = _wifi_clientCert.cert_len;

    const unsigned char *private_key = _wifi_clientCert.pkey;
    int private_key_len = _wifi_clientCert.pkey_len;

    const unsigned char *private_key_password = _wifi_clientCert.pass;
    int private_key_passwd_len = _wifi_clientCert.pass_len;

    const unsigned char *ca_cert = _wifi_clientCert.cacert;
    int ca_cert_len = _wifi_clientCert.ca_len;

    // clear any previous EAP identity
    esp_eap_client_clear_identity();

    // set new EAP identity, certs
    esp_err_t id_err = esp_eap_client_set_identity((unsigned char const*)identity, strlen(identity)); // PEAP-TTLS? the ESP-IDF Wifi enterprise example uses this for EAP-TLS also    
    esp_err_t ca_err = esp_eap_client_set_ca_cert(ca_cert, ca_cert_len);
    esp_err_t cert_err = esp_eap_client_set_certificate_and_key(client_cert, client_cert_len, private_key, private_key_len, private_key_password, private_key_passwd_len);
    esp_err_t en_err = esp_wifi_sta_enterprise_enable(); // EAP

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );    

    // need to dynamically copy in SSID/Password here (i.e. strcpy, not just set pointer to SSID string)
    wifi_config_t wifi_config = {
        .sta = {
            .ssid = "MYSSID", 
            .password = "", 
            .threshold.authmode = WIFI_AUTH_OPEN,
            .pmf_cfg = {
                .capable = true,
                .required = false
            },
        },
    };

    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );

    // try connect
    wifi_newState(WIFI_ConnectAccessPointWaitForConnected, __LINE__);

    // - ESP_OK: succeed
    // - ESP_ERR_WIFI_NOT_INIT: WiFi is not initialized by esp_wifi_init
    // - ESP_ERR_INVALID_ARG: invalid argument
    // - ESP_ERR_NO_MEM: out of memory
    // - ESP_ERR_WIFI_CONN: WiFi internal error, station or soft-AP control block wrong
    // - ESP_FAIL: other WiFi internal errors        

    esp_err_t startResult = esp_wifi_start();  
    esp_err_t connectResult = esp_wifi_connect();   

    // startResult is returning ESP_OK

    esp_err_t err = esp_netif_get_ip_info(_wifi_stateVars.wifista_esp_netif, &ipInfo);     // return 0.0.0.0

    // At this point we wait for WIFI_EVENT events, and within <200ms we receive WIFI_EVENT_STA_DISCONNECTED with reason = WIFI_REASON_802_1X_AUTH_FAILED

For comparison, the example/wifi-enterprise code uses esp_wifi_set_config() before the enterprise API, but otherwise the same calls:

    // from example/wifi-enterprise
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
    ESP_ERROR_CHECK(esp_eap_client_set_identity((uint8_t *)EXAMPLE_EAP_ID, strlen(EXAMPLE_EAP_ID)) );
    ESP_ERROR_CHECK(esp_eap_client_set_ca_cert(ca_pem_start, ca_pem_bytes) );
    ESP_ERROR_CHECK(esp_eap_client_set_certificate_and_key(client_crt_start, client_crt_bytes,
                                      client_key_start, client_key_bytes, NULL, 0) );
    ESP_ERROR_CHECK(esp_wifi_sta_enterprise_enable());
    ESP_ERROR_CHECK(esp_wifi_start());

The Cisco enterprise AP event logs (where XXXXXX correctly displays my EAP identity)

Event: 5400 Authentication failed
Failure Reason: 12851 Received unexpected EAP NAK message. Client rejected the conversation
Resolution: Verify that the client's supplicant does not have any known compatibility issues and that it is properly configured.
Root cause: ISE expects for regular conversation continuation but client sent outer EAP method NAK message. It means that client rejected conversation for some reason that is unknown to ISE. Known issue: CSSC 5.1.1.10 sends outer EAP method NAK during EAP-FAST/EAP-GTC conversation to reject the conversation according to user's input.
Username: XXXXXX

And finally quote from my network engineer with logs from their Cisco Enterprise AP:

[1:49 pm] Joseph
    11001   Received RADIUS Access-Request
    11017   RADIUS created a new session
    15049   Evaluating Policy Group
    15008   Evaluating Service Selection Policy
    15048   Queried PIP - DEVICE.Wired Dot1x
    15048   Queried PIP - Radius.User-Name
    15048   Queried PIP - Normalised Radius.RadiusFlowType
    15048   Queried PIP - Cisco.cisco-av-pair
    15048   Queried PIP - Radius.Called-Station-ID
    15048   Queried PIP - DEVICE.Location
    11507   Extracted EAP-Response/Identity
    12500   Prepared EAP-Request proposing EAP-TLS with challenge
    12625   Valid EAP-Key-Name attribute received
    11006   Returned RADIUS Access-Challenge
    11001   Received RADIUS Access-Request
    11018   RADIUS is re-using an existing session
    12301   Extracted EAP-Response/NAK requesting to use PEAP instead (*****)
    12300   Prepared EAP-Request proposing PEAP with challenge
    12625   Valid EAP-Key-Name attribute received
    11006   Returned RADIUS Access-Challenge
    11001   Received RADIUS Access-Request
    11018   RADIUS is re-using an existing session
    12851   Received unexpected EAP NAK message. Client rejected the conversation
    11504   Prepared EAP-Failure
    11003   Returned RADIUS Access-Reject

[1:50 pm] Joseph
I can see your device is proposing PEAP on  line12301 (*****)
[1:50 pm] Joseph
That means it is doing username and password
tpbedford-oomdata commented 6 months ago

I've enabled further WPA supplicant logging, and it appears my cert is encrypted using an algorithm/digest that is not supported by mbedtls. Thus, possibly the ESP-IDF is failling to use a client cert so it reverts to PEAP (where I believe it is optional) even if a username/password isn't configured.

Method esp_eap_client_set_certificate_and_key() returns no error - it only fails to decrypt the password during the connection attempt in the background and seems to only print an error if "Print debug messages from WPA Supplicant" is turned on in idf.py menuconfig

kapilkedawat commented 6 months ago

Hi @tpbedford-oomdata, is it possible to share more details about the cert encryption? yes, wpa_supplicant logs are disabled by default.

tpbedford-oomdata commented 6 months ago

@kapilkedawat The encryption used for the private key was des-ede3-cbc which I can see is not compiled in (MBEDTLS_DES_C is not defined by default). Once I decrypted the cert it worked OK, so I'll recrypt it with a supported algorithm for my case.

So ultimately I suppose esp_eap_client_set_certificate_and_key() should not return ESP_OK when given an unsupported cert. My cert was encrypted with des-ede3-cbc which was not supported, but this method accepted my cert. Only after esp_wifi_connect() was called, that somewhere in the background this decryption failed, and there was no error message to indicate why. The WIFI_EVENT_STA_DISCONNECTED event gave reason as WIFI_REASON_802_1X_AUTH_FAILED which doesn't tell the user anything useful.

tpbedford-oomdata commented 6 months ago

@kapilkedawat Further, the esp_eap_client_set_certificate_and_key() method appears to require len+1 to work. I'm reading my cert file from FAT filesystem and gave the actual length of the file. Only once I added an additional NUL and used len+1 did it succeed.

e.g.

FILE* f = fopen("mycert.pem"); // is 3048 bytes
size_t cert_len = fread(cert_buf, ..., f); // len = 3048

esp_eap_client_set_certificate_and_key(cert_buf, cert_len, pkey_buf, pkey_len, NULL, 0); // FAILS if cert_len=3048

cert_buf[cert_len] = 0; // add NUL
esp_eap_client_set_certificate_and_key(cert_buf, cert_len+1, pkey_buf, pkey_len+1, NULL, 0); // succeeds passing 3049 instead
kapilkedawat commented 6 months ago

Hi @tpbedford-oomdata, Yes esp_eap_client_set_certificate_and_key() API needs zero terminated strings. Please see https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/network/esp_wifi.html?highlight=esp_eap_client_set_certificate_and_key#_CPPv438esp_eap_client_set_certificate_and_keyPKhiPKhiPKhi .

The API returns ESP_OK because we do not attempt to parse the certificates until initiating the EAP connection. Performing this operation before establishing the connection would cause unnecessary overhead.

tpbedford-oomdata commented 6 months ago

The API returns ESP_OK because we do not attempt to parse the certificates until initiating the EAP connection. Performing this operation before establishing the connection would cause unnecessary overhead.

Noted ("The client_cert, private_key, and private_key_password should be zero-terminated.") but, a) it's not clear from the doc that len argument must be strlen(cert)+1 and not strlen(cert) b) why require len as an argument at all if the cert strings are null-terminated?

I personally think the overhead of parsing the cert when calling esp_eap_client_set_certificate_and_key() would be OK: it's an infrequent operation, done once when initialising wifi, which takes several seconds to connect anyway, and otherwise there is no indication to the caller why the wifi connection attempt failed. The WIFI_EVENT event doesn't carry enough information to know if it's a bad cert, bad password, bad key, unsupported encryption algo, etc.

kapilkedawat commented 6 months ago

The API returns ESP_OK because we do not attempt to parse the certificates until initiating the EAP connection. Performing this operation before establishing the connection would cause unnecessary overhead.

Noted ("The client_cert, private_key, and private_key_password should be zero-terminated.") but, a) it's not clear from the doc that len argument must be strlen(cert)+1 and not strlen(cert) b) why require len as an argument at all if the cert strings are null-terminated?

len is required since there may be chain of certs.

I personally think the overhead of parsing the cert when calling esp_eap_client_set_certificate_and_key() would be OK: it's an infrequent operation, done once when initialising wifi, which takes several seconds to connect anyway, and otherwise there is no indication to the caller why the wifi connection attempt failed. The WIFI_EVENT event doesn't carry enough information to know if it's a bad cert, bad password, bad key, unsupported encryption algo, etc.

thanks for your input, we will take it into consideration.