Compiling application for Electron with raw DTLS (mbedtls) support (i.e. no Particle Cloud) via local build

Hi all,
I am trying to setup an app that will connect to a custom server via DTLS (no Particle Cloud connection).
It seems Particle uses the mbedtls library internally for its connection to the cloud. Ideally, I would like to leverage this library as well for my own purpose.
For now I have achieved getting a sample dtls_client to compile succesfully by copying the dtls_client.c code provided with mbedtls directly in my application file.
While the code compiles, I am getting undefined references for all mbedtls symbols. This suggest the app does not link against the mbedtls libraries by default. So the question is, how to I get my app to link against said library? Should I make a custom makefile for my app and link against the built libs prepared by compiling the system firmware?
The code looks like this:

#include "application.h"

#if !defined(MBEDTLS_CONFIG_FILE)
#include "mbedtls/config.h"
#else
#include MBEDTLS_CONFIG_FILE
#endif

#if defined(MBEDTLS_PLATFORM_C)
#include "mbedtls/platform.h"
#else
#include <stdio.h>
#define mbedtls_printf     Serial.printf
#define mbedtls_fprintf    fprintf
#endif

#include <string.h>

#include "mbedtls/net.h"
#include "mbedtls/debug.h"
#include "mbedtls/ssl.h"
#include "mbedtls/entropy.h"
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/error.h"
#include "mbedtls/certs.h"
#include "mbedtls/timing.h"

#define SERVER_PORT "4433"
#define SERVER_NAME "localhost"
#define SERVER_ADDR "127.0.0.1" /* forces IPv4 */
#define MESSAGE     "Echo this"

#define READ_TIMEOUT_MS 1000
#define MAX_RETRY       5

#define DEBUG_LEVEL 0

SYSTEM_MODE(MANUAL);

void dtls_client();

void setup()
{
    Serial.begin(9600);
    Cellular.on();
    Cellular.connect();
    Serial.println("Waiting to be connected to the cellular network:");
}

bool cellularConnected = false;

void loop()
{
    // Check connection to the nework.
    if(!cellularConnected)
    {
        if(!Cellular.ready())
        {
            Serial.print(".");
            return;
        }
        else
        {
            //Serial.println("Connected to cellular network with ip: %s", Cellular.localIP());
            Serial.println("Connected to cellular network.");
            cellularConnected = true;
        }
    }
    
    // Do a connection test with the server every 30 sec.
    delay(10000);
    dtls_client();
    delay(50000);
}



static void my_debug( void *ctx, int level,
                      const char *file, int line,
                      const char *str )
{
    ((void) level);

    //mbedtls_fprintf( (FILE *) ctx, "%s:%04d: %s", file, line, str );
    //fflush(  (FILE *) ctx  );
}

const char mbedtls_test_cas_pem[] = ""; //= TEST_CA_CRT_RSA TEST_CA_CRT_EC;
const size_t mbedtls_test_cas_pem_len = 0; //= sizeof( mbedtls_test_cas_pem );

void dtls_client()
{
    int ret, len;
    mbedtls_net_context server_fd;
    uint32_t flags;
    unsigned char buf[1024];
    const char *pers = "dtls_client";
    int retry_left = MAX_RETRY;

    mbedtls_entropy_context entropy;
    mbedtls_ctr_drbg_context ctr_drbg;
    mbedtls_ssl_context ssl;
    mbedtls_ssl_config conf;
    mbedtls_x509_crt cacert;
    mbedtls_timing_delay_context timer;

#if defined(MBEDTLS_DEBUG_C)
    mbedtls_debug_set_threshold( DEBUG_LEVEL );
#endif

    /*
     * 0. Initialize the RNG and the session data
     */
    mbedtls_net_init( &server_fd );
    mbedtls_ssl_init( &ssl );
    mbedtls_ssl_config_init( &conf );
    mbedtls_x509_crt_init( &cacert );
    mbedtls_ctr_drbg_init( &ctr_drbg );

    mbedtls_printf( "\n  . Seeding the random number generator..." );

    mbedtls_entropy_init( &entropy );
    if( ( ret = mbedtls_ctr_drbg_seed( &ctr_drbg, mbedtls_entropy_func, &entropy,
                               (const unsigned char *) pers,
                               strlen( pers ) ) ) != 0 )
    {
        mbedtls_printf( " failed\n  ! mbedtls_ctr_drbg_seed returned %d\n", ret );
        goto exit;
    }

    mbedtls_printf( " ok\n" );

    /*
     * 0. Load certificates
     */
    mbedtls_printf( "  . Loading the CA root certificate ..." );
    //fflush( stdout );

    ret = mbedtls_x509_crt_parse( &cacert, (const unsigned char *) mbedtls_test_cas_pem,
                          mbedtls_test_cas_pem_len );
    if( ret < 0 )
    {
        mbedtls_printf( " failed\n  !  mbedtls_x509_crt_parse returned -0x%x\n\n", -ret );
        goto exit;
    }

    mbedtls_printf( " ok (%d skipped)\n", ret );

    /*
     * 1. Start the connection
     */
    mbedtls_printf( "  . Connecting to udp/%s/%s...", SERVER_NAME, SERVER_PORT );
    
    if( ( ret = mbedtls_net_connect( &server_fd, SERVER_ADDR,
                                         SERVER_PORT, MBEDTLS_NET_PROTO_UDP ) ) != 0 )
    {
        mbedtls_printf( " failed\n  ! mbedtls_net_connect returned %d\n\n", ret );
        goto exit;
    }

    mbedtls_printf( " ok\n" );

    /*
     * 2. Setup stuff
     */
    mbedtls_printf( "  . Setting up the DTLS structure..." );

    if( ( ret = mbedtls_ssl_config_defaults( &conf,
                   MBEDTLS_SSL_IS_CLIENT,
                   MBEDTLS_SSL_TRANSPORT_DATAGRAM,
                   MBEDTLS_SSL_PRESET_DEFAULT ) ) != 0 )
    {
        mbedtls_printf( " failed\n  ! mbedtls_ssl_config_defaults returned %d\n\n", ret );
        goto exit;
    }

    /* OPTIONAL is usually a bad choice for security, but makes interop easier
     * in this simplified example, in which the ca chain is hardcoded.
     * Production code should set a proper ca chain and use REQUIRED. */
    mbedtls_ssl_conf_authmode( &conf, MBEDTLS_SSL_VERIFY_OPTIONAL );
    mbedtls_ssl_conf_ca_chain( &conf, &cacert, NULL );
    mbedtls_ssl_conf_rng( &conf, mbedtls_ctr_drbg_random, &ctr_drbg );
    mbedtls_ssl_conf_dbg( &conf, my_debug, stdout );

    if( ( ret = mbedtls_ssl_setup( &ssl, &conf ) ) != 0 )
    {
        mbedtls_printf( " failed\n  ! mbedtls_ssl_setup returned %d\n\n", ret );
        goto exit;
    }

    if( ( ret = mbedtls_ssl_set_hostname( &ssl, SERVER_NAME ) ) != 0 )
    {
        mbedtls_printf( " failed\n  ! mbedtls_ssl_set_hostname returned %d\n\n", ret );
        goto exit;
    }

    mbedtls_ssl_set_bio( &ssl, &server_fd,
                         mbedtls_net_send, mbedtls_net_recv, mbedtls_net_recv_timeout );

    mbedtls_ssl_set_timer_cb( &ssl, &timer, mbedtls_timing_set_delay,
                                            mbedtls_timing_get_delay );

    mbedtls_printf( " ok\n" );

    /*
     * 4. Handshake
     */
    mbedtls_printf( "  . Performing the SSL/TLS handshake..." );

    do ret = mbedtls_ssl_handshake( &ssl );
    while( ret == MBEDTLS_ERR_SSL_WANT_READ ||
           ret == MBEDTLS_ERR_SSL_WANT_WRITE );

    if( ret != 0 )
    {
        mbedtls_printf( " failed\n  ! mbedtls_ssl_handshake returned -0x%x\n\n", -ret );
        goto exit;
    }

    mbedtls_printf( " ok\n" );

    /*
     * 5. Verify the server certificate
     */
    mbedtls_printf( "  . Verifying peer X.509 certificate..." );

    /* In real life, we would have used MBEDTLS_SSL_VERIFY_REQUIRED so that the
     * handshake would not succeed if the peer's cert is bad.  Even if we used
     * MBEDTLS_SSL_VERIFY_OPTIONAL, we would bail out here if ret != 0 */
    if( ( flags = mbedtls_ssl_get_verify_result( &ssl ) ) != 0 )
    {
        char vrfy_buf[512];

        mbedtls_printf( " failed\n" );

        mbedtls_x509_crt_verify_info( vrfy_buf, sizeof( vrfy_buf ), "  ! ", flags );

        mbedtls_printf( "%s\n", vrfy_buf );
    }
    else
        mbedtls_printf( " ok\n" );

    /*
     * 6. Write the echo request
     */
send_request:
    mbedtls_printf( "  > Write to server:" );

    len = sizeof( MESSAGE ) - 1;

    do ret = mbedtls_ssl_write( &ssl, (unsigned char *) MESSAGE, len );
    while( ret == MBEDTLS_ERR_SSL_WANT_READ ||
           ret == MBEDTLS_ERR_SSL_WANT_WRITE );

    if( ret < 0 )
    {
        mbedtls_printf( " failed\n  ! mbedtls_ssl_write returned %d\n\n", ret );
        goto exit;
    }

    len = ret;
    mbedtls_printf( " %d bytes written\n\n%s\n\n", len, MESSAGE );

    /*
     * 7. Read the echo response
     */
    mbedtls_printf( "  < Read from server:" );

    len = sizeof( buf ) - 1;
    memset( buf, 0, sizeof( buf ) );

    do ret = mbedtls_ssl_read( &ssl, buf, len );
    while( ret == MBEDTLS_ERR_SSL_WANT_READ ||
           ret == MBEDTLS_ERR_SSL_WANT_WRITE );

    if( ret <= 0 )
    {
        switch( ret )
        {
            case MBEDTLS_ERR_SSL_TIMEOUT:
                mbedtls_printf( " timeout\n\n" );
                if( retry_left-- > 0 )
                    goto send_request;
                goto exit;

            case MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY:
                mbedtls_printf( " connection was closed gracefully\n" );
                ret = 0;
                goto close_notify;

            default:
                mbedtls_printf( " mbedtls_ssl_read returned -0x%x\n\n", -ret );
                goto exit;
        }
    }

    len = ret;
    mbedtls_printf( " %d bytes read\n\n%s\n\n", len, buf );

    /*
     * 8. Done, cleanly close the connection
     */
close_notify:
    mbedtls_printf( "  . Closing the connection..." );

    /* No error checking, the connection might be closed already */
    do ret = mbedtls_ssl_close_notify( &ssl );
    while( ret == MBEDTLS_ERR_SSL_WANT_WRITE );
    ret = 0;

    mbedtls_printf( " done\n" );

    /*
     * 9. Final clean-ups and exit
     */
exit:

#ifdef MBEDTLS_ERROR_C
    if( ret != 0 )
    {
        char error_buf[100];
        mbedtls_strerror( ret, error_buf, 100 );
        mbedtls_printf( "Last error was: %d - %s\n\n", ret, error_buf );
    }
#endif

    mbedtls_net_free( &server_fd );

    mbedtls_x509_crt_free( &cacert );
    mbedtls_ssl_free( &ssl );
    mbedtls_ssl_config_free( &conf );
    mbedtls_ctr_drbg_free( &ctr_drbg );
    mbedtls_entropy_free( &entropy );

#if defined(_WIN32)
    mbedtls_printf( "  + Press Enter to exit this program.\n" );
#endif

    /* Shell can not handle large exit numbers -> 1 for errors */
    if( ret < 0 )
        ret = 1;
}

