Last active
March 11, 2026 06:20
-
-
Save masakielastic/7d2d29768bc73ea831db9b95b70ea31b to your computer and use it in GitHub Desktop.
Minimal TLS Client in C with Explicit Connection State (QUIC Preparation)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| Minimal TLS Client in C (Architecture Exercise for QUIC Preparation) | |
| This program implements a small TLS + TCP client using OpenSSL. | |
| The goal of this code is not merely to demonstrate how to perform | |
| a TLS request, but to illustrate a connection-oriented design that | |
| resembles modern network protocol libraries. | |
| Instead of writing everything inside main(), the implementation | |
| intentionally separates responsibilities into multiple functions: | |
| - tcp connection setup | |
| - TLS context creation | |
| - TLS session creation | |
| - handshake phase | |
| - application data exchange | |
| - connection teardown | |
| The connection state is also tracked explicitly using a small | |
| state machine: | |
| INIT | |
| TCP_OPEN | |
| TLS_OPEN | |
| HANDSHAKING | |
| READY | |
| CLOSED | |
| ERROR | |
| This structure mirrors how real protocol implementations are often | |
| designed. Libraries for modern protocols such as QUIC typically | |
| separate: | |
| transport layer | |
| crypto layer | |
| connection lifecycle | |
| application protocol | |
| Therefore this example can be used as a preparation exercise before | |
| studying QUIC or HTTP/3 libraries. | |
| Another design goal of this example is to make learning easier. | |
| Because responsibilities are split into small functions, the code | |
| can be modified incrementally. Readers can copy the code and | |
| experiment with individual parts without rewriting the entire | |
| program. | |
| Typical experiments include: | |
| - introducing non-blocking I/O | |
| - integrating an event loop | |
| - adding ALPN negotiation | |
| - extending the connection state machine | |
| Build example: | |
| gcc tls-client.c -Wall -Wextra -O2 \ | |
| -o tls-client $(pkg-config --cflags --libs openssl) | |
| This code is intended for educational purposes. | |
| */ | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <unistd.h> | |
| #include <netdb.h> | |
| #include <sys/socket.h> | |
| #include <openssl/ssl.h> | |
| #include <openssl/err.h> | |
| #include <openssl/x509_vfy.h> | |
| typedef struct tls_client_config { | |
| const char *host; | |
| const char *port; | |
| const char *server_name; | |
| const char *ca_file; | |
| const char *ca_path; | |
| int min_proto_version; | |
| int verify_peer; | |
| } tls_client_config; | |
| typedef enum tls_client_state { | |
| TLS_CLIENT_STATE_INIT = 0, | |
| TLS_CLIENT_STATE_TCP_OPEN, | |
| TLS_CLIENT_STATE_TLS_OPEN, | |
| TLS_CLIENT_STATE_HANDSHAKING, | |
| TLS_CLIENT_STATE_READY, | |
| TLS_CLIENT_STATE_CLOSED, | |
| TLS_CLIENT_STATE_ERROR | |
| } tls_client_state; | |
| typedef struct tls_client { | |
| tls_client_config config; | |
| int fd; | |
| SSL_CTX *ctx; | |
| SSL *ssl; | |
| tls_client_state state; | |
| } tls_client; | |
| static void print_openssl_error(const char *where) { | |
| fprintf(stderr, "%s\n", where); | |
| ERR_print_errors_fp(stderr); | |
| } | |
| static int tcp_connect(const char *host, const char *port) { | |
| struct addrinfo hints; | |
| struct addrinfo *res = NULL; | |
| struct addrinfo *rp; | |
| int fd = -1; | |
| int rc; | |
| memset(&hints, 0, sizeof(hints)); | |
| hints.ai_family = AF_UNSPEC; | |
| hints.ai_socktype = SOCK_STREAM; | |
| rc = getaddrinfo(host, port, &hints, &res); | |
| if (rc != 0) { | |
| fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rc)); | |
| return -1; | |
| } | |
| for (rp = res; rp != NULL; rp = rp->ai_next) { | |
| fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); | |
| if (fd < 0) { | |
| continue; | |
| } | |
| if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) { | |
| break; | |
| } | |
| close(fd); | |
| fd = -1; | |
| } | |
| freeaddrinfo(res); | |
| return fd; | |
| } | |
| static SSL_CTX *tls_client_ctx_create(const tls_client_config *cfg) { | |
| SSL_CTX *ctx = NULL; | |
| if (cfg == NULL) { | |
| fprintf(stderr, "tls_client_ctx_create: cfg is NULL\n"); | |
| return NULL; | |
| } | |
| if (OPENSSL_init_ssl(0, NULL) != 1) { | |
| print_openssl_error("OPENSSL_init_ssl failed"); | |
| return NULL; | |
| } | |
| ctx = SSL_CTX_new(TLS_client_method()); | |
| if (ctx == NULL) { | |
| print_openssl_error("SSL_CTX_new failed"); | |
| return NULL; | |
| } | |
| if (cfg->min_proto_version != 0) { | |
| if (SSL_CTX_set_min_proto_version(ctx, cfg->min_proto_version) != 1) { | |
| print_openssl_error("SSL_CTX_set_min_proto_version failed"); | |
| SSL_CTX_free(ctx); | |
| return NULL; | |
| } | |
| } | |
| if (cfg->verify_peer) { | |
| SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); | |
| if (cfg->ca_file != NULL || cfg->ca_path != NULL) { | |
| if (SSL_CTX_load_verify_locations(ctx, cfg->ca_file, cfg->ca_path) != 1) { | |
| print_openssl_error("SSL_CTX_load_verify_locations failed"); | |
| SSL_CTX_free(ctx); | |
| return NULL; | |
| } | |
| } else { | |
| if (SSL_CTX_set_default_verify_paths(ctx) != 1) { | |
| print_openssl_error("SSL_CTX_set_default_verify_paths failed"); | |
| SSL_CTX_free(ctx); | |
| return NULL; | |
| } | |
| } | |
| } else { | |
| SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL); | |
| } | |
| return ctx; | |
| } | |
| static SSL *tls_client_ssl_create(SSL_CTX *ctx, int fd, const tls_client_config *cfg) { | |
| SSL *ssl = NULL; | |
| if (ctx == NULL || cfg == NULL) { | |
| fprintf(stderr, "tls_client_ssl_create: invalid argument\n"); | |
| return NULL; | |
| } | |
| ssl = SSL_new(ctx); | |
| if (ssl == NULL) { | |
| print_openssl_error("SSL_new failed"); | |
| return NULL; | |
| } | |
| if (SSL_set_fd(ssl, fd) != 1) { | |
| print_openssl_error("SSL_set_fd failed"); | |
| SSL_free(ssl); | |
| return NULL; | |
| } | |
| if (cfg->server_name != NULL) { | |
| if (SSL_set_tlsext_host_name(ssl, cfg->server_name) != 1) { | |
| print_openssl_error("SSL_set_tlsext_host_name failed"); | |
| SSL_free(ssl); | |
| return NULL; | |
| } | |
| if (cfg->verify_peer) { | |
| if (SSL_set1_host(ssl, cfg->server_name) != 1) { | |
| print_openssl_error("SSL_set1_host failed"); | |
| SSL_free(ssl); | |
| return NULL; | |
| } | |
| } | |
| } | |
| return ssl; | |
| } | |
| static void tls_client_init(tls_client *client, const tls_client_config *cfg) | |
| { | |
| memset(client, 0, sizeof(*client)); | |
| client->fd = -1; | |
| client->state = TLS_CLIENT_STATE_INIT; | |
| if (cfg != NULL) { | |
| client->config = *cfg; | |
| } | |
| } | |
| static int tls_client_open_tcp(tls_client *client) | |
| { | |
| if (client->state != TLS_CLIENT_STATE_INIT) { | |
| fprintf(stderr, "invalid state for tcp open\n"); | |
| return 0; | |
| } | |
| client->fd = tcp_connect(client->config.host, client->config.port); | |
| if (client->fd < 0) { | |
| client->state = TLS_CLIENT_STATE_ERROR; | |
| return 0; | |
| } | |
| client->state = TLS_CLIENT_STATE_TCP_OPEN; | |
| return 1; | |
| } | |
| static int tls_client_open_tls(tls_client *client) | |
| { | |
| if (client->state != TLS_CLIENT_STATE_TCP_OPEN) { | |
| fprintf(stderr, "tcp not opened\n"); | |
| return 0; | |
| } | |
| client->ctx = tls_client_ctx_create(&client->config); | |
| if (!client->ctx) { | |
| client->state = TLS_CLIENT_STATE_ERROR; | |
| return 0; | |
| } | |
| client->ssl = tls_client_ssl_create(client->ctx, client->fd, &client->config); | |
| if (!client->ssl) { | |
| client->state = TLS_CLIENT_STATE_ERROR; | |
| return 0; | |
| } | |
| client->state = TLS_CLIENT_STATE_TLS_OPEN; | |
| return 1; | |
| } | |
| static int tls_client_do_handshake(tls_client *client) | |
| { | |
| int rc; | |
| if (client->state != TLS_CLIENT_STATE_TLS_OPEN) { | |
| fprintf(stderr, "tls not ready\n"); | |
| return 0; | |
| } | |
| client->state = TLS_CLIENT_STATE_HANDSHAKING; | |
| rc = SSL_connect(client->ssl); | |
| if (rc != 1) { | |
| long vr = SSL_get_verify_result(client->ssl); | |
| fprintf(stderr, "SSL_connect failed\n"); | |
| fprintf(stderr, | |
| "verify_result=%ld: %s\n", | |
| vr, | |
| X509_verify_cert_error_string(vr)); | |
| ERR_print_errors_fp(stderr); | |
| client->state = TLS_CLIENT_STATE_ERROR; | |
| return 0; | |
| } | |
| client->state = TLS_CLIENT_STATE_READY; | |
| return 1; | |
| } | |
| static int tls_client_connect(tls_client *client) { | |
| if (!tls_client_open_tls(client)) { | |
| return 0; | |
| } | |
| if (!tls_client_open_tls(client)) { | |
| return 0; | |
| } | |
| if (!tls_client_do_handshake(client)) { | |
| return 0; | |
| } | |
| return 1; | |
| } | |
| static int tls_client_write_all(tls_client *client, const void *buf, size_t len) | |
| { | |
| const unsigned char *p = buf; | |
| size_t written = 0; | |
| if (client->state != TLS_CLIENT_STATE_READY) { | |
| fprintf(stderr, "connection not ready\n"); | |
| return 0; | |
| } | |
| while (written < len) { | |
| int n = SSL_write(client->ssl, | |
| p + written, | |
| (int)(len - written)); | |
| if (n <= 0) { | |
| print_openssl_error("SSL_write failed"); | |
| client->state = TLS_CLIENT_STATE_ERROR; | |
| return 0; | |
| } | |
| written += n; | |
| } | |
| return 1; | |
| } | |
| static int tls_client_read_to_stdout(tls_client *client) | |
| { | |
| char buf[4096]; | |
| int n; | |
| if (client->state != TLS_CLIENT_STATE_READY) { | |
| fprintf(stderr, "connection not ready\n"); | |
| return 0; | |
| } | |
| while ((n = SSL_read(client->ssl, buf, sizeof(buf))) > 0) { | |
| if (fwrite(buf, 1, n, stdout) != (size_t)n) { | |
| perror("fwrite"); | |
| client->state = TLS_CLIENT_STATE_ERROR; | |
| return 0; | |
| } | |
| } | |
| return 1; | |
| } | |
| static void tls_client_close(tls_client *client) | |
| { | |
| if (!client) return; | |
| if (client->ssl) { | |
| SSL_shutdown(client->ssl); | |
| SSL_free(client->ssl); | |
| client->ssl = NULL; | |
| } | |
| if (client->ctx) { | |
| SSL_CTX_free(client->ctx); | |
| client->ctx = NULL; | |
| } | |
| if (client->fd >= 0) { | |
| close(client->fd); | |
| client->fd = -1; | |
| } | |
| client->state = TLS_CLIENT_STATE_CLOSED; | |
| } | |
| int main(void) | |
| { | |
| tls_client client; | |
| tls_client_config cfg = { | |
| .host = "example.com", | |
| .port = "443", | |
| .server_name = "example.com", | |
| .ca_file = NULL, | |
| .ca_path = NULL, | |
| .min_proto_version = TLS1_2_VERSION, | |
| .verify_peer = 1 | |
| }; | |
| const char *req = | |
| "GET / HTTP/1.1\r\n" | |
| "Host: example.com\r\n" | |
| "Connection: close\r\n" | |
| "\r\n"; | |
| tls_client_init(&client, &cfg); | |
| if (!tls_client_open_tcp(&client)) | |
| goto cleanup; | |
| if (!tls_client_open_tls(&client)) | |
| goto cleanup; | |
| if (!tls_client_do_handshake(&client)) | |
| goto cleanup; | |
| if (!tls_client_write_all(&client, req, strlen(req))) | |
| goto cleanup; | |
| tls_client_read_to_stdout(&client); | |
| cleanup: | |
| tls_client_close(&client); | |
| return 0; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment