8.4.1.2. Minimal DTLS implementation

Note

Code related to this tutorial can be found under examples/custom-tls/minimal in the Anjay source directory.

8.4.1.2.1. Introduction

This tutorial builds up on the previous one and adds logic related to actually initializing the SSL context state and performing the DTLS handshake.

Only the bare minimum functionality necessary to use DTLS in PSK mode is implemented for now - but this is enough to register to a LwM2M server in PSK mode.

8.4.1.2.2. Implementation of the DTLS socket

8.4.1.2.2.1. Initialization

avs_error_t _avs_net_create_dtls_socket(avs_net_socket_t **socket_ptr,
                                        const void *configuration_) {
    assert(socket_ptr);
    assert(!*socket_ptr);
    assert(configuration_);
    const avs_net_ssl_configuration_t *configuration =
            (const avs_net_ssl_configuration_t *) configuration_;
    tls_socket_impl_t *socket =
            (tls_socket_impl_t *) avs_calloc(1, sizeof(tls_socket_impl_t));
    if (!socket) {
        return avs_errno(AVS_ENOMEM);
    }
    *socket_ptr = (avs_net_socket_t *) socket;
    socket->operations = &TLS_SOCKET_VTABLE;

    avs_error_t err = AVS_OK;
    if (avs_is_ok((err = avs_net_udp_socket_create(
                           &socket->backend_socket,
                           &configuration->backend_configuration)))
            && !(socket->ctx = SSL_CTX_new(DTLS_method()))) {
        err = avs_errno(AVS_ENOMEM);
    }
    if (avs_is_ok(err)) {
        switch (configuration->security.mode) {
        case AVS_NET_SECURITY_PSK:
            err = configure_psk(socket, &configuration->security.data.psk);
            break;
        default:
            err = avs_errno(AVS_ENOTSUP);
        }
    }
    if (avs_is_err(err)) {
        avs_net_socket_cleanup(socket_ptr);
        return err;
    }
    SSL_CTX_set_mode(socket->ctx, SSL_MODE_AUTO_RETRY);
    return AVS_OK;
}

The flow of this function is as follows:

  • First, the socket object is allocated and the virtual method table is assigned. This is conceptually identical to the initialization of the unencrypted UDP socket.

  • Then, the underlying UDP socket and the SSL_CTX object are created.

  • Initialization related to the security credentials is delegated to a separate function that will be described next. We only support the PSK mode for now, so we check that it is indeed selected, and call configure_psk().

  • Finally, the auto-retry mode is enabled in OpenSSL. This is the preferred mode that simplifies the implementation when the non-blocking mode is not used. See SSL_CTX_set_mode() for details.

8.4.1.2.2.1.1. The avs_crypto_security_info_union_t type

Loading of security credentials in avs_net and avs_crypto is centered around the avs_crypto_security_info_union_t type, declared as follows:

typedef enum {
    AVS_CRYPTO_SECURITY_INFO_CERTIFICATE_CHAIN,
    AVS_CRYPTO_SECURITY_INFO_PRIVATE_KEY,
    AVS_CRYPTO_SECURITY_INFO_CERT_REVOCATION_LIST,
    AVS_CRYPTO_SECURITY_INFO_PSK_IDENTITY,
    AVS_CRYPTO_SECURITY_INFO_PSK_KEY
} avs_crypto_security_info_tag_t;

typedef enum {
    AVS_CRYPTO_DATA_SOURCE_EMPTY,
    AVS_CRYPTO_DATA_SOURCE_FILE,
    AVS_CRYPTO_DATA_SOURCE_PATH,
    AVS_CRYPTO_DATA_SOURCE_BUFFER,
    AVS_CRYPTO_DATA_SOURCE_ARRAY,
    AVS_CRYPTO_DATA_SOURCE_LIST,
#if defined(AVS_COMMONS_WITH_AVS_CRYPTO_PKI_ENGINE) \
        || defined(AVS_COMMONS_WITH_AVS_CRYPTO_PSK_ENGINE)
    AVS_CRYPTO_DATA_SOURCE_ENGINE
#endif /* defined(AVS_COMMONS_WITH_AVS_CRYPTO_PKI_ENGINE) || \
        defined(AVS_COMMONS_WITH_AVS_CRYPTO_PSK_ENGINE) */
    } avs_crypto_data_source_t;

