ABRÉVIATIONS
- CA : Certificate Authority
- .CRT : Certificate
- .CSR : Certificate Signing Request
- CTR : Counter
- DER : Distinguished Encoding Rules
- DRGB : Dynamic Red-Green-Blue
- DTLS : Datagram Transport Layer Security
- IoT : Internet of Things
- IP : Internet Protocol
- .KEY : Private Key
- M2M : Machine to Machine
- MITM : Man-in-the-Middle
- PSK : Pre-Shared Key
- .PEM : Privacy-Enhanced Mail
- RSA : Rivest-Shamir-Adleman
- SSL : Secure Socket Layer
- TCP : Transmission Control Protocol
- TLS : Transport Layer Security
- UDP : User Datagram Protocol
- VoIP : Voice over Internet Protocol
INTRODUCTION
Dans le cadre d’un projet, j’ai été amené à travailler sur l'implémentation d'un serveur DTLS. Le serveur DTLS constitue un élément crucial dans la sécurisation des communications UDP au sein de nos systèmes, notamment pour la transmission sécurisée des données en temps réel. Cette sécurisation est essentielle pour garantir l'intégrité et la confidentialité des informations échangées entre les dispositifs et les serveurs centraux. Dans un contexte où ces données sont utilisées pour des analyses critiques et des décisions stratégiques, la protection contre les interceptions et les altérations devient impérative. Dans cet article, nous explorerons en détail le rôle et l'importance du serveur DTLS dans le contexte des communications sécurisées, en mettant l'accent sur son intégration dans des environnements professionnels et la transmission des données en temps réel. Cette exploration se déroulera en deux parties distinctes : une partie théorique, où nous aborderons les concepts fondamentaux du DTLS et son utilisation dans le cadre des applications d'entreprise, et une partie pratique, où nous examinerons les outils et les méthodes pour mettre en œuvre un serveur DTLS efficace.
Partie 1 : Comprendre l'essentiel du serveur DTLS
Définition et contexte d'utilisation
DTLS est une extension du protocole TLS conçue pour sécuriser les communications basées sur le protocole UDP. Contrairement à TLS qui fonctionne sur TCP, DTLS est optimisé pour les flux de données UDP qui sont généralement utilisés dans des environnements où la vitesse et l'efficacité sont prioritaires. DTLS assure la confidentialité, l'intégrité et l'authenticité des données échangées en utilisant des techniques de chiffrement symétrique et asymétrique, ainsi que des mécanismes de vérification des empreintes pour garantir l'authenticité des entités communicantes.
Le choix d'utiliser DTLS pour sécuriser les communications UDP est motivé par la nécessité de concilier faible latence et sécurité. En effet, UDP est largement utilisé dans des applications nécessitant une communication rapide, mais il présente un certain nombre de vulnérabilités en termes de sécurité intégrée. DTLS remédie à ces vulnérabilités en fournissant une couche de sécurité supplémentaire au-dessus d'UDP. En utilisant DTLS, on bénéficie des fonctionnalités de sécurité avancées offerte par TLS, tout en conservant les avantages de l'efficacité et de la vitesse propres à UDP. Cette combinaison permet de sécuriser efficacement les communications dans des environnements où la latence et la fiabilité sont essentielles.
Domaines d'application du DTLS
DTLS est utilisé pour sécuriser les communications entre les périphériques IoT et les serveurs, assurant ainsi la confidentialité et l'intégrité des données échangées. Cela est particulièrement important dans les applications telles que les maisons intelligentes, les villes intelligentes et l'industrie 4.0, où la sécurité des données est essentielle. Les applications VoIP utilisent souvent UDP pour transmettre les flux audio en temps réel. DTLS est utilisé pour sécuriser ces flux audio, en garantissant le chiffrement et l'authentification des données, ce qui protège contre les interceptions malveillantes et les attaques de type MITM.
Partie 2 : Mise en pratique du serveur DTLS
Dans cette seconde partie, nous allons détailler les différentes étapes nécessaires pour implémenter un serveur DTLS fonctionnel en utilisant le langage de programmation "C". Nous aborderons les éléments clés tels que l'initialisation des bibliothèques de sécurité, la configuration des clés et des certificats et la gestion des connexions réseaux pour garantir une communication sécurisée.
La suite sera structurée de manière à expliquer chaque composant et étape clé de l'implémentation. Nous commencerons par l'initialisation des bibliothèques de sécurité, suivie de la création des structures et de la configuration des certificats. Ensuite nous aborderons la gestion des sockets réseau, avant de conclure par l'établissement et la gestion des connexions sécurisées avec les clients. Chaque section inclura des explications détaillées sur le rôle et l'importance de chaque élément dans le contexte du serveur DTLS, afin de fournir une compréhension complète et pédagogique de processus.
Bibliothèque de sécurité
Dans notre cas, la bibliothèque mbedTLS a été choisie en raison de son large usage comme bibliothèque open source pour implémenter des protocoles de sécurité, y compris TLS/SSL et DTLS. mbedTLS fournit toutes les fonctionnalités nécessaires pour gérer la cryptographie et les protocoles de communications sécurisées. Voici en-dessous, les inclusions nécessaires pour travailler avec mbedTLS :
#include "mbedtls/entropy.h"
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/certs.h"
#include "mbedtls/x509.h"
#include "mbedtls/ssl.h"
#include "mbedtls/ssl_cookie.h"
#include "mbedtls/net_sockets.h"
#include "mbedtls/error.h"
#include "mbedtls/debug.h"
#include "mbedtls/timing.h"
#include <pthread.h>
#include <stdlib.h>
#if defined(MBEDTLS_SSL_CACHE_C)
#include "mbedtls/ssl_cache.h"
#endif
Structure pour les paramètres du serveur et du client
D'abord qu'est ce qu'une structure en langage de programmation C?
C'est une collection de variables sous un même nom, permettant de regrouper différentes données liées entre elles. Cela facilite l'organisation et la gestion des données, en particulier dans des projets complexes comme le notre, où plusieurs variables doivent être manipulées ensemble. Dans notre cas, pour gérer les paramètres et le contexte nécessaires à l’établissement et au maintien de la connexion, nous définissons deux structures.
- server_data_t : Cette structure contient les informations et les contextes nécessaires pour configurer et gérer le serveur, notamment le réseau, les certificats, les clés privées, les paramètres de configuration SSL, etc
typedef struct {
mbedtls_net_context listen_fd;
mbedtls_ssl_config conf;
mbedtls_x509_crt srvcert;
mbedtls_pk_context pkey;
mbedtls_timing_delay_context timer;
mbedtls_ctr_drbg_context ctr_drbg;
mbedtls_entropy_context entropy;
mbedtls_ssl_cookie_ctx cookie_ctx;
#if defined(MBEDTLS_SSL_CACHE_C)
mbedtls_ssl_cache_context cache;
#endif
} server_data_t;
- client_data_t : Cette structure contient les informations nécessaires pour gérer une connexion client, y compris le contexte réseau et le contexte SSL.
typedef struct {
mbedtls_net_context client_fd;
mbedtls_ssl_context ssl;
} client_data_t;
Structure pour passer les paramètres aux threads et callbacks
Avant d'entrer dans les détails des structures spécifiques utilisées, définissons ce qu'est un thread et une callback :
- Un thread est une unité d'exécution distincte au sein d'un programme. Ils permettent à un programme d'effectuer plusieurs taches simultanément (par exemple notre serveur qui doit gérer plusieurs clients simultanément). Chaque thread fonctionne de manière indépendante, mais ils partagent les mêmes ressources et le même espace mémoire du programme parent.
- On définit donc la structure thread_args_t : utilisée pour passer les paramètres aux threads du contexte, de la callback de lecture et des paramètres supplémentaires.
//Structure pour passer les paramètres au thread
typedef struct {
client_data_t *client_ctx;
read_callback_t cb;
void *params;
} thread_args_t;
- Une callback est une fonction qui est passée en tant qu'argument à une autre fonction. Elle est appelée à un moment donné pour exécuter une tâche spécifique.
- On définit donc la structure my_params_t: Utilisée pour passer des paramètres spécifiques aux callbacks de nouvelle connexion.
// Structure pour passer les paramètres à la callback de new connexion
typedef struct {
server_data_t* server_ctx;
char* storage_location;
}my_params_t;
Fonctions de création d'une instance 'server_data_t' et de débogage
Des fonctions utilitaires ont été créées pour instancier "server_data_t" et pour formater et afficher les messages de débogage. Ces fonctions simplifient la gestion de la mémoire et le diagnostic des problèmes.
->Création d'une nouvelle instance de "server_data":
server_data_t *create_server_data()
{
// Allouer de la mémoire pour une nouvelle instance de server_data_t
server_data_t *server_data = malloc(sizeof(server_data_t));
if (server_data == NULL)
{
// Gérer l'échec de l'allocation mémoire
fprintf(stderr, "Erreur d'allocation de mémoire pour server_data_t\n");
exit(EXIT_FAILURE);
}
return server_data;
}
->Fonction de débogage personnalisée:
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 );
}
Initialisation du contexte, des certificats et des sockets réseau du server
Dans cette partie, nous allons aborder les notions de contexte, de certificat et de sockets réseau. Chacun de ces éléments jouant un rôle crucial dans la configuration et le fonctionnement du serveur sécurisé.
->Contexte : Un contexte est une structure de données utilisées pour stocker les informations nécessaires au bon fonctionnement de différentes opérations. Dans mbedTLS, nous avons plusieurs types de contextes, chacun ayant un rôle spécifique.
- mbedtls_entropy_context: gère l'entropie, qui est une mesure de l'aléa nécessaire pour générer des nombres aléatoires de haute qualité. En cryptographie, une bonne source d'entropie est essentielle pour sécuriser les opérations cryptographiques. Voici un lien vers une définition plus approfondie de l'entropie https://www.arsouyes.org/articles/2023/2023-04-28_Entropie/.
- mbedtls_ctr_drbg_context: gère le générateur de nombres aléatoires déterministe (DRBG) utilisant l'algorithme CTR (counter mode). Il fournit des nombres aléatoires sécurisés basés sur l'entropie collectée.
- mbedtls_ssl_config: Cette structure contient tous les paramètres nécessaires pour configurer une session SSL/TLS. Elle inclut les options de sécurité, les paramètres de chiffrement et les configurations spécifiques à DTLS.
int init_server_data(server_data_t* server_ctx)
{
mbedtls_entropy_init( &server_ctx->entropy );
mbedtls_ctr_drbg_init( &server_ctx->ctr_drbg );
mbedtls_net_init( &server_ctx->listen_fd );
mbedtls_ssl_config_init( &server_ctx->conf );
mbedtls_ssl_cookie_init( &server_ctx->cookie_ctx );
mbedtls_x509_crt_init(&server_ctx->srvcert);
mbedtls_pk_init( &server_ctx->pkey );
#if defined(MBEDTLS_SSL_CACHE_C)
mbedtls_ssl_cache_init( &server_ctx->cache );
#endif
#if defined(MBEDTLS_DEBUG_C)
mbedtls_debug_set_threshold( DEBUG_LEVEL );
#endif
// force utf-8
setlocale(LC_CTYPE, "");
printf("\n Server data init with success! \n");
return 0;
}
->Certificats: Un certificat est un document électronique utilisé pour prouver l'identité d'une entité (comme un serveur ou un client) et pour échanger des clés de chiffrement. Les certificats sont émis par des autorités de certification (CA) et contiennent des informations telles que le nom du propriétaire, la clé publique, le nom de l'émetteur (la CA) et la signature numérique de la CA. Dans notre cas, on utilise des certificats auto-signés.
- .pem (Privacy-Enhanced Mail): Format de fichier couramment utilisé pour stocker des certificats et des clés cryptographiques. Un fichier .pem peut contenir un certificat ou une clé privée encodée en Base64 avec des en-têtes et pieds de page spécifiques.
- .csr (Certificate Signing Request): Demande de signature de certificat. Il s'agit d'un fichier envoyé à une CA pour demander la signature d'un certificat. Il contient la clé publique et les informations d'identification.
- .crt: Fichier de certificat. Utilisé pour stocker des certificats dans divers formats (comme PEM ou DER). Ce fichier contient la clé publique et les informations d'identification signées par une CA.
- .key: Fichier de clé privée. Contient la clé privée utilisée pour déchiffrer les informations chiffrées envoyées à l'entité propriétaire de la clé.
Étapes pour générer des certificats auto-signé avec openssl
Génération de clé privée
openssl genpkey -algorithm RSA -out key.pem
Création d'une demande de signature de certificat (CSR)
openssl req -new -key key.pem -out csr.pem
Génération du certificat auto-signé
openssl req -x509 -key key.pem -in csr.pem -out cert.pem -days 365
Pour ceux qui veulent aller plus loin, voici un article d'IBM qui explique en détails comment créer un certificat SSL : https://www.ibm.com/docs/fr/rpa/21.0?topic=environment-create-ssl-certificate
- mbedtls_x509_crt: Gère les certificats X.509, qui sont utilisés pour vérifier l'identité des parties en communication. Les certificats X.509 sont un standard pour les certificats de clé publique.
- mbedtls_pk_context: Gère les clés privées associées aux certificats. Ces clés permettent de chiffrer et déchiffrer les données, assurant ainsi la confidentialité et l'intégrité des communications.
int init_certificates(server_data_t* server_ctx, const char *cert_file, const char *key_file)
{
// Charger le certificat depuis un fichier
if (mbedtls_x509_crt_parse_file(&server_ctx->srvcert, cert_file) != 0)
{
printf("\nFailed to parse certificate\n");
return -1;
}
// Charger la clé privée depuis un fichier
if (mbedtls_pk_parse_keyfile(&server_ctx->pkey, key_file, NULL) != 0)
{
printf("Failed to parse private key\n");
return -1;
}
printf("\n Certificates init with success! \n");
return 0;
}
->Sockets réseau: C'est une interface de communication entre deux machines (ou plus) sur un réseau. Il permet d'envoyer et de recevoir des données à travers le réseau en utilisant des protocoles tels que TCP ou UDP. Ils sont essentielles pour la communications en réseau car ils définissent comment les données doivent être structurées et transmises.
- mbedtls_net_context: Cette structure gère les opérations de socket réseau. Elle est utilisée pour ouvrir, fermer, lire et écrire sur les sockets réseau, facilitant la communication entre le serveur et les clients.
int init_network_sockets(server_data_t* server_ctx, const char *ip_address, const char *port)
{
printf( " . Bind on udp://%s:%s ...\n", ip_address, port);
fflush( stdout );
int ret = mbedtls_net_bind(&server_ctx->listen_fd, ip_address, port, MBEDTLS_NET_PROTO_UDP);
if (ret != 0)
{
char buffer[100];
mbedtls_strerror(ret, buffer, sizeof(buffer));
printf("Failed to bind socket: %s\n", buffer);
return ret;
}
printf( " ok\n" );
printf("\n Network sockets init with success! \n");
return 0;
}
Configuration SSL/TLS
La fonction suivante configure les paramètres SSL/TLS pour le serveur DTLS, y compris le générateur de nombres aléatoires, la configuration SSL/TLS par défaut, les certificats et les clés, ainsi que la gestion des cookies et les paramètres PSK (Pre-Shared Key). Chaque étape est essentielle pour garantir que le serveur peut établir des connexions sécurisées avec les clients.
->Définition de la fonction:
int setup_ssl(server_data_t* server_ctx, const unsigned char input_psk[MBEDTLS_PSK_MAX_LEN],
char* input_psk_list, const uint8_t input_psk_identity[])
->Initialisation du générateur de nombres aléatoires
mbedtls_ctr_drbg_seed: Initialise le générateur de nombres aléatoires en utilisant une source d'entropie.
printf(" . Seeding the random number generator..."); const char *pers = "dtls_server"; int ret = mbedtls_ctr_drbg_seed(&server_ctx->ctr_drbg, mbedtls_entropy_func, &server_ctx->entropy, (const unsigned char *) pers, strlen(pers)); if (ret != 0) { printf("Failed to seed RNG: %d\n", ret); return ret; } printf(" ok\n");
->Configurer le certificat et la clé privée
mbedtls_ssl_conf_own_cert: Configure le certificat et la clé privée pour le serveur. Si le certificat ou la clé privée n'existe pas, un message d’avertissement est affiché.
if (server_ctx->srvcert.version != 0 && server_ctx->pkey.pk_info != NULL) { printf("The certificate and private key exist and are initialized.\n"); mbedtls_ssl_conf_own_cert(&server_ctx->conf, &server_ctx->srvcert, &server_ctx->pkey); } else { printf("The certificate and private key do not exist, if you just want to use PSK you can ignore this message.\n"); }
->Configurer les autres paramètres SSL/TLS
- mbedtls_ssl_conf_rng: Configure le générateur de nombres aléatoires.
- mbedtls_ssl_conf_dbg: Configure la fonction de débogage.
mbedtls_ssl_conf_read_timeout: Configure le délai d'attente pour les opérations de lecture.
mbedtls_ssl_conf_rng(&server_ctx->conf, mbedtls_ctr_drbg_random, &server_ctx->ctr_drbg); mbedtls_ssl_conf_dbg(&server_ctx->conf, my_debug, stdout); mbedtls_ssl_conf_read_timeout(&server_ctx->conf, READ_TIMEOUT_MS);
->Initialiser le cache SSL/TLS (si activé): Utilisé pour stocker les sessions SSL/TLS, permettant de réutiliser les sessions précédentes et d'améliorer les performances.
mbedtls_ssl_conf_session_cache: Configure le cache de session, en définissant les fonctions de récupération et de stockage des sessions.
#if defined(MBEDTLS_SSL_CACHE_C) mbedtls_ssl_conf_session_cache(&server_ctx->conf, &server_ctx->cache, mbedtls_ssl_cache_get, mbedtls_ssl_cache_set); #endif
->Configurer le gestionnaire de cookies : Utilisé pour gérer les cookies DTLS, ce qui aide à protéger contre les attaques par déni de service.
- mbedtls_ssl_cookie_setup: Initialise le gestionnaire de cookies.
mbedtls_ssl_conf_dtls_cookies: Configure les fonctions de gestion de cookies.
ret = mbedtls_ssl_cookie_setup(&server_ctx->cookie_ctx, mbedtls_ctr_drbg_random, &server_ctx->ctr_drbg); if (ret != 0) { printf("Failed to setup cookie context: %d\n", ret); return ret; } mbedtls_ssl_conf_dtls_cookies(&server_ctx->conf, mbedtls_ssl_cookie_write, mbedtls_ssl_cookie_check, &server_ctx->cookie_ctx);
->Configurer le PSK (Pre-Shared Key): Méthode d'authentification où une clé partagée est utilisée pour sécuriser la connexion.
- mbedtls_ssl_conf_psk: Configure la clé PSK et l'identité PSK
mbedtls_ssl_conf_psk_cb: Configure une fonction de rappel pour gérer les clés PSK dynamiques.
if (input_psk_len != 0 && strlen((char*)input_psk_identity) != 0) { ret = mbedtls_ssl_conf_psk(&server_ctx->conf, input_psk, input_psk_len, (const unsigned char *) input_psk_identity, strlen((char*) input_psk_identity)); if (ret != 0) { printf("Failed to configure PSK: -0x%04X\n\n", -ret); return ret; } } if (input_psk_list != NULL) mbedtls_ssl_conf_psk_cb(&server_ctx->conf, psk_callback, psk_info);
Pour approfondir les notions techniques mentionnées ci-dessus, vous pouvez consulter un article de DIGIDOP qui aborde ce sujet via le lien : https://www.digidop.fr/blog/ssl-tls-https-securites-web
->Finalisation: Afficher un message de succès si toutes les étapes de configuration sont réussies
printf("\n SSL setup with success! \n");
return 0;
Acceptation des connexions
La fonction suivante gère l'acceptation des connexions entrantes pour le serveur. Elle initialise le contexte client, configure les paramètres SSL/TLS, et effectue la procédure de handshake pour établir une connexion sécurisée.
->Définition de la fonction:
int accept_connections(server_data_t* server_ctx, new_connection_callback_t cb, void* params)
->Allocation et initialisation du contexte client:
- client_data_t client_ctx*: Pointeur vers la structure contenant les données du client
malloc: Alloue de la mémoire pour la structure du client. Si l'allocation échoue, un message d'erreur est affiché et le programme s’arrête.
int ret = 0; // Données pour la connexion client unsigned char client_ip[16] = { 0 }; size_t cliip_len; client_data_t *client_ctx = malloc(sizeof(client_data_t)); if (client_ctx == NULL) { // Gérer l'échec de l'allocation mémoire fprintf(stderr, "Erreur d'allocation de mémoire pour le client\n"); exit(EXIT_FAILURE); }
->Initialisation des composants réseau et SSL du client:
- mbedtls_net_init: Initialise la structure de gestion de la connexion réseau.
mbedtls_ssl_init: Initialise la structure SSL/TLS
// Initialisation mbedtls_net_init(&client_ctx->client_fd); mbedtls_ssl_init(&client_ctx->ssl);
->Configuration SSL/TLS pour le client:
mbedtls_ssl_setup: Applique la configuration SSL/TLS du serveur à la structure SSL du client . Si cette opération échoue, la fonction retourne une erreur.
// Appliquer la configuration SSL/TLS à la structure SSL ret = mbedtls_ssl_setup(&client_ctx->ssl, &server_ctx->conf); if (ret != 0) { printf("Failed to setup SSL context: %d\n", ret); return ret; }
->Fonction de temporisation SSL/TLS:
mbedtls_ssl_set_timer_cb: Configure les fonctions de temporisation pour gérer les détails de transmission et de réception.
// Définir la fonction de temporisation SSL/TLS mbedtls_ssl_set_timer_cb(&client_ctx->ssl, &server_ctx->timer, mbedtls_timing_set_delay, mbedtls_timing_get_delay);
->Réinitialisation de la session et attente de la connexion client:
- mbedtls_net_free: Libère les ressources associées à la connexion réseau du client.
- mbedtls_ssl_session_reset: Réinitialise la session SSL/TLS du client.
mbedtls_net_accept: Attend qu'un client se connecte et accepte la connexion. Si cette opération échoue, une erreur est retournée.
relaunch: // Reset session mbedtls_net_free(&client_ctx->client_fd); mbedtls_ssl_session_reset(&client_ctx->ssl); // Attendre qu'un client se connecte printf(" . Waiting for a remote connection...\n"); fflush(stdout); const int accept_result = mbedtls_net_accept(&server_ctx->listen_fd, &client_ctx->client_fd, client_ip, sizeof(client_ip), &cliip_len); if (accept_result != 0) { printf("Failed to accept client connection: %d\n", accept_result); free(client_ctx); return accept_result; }
->Configuration de l'identifiant de transport client:
mbedtls_ssl_set_client_transport_id: Configure l'identifiant de transport pour le client, utilisé pour gérer les cookies DTLS et éviter les attaques par déni de service.
// Pour les cookies HelloVerifyRequest if ((ret = mbedtls_ssl_set_client_transport_id(&client_ctx->ssl, client_ip, cliip_len)) != 0) { printf("Failed to set client transport id: %d\n", ret); free(client_ctx); return ret; }
->Configuration des fonctions de transmission et réception:
mbedtls_ssl_set_bio: Configure les fonctions de transmission (send) et de réception (recv) pour la connexion SSL/TLS.
mbedtls_ssl_set_bio(&client_ctx->ssl, &client_ctx->client_fd, mbedtls_net_send, mbedtls_net_recv, mbedtls_net_recv_timeout);
->Procédure de handshake SSL/TLS:
mbedtls_ssl_handshake: Effectue la procédure de handshake DTLS pour établir une connexion sécurisée. Si un message HelloVerifyRequest est reçu, la procédure de handshake est relancée. En cas d'échec, une erreur est retournée. Pour plus d'informations, consultez cet article de CLOUDFLARE sur le sujet : https://www.cloudflare.com/fr-fr/learning/ssl/what-happens-in-a-tls-handshake/
printf(" . Performing the DTLS handshake...\n"); fflush(stdout); do ret = mbedtls_ssl_handshake(&client_ctx->ssl); while (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE); if (ret == MBEDTLS_ERR_SSL_HELLO_VERIFY_REQUIRED) { printf("Hello verification requested\n"); goto relaunch; } else if (ret != 0) { printf("Failed to perform handshake: -0x%x\n", -ret); free(client_ctx); return ret; }
->Appel de la fonction de rappel et finalisation: Affiche un message de succès si la procédure de handshake est réussie et retourne 0
- cb: Appelle la fonction de rappel pour gérer la nouvelle connexion.
cb(client_ctx, params);
printf(" ok\n");
printf("Handshake successful\n");
ret = 0;
return ret;
Lecture des données reçues du client
La fonction suivante lit les données envoyées par un client au serveur.
->Définition de la fonction:
int read_client_data(client_data_t* client_ctx, read_callback_t cb, void *params)
->Déclaration des variables et initialisation du buffer:
- buf: Buffer pour stocker les données lues depuis le client.
- len: Initialisé à la taille maximale du buffer moins un pour s'assurer qu'il y a toujours de la place pour le caractère nul de la fin de chaîne.
memset: Initialise le buffer à zéro
int ret, len = 0; unsigned char buf[1024]; // Lire des données avec le client printf(" < Read from client:\n"); fflush(stdout); len = sizeof(buf) - 1; memset(buf, 0, sizeof(buf));
->Lecture des données envoyées par le client:
mbedtls_ssl_read: Lit les données envoyées par le client. La boucle 'do-while' continue de lire tant que l'opération de lecture est interrompue par les erreurs 'MBEDTLS_ERR_SSL_WANT_READ' ou 'MBEDTLS_ERR_SSL_WANT_WRITE', indiquant que l'opération doit être réessayée.
do { ret = mbedtls_ssl_read(&(client_ctx->ssl), buf, len); } while (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE);
->Gestion des erreurs de lecture:
- MBEDTLS_ERR_SSL_TIMEOUT: Timeout pendant la lecture.
MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY: Le client a fermé la connexion de manière propre.
if (ret <= 0) { switch (ret) { case MBEDTLS_ERR_SSL_TIMEOUT: printf("Timeout\n"); break; case MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY: printf("Connection closed gracefully\n"); break; default: printf("SSL read error: -0x%x\n", -ret); break; } return ret; }
->Traitement des données lues et finalisation:
- cb: Appelle la fonction de rappel avec les données lues et les paramètres supplémentaires. Si aucune donnée n'a été lue, la fonction de rappel est appelée avec NULL et une longueur de 0.
len = ret;
printf("%d bytes read\n\n%s\n\n", len, buf);
//Appel de la callback
if (len > 0)
{
cb(buf, len, params);
}
else
{
cb(NULL, 0, params);
}
return ret;
Gestion des threads
La fonction suivante est utilisée pour gérer les threads qui traitent les connexions clients dans notre serveur.
->Définition de la fonction:
void thread_function(void *arg)
->Récupération des arguments:
*thread_args_t args: Convertion du pointeur d'argument générique en pointeur vers la structure 'thread_args_t', qui contient les informations nécessaires pour le thread.
// Récupération des arguments thread_args_t *args = (thread_args_t *)arg;
->Exécution de la fonction de gestion des connexions client:
- read_client_data: Lit les données envoyées par le client. Si la lecture retourne une valeur inférieure ou égale à zéro, cela signifie qu'il y a eu une erreur ou que la connexion a été fermée.
close_client_connection: Ferme la connexion client en libérant les ressources associées.
// Exécution de la fonction while (1) { // read_client_data(args->client_ctx, args->cb, args->params); if (read_client_data(args->client_ctx, args->cb, args->params) <= 0) { close_client_connection(args->client_ctx); break; } }
->Libération de la mémoire et finalisation
// Libération de la mémoire des arguments
free(args);
// Fin du thread
pthread_exit(NULL);
Nettoyage et fermeture de connexion
Les fonctions "cleanup" et "close_client_connection" assurent la libération des ressources allouées pour les contextes du serveur et du client respectivement.
- mbedtls_net_free: Libère la ressource réseau du serveur.
- mbedtls_x509_crt_free: Libère le certificat X.509 du serveur.
- mbedtls_pk_free: Libère la clé privée du serveur.
- mbedtls_ssl_config_free: Libère la configuration SSL/TLS du serveur.
- mbedtls_ssl_cookie_free: Libère le contexte des cookies SSL/TLS.
- mbedtls_ctr_drbg_free: Libère le générateur de nombres pseudo-aléatoires.
- mbedtls_entropy_free: LIbère le contexte de l'entropie.
- mbedtls_ssl_cache_free: Libère le cache SSL/TLS (si défini).
- mbedtls_ssl_close_notify: Envoie une notification de fermeture SSL/TLS au client, indiquant que la connexion va être fermée.
- mbedtls_net_free: Libère la ressource réseau du client.
void cleanup(server_data_t* server_ctx)
{
mbedtls_net_free(&server_ctx->listen_fd);
mbedtls_x509_crt_free(&server_ctx->srvcert);
mbedtls_pk_free(&server_ctx->pkey);
mbedtls_ssl_config_free(&server_ctx->conf);
mbedtls_ssl_cookie_free(&server_ctx->cookie_ctx);
mbedtls_ctr_drbg_free(&server_ctx->ctr_drbg);
mbedtls_entropy_free(&server_ctx->entropy);
#if defined(MBEDTLS_SSL_CACHE_C)
mbedtls_ssl_cache_free(&server_ctx->cache);
#endif
}
void close_client_connection(client_data_t* client_ctx)
{
mbedtls_ssl_close_notify(&(client_ctx->ssl));
mbedtls_net_free(&(client_ctx->client_fd));
}
Exemple de fonctions de callback et de création de threads
Les fonctions de callback et de création de threads sont essentielles pour gérer les connexions et les échanges de données dans notre serveur.
->Fonction de callback pour la lecture des données: Cette fonction est appelée chaque fois que données sont lues depuis une connexion client. Elle gère le stockage et l'affichage de ces données.
- data: les données lues depuis la connexion client.
- data_len: la longueur des données lues.
storage_location: un emplacement mémoire où les données lues seront stockées.
void my_read_callback(const char* data, unsigned int data_len, void* storage_location) { // snprintf est une fonction qui copie une chaîne de caractères dans un buffer avec une limite de taille // Si data_len est supérieur à 32, on limite à 32 caractères, sinon on utilise data_len snprintf(storage_location, (data_len > 32) ? 32 : data_len, "%s", data); // Affiche les données lues printf("I read some data : %s\n", storage_location); }
->Fonction de callback pour l'Acceptation des connexions: Elle est appelée chaque fois qu'une nouvelle connexion client est acceptée. Elle initialise les threads pour gérer les échanges de données avec le client.
- client_ctx: le contexte client, contenant les informations sur la connexion.
params: les paramètres supplémentaires passés à la fonction (non obligatoire, on peut passer la valeur NULL).
void my_accept_connections_callback(client_data_t* client_ctx, void* params) { // Convertit les paramètres génériques en un type spécifique my_params_t typed_params = *(my_params_t*)params; // Alloue de la mémoire pour le thread et ses arguments pthread_t* thread = malloc(sizeof(pthread_t)); thread_args_t *args = malloc(sizeof(thread_args_t)); if (args == NULL) { fprintf(stderr, "Erreur d'allocation de mémoire pour les arguments du thread\n"); exit(EXIT_FAILURE); } // Initialise les arguments du thread args->client_ctx = client_ctx; args->cb = my_read_callback; args->params = typed_params.storage_location; printf("New connection =====================================!\n"); // Crée un nouveau thread pour gérer la connexion client int rc = pthread_create(thread, NULL, &thread_function, args); if (rc) { fprintf(stderr, "Erreur lors de la création du thread : %d\n", rc); exit(EXIT_FAILURE); } }
Fonction principale (main)
Cette fonction est le point d'entrée du programme et coordonne l'initialisation des composants du serveur DTLS, la gestion des connexions et le nettoyage des ressources.
NB : Pour l'authentification du client, on peut choisir soit l'utilisation du PSK, soit l'utilisation des certificats, ou même utiliser les deux à la fois. L'utilisation du PSK et des certificats à la fois permet de renforcer davantage la performance et la robustesse du système.
- Initialiser les variables mbedTLS: 'init_server_data(server)' initialise les structures de données nécessaires pour mbedTLS.
- Initialiser les certificats: 'init_certificates(server, cert_file, key_file)' charge le certificat et la clé privée dans les structures appropriées.
- Initialiser les sockets réseau: 'init_network_sockets(server, ip_address, port)': configure les sockets pour écouter les connexions entrantes.
- Configurer la couche SSL/TLS: 'setup_ssl(server, input_psk, input_psk_list, input_psk_identity)' configure les paramètres SSL/TLS, y compris le PSK.
- Accepter les connexions: Une boucle infinie 'while(1)' attend et accepte les connexions clients en appelant 'accept_connections(server, my_accept_connections_callback, ¶ms)'. Pour chaque connexion acceptée, un nouveau thread est créé pour gérer la communication avec le client.
- Nettoyer les ressources: 'cleanup(server)' libère toutes les ressources allouées pour mbedTLS et le serveur. 'free(server)' libère la mémoire allouée pour la structure de données du serveur.
int ret;
char data_received_in_cb[32];
server_data_t* server = create_server_data();
//Déclarations des variables nécessaires à la mise en place du PSK
const unsigned char input_psk[MBEDTLS_PSK_MAX_LEN] = {'V','o','t','r','e','','P','S','K'};//Remplacer par votre PSK réel
char* input_psk_list = "Votre PSK list";// Remplaçer par votre liste de PSK réelle
const uint8_t input_psk_identity[] = "Votre PSK identity";//Remplacer par votre PSK identity réelle
const char *ip_address = "adresse de votre serveur";//Choisir une adresse ip du réseau pour le serveur
const char *port = "votre port d'écoute";//Le port par défaut est le "5683"
//Déclarations des variables nécessaires à la mise en place du certificat et de la clé privée
const char *cert_file = "chemin/vers/votre/certificat.pem";//Remplacer par votre certificat réel
const char *key_file = "chemin/vers/votre/cle.pem";//Remplacer par votre clé réel
int main()
{
//Initialiser toutes les variables nécessaire à mbedtls
ret = init_server_data(server);
if (ret != 0)
{
printf("Failed to initialize mbedtls variables\n");
return ret;
}
// Initialiser les certificats
ret = init_certificates(server, cert_file, key_file);
if (ret != 0)
{
printf("Failed to initialize certificates\n");
return ret;
}
// Initialiser les sockets réseau
ret = init_network_sockets(server, ip_address, port);
if (ret != 0)
{
printf("Failed to initialize network sockets\n");
cleanup(server);
return ret;
}
// Configurer la couche SSL/TLS
ret = setup_ssl(server, input_psk, input_psk_list, input_psk_identity);
if (ret != 0)
{
printf("Failed to setup SSL/TLS\n");
cleanup(server);
return ret;
}
my_params_t params = {server, data_received_in_cb};
// Communiquer avec le client
while (1)
{
accept_connections(server, my_accept_connections_callback, ¶ms);
}
// Nettoyer les ressources à la fin
cleanup(server);
free(server);
return 0;
}
Résultats des tests
Serveur en attente d'une connexion client
Connexion client au serveur
CONCLUSION
En conclusion, la mise en place d'un serveur DTLS permet de sécuriser efficacement les communications sur les réseaux UDP, garantissant ainsi la confidentialité, l'intégrité et l'authenticité des données échangées.
Après avoir abordé les concepts théoriques essentiels du DTLS et son utilisation dans divers domaines, nous avons démontré dans la deuxième partie pratique, comment implémenter un serveur DTLS en utilisant la bibliothèque mbedTLS.
Cette partie pratique a couvert les aspects techniques, notamment la configuration des certificats et des clés pré-partagées, la mise en place des sockets réseau, et l'intégration de fonctions pour accepter et gérer les connexions sécurisées des clients.
Grâce à cette approche détaillée et méthodique, nous avons non seulement sécurisé les communications critiques en temps réel mais également offert une solution adaptable et robuste aux divers défis de sécurité des environnements modernes. La combinaison de la théorie et de la pratique présentée dans cet article permet de mieux comprendre et d'implémenter un serveur DTLS efficace, renforçant ainsi la sécurité des systèmes embarqués et des infrastructures connectées.
Tableau Comparatif : UDP vs UDP avec DTLS vs TCP
Critère | UDP | UDP avec DTLS | TCP |
Type de connexion | Non orienté connexion | Non orienté connexion, mais sécurisé | Orienté connexion |
Sécurité | Pas de sécurité | Sécurisé avec DTLS (chiffrement et intégrité) | Sécurisé avec TLS (si activé) |
Fiabilité | Pas de garantie de livraison | Pas de garantie de livraison, mais sécurisé | Fiable (retransmission en cas de perte) |
Vitesse | Très rapide, faible latence | Plus lent que UDP en raison du chiffrement | Plus lent qu'UDP (retransmissions et contrôle d'erreurs) |
Complexité | Simple, facile à implémenter | Plus complexe avec l'ajout de DTLS | Complexe, mécanismes de contrôle lourds |
Utilisation principale | Streaming, jeux vidéo, VoIP | Streaming sécurisé, appels vidéo sécurisés | Web, emails, fichiers, transactions |
Contrôle de congestion | Non | Non | Oui (contrôle de flux et congestion) |
Tolérance aux erreurs | Pas de correction | Pas de correction, mais garantit l'intégrité | Correction via retransmissions |
Handshake nécessaire | Non | Oui (handshake DTLS) | Oui (handshake TCP) |
Exemples d'utilisation | DNS, VoIP, diffusion en direct | Applications nécessitant la sécurité sur UDP | Transferts de fichiers, HTTP/HTTPS |