8.4.1.5. Basic certificate support
Note
Code related to this tutorial can be found under examples/custom-tls/certificates-basic in the Anjay source directory.
8.4.1.5.1. Introduction
This tutorials builds up on the previous one and adds basic support for the security mode based on certificates and public key infrastructure.
Note
In this tutorial, the main.c
file has been replaced with a slightly
modified version of the one from the
DTLS connection using certificates tutorial.
This also means that for simplicity, the tutorial project now depends on
the WITH_EVENT_LOOP
CMake option enabled in Anjay, and the
Event loop support in the networking
layer.
Compared to the main.c
file from the
DTLS connection using certificates tutorial, logic related to
loading the server certificate has been removed. This means that the
security information is now configured as follows:
if (load_buffer_from_file(
(uint8_t **) &security_instance.public_cert_or_psk_identity,
&security_instance.public_cert_or_psk_identity_size,
"client_cert.der")
|| load_buffer_from_file(
(uint8_t **) &security_instance.private_cert_or_psk_key,
&security_instance.private_cert_or_psk_key_size,
"client_key.der")) {
result = -1;
goto cleanup;
}
Warning
Verifying the server certificate, as specified in LwM2M, does not work with the set of features implemented in this article. See the Limitations section and the next article for details.
8.4.1.5.2. Adding support for the certificate mode
In avs_net
, the security mode of the socket - either PSK or certificates -
is configured by the mode
field in the avs_net_security_mode_t
structure
(which is a tagged union, with the mode
field acting as the tag), which
itself is contained in the security
field of
avs_net_ssl_configuration_t
.
In the minimal implementation tutorial, we have
written a switch
over that field in _avs_net_create_dtls_socket()
, even
though only the PSK mode was supported there. It is now time to add the second
case for the certificate mode:
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)) {
err = configure_dtls_version(socket, configuration->version);
}
if (avs_is_ok(err)) {
switch (configuration->security.mode) {
case AVS_NET_SECURITY_PSK:
err = configure_psk(socket, &configuration->security.data.psk);
break;
case AVS_NET_SECURITY_CERTIFICATE:
err = configure_certs(socket, &configuration->security.data.cert);
break;
default:
err = avs_errno(AVS_ENOTSUP);
}
}
if (avs_is_err(err)
|| avs_is_err((
err = configure_dtls_handshake_timeouts(
socket, configuration->dtls_handshake_timeouts)))
|| avs_is_err((err = configure_ciphersuites(
socket, &configuration->ciphersuites)))
|| avs_is_err((err = configure_sni(
socket,
configuration->server_name_indication)))) {
avs_net_socket_cleanup(socket_ptr);
return err;
}
SSL_CTX_set_mode(socket->ctx, SSL_MODE_AUTO_RETRY);
if (configuration->session_resumption_buffer_size > 0) {
assert(configuration->session_resumption_buffer);
socket->session_resumption_buffer =
configuration->session_resumption_buffer;
socket->session_resumption_buffer_size =
configuration->session_resumption_buffer_size;
SSL_CTX_set_session_cache_mode(
socket->ctx,
SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL_STORE);
SSL_CTX_sess_set_new_cb(socket->ctx, new_session_cb);
}
return AVS_OK;
}
The configure_certs()
function mentioned in the snippet above is an analog
of configure_psk()
, that loads and configures all the necessary security
credentials:
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);
}
if (certs->client_cert.desc.source != AVS_CRYPTO_DATA_SOURCE_EMPTY) {
avs_error_t err;
if (avs_is_err((err = configure_client_cert(sock->ctx,
&certs->client_cert)))
|| avs_is_err(err = configure_client_key(sock->ctx,
&certs->client_key))) {
return err;
}
}
return AVS_OK;
}
The server_cert_validation
field acts as a master switch that controls
whether the peer certificate shall be verified at all. This controls the
verification mode set using SSL_CTX_set_verify()
, but also all logic related
to loading the trust store is disabled if it is set to false
.
The ignore_system_trust_store
flag controls whether the default system trust
store shall be loaded for this socket. In Anjay, it is usually set to true
.
It may only be false
, if the use_system_trust_store
is enabled in
anjay_configuration_t
. If your platform does not have a concept of a system
trust store, it is safe to ignore this setting altogether.
The rest of the code in this function calls auxiliary functions that load all the security credential types: trusted certificates, certificate revocation lists, the client certificate and the client private key.
8.4.1.5.3. Loading security credentials
The security credentials related to the public key infrastructure utilize the
avs_crypto_security_info_union_t
that has been previously described in
The avs_crypto_security_info_union_t type chapter. You might want to recap the
information contained there before continuing with this tutorial.
Important
The security credential objects passed to the
_avs_net_create_dtls_socket()
may be deleted after that call completes.
For this reason, the credential data needs to be actually copied.
Please carefully check whether credentials are passed by value or by reference in the TLS backend you are integrating with.
8.4.1.5.3.1. Loading client certificates
Client certificates is the simplest case, as we only need to load a single
certificate, handling the AVS_CRYPTO_DATA_SOURCE_BUFFER
case:
static avs_error_t
configure_client_cert(SSL_CTX *ctx,
const avs_crypto_certificate_chain_info_t *client_cert) {
switch (client_cert->desc.source) {
case AVS_CRYPTO_DATA_SOURCE_BUFFER: {
const unsigned char *ptr =
(const unsigned char *) client_cert->desc.info.buffer.buffer;
X509 *cert = d2i_X509(NULL, &ptr,
(long) client_cert->desc.info.buffer.buffer_size);
if (!cert) {
return avs_errno(AVS_EPROTO);
}
int result = SSL_CTX_use_certificate(ctx, cert);
X509_free(cert);
if (result != 1) {
return avs_errno(AVS_EPROTO);
}
return AVS_OK;
}
default:
return avs_errno(AVS_ENOTSUP);
}
}
Note
In this tutorial, only DER-encoded credentials are supported. This is most important and enough for compatibility with LwM2M. However, you may want to also support the PEM format. If both formats are supported, they shall be autodetected based on the contents of the file or buffer.
8.4.1.5.3.2. Loading client private keys
The code for loading client private keys is very similar, although we want to
make sure that the password
field is not used.
static avs_error_t
configure_client_key(SSL_CTX *ctx,
const avs_crypto_private_key_info_t *client_key) {
switch (client_key->desc.source) {
case AVS_CRYPTO_DATA_SOURCE_BUFFER: {
if (client_key->desc.info.buffer.password) {
return avs_errno(AVS_ENOTSUP);
}
const unsigned char *ptr =
(const unsigned char *) client_key->desc.info.buffer.buffer;
EVP_PKEY *key = d2i_AutoPrivateKey(
NULL, &ptr, (long) client_key->desc.info.buffer.buffer_size);
if (!key) {
return avs_errno(AVS_EPROTO);
}
int result = SSL_CTX_use_PrivateKey(ctx, key);
EVP_PKEY_free(key);
if (result != 1) {
return avs_errno(AVS_EPROTO);
}
return AVS_OK;
}
default:
return avs_errno(AVS_ENOTSUP);
}
}
8.4.1.5.3.3. Loading trusted certificates
For the trusted certificates, we need to support the empty and compound sources in addition to loading a simple single buffer:
#include <openssl/err.h>
// ...
static avs_error_t
configure_trusted_certs(X509_STORE *store,
const avs_crypto_security_info_union_t *trusted_certs) {
if (!trusted_certs) {
return avs_errno(AVS_EINVAL);
}
switch (trusted_certs->source) {
case AVS_CRYPTO_DATA_SOURCE_EMPTY:
return AVS_OK;
case AVS_CRYPTO_DATA_SOURCE_BUFFER: {
const unsigned char *ptr =
(const unsigned char *) trusted_certs->info.buffer.buffer;
X509 *cert = d2i_X509(NULL, &ptr,
(long) trusted_certs->info.buffer.buffer_size);
if (!cert) {
return avs_errno(AVS_EPROTO);
}
ERR_clear_error();
int result = X509_STORE_add_cert(store, cert);
X509_free(cert);
if (!result
&& ERR_GET_REASON(ERR_get_error())
!= X509_R_CERT_ALREADY_IN_HASH_TABLE) {
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 < trusted_certs->info.array.element_count;
++i) {
err = configure_trusted_certs(
store, &trusted_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, trusted_certs->info.list.list_head) {
if (avs_is_err((err = configure_trusted_certs(store, entry)))) {
break;
}
}
return AVS_OK;
}
default:
return avs_errno(AVS_ENOTSUP);
}
}
Please note the following additional alterations:
This function takes an argument of type
avs_crypto_security_info_union_t
instead of theavs_crypto_certificate_chain_info_t
wrapper. This has been done so that it can be more easily called recursively.There is a special case for the
X509_R_CERT_ALREADY_IN_HASH_TABLE
error. Loading the same certificate multiple times shall be permitted, in case e.g. an explicitly specified certificate is already present in the system trust store.
8.4.1.5.3.4. Loading certificate revocation lists
The CRL loading function is actually almost identical to the certificate chain loading one:
static avs_error_t configure_cert_revocation_lists(
X509_STORE *store,
const avs_crypto_security_info_union_t *cert_revocation_lists) {
if (!cert_revocation_lists) {
return avs_errno(AVS_EINVAL);
}
switch (cert_revocation_lists->source) {
case AVS_CRYPTO_DATA_SOURCE_EMPTY:
return AVS_OK;
case AVS_CRYPTO_DATA_SOURCE_BUFFER: {
const unsigned char *ptr =
(const unsigned char *)
cert_revocation_lists->info.buffer.buffer;
X509_CRL *crl = d2i_X509_CRL(
NULL, &ptr,
(long) cert_revocation_lists->info.buffer.buffer_size);
if (!crl) {
return avs_errno(AVS_EPROTO);
}
ERR_clear_error();
int result = X509_STORE_add_crl(store, crl);
X509_CRL_free(crl);
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 < cert_revocation_lists->info.array.element_count;
++i) {
err = configure_cert_revocation_lists(
store, &cert_revocation_lists->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, cert_revocation_lists->info.list.list_head) {
if (avs_is_err((
err = configure_cert_revocation_lists(store, entry)))) {
break;
}
}
return AVS_OK;
}
default:
return avs_errno(AVS_ENOTSUP);
}
}
8.4.1.5.4. Enabling hostname verification
Now that all the credentials are properly loaded, the only thing left is to
inform the TLS library of the hostname, so that the CN or SAN fields of the
server certificate can be properly verified. This can be done by calling
SSL_set1_host()
just before the 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);
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;
}
8.4.1.5.5. Limitations
The implementation above is a complete basic integration with the private key
infrastructure, however it lacks a number of features that are supported by the
avs_net
API:
Lack of DANE support. This means that the Server Public Key LwM2M resource is not supported, and will cause a failure if used. This is because LwM2M does not use standard certificate validation logic based on a trust store, using a custom mechanism instead. However, that mechanism is almost identical to the one used by DANE, so it is implemented in terms of that mechanism in Anjay and
avs_net
.This feature will be discussed in the next tutorial.
Lack of support for loading chains of more than one certificate as the client certificate chain, although this is rarely used.
Lack of support for loading credential information from other sources than memory buffers (e.g. files). This may be used e.g. for HTTPS downloads and support of Hardware Security Modules.
Lack of support for PEM encoding. This is not generally necessary for LwM2M compliance, but may be important for other cases, for example loading credentials from files, as mentioned above.
Lack of support for the
rebuild_client_cert_chain
flag inavs_net_certificate_info_t
. When that flag is supported and enabled, the TLS implementation shall find appropriate CA certificates in the trust store, to rebuild the full certification chain of the single certificate specified as the client certificate, and send that complete chain to the server during the handshake.This feature may be required for communication with some servers. However, it is complex to implement, usually requiring the use of advanced low-level APIs of the TLS library. For this reason it will not be discussed further in the tutorial.