From e28150397116bf24c4970ad42a4bc055cf9c1a07 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Mon, 27 Nov 2023 20:04:04 +0100 Subject: [PATCH] fix(modem): Fix ota-test to verify server cert and CN --- .../include/mbedtls_wrap.hpp | 1 + .../extra_tcp_transports/mbedtls_wrap.cpp | 10 + .../esp_modem/test/target_ota/README.md | 6 + .../components/manual_ota/manual_ota.cpp | 230 +++++++++++------- .../components/manual_ota/manual_ota.hpp | 72 ++++-- .../manual_ota/transport_batch_tls.cpp | 25 +- .../manual_ota/transport_batch_tls.hpp | 18 ++ .../test/target_ota/main/Kconfig.projbuild | 13 + .../test/target_ota/main/ota_test.cpp | 8 +- .../test/target_ota/sdkconfig.defaults | 14 ++ 10 files changed, 286 insertions(+), 111 deletions(-) create mode 100644 components/esp_modem/test/target_ota/sdkconfig.defaults diff --git a/components/esp_modem/examples/modem_tcp_client/components/extra_tcp_transports/include/mbedtls_wrap.hpp b/components/esp_modem/examples/modem_tcp_client/components/extra_tcp_transports/include/mbedtls_wrap.hpp index 5590801e849..5dc1df71ab4 100644 --- a/components/esp_modem/examples/modem_tcp_client/components/extra_tcp_transports/include/mbedtls_wrap.hpp +++ b/components/esp_modem/examples/modem_tcp_client/components/extra_tcp_transports/include/mbedtls_wrap.hpp @@ -29,6 +29,7 @@ class Tls { int read(unsigned char *buf, size_t len); [[nodiscard]] bool set_own_cert(const_buf crt, const_buf key); [[nodiscard]] bool set_ca_cert(const_buf crt); + bool set_hostname(const char *name); virtual int send(const unsigned char *buf, size_t len) = 0; virtual int recv(unsigned char *buf, size_t len) = 0; size_t get_available_bytes(); diff --git a/components/esp_modem/examples/modem_tcp_client/components/extra_tcp_transports/mbedtls_wrap.cpp b/components/esp_modem/examples/modem_tcp_client/components/extra_tcp_transports/mbedtls_wrap.cpp index 026f41020d0..eda6045fec9 100644 --- a/components/esp_modem/examples/modem_tcp_client/components/extra_tcp_transports/mbedtls_wrap.cpp +++ b/components/esp_modem/examples/modem_tcp_client/components/extra_tcp_transports/mbedtls_wrap.cpp @@ -116,6 +116,16 @@ bool Tls::set_ca_cert(const_buf crt) return true; } +bool Tls::set_hostname(const char *name) +{ + int ret = mbedtls_ssl_set_hostname(&ssl_, name); + if (ret < 0) { + print_error("mbedtls_ssl_set_hostname", ret); + return false; + } + return true; +} + Tls::Tls() { mbedtls_x509_crt_init(&public_cert_); diff --git a/components/esp_modem/test/target_ota/README.md b/components/esp_modem/test/target_ota/README.md index 74b98ef4a66..434fa03b1d7 100644 --- a/components/esp_modem/test/target_ota/README.md +++ b/components/esp_modem/test/target_ota/README.md @@ -19,3 +19,9 @@ sudo pppd /dev/ttyUSB1 115200 192.168.11.1:192.168.11.2 ms-dns 8.8.8.8 modem loc ``` * MQTT broker: Running mosquitto in the default config is enough, configuring the broker's URL to the local PPP address: `config.broker.address.uri = "mqtt://192.168.11.1";` * HTTP server: Need to support HTTP/1.1 (to support ranges). You can use the script `http_server.py` and configure the OTA endpoint as `"https://192.168.11.1:1234/esp32.bin"` + +## Potential issues + +When running this test it is expected to experience some buffer overflows or connection interruption. +The modem library should recover from these failure modes and continue and complete OTA update. +These issues are expected, since UART ISR is deliberately not placed into IRAM in the test configuration to exhibit some minor communication glitches. diff --git a/components/esp_modem/test/target_ota/components/manual_ota/manual_ota.cpp b/components/esp_modem/test/target_ota/components/manual_ota/manual_ota.cpp index c1aaa3d36b2..1bac93ec90b 100644 --- a/components/esp_modem/test/target_ota/components/manual_ota/manual_ota.cpp +++ b/components/esp_modem/test/target_ota/components/manual_ota/manual_ota.cpp @@ -20,14 +20,6 @@ bool manual_ota::begin() ESP_LOGE(TAG, "Invalid state"); return false; } - status = state::INIT; - esp_transport_handle_t tcp = esp_transport_tcp_init(); - ssl_ = esp_transport_batch_tls_init(tcp, max_buffer_size_); - - esp_http_client_config_t config = { }; - config.skip_cert_common_name_check = true; - config.url = uri_; - config.transport = ssl_; const esp_partition_t *configured = esp_ota_get_boot_partition(); const esp_partition_t *running = esp_ota_get_running_partition(); @@ -36,38 +28,36 @@ bool manual_ota::begin() return false; } - http_ = esp_http_client_init(&config); - if (http_ == nullptr) { - ESP_LOGE(TAG, "Failed to initialise HTTP connection"); - return false; - } - esp_http_client_set_method(http_, HTTP_METHOD_HEAD); - esp_err_t err = esp_http_client_perform(http_); - if (err == ESP_OK) { - int http_status = esp_http_client_get_status_code(http_); - if (http_status != HttpStatus_Ok) { - ESP_LOGE(TAG, "Received incorrect http status %d", http_status); - return false; + status = state::INIT; + max_buffer_size_ = size_ * 1024; + if (mode_ == mode::BATCH) { + esp_transport_handle_t tcp = esp_transport_tcp_init(); + ssl_ = esp_transport_batch_tls_init(tcp, max_buffer_size_); + http_.config_.transport = ssl_; + if (!esp_transport_batch_set_ca_cert(ssl_, http_.config_.cert_pem, 0)) { + return fail(); } - } else { - ESP_LOGE(TAG, "ESP HTTP client perform failed: %d", err); - return false; + if (!esp_transport_batch_set_cn(ssl_, common_name_)) { + return fail(); + } + + } + + if (!http_.init()) { + return fail(); + } + + image_length_ = http_.get_image_len(); + if (image_length_ <= 0) { + return fail(); } - image_length_ = esp_http_client_get_content_length(http_); - ESP_LOGI(TAG, "image_length = %lld", image_length_); - esp_http_client_close(http_); if (image_length_ > size_) { - char *header_val = nullptr; - asprintf(&header_val, "bytes=0-%d", max_buffer_size_ - 1); - if (header_val == nullptr) { - ESP_LOGE(TAG, "Failed to allocate memory for HTTP header"); - return false; + if (!http_.set_range(0, max_buffer_size_ - 1)) { + return fail(); } - esp_http_client_set_header(http_, "Range", header_val); - free(header_val); } - esp_http_client_set_method(http_, HTTP_METHOD_GET); + esp_http_client_set_method(http_.handle_, HTTP_METHOD_GET); partition_ = esp_ota_get_next_update_partition(nullptr); if (partition_ == nullptr) { @@ -89,37 +79,39 @@ bool manual_ota::perform() ESP_LOGE(TAG, "Invalid state"); return false; } - esp_err_t err = esp_http_client_open(http_, 0); + esp_err_t err = esp_http_client_open(http_.handle_, 0); if (err != ESP_OK) { if (image_length_ == file_length_) { status = state::END; return false; } - esp_http_client_close(http_); + esp_http_client_close(http_.handle_); ESP_LOGI(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err)); if (reconnect_attempts_++ < max_reconnect_attempts_) { - if (prepare_reconnect()) { - return true; // will retry in the next iteration - } + return true; // will retry in the next iteration } - return fail_cleanup(); + return fail(); } - esp_http_client_fetch_headers(http_); + esp_http_client_fetch_headers(http_.handle_); - int batch_len = esp_transport_batch_tls_pre_read(ssl_, max_buffer_size_, timeout_ * 1000); - if (batch_len < 0) { - ESP_LOGE(TAG, "Error: Failed to pre-read plain text data!"); - return fail_cleanup(); + int batch_len = max_buffer_size_; + if (mode_ == mode::BATCH) { + batch_len = esp_transport_batch_tls_pre_read(ssl_, max_buffer_size_, timeout_ * 1000); + if (batch_len < 0) { + ESP_LOGE(TAG, "Error: Failed to pre-read plain text data!"); + fail(); + return false; + } } - int data_read = esp_http_client_read(http_, buffer_.data(), batch_len); + int data_read = esp_http_client_read(http_.handle_, buffer_.data(), batch_len); if (data_read < 0) { ESP_LOGE(TAG, "Error: SSL data read error"); - return fail_cleanup(); + return fail(); } else if (data_read > 0) { - esp_http_client_close(http_); + esp_http_client_close(http_.handle_); if (status == state::IMAGE_CHECK) { esp_app_desc_t new_app_info; @@ -146,7 +138,7 @@ bool manual_ota::perform() ESP_LOGW(TAG, "New version is the same as invalid version."); ESP_LOGW(TAG, "Previously, there was an attempt to launch the firmware with %s version, but it failed.", invalid_app_info.version); ESP_LOGW(TAG, "The firmware has been rolled back to the previous version."); - return fail_cleanup(); + return fail(); } } @@ -154,20 +146,18 @@ bool manual_ota::perform() err = esp_ota_begin(partition_, OTA_WITH_SEQUENTIAL_WRITES, &update_handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ota_begin failed (%s)", esp_err_to_name(err)); - esp_ota_abort(update_handle_); - return fail_cleanup(); + return fail(); } + ota_begin = true; ESP_LOGI(TAG, "esp_ota_begin succeeded"); } else { ESP_LOGE(TAG, "Received chunk doesn't contain app descriptor"); - esp_ota_abort(update_handle_); - return fail_cleanup(); + return fail(); } } err = esp_ota_write(update_handle_, (const void *)buffer_.data(), data_read); if (err != ESP_OK) { - esp_ota_abort(update_handle_); - return fail_cleanup(); + return fail(); } file_length_ += data_read; ESP_LOGI(TAG, "Written image length %d", file_length_); @@ -178,28 +168,22 @@ bool manual_ota::perform() } if (!prepare_reconnect()) { - esp_ota_abort(update_handle_); - return fail_cleanup(); + return fail(); } } else if (data_read == 0) { if (file_length_ == 0) { // Try to handle possible HTTP redirections - int status_code = esp_http_client_get_status_code(http_); - ESP_LOGW(TAG, "Status code: %d", status_code); - err = esp_http_client_set_redirection(http_); - if (err != ESP_OK) { - ESP_LOGE(TAG, "URL redirection Failed"); - esp_ota_abort(update_handle_); - return fail_cleanup(); + if (!http_.handle_redirects()) { + return fail(); } - err = esp_http_client_open(http_, 0); + err = esp_http_client_open(http_.handle_, 0); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err)); - return fail_cleanup(); + return fail(); } - esp_http_client_fetch_headers(http_); + esp_http_client_fetch_headers(http_.handle_); } } @@ -208,26 +192,13 @@ bool manual_ota::perform() bool manual_ota::prepare_reconnect() { - esp_http_client_set_method(http_, HTTP_METHOD_GET); - char *header_val = nullptr; - if ((image_length_ - file_length_) > max_buffer_size_) { - asprintf(&header_val, "bytes=%d-%d", file_length_, (file_length_ + max_buffer_size_ - 1)); - } else { - asprintf(&header_val, "bytes=%d-", file_length_); - } - if (header_val == nullptr) { - ESP_LOGE(TAG, "Failed to allocate memory for HTTP header"); - return false; - } - esp_http_client_set_header(http_, "Range", header_val); - free(header_val); - return true; + esp_http_client_set_method(http_.handle_, HTTP_METHOD_GET); + return http_.set_range(file_length_, + (image_length_ - file_length_) > max_buffer_size_ ? (file_length_ + max_buffer_size_ - 1) : 0); } -bool manual_ota::fail_cleanup() +bool manual_ota::fail() { - esp_http_client_close(http_); - esp_http_client_cleanup(http_); status = state::FAIL; return false; } @@ -235,9 +206,9 @@ bool manual_ota::fail_cleanup() bool manual_ota::end() { if (status == state::END) { - if (!esp_http_client_is_complete_data_received(http_)) { + if (!http_.is_data_complete()) { ESP_LOGE(TAG, "Error in receiving complete file"); - return fail_cleanup(); + return fail(); } esp_err_t err = esp_ota_end(update_handle_); if (err != ESP_OK) { @@ -246,14 +217,97 @@ bool manual_ota::end() } else { ESP_LOGE(TAG, "esp_ota_end failed (%s)!", esp_err_to_name(err)); } - return fail_cleanup(); + return fail(); } + ota_begin = false; err = esp_ota_set_boot_partition(partition_); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ota_set_boot_partition failed (%s)!", esp_err_to_name(err)); - return fail_cleanup(); + return fail(); } return true; } return false; } + +manual_ota::~manual_ota() +{ + if (ota_begin) { + esp_ota_abort(update_handle_); + } +} + +bool manual_ota::http_client::handle_redirects() +{ + int status_code = esp_http_client_get_status_code(handle_); + ESP_LOGW(TAG, "Status code: %d", status_code); + esp_err_t err = esp_http_client_set_redirection(handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "URL redirection Failed"); + return false; + } + + err = esp_http_client_open(handle_, 0); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err)); + return false; + } + esp_http_client_fetch_headers(handle_); + return true; +} + +bool manual_ota::http_client::set_range(size_t from, size_t to) +{ + char *header_val = nullptr; + if (to != 0) { + asprintf(&header_val, "bytes=%d-%d", from, to); + } else { + asprintf(&header_val, "bytes=%d-", from); + } + if (header_val == nullptr) { + ESP_LOGE(TAG, "Failed to allocate memory for HTTP header"); + return false; + } + esp_http_client_set_header(handle_, "Range", header_val); + free(header_val); + return true; +} + +bool manual_ota::http_client::is_data_complete() +{ + return esp_http_client_is_complete_data_received(handle_); +} + +manual_ota::http_client::~http_client() +{ + if (handle_) { + esp_http_client_close(handle_); + esp_http_client_cleanup(handle_); + } +} + +bool manual_ota::http_client::init() +{ + handle_ = esp_http_client_init(&config_); + return handle_ != nullptr; +} + +int64_t manual_ota::http_client::get_image_len() +{ + esp_http_client_set_method(handle_, HTTP_METHOD_HEAD); + esp_err_t err = esp_http_client_perform(handle_); + if (err == ESP_OK) { + int http_status = esp_http_client_get_status_code(handle_); + if (http_status != HttpStatus_Ok) { + ESP_LOGE(TAG, "Received incorrect http status %d", http_status); + return -1; + } + } else { + ESP_LOGE(TAG, "ESP HTTP client perform failed: %d", err); + return -1; + } + int64_t image_length = esp_http_client_get_content_length(handle_); + ESP_LOGI(TAG, "image_length = %lld", image_length); + esp_http_client_close(handle_); + return image_length; +} diff --git a/components/esp_modem/test/target_ota/components/manual_ota/manual_ota.hpp b/components/esp_modem/test/target_ota/components/manual_ota/manual_ota.hpp index ad09e748de3..6ddaad36a31 100644 --- a/components/esp_modem/test/target_ota/components/manual_ota/manual_ota.hpp +++ b/components/esp_modem/test/target_ota/components/manual_ota/manual_ota.hpp @@ -12,23 +12,58 @@ class manual_ota { public: - enum class state { - UNDEF, - INIT, - IMAGE_CHECK, - START, - END, - FAIL, - }; + /** + * @brief Set the preferred mode + */ + enum class mode { + BATCH, /**< Read data chunk from TCP and pass it to SSL, restore session on reconnection */ + NORMAL /**< Use standard partial download, continuously passing data from TCP to mbedTLS */ + } mode_ {mode::BATCH}; + + /** + * @brief Set the OTA batch size in kB + * + * This would allocate two big buffers: + * - one for reading from TCP socket and + * - one for passing to mbedTLS for description + */ size_t size_{32}; + + /** + * @brief Set timeout in seconds + * + * This is the network timeout, so if less data than the batch size received + * the timeout (and no EOF) we should proceed with passing the data to mbedtls + */ int timeout_{2}; + /** + * @brief Set common name of the server to verify + */ + const char *common_name_; + /** + * @brief Wrapper around the http client -- Please set the http config + */ + class http_client { + friend class manual_ota; + http_client() {} + ~http_client(); + bool init(); + esp_http_client_handle_t handle_{nullptr}; + bool handle_redirects(); + bool set_range(size_t from, size_t to); + bool is_data_complete(); + int64_t get_image_len(); + public: + esp_http_client_config_t config_{}; /**< Configure the http connection parameters */ + } http_; + /** * @brief Construct a new manual ota object - * - * @param uri URI of the binary image */ - explicit manual_ota(const char *uri): uri_(uri) {} + explicit manual_ota() {} + + ~manual_ota(); /** * @brief Start the manual OTA process @@ -53,11 +88,17 @@ class manual_ota { bool end(); private: - const char *uri_{}; - esp_http_client_handle_t http_; + enum class state { + UNDEF, + INIT, + IMAGE_CHECK, + START, + END, + FAIL, + }; int64_t image_length_; size_t file_length_; - const size_t max_buffer_size_{size_ * 1024}; + size_t max_buffer_size_{size_ * 1024}; const esp_partition_t *partition_{nullptr}; state status{state::UNDEF}; std::vector buffer_{}; @@ -65,7 +106,8 @@ class manual_ota { const int max_reconnect_attempts_{3}; esp_transport_handle_t ssl_; esp_ota_handle_t update_handle_{0}; + bool ota_begin; bool prepare_reconnect(); - bool fail_cleanup(); + bool fail(); }; diff --git a/components/esp_modem/test/target_ota/components/manual_ota/transport_batch_tls.cpp b/components/esp_modem/test/target_ota/components/manual_ota/transport_batch_tls.cpp index 013b4ef6b16..9e041da57fa 100644 --- a/components/esp_modem/test/target_ota/components/manual_ota/transport_batch_tls.cpp +++ b/components/esp_modem/test/target_ota/components/manual_ota/transport_batch_tls.cpp @@ -40,11 +40,11 @@ class TlsTransport: public Tls { esp_transport_handle_t esp_transport_tls_init(esp_transport_handle_t parent) { - esp_transport_handle_t ssl = esp_transport_init(); - auto *tls = new TlsTransport(parent); - esp_transport_set_context_data(ssl, tls); - TlsTransport::set_func(ssl); - return ssl; + esp_transport_handle_t transport_handle = esp_transport_init(); + auto *tls_context = new TlsTransport(parent); + esp_transport_set_context_data(transport_handle, tls_context); + TlsTransport::set_func(transport_handle); + return transport_handle; } int TlsTransport::send(const unsigned char *buf, size_t len) @@ -105,7 +105,7 @@ void TlsTransport::delay() int TlsTransport::transport::connect(esp_transport_handle_t t, const char *host, int port, int timeout_ms) { auto tls = static_cast(esp_transport_get_context_data(t)); - tls->init(is_server{false}, do_verify{false}); + tls->init(is_server{false}, do_verify{true}); ESP_LOGD(TAG, "TLS-connect"); auto ret = tls->connect(host, port, timeout_ms); @@ -202,6 +202,19 @@ esp_transport_handle_t esp_transport_batch_tls_init(esp_transport_handle_t paren return ssl; } +bool esp_transport_batch_set_ca_cert(esp_transport_handle_t t, const char *ca_cert, size_t cert_len = 0) +{ + auto tls = static_cast(esp_transport_get_context_data(t)); + const_buf cert((const unsigned char *)ca_cert, cert_len ? cert_len : strlen(ca_cert) + 1); + return tls->set_ca_cert(cert); +} + +bool esp_transport_batch_set_cn(esp_transport_handle_t t, const char *name) +{ + auto tls = static_cast(esp_transport_get_context_data(t)); + return tls->set_hostname(name); +} + int TlsTransport::preread(size_t len, int timeout_ms) { while (len != read_len) { diff --git a/components/esp_modem/test/target_ota/components/manual_ota/transport_batch_tls.hpp b/components/esp_modem/test/target_ota/components/manual_ota/transport_batch_tls.hpp index e91b8b62e45..5d36123e9cf 100644 --- a/components/esp_modem/test/target_ota/components/manual_ota/transport_batch_tls.hpp +++ b/components/esp_modem/test/target_ota/components/manual_ota/transport_batch_tls.hpp @@ -23,3 +23,21 @@ esp_transport_handle_t esp_transport_batch_tls_init(esp_transport_handle_t paren * @return true If read from the parent transport completed successfully */ bool esp_transport_batch_tls_pre_read(esp_transport_handle_t t, int len, int timeout_ms); + +/** + * @brief Set the CA Certificate to verify the server + * + * @param ca_cert Pointer to the CA Cert data + * @param cert_len CA Cert data len (set to 0 if null terminated string, i.e. PEM format) + * @return true on success + */ +bool esp_transport_batch_set_ca_cert(esp_transport_handle_t t, const char *ca_cert, size_t cert_len); + +/** + * @brief Set comman name + * @param t + * @param ca_cert + * @param cert_len + * @return + */ +bool esp_transport_batch_set_cn(esp_transport_handle_t t, const char *name); diff --git a/components/esp_modem/test/target_ota/main/Kconfig.projbuild b/components/esp_modem/test/target_ota/main/Kconfig.projbuild index 3d973fb005c..3d2de807158 100644 --- a/components/esp_modem/test/target_ota/main/Kconfig.projbuild +++ b/components/esp_modem/test/target_ota/main/Kconfig.projbuild @@ -38,6 +38,19 @@ menu "Test Configuration" help HTTPS address of the update binary. + config TEST_OTA_CA_CERT + string "Server certificate" + default "---paste the server side certificate here---" + help + Insert the CA cert of the server side here. copy the base64 text between -----BEGIN CERTIFICATE----- + and -----END CERTIFICATE-----. + + config TEST_OTA_CN + string "Server common name" + default "192.168.11.1" + help + Insert the server's common name to be checked within verification of the Server side certificat + config BROKER_URI string "Broker URL" default "mqtt://test.mosquitto.org" diff --git a/components/esp_modem/test/target_ota/main/ota_test.cpp b/components/esp_modem/test/target_ota/main/ota_test.cpp index ec0edfbb071..53ff43d83b8 100644 --- a/components/esp_modem/test/target_ota/main/ota_test.cpp +++ b/components/esp_modem/test/target_ota/main/ota_test.cpp @@ -145,9 +145,13 @@ static constexpr auto OTA_FAILED = SignalGroup::bit1; void ota_task(void *ctx) { + static const char *ca_cert_pem = "-----BEGIN CERTIFICATE-----\n" CONFIG_TEST_OTA_CA_CERT "\n-----END CERTIFICATE-----"; auto ota_done = static_cast(ctx); - manual_ota ota(CONFIG_TEST_OTA_URI); - ota.size_ = 64; + manual_ota ota; + ota.http_.config_.url = CONFIG_TEST_OTA_URI; + ota.http_.config_.cert_pem = ca_cert_pem; + ota.size_ = 32; + ota.common_name_ = CONFIG_TEST_OTA_CN; ota.begin(); while (true) { diff --git a/components/esp_modem/test/target_ota/sdkconfig.defaults b/components/esp_modem/test/target_ota/sdkconfig.defaults new file mode 100644 index 00000000000..d4ed6a63c72 --- /dev/null +++ b/components/esp_modem/test/target_ota/sdkconfig.defaults @@ -0,0 +1,14 @@ +CONFIG_PARTITION_TABLE_TWO_OTA=y +CONFIG_TEST_USE_VFS_TERM=y +CONFIG_TEST_OTA_URI="https://raw.githubusercontent.com/david-cermak/esp-network-examples/test/ota/hello_world.bin" +CONFIG_TEST_OTA_CA_CERT="MIIEvjCCA6agAwIBAgIQBtjZBNVYQ0b2ii+nVCJ+xDANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0yMTA0MTQwMDAwMDBaFw0zMTA0MTMyMzU5NTlaME8xCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxKTAnBgNVBAMTIERpZ2lDZXJ0IFRMUyBSU0EgU0hBMjU2IDIwMjAgQ0ExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwUuzZUdwvN1PWNvsnO3DZuUfMRNUrUpmRh8sCuxkB+Uu3Ny5CiDt3+PE0J6aqXodgojlEVbbHp9YwlHnLDQNLtKS4VbL8Xlfs7uHyiUDe5pSQWYQYE9XE0nw6Ddng9/n00tnTCJRpt8OmRDtV1F0JuJ9x8piLhMbfyOIJVNvwTRYAIuE//i+p1hJInuWraKImxW8oHzf6VGo1bDtN+I2tIJLYrVJmuzHZ9bjPvXj1hJeRPG/cUJ9WIQDgLGBAfr5yjK7tI4nhyfFK3TUqNaX3sNk+crOU6JWvHgXjkkDKa77SU+kFbnO8lwZV21reacroicgE7XQPUDTITAHk+qZ9QIDAQABo4IBgjCCAX4wEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUt2ui6qiqhIx56rTaD5iyxZV2ufQwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjB2BggrBgEFBQcBAQRqMGgwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBABggrBgEFBQcwAoY0aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNydDBCBgNVHR8EOzA5MDegNaAzhjFodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRHbG9iYWxSb290Q0EuY3JsMD0GA1UdIAQ2MDQwCwYJYIZIAYb9bAIBMAcGBWeBDAEBMAgGBmeBDAECATAIBgZngQwBAgIwCAYGZ4EMAQIDMA0GCSqGSIb3DQEBCwUAA4IBAQCAMs5eC91uWg0Kr+HWhMvAjvqFcO3aXbMM9yt1QP6FCvrzMXi3cEsaiVi6gL3zax3pfs8LulicWdSQ0/1s/dCYbbdxglvPbQtaCdB73sRD2Cqk3p5BJl+7j5nL3a7hqG+fh/50tx8bIKuxT8b1Z11dmzzp/2n3YWzW2fP9NsarA4h20ksudYbj/NhVfSbCEXffPgK2fPOre3qGNm+499iTcc+G33Mw+nur7SpZyEKEOxEXGlLzyQ4UfaJbcme6ce1XR2bFuAJKZTRei9AqPCCcUZlM51Ke92sRKw2Sfh3oius2FkOH6ipjv3U/697EA7sKPPcw7+uvTPyLNhBzPvOk" +CONFIG_TEST_OTA_CN="github.com" +CONFIG_COMPILER_CXX_EXCEPTIONS=y +CONFIG_ESP_HTTP_CLIENT_ENABLE_CUSTOM_TRANSPORT=y +CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT=y +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 +CONFIG_LWIP_PPP_SUPPORT=y +CONFIG_LWIP_PPP_ENABLE_IPV6=n +CONFIG_LWIP_ENABLE_LCP_ECHO=y +CONFIG_LWIP_LCP_ECHOINTERVAL=1 +CONFIG_LWIP_LCP_MAXECHOFAILS=2