/**
 * This struct is for internal use only and should not be filled manually. One
 * should construct appropriate instances of:
 * - @ref avs_crypto_certificate_chain_info_t,
 * - @ref avs_crypto_private_key_info_t
 * - @ref avs_crypto_cert_revocation_list_info_t
 * - @ref avs_crypto_psk_identity_info_t
 * - @ref avs_crypto_psk_key_info_t
 * using methods declared in @c avs_crypto_pki.h and @c avs_crypto_psk.h.
 */
struct avs_crypto_security_info_union_struct {
    avs_crypto_security_info_tag_t type;
    avs_crypto_data_source_t source;
    union {
        avs_crypto_security_info_union_internal_file_t file;
        avs_crypto_security_info_union_internal_path_t path;
        avs_crypto_security_info_union_internal_buffer_t buffer;
        avs_crypto_security_info_union_internal_array_t array;
        avs_crypto_security_info_union_internal_list_t list;
#if defined(AVS_COMMONS_WITH_AVS_CRYPTO_PKI_ENGINE) \
        || defined(AVS_COMMONS_WITH_AVS_CRYPTO_PSK_ENGINE)
        avs_crypto_security_info_union_internal_engine_t engine;
#endif /* defined(AVS_COMMONS_WITH_AVS_CRYPTO_PKI_ENGINE) || \
        defined(AVS_COMMONS_WITH_AVS_CRYPTO_PSK_ENGINE) */
        } info;
    };

The source fields acts as a tag to the info union, deciding from which source the credential shall be loaded. There are a number of “simple” sources supported:

  • AVS_CRYPTO_DATA_SOURCE_EMPTY - signifies that the object does not represent any valid credential information

  • AVS_CRYPTO_DATA_SOURCE_FILE - the credential shall be loaded from a file, specified as a file path (info.file.filename); in case of private keys, an optional password for encrypted PEM keys can be specified (info.file.password)

  • AVS_CRYPTO_DATA_SOURCE_PATH - the credentials shall be loaded from a directory, specified as a file system path (info.path.path); this generally only makes sense for certificate chains

  • AVS_CRYPTO_DATA_SOURCE_BUFFER - the credentials shall be loaded from a memory buffer (info.buffer.buffer of the size info.buffer.buffer_size); in case of private keys, an optional password for encrypted PEM keys can be specified (info.buffer.password); this is the case that is almost exclusively used in Anjay

  • AVS_CRYPTO_DATA_SOURCE_ENGINE - the object refers to a credential stored in a hardware cryptography source, such as a secure element; information on the credential is stored as a “query string” at info.engine.query; the format of the query string is platform-specific and may be arbitrary; this case is supported in the HSM Feature

In addition to the “simple” sources listed above, two additional “compound” sources are supported:

  • AVS_CRYPTO_DATA_SOURCE_ARRAY - the object specifies multiple credentials, stored as an array of other avs_crypto_security_info_union_t objects - info.array.element_count structures stored at info.array.array_ptr

  • AVS_CRYPTO_DATA_SOURCE_LIST - the object specifies multiple credentials, stored as an AVS_LIST whose first element is info.list.list_head; the AVS_LIST macro is not explicitly used in the declaration of the list_head field for dependency management reasons, but that field shall still be treated as such

Note

“Compound” credential sources are most commonly used for trust store information, i.e. trusted certificates and certificate revocation lists.

“Compound” credential sources are not used for private keys, PSK keys or PSK identities.

“Compound” credential sources MAY be used for client certificates, to signify additional CA certificates that shall be sent to the server during handshake.

“Compound” credential sources, in general, MAY contain other “compound” credential sources, forming a tree-like structure. Those SHOULD be loaded recursively. However, the credentials provided by Anjay are expected to not be formed in this way.

