C5: restrict the TLS 1.2 and below cipher list to ECDHE AEAD suites (ECDHE+AESGCM:ECDHE+CHACHA20, minus NULL/eNULL/MD5/RC4/3DES) instead of HIGH (which includes CBC), and set SSL_OP_CIPHER_SERVER_PREFERENCE so the server's order decides the negotiated cipher. Client and server share create_ssl_ctx, so both are updated. C7: load the private key through an O_RDONLY|O_NOFOLLOW|O_CLOEXEC fd, fstat that fd and validate owner/mode (now also rejecting group/other execute bits), then load from the fd via BIO_new_fd. This removes the stat-to-load TOCTOU race while keeping the exact-owner/0600 policy. C8: verify an IP-literal client hostname against the certificate IP SAN with X509_VERIFY_PARAM_set1_ip_asc instead of SSL_set1_host (a DNS check), falling back to SSL_set1_host for real names. Unit tests assert the server-preference option, the absence of CBC/RC4/ 3DES suites, and that context creation still succeeds.
315 lines
10 KiB
C
315 lines
10 KiB
C
#include "transport_tls.h"
|
|
#include "log.h"
|
|
#include "protocol.h"
|
|
#include "transport_tcp.h"
|
|
#include "utils.h"
|
|
#include <arpa/inet.h>
|
|
#include <fcntl.h>
|
|
#include <openssl/err.h>
|
|
#include <openssl/pem.h>
|
|
#include <openssl/ssl.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/wait.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
bool tls_global_init(void) {
|
|
#if OPENSSL_VERSION_NUMBER < 0x10100000L
|
|
SSL_library_init();
|
|
OpenSSL_add_all_algorithms();
|
|
SSL_load_error_strings();
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
static void log_ssl_errors(void) {
|
|
unsigned long err;
|
|
char buf[256];
|
|
while ((err = ERR_get_error()) != 0) {
|
|
ERR_error_string_n(err, buf, sizeof(buf));
|
|
log_message(LOG_LEVEL_ERROR, "SSL error: %s", buf);
|
|
}
|
|
}
|
|
|
|
/* Load the TLS private key through an already-opened, no-follow descriptor so
|
|
* the owner/mode policy is checked on the SAME file object that is loaded: an
|
|
* attacker cannot swap the path between a stat() and a later open() (TOCTOU).
|
|
* The exact-owner / 0600 policy is preserved and group/other execute bits are
|
|
* rejected as well. Ownership of the descriptor passes to the BIO and is
|
|
* released exactly once by BIO_free() (BIO_CLOSE). */
|
|
static bool load_private_key_secure(SSL_CTX* ctx, const char* key) {
|
|
int fd = open(key, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
|
|
if (fd < 0) {
|
|
char* escaped = output_escape(key, false);
|
|
log_message(LOG_LEVEL_ERROR, "Failed to open private key: %s",
|
|
escaped ? escaped : "<allocation failed>");
|
|
free(escaped);
|
|
return false;
|
|
}
|
|
struct stat key_stat;
|
|
if (fstat(fd, &key_stat) != 0 || !S_ISREG(key_stat.st_mode) || key_stat.st_uid != geteuid() ||
|
|
(key_stat.st_mode & (S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH | S_IXGRP | S_IXOTH))) {
|
|
log_message(LOG_LEVEL_ERROR,
|
|
"TLS private key must be a regular file owned by the current user and private "
|
|
"(mode 0600)");
|
|
close(fd);
|
|
return false;
|
|
}
|
|
BIO* bio = BIO_new_fd(fd, BIO_CLOSE);
|
|
if (!bio) {
|
|
close(fd);
|
|
log_message(LOG_LEVEL_ERROR, "Failed to read private key");
|
|
return false;
|
|
}
|
|
EVP_PKEY* pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL);
|
|
BIO_free(bio); /* releases fd via BIO_CLOSE */
|
|
if (!pkey) {
|
|
char* escaped = output_escape(key, false);
|
|
log_message(LOG_LEVEL_ERROR, "Failed to load private key: %s",
|
|
escaped ? escaped : "<allocation failed>");
|
|
free(escaped);
|
|
log_ssl_errors();
|
|
return false;
|
|
}
|
|
int use_ok = SSL_CTX_use_PrivateKey(ctx, pkey);
|
|
EVP_PKEY_free(pkey);
|
|
if (use_ok != 1) {
|
|
char* escaped = output_escape(key, false);
|
|
log_message(LOG_LEVEL_ERROR, "Failed to use private key: %s",
|
|
escaped ? escaped : "<allocation failed>");
|
|
free(escaped);
|
|
log_ssl_errors();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static SSL_CTX* create_ssl_ctx(bool is_server, const char* cert, const char* key,
|
|
const char* ca_path) {
|
|
if (!is_server && !ca_path) {
|
|
log_message(LOG_LEVEL_ERROR, "TLS clients require a CA certificate path");
|
|
return NULL;
|
|
}
|
|
const SSL_METHOD* method = is_server ? TLS_server_method() : TLS_client_method();
|
|
SSL_CTX* ctx = SSL_CTX_new(method);
|
|
if (!ctx) {
|
|
log_message(LOG_LEVEL_ERROR, "Unable to create SSL context");
|
|
log_ssl_errors();
|
|
return NULL;
|
|
}
|
|
|
|
/* Harden the context: never negotiate TLS compression (the CRIME attack
|
|
* vector) and never honour a post-handshake renegotiation request.
|
|
* SSL_OP_NO_RENEGOTIATION is only available from OpenSSL 1.1.1, so it is
|
|
* guarded to keep older headers building. */
|
|
SSL_CTX_set_options(ctx, SSL_OP_NO_COMPRESSION);
|
|
#ifdef SSL_OP_NO_RENEGOTIATION
|
|
SSL_CTX_set_options(ctx, SSL_OP_NO_RENEGOTIATION);
|
|
#endif
|
|
/* Let the server's own preference order decide the negotiated cipher rather
|
|
* than the client's, so a client cannot steer both peers into a weaker (but
|
|
* still offered) suite. */
|
|
SSL_CTX_set_options(ctx, SSL_OP_CIPHER_SERVER_PREFERENCE);
|
|
|
|
if (SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION) != 1) {
|
|
SSL_CTX_free(ctx);
|
|
return NULL;
|
|
}
|
|
/* TLS 1.2 and below: an AEAD-only suite list. "HIGH" still includes CBC
|
|
* suites (Lucky13/POODLE-adjacent MAC-then-encrypt constructions), so restrict
|
|
* the list to ECDHE key agreement with an AEAD record cipher (AES-GCM or
|
|
* ChaCha20-Poly1305). A NULL/weak/3DES cipher is never selectable. */
|
|
if (SSL_CTX_set_cipher_list(ctx, "ECDHE+AESGCM:ECDHE+CHACHA20:!aNULL:!eNULL:!MD5:!RC4:!3DES") !=
|
|
1) {
|
|
SSL_CTX_free(ctx);
|
|
return NULL;
|
|
}
|
|
/* TLS 1.3 ciphersuites are configured separately from the TLS 1.2 and below
|
|
* cipher list above. Pin the three AEAD suites OpenSSL offers, dropping
|
|
* TLS_AES_128_CCM_SHA256 and the CCM_8 variant, and fail closed if the
|
|
* library rejects the policy. SSL_CTX_set_ciphersuites needs OpenSSL 1.1.1;
|
|
* earlier versions have no TLS 1.3, so the call is compile-guarded. */
|
|
#if OPENSSL_VERSION_NUMBER >= 0x10101000L
|
|
if (SSL_CTX_set_ciphersuites(
|
|
ctx, "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256") != 1) {
|
|
SSL_CTX_free(ctx);
|
|
return NULL;
|
|
}
|
|
#endif
|
|
|
|
if (cert && key) {
|
|
if (SSL_CTX_use_certificate_file(ctx, cert, SSL_FILETYPE_PEM) <= 0) {
|
|
char* escaped = output_escape(cert, false);
|
|
log_message(LOG_LEVEL_ERROR, "Failed to load certificate: %s",
|
|
escaped ? escaped : "<allocation failed>");
|
|
free(escaped);
|
|
log_ssl_errors();
|
|
SSL_CTX_free(ctx);
|
|
return NULL;
|
|
}
|
|
if (!load_private_key_secure(ctx, key)) {
|
|
SSL_CTX_free(ctx);
|
|
return NULL;
|
|
}
|
|
if (!SSL_CTX_check_private_key(ctx)) {
|
|
log_message(LOG_LEVEL_ERROR, "Private key does not match certificate");
|
|
SSL_CTX_free(ctx);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
if (ca_path) {
|
|
if (!SSL_CTX_load_verify_locations(ctx, ca_path, NULL)) {
|
|
char* escaped = output_escape(ca_path, false);
|
|
log_message(LOG_LEVEL_ERROR, "Failed to load CA: %s",
|
|
escaped ? escaped : "<allocation failed>");
|
|
free(escaped);
|
|
log_ssl_errors();
|
|
SSL_CTX_free(ctx);
|
|
return NULL;
|
|
}
|
|
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL);
|
|
SSL_CTX_set_verify_depth(ctx, 4);
|
|
} else {
|
|
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
|
|
}
|
|
|
|
return ctx;
|
|
}
|
|
|
|
static SSL* wrap_fd_with_ssl(int fd, SSL_CTX* ctx, bool is_server, const char* hostname) {
|
|
SSL* ssl = SSL_new(ctx);
|
|
if (!ssl) {
|
|
log_message(LOG_LEVEL_ERROR, "Failed to create SSL object");
|
|
return NULL;
|
|
}
|
|
if (SSL_set_fd(ssl, fd) != 1) {
|
|
SSL_free(ssl);
|
|
return NULL;
|
|
}
|
|
|
|
// Enable hostname verification for client connections when a hostname is provided.
|
|
// Must be done before SSL_connect to take effect during the handshake.
|
|
if (!is_server && hostname) {
|
|
/* An IP-literal host must be verified against the certificate's IP SAN
|
|
* (X509_check_ip_asc), not as a DNS name: SSL_set1_host would look for a
|
|
* DNS SAN that a legitimate IP-SAN certificate never carries. */
|
|
struct in_addr ipv4;
|
|
struct in6_addr ipv6;
|
|
bool is_ip_literal =
|
|
inet_pton(AF_INET, hostname, &ipv4) == 1 || inet_pton(AF_INET6, hostname, &ipv6) == 1;
|
|
int set_ok = is_ip_literal ? X509_VERIFY_PARAM_set1_ip_asc(SSL_get0_param(ssl), hostname)
|
|
: SSL_set1_host(ssl, hostname);
|
|
if (set_ok != 1) {
|
|
SSL_free(ssl);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
// Retry SSL_accept/SSL_connect on WANT_READ/WANT_WRITE (non-blocking handshake)
|
|
time_t deadline = time(NULL) + (is_server ? tcp_get_timeout_sec() : tcp_get_contimeout_sec());
|
|
int ret;
|
|
do {
|
|
if (is_server)
|
|
ret = SSL_accept(ssl);
|
|
else
|
|
ret = SSL_connect(ssl);
|
|
|
|
if (ret <= 0) {
|
|
int ssl_err = SSL_get_error(ssl, ret);
|
|
if ((ssl_err == SSL_ERROR_WANT_READ || ssl_err == SSL_ERROR_WANT_WRITE) &&
|
|
time(NULL) < deadline)
|
|
continue;
|
|
log_message(LOG_LEVEL_ERROR, "SSL %s failed", is_server ? "accept" : "connect");
|
|
log_ssl_errors();
|
|
SSL_free(ssl);
|
|
return NULL;
|
|
}
|
|
} while (ret <= 0);
|
|
return ssl;
|
|
}
|
|
|
|
bool server_create_tls(Server* server, const char* cert_path, const char* key_path,
|
|
const char* ca_path) {
|
|
SSL_CTX* ctx = create_ssl_ctx(true, cert_path, key_path, ca_path);
|
|
if (!ctx)
|
|
return false;
|
|
server->ssl_ctx = ctx;
|
|
return true;
|
|
}
|
|
|
|
struct tls_child_ctx {
|
|
void (*handler)(int);
|
|
SSL_CTX* ssl_ctx;
|
|
};
|
|
|
|
static void tls_child_fn(int fd, void* arg) {
|
|
struct tls_child_ctx* ctx = (struct tls_child_ctx*)arg;
|
|
SSL* ssl = wrap_fd_with_ssl(fd, ctx->ssl_ctx, true, NULL);
|
|
if (!ssl) {
|
|
io_set_ssl(NULL);
|
|
close(fd);
|
|
return;
|
|
}
|
|
io_set_ssl(ssl);
|
|
ctx->handler(fd);
|
|
/* Shut the TLS layer down before releasing the fd: handler() no longer
|
|
* closes it, so SSL_shutdown still has a valid socket. The child owns the
|
|
* single fd close, performed last. */
|
|
SSL_shutdown(ssl);
|
|
SSL_free(ssl);
|
|
io_set_ssl(NULL);
|
|
close(fd);
|
|
}
|
|
|
|
bool server_listen_tls(Server* server, void (*handler)(int file_descriptor)) {
|
|
struct tls_child_ctx ctx = {handler, (SSL_CTX*)server->ssl_ctx};
|
|
server_accept_loop(server, tls_child_fn, &ctx, "Received TLS Connection");
|
|
return true;
|
|
}
|
|
|
|
bool client_connect_tls_ex(Client* client, const char* host, int port, const char* cert_path,
|
|
const char* key_path, const char* ca_path,
|
|
const TcpConnectOptions* opts) {
|
|
if (!tcp_connect_socket_ex(client, host, port, opts)) {
|
|
if (client->file_descriptor >= 0)
|
|
close(client->file_descriptor);
|
|
client->file_descriptor = -1;
|
|
return false;
|
|
}
|
|
|
|
SSL_CTX* ctx = create_ssl_ctx(false, cert_path, key_path, ca_path);
|
|
if (!ctx) {
|
|
close(client->file_descriptor);
|
|
client->file_descriptor = -1;
|
|
return false;
|
|
}
|
|
client->ssl_ctx = ctx;
|
|
|
|
// Pass the server hostname for TLS hostname verification (SSL_set1_host
|
|
// is called inside wrap_fd_with_ssl before the handshake when ca_path is set).
|
|
const char* verify_host = ca_path ? host : NULL;
|
|
SSL* ssl = wrap_fd_with_ssl(client->file_descriptor, ctx, false, verify_host);
|
|
if (!ssl) {
|
|
SSL_CTX_free(ctx);
|
|
client->ssl_ctx = NULL;
|
|
close(client->file_descriptor);
|
|
client->file_descriptor = -1;
|
|
return false;
|
|
}
|
|
|
|
client->ssl = ssl;
|
|
io_set_ssl(ssl);
|
|
return true;
|
|
}
|
|
|
|
bool client_connect_tls(Client* client, const char* host, int port, const char* cert_path,
|
|
const char* key_path, const char* ca_path) {
|
|
return client_connect_tls_ex(client, host, port, cert_path, key_path, ca_path, NULL);
|
|
}
|