8.4.1.6. Advanced certificate support

Note

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

8.4.1.6.1. Introduction

This tutorial builds up on the previous one and adds:

  • Ability to load multiple certificates as the client certificate chain

  • Support for DANE; this will also add proper support for the Server Public Key LwM2M resource

Note

In this tutorial, the main.c file is identical to the one from the DTLS connection using certificates tutorial.

In fact, in the repository, it is a symbolic link to the file from that tutorial.

8.4.1.6.2. Support for multiple client certificates

Note

Loading multiple client certificates is only possible by setting the public_cert field in anjay_security_instance_t.

If you don’t intend to use that field there is no point in making this change.

Using multiple certificates in the client certificate chain is rarely used, but it may be necessary to properly initiate a connection to some LwM2M servers.

To support this case, the AVS_CRYPTO_DATA_SOURCE_ARRAY and AVS_CRYPTO_DATA_SOURCE_LIST cases need to be supported in the configure_client_cert() function, much like is the case for configure_trusted_certs().

static avs_error_t
configure_client_certs(SSL_CTX *ctx,
                       const avs_crypto_security_info_union_t *client_certs) {
    if (!client_certs) {
        return avs_errno(AVS_EINVAL);
    }
    switch (client_certs->source) {
    case AVS_CRYPTO_DATA_SOURCE_EMPTY:
        return AVS_OK;
    case AVS_CRYPTO_DATA_SOURCE_BUFFER: {
        const unsigned char *ptr =
                (const unsigned char *) client_certs->info.buffer.buffer;
        X509 *cert = d2i_X509(NULL, &ptr,
                              (long) client_certs->info.buffer.buffer_size);
        if (!cert) {
            return avs_errno(AVS_EPROTO);
        }

        int result;
        if (!SSL_CTX_get0_certificate(ctx)) {
            result = SSL_CTX_use_certificate(ctx, cert);
        } else {
            result = SSL_CTX_add1_chain_cert(ctx, cert);
        }
        X509_free(cert);
        if (result != 1) {
            return avs_errno(AVS_EPROTO);
        }
        return AVS_OK;
    }
    case AVS_CRYPTO_DATA_SOURCE_ARRAY: {
        avs_error_t err = AVS_OK;
        for (size_t i = 0;
             avs_is_ok(err) && i < client_certs->info.array.element_count;
             ++i) {
            err = configure_client_certs(
                    ctx, &client_certs->info.array.array_ptr[i]);
        }
        return err;
    }
    case AVS_CRYPTO_DATA_SOURCE_LIST: {
        avs_error_t err = AVS_OK;
        AVS_LIST(avs_crypto_security_info_union_t) entry;
        AVS_LIST_FOREACH(entry, client_certs->info.list.list_head) {
            if (avs_is_err((err = configure_client_certs(ctx, entry)))) {
                break;
            }
        }
        return AVS_OK;
    }
    default:
        return avs_errno(AVS_ENOTSUP);
    }
}

The function has been slightly refactored to take avs_crypto_security_info_union_t as an argument to make recursive calls easier. client_cert in the function and argument names has also been pluralized.

Aside from these trivial changes, the AVS_CRYPTO_DATA_SOURCE_ARRAY and AVS_CRYPTO_DATA_SOURCE_LIST have been implemented in essentially the same way as in configure_trusted_certs() and configure_cert_revocation_lists(), and the AVS_CRYPTO_DATA_SOURCE_BUFFER case has been updated so that SSL_CTX_add1_chain_cert() is used for the second and all subsequent certificate entries. This means that the first loaded certificate is always the actual client certificate, with any subsequent ones forming the rest of the certification path up towards the root CA certificate.

8.4.1.6.3. DANE support

Instead of the standard PKIX rules for certificate verification, from LwM2M 1.1 onwards, the server certificates are verified using a custom mechanism that operates on concepts almost identical to those used by DANE.

The LwM2M 1.0 semantics are mirrored by the default settings used in LwM2M 1.1, which is to use the “domain-issued certificate” mode.

For the above reasons, server certificate validation in Anjay is largely implemented in terms of DANE, which needs to be provided in the secure socket implementation.

Note

Standard PKIX certificate validation may also be used in conjunction with DANE, particularly when certificate usage mode is set to “CA constraint” or “service certificate constraint”, or when EST is used.

DANE is supported natively since OpenSSL 1.1, which makes it easy to implement for the purpose of this tutorial.

Important

DANE is not widely supported in other TLS backend libraries or hardware implementations.

Please look at the Minimum viable subset section if implementing proper DANE support is impossible or infeasible in your case.

8.4.1.6.3.1. Initialization