Important

Anjay uses both AVS_CRYPTO_DATA_SOURCE_ARRAY and AVS_CRYPTO_DATA_SOURCE_LIST for different purposes, so support for both needs to be implemented.

The avs_crypto_security_info_union_t structure additionally contains the type field, which may be used for validating the credential type (i.e., whether the object represents a certificate chain, certificate revocation lists, or a private key.

In typical usage, the type is conveyed by composing the avs_crypto_security_info_union_t object into one of the wrapper objects:

typedef struct avs_crypto_certificate_chain_info_struct {
    avs_crypto_security_info_union_t desc;
} avs_crypto_certificate_chain_info_t;
typedef struct {
    avs_crypto_security_info_union_t desc;
} avs_crypto_cert_revocation_list_info_t;
typedef struct {
    avs_crypto_security_info_union_t desc;
} avs_crypto_private_key_info_t;
typedef struct {
    avs_crypto_security_info_union_t desc;
} avs_crypto_psk_identity_info_t;
typedef struct {
    avs_crypto_security_info_union_t desc;
} avs_crypto_psk_key_info_t;

We will only implement support for the AVS_CRYPTO_DATA_SOURCE_BUFFER mode for the PSK mode; in later tutorials, where configuration of the certificate mode is described, AVS_CRYPTO_DATA_SOURCE_ARRAY and AVS_CRYPTO_DATA_SOURCE_LIST will also be implemented for some cases.

8.4.1.2.2.1.2. Initialization of PSK credentials

In OpenSSL, credentials for the PSK mode are provided through a callback - a function is set using SSL_CTX_set_psk_client_callback() and it is called whenever the library needs the PSK credentials - this means that they need to be stored for later access on demand.

The credentials are passed within the avs_net_ssl_configuration_t stucture passed to _avs_net_create_dtls_socket() or _avs_net_create_ssl_socket(). However, the structure passed there shall be treated as ephemeral, so in case of the OpenSSL API, the credentials need to be copied into the socket state.

This means that the first thing is to add appropriate fields to the tls_socket_impl_t structure:

typedef struct {
    const avs_net_socket_v_table_t *operations;
    avs_net_socket_t *backend_socket;
    SSL_CTX *ctx;
    SSL *ssl;

    char psk[256];
    size_t psk_size;
    char identity[128];
    size_t identity_size;
} tls_socket_impl_t;

Note

Different TLS libraries have different data lifetime contracts. For example, in contrast to the OpenSSL API, mbedtls_ssl_conf_psk() in Mbed TLS copies the data passed as arguments into internal structures and thus it is not necessary to make explicit copies.

Please carefully check whether credentials are passed by value or by reference in the TLS backend you are integrating with.

We are now ready to implement the configure_psk() function, and the psk_client_cb() callback that will be passed to SSL_CTX_set_psk_client_callback(). As mentioned above, only the AVS_CRYPTO_DATA_SOURCE_BUFFER source is handled for both the key and identity.

static unsigned int psk_client_cb(SSL *ssl,
                                  const char *hint,
                                  char *identity,
                                  unsigned int max_identity_len,
                                  unsigned char *psk,
                                  unsigned int max_psk_len) {
    tls_socket_impl_t *sock = (tls_socket_impl_t *) SSL_get_app_data(ssl);

    (void) hint;

    if (!sock || max_psk_len < sock->psk_size
            || max_identity_len < sock->identity_size + 1) {
        return 0;
    }

    memcpy(psk, sock->psk, sock->psk_size);
    memcpy(identity, sock->identity, sock->identity_size);
    identity[sock->identity_size] = '\0';

    return (unsigned int) sock->psk_size;
}

static avs_error_t configure_psk(tls_socket_impl_t *sock,
                                 const avs_net_psk_info_t *psk) {
    if (psk->key.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER
            || psk->identity.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER) {
        return avs_errno(AVS_EINVAL);
    }

    const void *key_ptr = psk->key.desc.info.buffer.buffer;
    size_t key_size = psk->key.desc.info.buffer.buffer_size;

    const void *identity_ptr = psk->identity.desc.info.buffer.buffer;
    size_t identity_size = psk->identity.desc.info.buffer.buffer_size;

    if (key_size > sizeof(sock->psk)
            || identity_size > sizeof(sock->identity)) {
        return avs_errno(AVS_EINVAL);
    }
    memcpy(sock->psk, key_ptr, key_size);
    sock->psk_size = key_size;
    memcpy(sock->identity, identity_ptr, identity_size);
    sock->identity_size = identity_size;
    SSL_CTX_set_cipher_list(sock->ctx, "PSK");
    SSL_CTX_set_psk_client_callback(sock->ctx, psk_client_cb);
    SSL_CTX_set_verify(sock->ctx, SSL_VERIFY_PEER, NULL);
    return AVS_OK;
}

Note that OpenSSL does not automatically disable ciphersuites and functionality related to certificates when a PSK callback is provided. For this reason additional settings are changed:

  • SSL_CTX_set_cipher_list() is called to limit the set of allowed ciphersuites to only those that depend on the PSK mode.

  • SSL_CTX_set_verify() is also set to SSL_VERIFY_PEER so that a server that attempts to use certificate-based authentication shall be verified - this verification will invariably fail, as there are no trusted certificates configured for this connection.

Also note that the tls_socket_impl_t structure is accessed using SSL_get_app_data(). This will be set while Performing the handshake.

8.4.1.2.2.2. Cleanup

Knowing what is happening during initialization, we can now reverse this process in the cleanup function:

static avs_error_t tls_cleanup(avs_net_socket_t **sock_ptr) {
    avs_error_t err = AVS_OK;
    if (sock_ptr && *sock_ptr) {
        tls_socket_impl_t *sock = (tls_socket_impl_t *) *sock_ptr;
        tls_close(*sock_ptr);
        avs_net_socket_cleanup(&sock->backend_socket);
        if (sock->ctx) {
            SSL_CTX_free(sock->ctx);
        }
        avs_free(sock);
        *sock_ptr = NULL;
    }
    return err;
}

8.4.1.2.2.3. Performing the handshake

The perform_handshake() function is now relatively straightforward to implement:

  • The new SSL object is created using SSL_new()

  • The pointer to the socket structure is set as the application data so that it can be retrieved in psk_client_cb()

  • The hostname to which the socket is being connected is set to be used in the Server Name Identification TLS extension

  • A new datagram BIO object is created, configured and set for use by the SSL object

    • OpenSSL’s datagram BIO object uses sendto() instead of send() internally, so it needs to be explicitly informed of the address of the peer the socket is connected to. This is performed using BIO_ctrl(), with the raw server address queried using getpeername().

  • SSL_connect() is called to perform the actual client-side (D)TLS handshake

static avs_error_t perform_handshake(tls_socket_impl_t *sock,
                                     const char *host) {
    union {
        struct sockaddr addr;
        struct sockaddr_storage storage;
    } peername;
    const void *fd_ptr = avs_net_socket_get_system(sock->backend_socket);
    if (!fd_ptr
            || getpeername(*(const int *) fd_ptr, &peername.addr,
                           &(socklen_t) { sizeof(peername) })) {
        return avs_errno(AVS_EBADF);
    }

    sock->ssl = SSL_new(sock->ctx);
    if (!sock->ssl) {
        return avs_errno(AVS_ENOMEM);
    }

    SSL_set_app_data(sock->ssl, sock);
    SSL_set_tlsext_host_name(sock->ssl, host);

    BIO *bio = BIO_new_dgram(*(const int *) fd_ptr, 0);
    if (!bio) {
        return avs_errno(AVS_ENOMEM);
    }
    BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_CONNECTED, 0, &peername.addr);
    SSL_set_bio(sock->ssl, bio, bio);

    if (SSL_connect(sock->ssl) <= 0) {
        return avs_errno(AVS_EPROTO);
    }
    return AVS_OK;
}

