diff --git a/README.md b/README.md index b3db0a9..8eaff0e 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ App|Description [picow_ntp_client](pico_w/ntp_client)| Connects to an NTP server to fetch and display the current time. [picow_tcp_client](pico_w/tcp_client)| A simple TCP client. You can run [python_test_tcp_server.py](pico_w/python_test_tcp/python_test_tcp_server.py) for it to connect to. [picow_tcp_server](pico_w/tcp_server)| A simple TCP server. You can use [python_test_tcp_client.py](pico_w/python_test_tcp/python_test_tcp_client.py) to connect to it. +[picow_tls_client](pico_w/tls_client)| Demonstrates how to make a HTTPS request using TLS. [picow_wifi_scan](pico_w/wifi_scan)| Scans for WiFi networks and prints the results. [picow_udp_beacon](pico_w/udp_beacon)| A simple UDP transmitter. diff --git a/pico_w/CMakeLists.txt b/pico_w/CMakeLists.txt index e939dcb..4de3936 100644 --- a/pico_w/CMakeLists.txt +++ b/pico_w/CMakeLists.txt @@ -33,6 +33,12 @@ if (PICO_CYW43_SUPPORTED) # set by PICO_BOARD=pico_w add_subdirectory(tcp_server) add_subdirectory(freertos) add_subdirectory(udp_beacon) + + if (NOT PICO_MBEDTLS_PATH) + message("Skipping tls examples as PICO_MBEDTLS_PATH is not defined") + else() + add_subdirectory(tls_client) + endif() endif() endif() endif() diff --git a/pico_w/tls_client/CMakeLists.txt b/pico_w/tls_client/CMakeLists.txt new file mode 100644 index 0000000..3ec9e68 --- /dev/null +++ b/pico_w/tls_client/CMakeLists.txt @@ -0,0 +1,44 @@ +add_executable(picow_tls_client_background + picow_tls_client.c + ) +target_compile_definitions(picow_tls_client_background PRIVATE + WIFI_SSID=\"${WIFI_SSID}\" + WIFI_PASSWORD=\"${WIFI_PASSWORD}\" + ) +target_include_directories(picow_tls_client_background PRIVATE + ${CMAKE_CURRENT_LIST_DIR} + ${CMAKE_CURRENT_LIST_DIR}/.. # for our common lwipopts + ) +target_link_libraries(picow_tls_client_background + pico_cyw43_arch_lwip_threadsafe_background + pico_lwip_mbedtls + pico_mbedtls + pico_stdlib + ) +pico_add_extra_outputs(picow_tls_client_background) + +add_executable(picow_tls_client_poll + picow_tls_client.c + ) +target_compile_definitions(picow_tls_client_poll PRIVATE + WIFI_SSID=\"${WIFI_SSID}\" + WIFI_PASSWORD=\"${WIFI_PASSWORD}\" + ) +target_include_directories(picow_tls_client_poll PRIVATE + ${CMAKE_CURRENT_LIST_DIR} + ${CMAKE_CURRENT_LIST_DIR}/.. # for our common lwipopts + ) +target_link_libraries(picow_tls_client_poll + pico_cyw43_arch_lwip_poll + pico_lwip_mbedtls + pico_mbedtls + pico_stdlib + ) +pico_add_extra_outputs(picow_tls_client_poll) + +# Ignore warnings from lwip code +set_source_files_properties( + ${PICO_LWIP_PATH}/src/apps/altcp_tls/altcp_tls_mbedtls.c + PROPERTIES + COMPILE_OPTIONS "-Wno-unused-result" + ) \ No newline at end of file diff --git a/pico_w/tls_client/lwipopts.h b/pico_w/tls_client/lwipopts.h new file mode 100644 index 0000000..6cf5883 --- /dev/null +++ b/pico_w/tls_client/lwipopts.h @@ -0,0 +1,19 @@ +#ifndef _LWIPOPTS_H +#define _LWIPOPTS_H + +#include "lwipopts_examples_common.h" + +/* TCP WND must be at least 16 kb to match TLS record size + or you will get a warning "altcp_tls: TCP_WND is smaller than the RX decrypion buffer, connection RX might stall!" */ +#undef TCP_WND +#define TCP_WND 16384 + +#define LWIP_ALTCP 1 +#define LWIP_ALTCP_TLS 1 +#define LWIP_ALTCP_TLS_MBEDTLS 1 + +#define LWIP_DEBUG 1 +#define ALTCP_MBEDTLS_DEBUG LWIP_DBG_ON + +#endif + diff --git a/pico_w/tls_client/mbedtls_config.h b/pico_w/tls_client/mbedtls_config.h new file mode 100644 index 0000000..dae3395 --- /dev/null +++ b/pico_w/tls_client/mbedtls_config.h @@ -0,0 +1,63 @@ +/* Workaround for some mbedtls source files using INT_MAX without including limits.h */ +#include + +#define MBEDTLS_NO_PLATFORM_ENTROPY +#define MBEDTLS_ENTROPY_HARDWARE_ALT + +#define MBEDTLS_SSL_OUT_CONTENT_LEN 2048 + +#define MBEDTLS_ALLOW_PRIVATE_ACCESS +#define MBEDTLS_HAVE_TIME + +#define MBEDTLS_CIPHER_MODE_CBC +#define MBEDTLS_ECP_DP_SECP192R1_ENABLED +#define MBEDTLS_ECP_DP_SECP224R1_ENABLED +#define MBEDTLS_ECP_DP_SECP256R1_ENABLED +#define MBEDTLS_ECP_DP_SECP384R1_ENABLED +#define MBEDTLS_ECP_DP_SECP521R1_ENABLED +#define MBEDTLS_ECP_DP_SECP192K1_ENABLED +#define MBEDTLS_ECP_DP_SECP224K1_ENABLED +#define MBEDTLS_ECP_DP_SECP256K1_ENABLED +#define MBEDTLS_ECP_DP_BP256R1_ENABLED +#define MBEDTLS_ECP_DP_BP384R1_ENABLED +#define MBEDTLS_ECP_DP_BP512R1_ENABLED +#define MBEDTLS_ECP_DP_CURVE25519_ENABLED +#define MBEDTLS_KEY_EXCHANGE_RSA_ENABLED +#define MBEDTLS_PKCS1_V15 +#define MBEDTLS_SHA256_SMALLER +#define MBEDTLS_SSL_SERVER_NAME_INDICATION +#define MBEDTLS_AES_C +#define MBEDTLS_ASN1_PARSE_C +#define MBEDTLS_BIGNUM_C +#define MBEDTLS_CIPHER_C +#define MBEDTLS_CTR_DRBG_C +#define MBEDTLS_ENTROPY_C +#define MBEDTLS_ERROR_C +#define MBEDTLS_MD_C +#define MBEDTLS_MD5_C +#define MBEDTLS_OID_C +#define MBEDTLS_PKCS5_C +#define MBEDTLS_PK_C +#define MBEDTLS_PK_PARSE_C +#define MBEDTLS_PLATFORM_C +#define MBEDTLS_RSA_C +#define MBEDTLS_SHA1_C +#define MBEDTLS_SHA224_C +#define MBEDTLS_SHA256_C +#define MBEDTLS_SHA512_C +#define MBEDTLS_SSL_CLI_C +#define MBEDTLS_SSL_SRV_C +#define MBEDTLS_SSL_TLS_C +#define MBEDTLS_X509_CRT_PARSE_C +#define MBEDTLS_X509_USE_C +#define MBEDTLS_AES_FEWER_TABLES + +/* TLS 1.2 */ +#define MBEDTLS_SSL_PROTO_TLS1_2 +#define MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED +#define MBEDTLS_GCM_C +#define MBEDTLS_ECDH_C +#define MBEDTLS_ECP_C +#define MBEDTLS_ECDSA_C +#define MBEDTLS_ASN1_WRITE_C + diff --git a/pico_w/tls_client/picow_tls_client.c b/pico_w/tls_client/picow_tls_client.c new file mode 100644 index 0000000..4094f73 --- /dev/null +++ b/pico_w/tls_client/picow_tls_client.c @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2023 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include +#include + +#include "pico/stdlib.h" +#include "pico/cyw43_arch.h" +#include "lwip/pbuf.h" +#include "lwip/altcp_tcp.h" +#include "lwip/altcp_tls.h" +#include "lwip/dns.h" + +#define TLS_CLIENT_SERVER "worldtimeapi.org" +#define TLS_CLIENT_HTTP_REQUEST "GET /api/ip HTTP/1.1\r\n" \ + "Host: " TLS_CLIENT_SERVER "\r\n" \ + "Connection: close\r\n" \ + "\r\n" +#define TLS_CLIENT_TIMEOUT_SECS 15 + + +typedef struct TLS_CLIENT_T_ { + struct altcp_pcb *pcb; + bool complete; +} TLS_CLIENT_T; + +static struct altcp_tls_config *tls_config = NULL; + +static err_t tls_client_close(void *arg) { + TLS_CLIENT_T *state = (TLS_CLIENT_T*)arg; + err_t err = ERR_OK; + + state->complete = true; + if (state->pcb != NULL) { + altcp_arg(state->pcb, NULL); + altcp_poll(state->pcb, NULL, 0); + altcp_recv(state->pcb, NULL); + altcp_err(state->pcb, NULL); + err = altcp_close(state->pcb); + if (err != ERR_OK) { + printf("close failed %d, calling abort\n", err); + altcp_abort(state->pcb); + err = ERR_ABRT; + } + state->pcb = NULL; + } + return err; +} + +static err_t tls_client_connected(void *arg, struct altcp_pcb *pcb, err_t err) { + TLS_CLIENT_T *state = (TLS_CLIENT_T*)arg; + if (err != ERR_OK) { + printf("connect failed %d\n", err); + return tls_client_close(state); + } + + printf("connected to server, sending request\n"); + err = altcp_write(state->pcb, TLS_CLIENT_HTTP_REQUEST, strlen(TLS_CLIENT_HTTP_REQUEST), TCP_WRITE_FLAG_COPY); + if (err != ERR_OK) { + printf("error writing data, err=%d", err); + return tls_client_close(state); + } + + return ERR_OK; +} + +static err_t tls_client_poll(void *arg, struct altcp_pcb *pcb) { + printf("timed out"); + return tls_client_close(arg); +} + +static void tls_client_err(void *arg, err_t err) { + TLS_CLIENT_T *state = (TLS_CLIENT_T*)arg; + printf("tls_client_err %d\n", err); + state->pcb = NULL; /* pcb freed by lwip when _err function is called */ +} + +static err_t tls_client_recv(void *arg, struct altcp_pcb *pcb, struct pbuf *p, err_t err) { + TLS_CLIENT_T *state = (TLS_CLIENT_T*)arg; + if (!p) { + printf("connection closed\n"); + return tls_client_close(state); + } + + if (p->tot_len > 0) { + /* For simplicity this examples creates a buffer on stack the size of the data pending here, + and copies all the data to it in one go. + Do be aware that the amount of data can potentially be a bit large (TLS record size can be 16 KB), + so you may want to use a smaller fixed size buffer and copy the data to it using a loop, if memory is a concern */ + char buf[p->tot_len + 1]; + + pbuf_copy_partial(p, buf, p->tot_len, 0); + buf[p->tot_len] = 0; + + printf("***\nnew data received from server:\n***\n\n%s\n", buf); + + altcp_recved(pcb, p->tot_len); + } + pbuf_free(p); + + return ERR_OK; +} + +static void tls_client_connect_to_server_ip(const ip_addr_t *ipaddr, TLS_CLIENT_T *state) +{ + err_t err; + u16_t port = 443; + + printf("connecting to server IP %s port %d\n", ipaddr_ntoa(ipaddr), port); + err = altcp_connect(state->pcb, ipaddr, port, tls_client_connected); + if (err != ERR_OK) + { + fprintf(stderr, "error initiating connect, err=%d\n", err); + tls_client_close(state); + } +} + +static void tls_client_dns_found(const char* hostname, const ip_addr_t *ipaddr, void *arg) +{ + if (ipaddr) + { + printf("DNS resolving complete\n"); + tls_client_connect_to_server_ip(ipaddr, (TLS_CLIENT_T *) arg); + } + else + { + printf("error resolving hostname %s\n", hostname); + tls_client_close(arg); + } +} + + +static bool tls_client_open(const char *hostname, void *arg) { + err_t err; + ip_addr_t server_ip; + TLS_CLIENT_T *state = (TLS_CLIENT_T*)arg; + + state->pcb = altcp_tls_new(tls_config, IPADDR_TYPE_ANY); + if (!state->pcb) { + printf("failed to create pcb\n"); + return false; + } + + altcp_arg(state->pcb, state); + altcp_poll(state->pcb, tls_client_poll, TLS_CLIENT_TIMEOUT_SECS * 2); + altcp_recv(state->pcb, tls_client_recv); + altcp_err(state->pcb, tls_client_err); + + /* Set SNI */ + mbedtls_ssl_set_hostname(altcp_tls_context(state->pcb), hostname); + + printf("resolving %s\n", hostname); + + // cyw43_arch_lwip_begin/end should be used around calls into lwIP to ensure correct locking. + // You can omit them if you are in a callback from lwIP. Note that when using pico_cyw_arch_poll + // these calls are a no-op and can be omitted, but it is a good practice to use them in + // case you switch the cyw43_arch type later. + cyw43_arch_lwip_begin(); + + err = dns_gethostbyname(hostname, &server_ip, tls_client_dns_found, state); + if (err == ERR_OK) + { + /* host is in DNS cache */ + tls_client_connect_to_server_ip(&server_ip, state); + } + else if (err != ERR_INPROGRESS) + { + printf("error initiating DNS resolving, err=%d\n", err); + tls_client_close(state->pcb); + } + + cyw43_arch_lwip_end(); + + return err == ERR_OK || err == ERR_INPROGRESS; +} + +// Perform initialisation +static TLS_CLIENT_T* tls_client_init(void) { + TLS_CLIENT_T *state = calloc(1, sizeof(TLS_CLIENT_T)); + if (!state) { + printf("failed to allocate state\n"); + return NULL; + } + + return state; +} + +void run_tls_client_test(void) { + /* No CA certificate checking */ + tls_config = altcp_tls_create_config_client(NULL, 0); + + TLS_CLIENT_T *state = tls_client_init(); + if (!state) { + return; + } + if (!tls_client_open(TLS_CLIENT_SERVER, state)) { + return; + } + while(!state->complete) { + // the following #ifdef is only here so this same example can be used in multiple modes; + // you do not need it in your code +#if PICO_CYW43_ARCH_POLL + // if you are using pico_cyw43_arch_poll, then you must poll periodically from your + // main loop (not from a timer) to check for WiFi driver or lwIP work that needs to be done. + cyw43_arch_poll(); + sleep_ms(1); +#else + // if you are not using pico_cyw43_arch_poll, then WiFI driver and lwIP work + // is done via interrupt in the background. This sleep is just an example of some (blocking) + // work you might be doing. + sleep_ms(1000); +#endif + } + free(state); + altcp_tls_free_config(tls_config); +} + +int main() { + stdio_init_all(); + + if (cyw43_arch_init()) { + printf("failed to initialise\n"); + return 1; + } + cyw43_arch_enable_sta_mode(); + + if (cyw43_arch_wifi_connect_timeout_ms(WIFI_SSID, WIFI_PASSWORD, CYW43_AUTH_WPA2_AES_PSK, 30000)) { + printf("failed to connect\n"); + return 1; + } + run_tls_client_test(); + + /* sleep a bit to let usb stdio write out any buffer to host */ + sleep_ms(100); + + cyw43_arch_deinit(); + return 0; +} +