So far, I have seen that mbedtls is indeed compiled with the system firmware, however, since the user firmware is dynamically linked against it, only “exposed” functions are available to the user code.
I have read in some posts that the system firmware can be compiled with MODULAR=n. I have however failed to find any related documentation and wonder what are all the implications of going down this road.
I guess I could also expose the mbedtls methods via new dynalibs?
Since I am fairly new with the Particle ecosystem, I would really like to get some advice on how to go about this.
Thanks.

Why not add the mbedtls sources to your app? Using a dynamic version of mbedtls could raise versioning issues - I’m not sure if mbedtls interfaces are ABI compatible between releases, so moving all the code to your app will side-step these concerns entirely.

Yes I am pretty sure it would work, however, I can see two potential problems with this approach. First, the Particle firmware passes the the -DMBEDTLS_CONFIG_FILE to gcc to provide a custom mbetls config header with specifically tweaked options. I can’t think of a clean (easy to maintain) way to prevent the Particle config header file to be used when compiling my version of mbedtls (unless there is a way to override the makefile CFLAGS from the user app mk files - the documentation does not indicate this is possible). So it looks like I would either have to modify the Particle makefile or edit the mbedtls code to force load my config header.
Second duplicating code just looks like wasting space on the MCU (which is probably a minor concern).

For now, I have resorted to compiling with MODULAR=n which allows me to reuse part of the existing code but this is not ideal either since Particle uses a tweaked/modified version of mbedtls and because I have to live with the options set in the Particle provided mbedtls config header file.

The mbedtls config file is deep within the communication library. This is not used when you compile your own application, so you can safely include all the mbedtls sources in your application code and your own config file.

Sure, duplicating the code does feel like wasted space (around 50k) but since you’re using a different config file, you’re in fact creating a distinct and different version that’s not duplicate at all (and hence no real wasted space.)

In your shoes, I would go with putting the code in the application. It’s the simplest way forward.

I just gave it a shot and it appears my mbedtls files are indeed compiled with MBEDTLS_CONFIG_FILE defined. To confirm this I added a pragma message within one of the newly added files:

applications/ElectronTest2/library/ssl_tls.c:40:9: note: #pragma message: MBEDTLS_CONFIG_FILE=<mbedtls_config.h>
 #pragma message ("MBEDTLS_CONFIG_FILE=" STRINGIFY(MBEDTLS_CONFIG_FILE))