It is necessary to store some additional state for DANE support, so the tls_socket_impl_t structure is extended accordingly:

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;

    bool dane_enabled;
    char dane_tlsa_association_data_buf[4096];
    avs_net_socket_dane_tlsa_record_t dane_tlsa_array[4];
    size_t dane_tlsa_array_size;

    void *session_resumption_buffer;
    size_t session_resumption_buffer_size;

    char server_name_indication[256];
    unsigned int dtls_hs_timeout_min_us;
    unsigned int dtls_hs_timeout_max_us;
} tls_socket_impl_t;
  • The dane_enabled field will store the information about whether DANE shall be used for this connection.

  • dane_tlsa_array will hold the DANE TLSA entries to be used for the connection; maximum of 4 entries is supported in this implementation, while dane_tlsa_array_size shall be the number of entries actually populated.

  • dane_tlsa_association_data_buf will store the actual certificate data; dane_tlsa_array entries will contain pointers into this buffer.

Note

In actual LwM2M use, at most 1 DANE TLSA entry is ever used.

This tutorial provides an implementation that support multiple entries for the sake of completeness, but support for only a single entry is sufficient to cover all the cases used by Anjay.

In OpenSSL, DANE needs to be enabled both for SSL_CTX and SSL objects. Enabling it for the SSL_CTX object needs to be done in the configure_certs() function, in accordance to the dane field in avs_net_certificate_info_t:

static avs_error_t configure_certs(tls_socket_impl_t *sock,
                                   const avs_net_certificate_info_t *certs) {
    if (certs->server_cert_validation) {
        if (!certs->ignore_system_trust_store) {
            SSL_CTX_set_default_verify_paths(sock->ctx);
        }
        X509_STORE *store = SSL_CTX_get_cert_store(sock->ctx);
        avs_error_t err;
        if (avs_is_err((err = configure_trusted_certs(
                                store, &certs->trusted_certs.desc)))
                || avs_is_err((err = configure_cert_revocation_lists(
                                       store,
                                       &certs->cert_revocation_lists.desc)))) {
            return err;
        }
        SSL_CTX_set_verify(sock->ctx, SSL_VERIFY_PEER, NULL);
    } else {
        SSL_CTX_set_verify(sock->ctx, SSL_VERIFY_NONE, NULL);
    }
    sock->dane_enabled = certs->dane;
    if (sock->dane_enabled) {
        SSL_CTX_dane_enable(sock->ctx);
    }
    if (certs->client_cert.desc.source != AVS_CRYPTO_DATA_SOURCE_EMPTY) {
        avs_error_t err;
        if (avs_is_err((err = configure_client_certs(sock->ctx,
                                                     &certs->client_cert.desc)))
                || avs_is_err(err = configure_client_key(sock->ctx,
                                                         &certs->client_key))) {
            return err;
        }
    }

    return AVS_OK;
}

8.4.1.6.3.2. Populating the array

DANE TLSA entries are passed into the socket object through the set_opt operation with the AVS_NET_SOCKET_OPT_DANE_TLSA_ARRAY key.

In OpenSSL, this information can only be provided after specifying the hostname for the SSL object, which in our code only happens during the connect operation. For this reason, we need to store the DANE TLSA entries in our internal structures first.

static avs_error_t tls_set_opt(avs_net_socket_t *sock_,
                               avs_net_socket_opt_key_t option_key,
                               avs_net_socket_opt_value_t option_value) {
    tls_socket_impl_t *sock = (tls_socket_impl_t *) sock_;
    switch (option_key) {
    case AVS_NET_SOCKET_OPT_DANE_TLSA_ARRAY: {
        if (option_value.dane_tlsa_array.array_element_count
                > AVS_ARRAY_SIZE(sock->dane_tlsa_array)) {
            return avs_errno(AVS_EINVAL);
        }
        avs_net_socket_dane_tlsa_record_t
                copied_array[AVS_ARRAY_SIZE(sock->dane_tlsa_array)];
        char copied_association_data[sizeof(
                sock->dane_tlsa_association_data_buf)];
        size_t copied_association_data_offset = 0;
        memcpy(copied_array, option_value.dane_tlsa_array.array_ptr,
               option_value.dane_tlsa_array.array_element_count
                       * sizeof(avs_net_socket_dane_tlsa_record_t));
        for (size_t i = 0; i < option_value.dane_tlsa_array.array_element_count;
             ++i) {
            if (copied_association_data_offset
                            + option_value.dane_tlsa_array.array_ptr[i]
                                      .association_data_size
                    > sizeof(copied_association_data)) {
                return avs_errno(AVS_EINVAL);
            }
            memcpy(copied_association_data + copied_association_data_offset,
                   option_value.dane_tlsa_array.array_ptr[i].association_data,
                   option_value.dane_tlsa_array.array_ptr[i]
                           .association_data_size);
            copied_array[i].association_data =
                    sock->dane_tlsa_association_data_buf
                    + copied_association_data_offset;
            copied_association_data_offset +=
                    option_value.dane_tlsa_array.array_ptr[i]
                            .association_data_size;
        }
        memcpy(sock->dane_tlsa_association_data_buf, copied_association_data,
               sizeof(copied_association_data));
        memcpy(sock->dane_tlsa_array, copied_array, sizeof(copied_array));
        sock->dane_tlsa_array_size =
                option_value.dane_tlsa_array.array_element_count;
        return AVS_OK;
    }
    default:
        return avs_net_socket_set_opt(sock->backend_socket, option_key,
                                      option_value);
    }
}