static avs_error_t
tls_connect(avs_net_socket_t *sock_, const char *host, const char *port) {
    tls_socket_impl_t *sock = (tls_socket_impl_t *) sock_;
    if (sock->ssl) {
        return avs_errno(AVS_EBADF);
    }
    avs_error_t err;
    if (avs_is_err((
                err = avs_net_socket_connect(sock->backend_socket, host, port)))
            || avs_is_err((err = perform_handshake(sock, host)))) {
        if (sock->ssl) {
            SSL_free(sock->ssl);
            sock->ssl = NULL;
        }
        avs_net_socket_close(sock->backend_socket);
    }
    return err;
}

An additional check is also added in tls_connect() to avoid creating the SSL object multiple times.

8.4.1.2.2.4. Fixing the socket option values

The tls_get_opt() function has been previously implemented by simply forwarding the call to the underlying unencrypted socket.

This yields inaccurate results for the AVS_NET_SOCKET_OPT_INNER_MTU option. The underlying socket will return the maximum number of bytes available on the UDP layer, while we need to take the DTLS headers into account.

It is also desirable to overload the AVS_NET_SOCKET_HAS_BUFFERED_DATA. This option is designed to notify the Anjay library whether all data received from the underlying system socket has been processed. This is used to make sure that when control is returned to the event loop, the poll() call will not stall waiting for new data, while in reality it is already available, but stuck in the (D)TLS layer buffer.