So it seems I am still inheriting from these CFLAGS :frowning:

The config file is simply a relative include - you can add mbedtls_config.h to your application folder and that will be included first. (Add an #error to that file to trigger an error to be sure.)

Unfortunately I tried this already. I tried adding an mbedtls_config.h at various places (root of my application folder, root of the applications folder, and the same directory as the C file being compiled) to no avail. This seems to indicate the communication/lib/mbedtls/include folder is listed before any of the local folders as an include directory (-I flag) when gcc is invoked. This is actually confirmed in the command line used to compile the code:

arm-none-eabi-gcc -DSTM32_DEVICE -DSTM32F2XX -DPLATFORM_THREADING=1 -DPLATFORM_ID=10 -DPLATFORM_NAME=electron -DUSBD_VID_SPARK=0x2B04 -DUSBD_PID_DFU=0xD00A -DUSBD_PID_CDC=0xC00A -DSPARK_PLATFORM -g3 -gdwarf-2 -Os -mcpu=cortex-m3 -mthumb -DINCLUDE_PLATFORM=1 -DPRODUCT_ID=10 -DPRODUCT_FIRMWARE_VERSION=65535 -DMBEDTLS_CONFIG_FILE="<mbedtls_config.h>" -DUSE_STDPERIPH_DRIVER -DDFU_BUILD_ENABLE -DSYSTEM_VERSION_STRING=0.4.8-rc.6 -DRELEASE_BUILD -I./inc -I../wiring/inc -I../system/inc -I../services/inc -I../communication/lib/mbedtls/include -I../communication/lib/tropicssl/include -I../communication/src -I../hal/inc -I../hal/shared -I/rtos/FreeRTOSv8.2.2/FreeRTOS/Source/include -I/rtos/FreeRTOSv8.2.2/FreeRTOS/Source/portable/GCC/ARM_CM3 -I../hal/src/electron -I../hal/src/stm32f2xx -I../hal/src/stm32 -I../platform/shared/inc -I../platform/MCU/STM32F2xx/CMSIS/Include -I../platform/MCU/STM32F2xx/CMSIS/Device/ST/Include -I../platform/MCU/STM32F2xx/SPARK_Firmware_Driver/inc -I../platform/MCU/shared/STM32/inc -I../platform/MCU/STM32F2xx/STM32_StdPeriph_Driver/inc -I../platform/MCU/STM32F2xx/STM32_USB_Device_Driver/inc -I../platform/MCU/STM32F2xx/STM32_USB_Host_Driver/inc -I../platform/MCU/STM32F2xx/STM32_USB_OTG_Driver/inc -I../dynalib/inc -I./applications/ElectronTest2 -I./libraries -I. -MD -MP -MF ../build/target/user/platform-10-m/applications/ElectronTest2/applications/ElectronTest2/library/ssl_tls.o.d -ffunction-sections -fdata-sections -Wall -Wno-switch -Wno-error=deprecated-declarations -fmessage-length=0 -fno-strict-aliasing -DSPARK=1 -DPARTICLE=1 -DSTART_DFU_FLASHER_SERIAL_SPEED=14400 -DSTART_YMODEM_FLASHER_SERIAL_SPEED=28800 -DSPARK_PLATFORM_NET=UBLOXSARA -fno-builtin-malloc -fno-builtin-free -fno-builtin-realloc  -DUSER_FIRMWARE_IMAGE_SIZE=0x20000 -DUSER_FIRMWARE_IMAGE_LOCATION=0x8080000 -DMODULAR_FIRMWARE=1 -DMODULE_VERSION=3 -DMODULE_FUNCTION=5 -DMODULE_INDEX=1 -DMODULE_DEPENDENCY=4,2,10  -Wno-pointer-sign -std=gnu99 -c -o ../build/target/user/platform-10-m/applications/ElectronTest2/applications/ElectronTest2/library/ssl_tls.o applications/ElectronTest2/library/ssl_tls.c

I just pushed a change to develop that moves the mbedtls config setting from public to private scope - it shouldn’t have been public to begin with since it’s an internal implementation detail for the communication module.

That should fix the problem you’re seeing.

Thanks, that will definitely help, however another change will be needed to avoid exposing -I../communication/lib/mbedtls/include externally as well.
Just did a quick test this morning and can now confirm MBEDTLS_CONFIG_FILE is not defined anymore. However, the Particle provided mbedtls header files are still used instead of my app’s. I would greatly appreciate if you could work your magic once again :wink:

There’s a PR here - https://github.com/spark/firmware/pull/940 that makes the mbedtls headers private.

Great! The PR works as advertised, I am now able to compile and link my user app fine. Now I just have to make my app work, which should be a breeze :smile:
Thanks a lot for everything!

1 Like