The above code essentially makes a deep copy of the data in option_value.dane_tlsa_array. The buffers pointed to by the association_data fields within array entries are copied into the sock->dane_tlsa_association_data_buf field and pointers in the copied array updated to point into that buffer as well.

Important

Any pointers passed to the set_opt function with the AVS_NET_SOCKET_OPT_DANE_TLSA_ARRAY options shall be treated as data that will be invalidated after returning from the function.

This means that this data needs to be either immediately loaded into the (D)TLS context, or a deep copy otherwise made.

8.4.1.6.3.3. Configuring the connection

Now with all the necessary information, we can configure the SSL object during the connect operation.

All the DANE configuration essentially takes place of the SSL_set_tlsext_host_name() call:

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);
    if (sock->dane_enabled) {
        // NOTE: SSL_dane_enable() calls SSL_set_tlsext_host_name() internally
        SSL_dane_enable(sock->ssl, host);
        bool have_usable_tlsa_records = false;
        for (size_t i = 0; i < sock->dane_tlsa_array_size; ++i) {
            if (SSL_CTX_get_verify_mode(sock->ctx) == SSL_VERIFY_NONE
                    && (sock->dane_tlsa_array[i].certificate_usage
                                == AVS_NET_SOCKET_DANE_CA_CONSTRAINT
                        || sock->dane_tlsa_array[i].certificate_usage
                                   == AVS_NET_SOCKET_DANE_SERVICE_CERTIFICATE_CONSTRAINT)) {
                // PKIX-TA and PKIX-EE constraints are unusable for
                // opportunistic clients
                continue;
            }
            SSL_dane_tlsa_add(
                    sock->ssl,
                    (uint8_t) sock->dane_tlsa_array[i].certificate_usage,
                    (uint8_t) sock->dane_tlsa_array[i].selector,
                    (uint8_t) sock->dane_tlsa_array[i].matching_type,
                    (unsigned const char *) sock->dane_tlsa_array[i]
                            .association_data,
                    sock->dane_tlsa_array[i].association_data_size);
            have_usable_tlsa_records = true;
        }
        if (SSL_CTX_get_verify_mode(sock->ctx) == SSL_VERIFY_NONE
                && have_usable_tlsa_records) {
            SSL_set_verify(sock->ssl, SSL_VERIFY_PEER, NULL);
        }
    } else {
        SSL_set_tlsext_host_name(sock->ssl, host);
    }
    SSL_set1_host(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);
    DTLS_set_timer_cb(sock->ssl, dtls_timer_cb);

    if (sock->session_resumption_buffer) {
        const unsigned char *ptr =
                (const unsigned char *) sock->session_resumption_buffer;
        SSL_SESSION *session =
                d2i_SSL_SESSION(NULL, &ptr,
                                sock->session_resumption_buffer_size);
        if (session) {
            SSL_set_session(sock->ssl, session);
            SSL_SESSION_free(session);
        }
    }

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

Note that “opportunistic DANE” is mentioned and supported in the code above. This means that even if server certificate verification is not otherwise enabled, but DANE-TA or DANE-EE entries are present, the client shall verify the server certificate against these entries.

Note

Opportunistic DANE is not used by Anjay. An implementation is provided here for the sake of completeness, but it is not necessary for LwM2M communication.

If only LwM2M compliance is targeted, it is safe to remove the if (SSL_CTX_get_verify_mode(sock->ctx) == SSL_VERIFY_NONE && ...) clauses and the have_usable_tlsa_records variable from the code above altogether.

8.4.1.6.3.4. Minimum viable subset

Warning

The approach described in this section is not fully compliant with DANE nor any version of LwM2M. It is intended only for use if implementing more complete support is not possible.

Support for DANE is, unfortunately, very limited among (D)TLS implementations. In fact, in the default Mbed TLS integration in avs_commons, it has been implemented from scratch in a custom certificate verification callback, see the verify_cert_cb() function there. In many cases, this approach might still be infeasible or even impossible, especially if (D)TLS is handled in hardware.

It is possible to emulate the most common case using standard PKIX concepts, which will allow LwM2M 1.0 (and 1.1 with typical configuration) to work, at least with some servers.

Important

This implementation will only work with self-signed server certificates.

Note

Code modified for this variant can be found under examples/custom-tls/certificates-advanced-fake-dane in the Anjay source directory.