In this example based on OpenSSL, this condition can be checked by calling the SSL_pending() function.

static avs_error_t tls_get_opt(avs_net_socket_t *sock_,
                               avs_net_socket_opt_key_t option_key,
                               avs_net_socket_opt_value_t *out_option_value) {
    tls_socket_impl_t *sock = (tls_socket_impl_t *) sock_;
    switch (option_key) {
    case AVS_NET_SOCKET_OPT_INNER_MTU: {
        avs_error_t err = avs_net_socket_get_opt(sock->backend_socket,
                                                 AVS_NET_SOCKET_OPT_INNER_MTU,
                                                 out_option_value);
        if (avs_is_ok(err)) {
            out_option_value->mtu = AVS_MAX(out_option_value->mtu - 64, 0);
        }
        return err;
    }
    case AVS_NET_SOCKET_HAS_BUFFERED_DATA:
        out_option_value->flag = (sock->ssl && SSL_pending(sock->ssl) > 0);
        return AVS_OK;
    default:
        return avs_net_socket_get_opt(sock->backend_socket, option_key,
                                      out_option_value);
    }
}

Note

In this simplistic implementation, the DTLS overhead has been hardcoded to 64 bytes, which is generally accepted as the upper limit for this value.

A more complete implementation could query or calculate the precise overhead for the current session, based on the specific ciphersuite in use.

8.4.1.2.3. Limitations

This minimal implementation is enough to communicate with an LwM2M server in PSK mode, but a number of functionalities will not work:

  • Session resumption is not implemented, which may cause otherwise unnecessary Register requests being sent after reconnecting. Note that a Register request also forces the server to reinitialize all the Observe requests, so this is very undesirable.

  • Certificate mode is not implemented.

  • TLS over TCP is not implemented, which means that e.g. HTTPS will not be supported.

  • DTLS Connection ID extension is not supported.

  • Various additional configuration options are not implemented as well, including:

    • Configurable TLS/DTLS version

    • Configurable DTLS handshake timers

    • Configurable ciphersuite list (note that in LwM2M they can be configured through the data model - this will be ignored by the current implementation)

    • Overriding the hostname used for Server Name Identification - useful for LwM2M 1.1 only

  • TLS alert codes are not forwarded to calling code, and LwM2M 1.1 exposes them through the data model.

  • Socket file descriptor is used directly instead of wrapping avs_net APIs, and the decorate function is not implemented - the secure SMS mode will thus not work in versions that include the SMS commercial feature.

We will expand this implementation to address these limitation in subsequent chapters.