Merge branch 'fix/sec-server' into fix/sec-integration
This commit is contained in:
+60
-6
@@ -37,6 +37,15 @@ static bool allow_unauthenticated;
|
||||
* root), so no super-user activity is attempted and any client --copy-as is
|
||||
* refused. Set once in main before the accept loop / stdio handler. */
|
||||
static bool server_no_super;
|
||||
/* --allow-super: locally-launched standalone TCP opt-in that preserves the
|
||||
* historical permissive super mode for a root receiver. When false, a
|
||||
* privileged standalone receiver forces SUPER_MODE_OFF for every connection
|
||||
* (C3), so a client cannot make it create device nodes / write raw devices /
|
||||
* apply client-chosen ownership. It is REJECTED for --stdio (the SSH remote
|
||||
* argv is composed by the client, so it must never be able to opt a root
|
||||
* receiver back into super mode); the --stdio path always keeps the secure
|
||||
* default. */
|
||||
static bool server_allow_super;
|
||||
static const char* required_client_cn;
|
||||
/* --iconv CONVERT_SPEC the server was itself started with (borrowed argv
|
||||
* pointer). Its LOCAL half may override the local charset the client assumed;
|
||||
@@ -187,13 +196,25 @@ static bool tls_client_identity_allowed(SSL* ssl) {
|
||||
X509* certificate = SSL_get1_peer_certificate(ssl);
|
||||
if (!certificate)
|
||||
return false;
|
||||
char common_name[256];
|
||||
int length = X509_NAME_get_text_by_NID(X509_get_subject_name(certificate), NID_commonName,
|
||||
common_name, sizeof(common_name));
|
||||
size_t required_length = strlen(required_client_cn);
|
||||
bool allowed = length >= 0 && (size_t)length == required_length &&
|
||||
required_length < sizeof(common_name) &&
|
||||
credentials_secure_equal(common_name, required_client_cn, required_length);
|
||||
bool allowed = false;
|
||||
X509_NAME* subject = X509_get_subject_name(certificate);
|
||||
int index = subject ? X509_NAME_get_index_by_NID(subject, NID_commonName, -1) : -1;
|
||||
if (index >= 0) {
|
||||
X509_NAME_ENTRY* entry = X509_NAME_get_entry(subject, index);
|
||||
ASN1_STRING* data = entry ? X509_NAME_ENTRY_get_data(entry) : NULL;
|
||||
/* Convert the CN to UTF-8 to get its FULL byte length: unlike
|
||||
* X509_NAME_get_text_by_NID (which truncates an over-long CN to the buffer
|
||||
* and reports the truncated length), ASN1_STRING_to_UTF8 never truncates, so
|
||||
* an exactly-required-length CN is accepted while an over-long one cannot be
|
||||
* prefix-matched by a shorter required name. */
|
||||
unsigned char* utf8 = NULL;
|
||||
int cn_length = data ? ASN1_STRING_to_UTF8(&utf8, data) : -1;
|
||||
if (cn_length >= 0 && (size_t)cn_length == required_length)
|
||||
allowed = credentials_secure_equal((const char*)utf8, required_client_cn, required_length);
|
||||
if (utf8)
|
||||
OPENSSL_free(utf8);
|
||||
}
|
||||
X509_free(certificate);
|
||||
return allowed;
|
||||
}
|
||||
@@ -592,6 +613,22 @@ static const char* server_module_gate(const Config* config, void* context) {
|
||||
if (gate_ctx)
|
||||
gate_ctx->super_mode_override = SUPER_MODE_OFF;
|
||||
}
|
||||
/* C3: a privileged (root) STANDALONE receiver defaults to SUPER_MODE_OFF.
|
||||
* Without this a client --devices/--write-devices/--super would let a root
|
||||
* server create arbitrary device nodes and write raw devices, and
|
||||
* client-chosen ownership (--numeric-ids/--chown/--usermap/--groupmap) would
|
||||
* be applied, with no operator opt-in. The operator must pass --allow-super
|
||||
* to restore the historical permissive behavior; the flag is rejected for
|
||||
* --stdio, whose client-composed argv must never defeat this default (an
|
||||
* operator exposing `fastsync-server --stdio` over SSH needs a forced command
|
||||
* to keep the permissive behavior). An unprivileged receiver is unaffected
|
||||
* (the kernel refuses the confined attempts) and the daemon path keeps its
|
||||
* per-module `client owner = yes` gate. */
|
||||
if (g_daemon_conf == NULL && geteuid() == 0 && !server_allow_super) {
|
||||
effective.super_mode = SUPER_MODE_OFF;
|
||||
if (gate_ctx)
|
||||
gate_ctx->super_mode_override = SUPER_MODE_OFF;
|
||||
}
|
||||
/* --copy-as (P7 Wave E, protocol 2.18.0): FastSync's safe subset forces the
|
||||
ownership of every written entry to the requested ids, which needs a
|
||||
privileged (root) receiver. An unprivileged receiver REFUSES the whole
|
||||
@@ -751,6 +788,13 @@ void handler(int file_descriptor) {
|
||||
goto done;
|
||||
}
|
||||
config->use_delete = config->use_delete && allow_delete;
|
||||
/* --force (receiver-side) is deletion authority too: it lets an incoming
|
||||
* regular file recursively remove a non-empty destination directory tree, and
|
||||
* lets --delete-missing-args remove a non-empty directory mirror. Without
|
||||
* the operator's --allow-delete it must be inert, exactly like --delete and
|
||||
* --delete-missing-args, so a client cannot use --force to bypass the delete
|
||||
* policy. */
|
||||
config->force_delete = config->force_delete && allow_delete;
|
||||
/* --iconv (protocol 2.16.0): install the receiver-side wire->local conversion
|
||||
now that the client's full CONVERT_SPEC has been received and validated,
|
||||
before any received file name is decoded. The server's own --iconv (if
|
||||
@@ -1010,6 +1054,13 @@ static void print_server_usage(void) {
|
||||
printf(" --no-super Operator veto: never attempt super-user activities\n");
|
||||
printf(" (ownership, device nodes) even as root, and refuse\n");
|
||||
printf(" any client --copy-as/--super request\n");
|
||||
printf(" --allow-super Standalone TCP listener only: keep super-user\n");
|
||||
printf(" activities enabled for a root receiver. Without it a\n");
|
||||
printf(" root standalone server forces SUPER_MODE_OFF, so client\n");
|
||||
printf(" --devices/--write-devices/--super and ownership\n");
|
||||
printf(" requests are refused/skipped. Never honored with\n");
|
||||
printf(" --stdio (the SSH remote argv is client-composed, so\n");
|
||||
printf(" super stays off there); no effect when not root\n");
|
||||
printf(" --iconv=LOCAL[,REMOTE] Declare this server's LOCAL charset for file-name\n");
|
||||
printf(" conversion: received names are translated to this\n");
|
||||
printf(" charset (the wire charset still comes from the\n");
|
||||
@@ -1134,6 +1185,9 @@ int main(int argc, char* argv[]) {
|
||||
trust_sender = opts.trust_sender;
|
||||
allow_unauthenticated = opts.allow_unauthenticated;
|
||||
server_no_super = opts.no_super;
|
||||
/* --stdio rejects --allow-super at parse time; force it off here as well so
|
||||
* this process-global policy cannot be re-enabled by a future caller. */
|
||||
server_allow_super = opts.allow_super && !opts.stdio_mode;
|
||||
server_iconv_spec = opts.iconv_spec;
|
||||
signal(SIGINT, cleanup);
|
||||
signal(SIGTERM, cleanup);
|
||||
|
||||
@@ -179,6 +179,8 @@ int server_cli_parse(int argc, char* argv[], ServerCliOptions* opts, char* err,
|
||||
opts->trust_sender = true;
|
||||
} else if (arg_is(argv[i], "--no-super")) {
|
||||
opts->no_super = true;
|
||||
} else if (arg_is(argv[i], "--allow-super")) {
|
||||
opts->allow_super = true;
|
||||
} else if (arg_is(argv[i], "--allow-unauthenticated")) {
|
||||
opts->allow_unauthenticated = true;
|
||||
} else if (arg_has_value(argv[i], "--iconv", &inline_value)) {
|
||||
@@ -259,6 +261,28 @@ int server_cli_parse(int argc, char* argv[], ServerCliOptions* opts, char* err,
|
||||
set_error(err, err_size, "--hash-credentials cannot be combined with --daemon or --stdio");
|
||||
return -1;
|
||||
}
|
||||
if (opts->allow_super && opts->no_super) {
|
||||
set_error(err, err_size, "--allow-super and --no-super are mutually exclusive");
|
||||
return -1;
|
||||
}
|
||||
if (opts->allow_super && opts->daemon_mode) {
|
||||
set_error(err, err_size,
|
||||
"--allow-super is for a locally-launched standalone TCP server; daemon modules opt "
|
||||
"in per module with 'client owner = yes'");
|
||||
return -1;
|
||||
}
|
||||
/* --stdio is the SSH transport: the remote server argv is composed by the
|
||||
* CLIENT (directly and via --remote-option), so a client could otherwise pass
|
||||
* --allow-super to a root --stdio receiver and defeat the C3 secure default.
|
||||
* Never honor it there; the super mode stays forced OFF. An operator who
|
||||
* must keep the historical permissive behavior over SSH has to launch the
|
||||
* receiver through a forced command, not via client-composed argv. */
|
||||
if (opts->allow_super && opts->stdio_mode) {
|
||||
set_error(err, err_size,
|
||||
"--allow-super is not accepted with --stdio (the remote argv is client-composed; "
|
||||
"use a forced command if the default must hold)");
|
||||
return -1;
|
||||
}
|
||||
if (opts->hash_iterations_set && opts->hash_credentials_file == NULL) {
|
||||
set_error(err, err_size, "--iterations requires --hash-credentials");
|
||||
return -1;
|
||||
|
||||
@@ -45,6 +45,17 @@ typedef struct ServerCliOptions {
|
||||
* device-node creation) even when running as root. Applies to --stdio and
|
||||
* --daemon alike; also makes the server refuse any client --copy-as. */
|
||||
bool no_super; /* --no-super */
|
||||
/* --allow-super: locally-launched standalone TCP listener opt-in that keeps
|
||||
* the historical permissive behavior for a PRIVILEGED (root) receiver.
|
||||
* Without it a root standalone server forces SUPER_MODE_OFF, so a client
|
||||
* --devices / --write-devices / --super / ownership request cannot make it
|
||||
* create device nodes, write raw devices, or apply client-chosen ownership.
|
||||
* It is rejected for --stdio: that path's remote argv is composed by the
|
||||
* client (directly and via --remote-option), so it must never opt a root
|
||||
* receiver back into super mode. Non-root receivers are unaffected (the
|
||||
* kernel refuses the confined attempts). The daemon path instead uses the
|
||||
* per-module `client owner = yes` opt-in. */
|
||||
bool allow_super; /* --allow-super */
|
||||
/* --iconv=CONVERT_SPEC: the server's own LOCAL charset declaration. The
|
||||
* client's full spec rides the wire config frame anyway; when the server is
|
||||
* started with its own --iconv, its LOCAL half overrides the local charset
|
||||
|
||||
+23
-6
@@ -613,19 +613,36 @@ int config_parse_transport_dest(Config* config) {
|
||||
int daemon_ret = config_parse_daemon_dest(config);
|
||||
if (daemon_ret != 0)
|
||||
return daemon_ret;
|
||||
config_parse_ssh_dest(config);
|
||||
return 0;
|
||||
/* 0 for a local destination (nothing parsed) or a valid SSH destination;
|
||||
* -1 (already logged) for an injection-shaped user@host. */
|
||||
return config_parse_ssh_dest(config);
|
||||
}
|
||||
|
||||
void config_parse_ssh_dest(Config* config) {
|
||||
int config_parse_ssh_dest(Config* config) {
|
||||
if (!config || !config->receive_root_directory)
|
||||
return 0;
|
||||
if (!config_is_remote_dest(config->receive_root_directory))
|
||||
return;
|
||||
return 0;
|
||||
const char* dest = config->receive_root_directory;
|
||||
const char* colon = strchr(dest, ':');
|
||||
/* The user@host token is passed to ssh in option position, so a user or host
|
||||
* beginning with '-' would be consumed by ssh as an option (argument
|
||||
* injection: e.g. "-oProxyCommand=..."). An empty host is likewise not a
|
||||
* valid destination. Validate before any wire/argv construction. */
|
||||
const char* at = memchr(dest, '@', (size_t)(colon - dest));
|
||||
const char* host = at ? at + 1 : dest;
|
||||
size_t host_len = (size_t)(colon - host);
|
||||
size_t user_len = at ? (size_t)(at - dest) : 0;
|
||||
if (host_len == 0 || host[0] == '-' || (user_len > 0 && dest[0] == '-'))
|
||||
return daemon_dest_parse_error("invalid remote destination user@host (must not be empty or "
|
||||
"start with '-')",
|
||||
dest);
|
||||
config->transport = TRANSPORT_SSH;
|
||||
config->ssh_destination = str_dup(config->receive_root_directory);
|
||||
const char* colon = strchr(config->receive_root_directory, ':');
|
||||
config->ssh_destination = str_dup(dest);
|
||||
char* path = str_dup(colon + 1);
|
||||
free(config->receive_root_directory);
|
||||
config->receive_root_directory = path;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void config_burn_auth(Config* config) {
|
||||
|
||||
+4
-1
@@ -851,7 +851,10 @@ bool config_send(int file_descriptor, const Config* config);
|
||||
bool config_send_wire_block(int file_descriptor, const Config* config);
|
||||
Config* config_receive(int file_descriptor);
|
||||
bool config_is_remote_dest(const char* s);
|
||||
void config_parse_ssh_dest(Config* config);
|
||||
/* Parse a single-colon host:path SSH destination (0 = not an SSH destination or
|
||||
* parsed successfully, -1 = rejected, e.g. a user@host beginning with '-'; the
|
||||
* reason is logged). */
|
||||
int config_parse_ssh_dest(Config* config);
|
||||
|
||||
/* A ConfigValidateFunc may return this sentinel to tell
|
||||
* config_receive_with_validate that the callback ALREADY sent a terminal status
|
||||
|
||||
@@ -158,13 +158,13 @@ static int hex_value(char c) {
|
||||
* digits. Such a line is refused loudly (and never accepted) so an operator
|
||||
* cannot keep a replayable bearer digest in place after the protocol bump. */
|
||||
static bool secret_is_legacy_hex(const char* s) {
|
||||
if (!s)
|
||||
if (!s || strlen(s) != 64)
|
||||
return false;
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if (hex_value(s[i]) < 0)
|
||||
return false;
|
||||
}
|
||||
return s[64] == '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
bool credentials_b64_encode(const uint8_t* in, size_t n, char* out, size_t out_sz) {
|
||||
|
||||
@@ -133,6 +133,7 @@ static bool store_host_list(char*** list, int* count, const char* value, const c
|
||||
return false;
|
||||
}
|
||||
char* save = NULL;
|
||||
int added = 0;
|
||||
for (char* token = strtok_r(copy, ", \t", &save); token; token = strtok_r(NULL, ", \t", &save)) {
|
||||
if (!host_pattern_valid(token)) {
|
||||
if (module_name)
|
||||
@@ -163,8 +164,20 @@ static bool store_host_list(char*** list, int* count, const char* value, const c
|
||||
return false;
|
||||
}
|
||||
(*list)[(*count)++] = dup;
|
||||
added++;
|
||||
}
|
||||
free(copy);
|
||||
/* A present key with an empty (or separator-only) value would otherwise
|
||||
* install a zero-length list, i.e. no ACL at all: a strict-parse config must
|
||||
* never silently turn a restrictive directive into "allow everyone". */
|
||||
if (added == 0) {
|
||||
if (module_name)
|
||||
set_error(err, err_size, "module '%s': '%s' must list at least one host pattern", module_name,
|
||||
key);
|
||||
else
|
||||
set_error(err, err_size, "'%s' must list at least one host pattern", key);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -403,6 +416,7 @@ static bool apply_module_key(DaemonModule* module, char* key, char* value, char*
|
||||
return false;
|
||||
}
|
||||
char* save = NULL;
|
||||
int added = 0;
|
||||
for (char* token = strtok_r(list, ",", &save); token; token = strtok_r(NULL, ",", &save)) {
|
||||
const char* user = trim_ws(token);
|
||||
if (*user == '\0')
|
||||
@@ -430,8 +444,16 @@ static bool apply_module_key(DaemonModule* module, char* key, char* value, char*
|
||||
return false;
|
||||
}
|
||||
module->auth_users[module->auth_user_count++] = dup;
|
||||
added++;
|
||||
}
|
||||
free(list);
|
||||
/* An empty/separator-only value must not silently disable authentication:
|
||||
* the key's presence is an explicit request for an allow-list. */
|
||||
if (added == 0) {
|
||||
set_error(err, err_size, "module '%s': 'auth users' must list at least one user",
|
||||
module->name);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (key_equals(key, "max connections"))
|
||||
@@ -439,10 +461,10 @@ static bool apply_module_key(DaemonModule* module, char* key, char* value, char*
|
||||
"max connections", module->name, err, err_size);
|
||||
if (key_equals(key, "hosts allow"))
|
||||
return store_host_list(&module->hosts_allow, &module->hosts_allow_count, value, "hosts allow",
|
||||
false, module->name, err, err_size);
|
||||
module->name, false, err, err_size);
|
||||
if (key_equals(key, "hosts deny"))
|
||||
return store_host_list(&module->hosts_deny, &module->hosts_deny_count, value, "hosts deny",
|
||||
false, module->name, err, err_size);
|
||||
module->name, false, err, err_size);
|
||||
set_error(err, err_size, "unknown key '%s' in module '%s'", key, module->name);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,15 @@ static int parse_remote_dest(const char* dest, RemoteDest* r) {
|
||||
memcpy(r->host, dest, host_len);
|
||||
r->host[host_len] = '\0';
|
||||
}
|
||||
/* The user@host token is handed to ssh in option position. Reject anything
|
||||
* that ssh would consume as an option (a leading '-') or an empty host, so a
|
||||
* crafted destination can never inject an ssh option such as
|
||||
* -oProxyCommand=... . This mirrors config_parse_ssh_dest's validation and
|
||||
* is defense-in-depth for callers that bypass it. */
|
||||
if (r->host[0] == '\0' || r->host[0] == '-' || (r->user[0] != '\0' && r->user[0] == '-')) {
|
||||
remote_dest_destroy(r);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -216,10 +225,10 @@ char** ssh_build_client_argv(const char* rsh_command, int port, const char* user
|
||||
nwords = 1;
|
||||
}
|
||||
|
||||
/* Fixed tail: three -o pairs (6) + optional -p/value (2) + user@host +
|
||||
* remote command + terminating NULL. */
|
||||
/* Fixed tail: three -o pairs (6) + optional -p/value (2) + the "--" end of
|
||||
* options marker + user@host + remote command + terminating NULL. */
|
||||
int port_extra = (port > 0 && port != 22) ? 2 : 0;
|
||||
size_t total = (size_t)nwords + 6 + (size_t)port_extra + 3;
|
||||
size_t total = (size_t)nwords + 6 + (size_t)port_extra + 4;
|
||||
char** argv = calloc(total, sizeof(char*));
|
||||
if (!argv) {
|
||||
for (int i = 0; i < nwords; i++)
|
||||
@@ -253,6 +262,13 @@ char** ssh_build_client_argv(const char* rsh_command, int port, const char* user
|
||||
goto fail_argv;
|
||||
ac++;
|
||||
}
|
||||
/* End of options: guarantees the user@host token that follows is treated as
|
||||
* the destination and never re-interpreted as an ssh option, even if every
|
||||
* caller-side validation were bypassed. */
|
||||
argv[ac] = str_dup("--");
|
||||
if (!argv[ac])
|
||||
goto fail_argv;
|
||||
ac++;
|
||||
argv[ac] = str_dup(userhost);
|
||||
if (!argv[ac])
|
||||
goto fail_argv;
|
||||
|
||||
+76
-15
@@ -4,7 +4,9 @@
|
||||
#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>
|
||||
@@ -34,6 +36,59 @@ static void log_ssl_errors(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
@@ -56,12 +111,21 @@ static SSL_CTX* create_ssl_ctx(bool is_server, const char* cert, const char* key
|
||||
#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;
|
||||
}
|
||||
if (SSL_CTX_set_cipher_list(ctx, "HIGH:!aNULL:!eNULL:!MD5:!RC4:!3DES") != 1) {
|
||||
/* 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;
|
||||
}
|
||||
@@ -79,13 +143,6 @@ static SSL_CTX* create_ssl_ctx(bool is_server, const char* cert, const char* key
|
||||
#endif
|
||||
|
||||
if (cert && key) {
|
||||
struct stat key_stat;
|
||||
if (stat(key, &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))) {
|
||||
log_message(LOG_LEVEL_ERROR, "TLS private key must be owned by the current user and private");
|
||||
SSL_CTX_free(ctx);
|
||||
return NULL;
|
||||
}
|
||||
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",
|
||||
@@ -95,12 +152,7 @@ static SSL_CTX* create_ssl_ctx(bool is_server, const char* cert, const char* key
|
||||
SSL_CTX_free(ctx);
|
||||
return NULL;
|
||||
}
|
||||
if (SSL_CTX_use_PrivateKey_file(ctx, key, SSL_FILETYPE_PEM) <= 0) {
|
||||
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();
|
||||
if (!load_private_key_secure(ctx, key)) {
|
||||
SSL_CTX_free(ctx);
|
||||
return NULL;
|
||||
}
|
||||
@@ -144,7 +196,16 @@ static SSL* wrap_fd_with_ssl(int fd, SSL_CTX* ctx, bool is_server, const char* h
|
||||
// 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) {
|
||||
if (SSL_set1_host(ssl, hostname) != 1) {
|
||||
/* 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user