This minimum implementation reverts the changed described earlier in the DANE support section. Instead, the following changes are made:

  • The only information about DANE that needs to be kept in the socket state is whether or not it is enabled.

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;

    bool dane_enabled;

    void *session_resumption_buffer;
    size_t session_resumption_buffer_size;

    char server_name_indication[256];
    unsigned int dtls_hs_timeout_min_us;
    unsigned int dtls_hs_timeout_max_us;
} tls_socket_impl_t;
static avs_error_t configure_certs(tls_socket_impl_t *sock,
                                   const avs_net_certificate_info_t *certs) {
    if (certs->server_cert_validation) {
        if (!certs->ignore_system_trust_store) {
            SSL_CTX_set_default_verify_paths(sock->ctx);
        }
        X509_STORE *store = SSL_CTX_get_cert_store(sock->ctx);
        avs_error_t err;
        if (avs_is_err((err = configure_trusted_certs(
                                store, &certs->trusted_certs.desc)))
                || avs_is_err((err = configure_cert_revocation_lists(
                                       store,
                                       &certs->cert_revocation_lists.desc)))) {
            return err;
        }
        SSL_CTX_set_verify(sock->ctx, SSL_VERIFY_PEER, NULL);
    } else {
        SSL_CTX_set_verify(sock->ctx, SSL_VERIFY_NONE, NULL);
    }
    sock->dane_enabled = certs->dane;
    if (certs->client_cert.desc.source != AVS_CRYPTO_DATA_SOURCE_EMPTY) {
        avs_error_t err;
        if (avs_is_err((err = configure_client_certs(sock->ctx,
                                                     &certs->client_cert.desc)))
                || avs_is_err(err = configure_client_key(sock->ctx,
                                                         &certs->client_key))) {
            return err;
        }
    }

    return AVS_OK;
}
  • The tls_set_opt() function is updated to put the server certificate into the trust store.

static avs_error_t tls_set_opt(avs_net_socket_t *sock_,
                               avs_net_socket_opt_key_t option_key,
                               avs_net_socket_opt_value_t option_value) {
    tls_socket_impl_t *sock = (tls_socket_impl_t *) sock_;
    switch (option_key) {
    case AVS_NET_SOCKET_OPT_DANE_TLSA_ARRAY: {
        if (option_value.dane_tlsa_array.array_element_count > 1) {
            return avs_errno(AVS_EINVAL);
        }
        if (!sock->dane_enabled
                || option_value.dane_tlsa_array.array_element_count == 0
                || option_value.dane_tlsa_array.array_ptr[0].certificate_usage
                               == AVS_NET_SOCKET_DANE_CA_CONSTRAINT
                || option_value.dane_tlsa_array.array_ptr[0].certificate_usage
                               == AVS_NET_SOCKET_DANE_SERVICE_CERTIFICATE_CONSTRAINT) {
            return AVS_OK;
        }
        X509_STORE *store = SSL_CTX_get_cert_store(sock->ctx);
        if (option_value.dane_tlsa_array.array_ptr[0].selector
                        != AVS_NET_SOCKET_DANE_CERTIFICATE
                || option_value.dane_tlsa_array.array_ptr[0].matching_type
                               != AVS_NET_SOCKET_DANE_MATCH_FULL
                || sk_X509_OBJECT_num(X509_STORE_get0_objects(store)) > 0) {
            return avs_errno(AVS_ENOTSUP);
        }
        avs_crypto_certificate_chain_info_t chain =
                avs_crypto_certificate_chain_info_from_buffer(
                        option_value.dane_tlsa_array.array_ptr[0]
                                .association_data,
                        option_value.dane_tlsa_array.array_ptr[0]
                                .association_data_size);
        avs_error_t err = configure_trusted_certs(store, &chain.desc);
        if (avs_is_ok(err)) {
            SSL_CTX_set_verify(sock->ctx, SSL_VERIFY_PEER, NULL);
        }
        return err;
    }
    default:
        return avs_net_socket_set_opt(sock->backend_socket, option_key,
                                      option_value);
    }
}
  • Due to configure_trusted_certs() being called in the code above, that function’s declaration needs to be moved above tls_set_opt(), with no other changes.

In the code above, only the DANE-TA and DANE-EE mode (Certificate Usage modes 2 and 3) entries are taken into account, only full certificate matching is supported, and the trust store needs to be empty at the time of calling this function. If all those conditions are met, the passed certificate is just added to the store - configure_trusted_certs() is called for that purpose as a wrapper to the d2i_X509() and X509_STORE_add_cert() functions.

This is enough for the logic used by Anjay for LwM2M 1.0 (and 1.1 on default settings) to work. However, as mentioned above, only self-signed server certificates are supported. DANE-EE mode will not function properly, as certificate verification will fail due to inability to find the CA certificate.