summaryrefslogtreecommitdiffstats
path: root/nts_ke_server.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-09 12:48:01 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-09 12:48:01 +0000
commitb2d2d555a704148968cb7e566735a2a1b1a2f189 (patch)
tree18549ff498338f40ecf7aa327620abf4c1c3ee43 /nts_ke_server.c
parentInitial commit. (diff)
downloadchrony-b2d2d555a704148968cb7e566735a2a1b1a2f189.tar.xz
chrony-b2d2d555a704148968cb7e566735a2a1b1a2f189.zip
Adding upstream version 4.5.upstream/4.5upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'nts_ke_server.c')
-rw-r--r--nts_ke_server.c1036
1 files changed, 1036 insertions, 0 deletions
diff --git a/nts_ke_server.c b/nts_ke_server.c
new file mode 100644
index 0000000..3fe99db
--- /dev/null
+++ b/nts_ke_server.c
@@ -0,0 +1,1036 @@
+/*
+ chronyd/chronyc - Programs for keeping computer clocks accurate.
+
+ **********************************************************************
+ * Copyright (C) Miroslav Lichvar 2020, 2022
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ **********************************************************************
+
+ =======================================================================
+
+ NTS-KE server
+ */
+
+#include "config.h"
+
+#include "sysincl.h"
+
+#include "nts_ke_server.h"
+
+#include "array.h"
+#include "conf.h"
+#include "clientlog.h"
+#include "local.h"
+#include "logging.h"
+#include "memory.h"
+#include "ntp_core.h"
+#include "nts_ke_session.h"
+#include "privops.h"
+#include "siv.h"
+#include "socket.h"
+#include "sched.h"
+#include "sys.h"
+#include "util.h"
+
+#define SERVER_TIMEOUT 2.0
+
+#define MAX_COOKIE_NONCE_LENGTH 16
+
+#define KEY_ID_INDEX_BITS 2
+#define MAX_SERVER_KEYS (1U << KEY_ID_INDEX_BITS)
+#define FUTURE_KEYS 1
+
+#define DUMP_FILENAME "ntskeys"
+#define DUMP_IDENTIFIER "NKS1\n"
+#define OLD_DUMP_IDENTIFIER "NKS0\n"
+
+#define INVALID_SOCK_FD (-7)
+
+typedef struct {
+ uint32_t key_id;
+} ServerCookieHeader;
+
+typedef struct {
+ uint32_t id;
+ unsigned char key[SIV_MAX_KEY_LENGTH];
+ SIV_Algorithm siv_algorithm;
+ SIV_Instance siv;
+ int nonce_length;
+} ServerKey;
+
+typedef struct {
+ uint32_t key_id;
+ uint32_t siv_algorithm;
+ unsigned char key[SIV_MAX_KEY_LENGTH];
+ IPAddr client_addr;
+ uint16_t client_port;
+ uint16_t _pad;
+} HelperRequest;
+
+/* ================================================== */
+
+static ServerKey server_keys[MAX_SERVER_KEYS];
+static int current_server_key;
+static double last_server_key_ts;
+static int key_rotation_interval;
+
+static int server_sock_fd4;
+static int server_sock_fd6;
+
+static int helper_sock_fd;
+static int is_helper;
+
+static int initialised = 0;
+
+/* Array of NKSN instances */
+static ARR_Instance sessions;
+static NKSN_Credentials server_credentials;
+
+/* ================================================== */
+
+static int handle_message(void *arg);
+
+/* ================================================== */
+
+static int
+handle_client(int sock_fd, IPSockAddr *addr)
+{
+ NKSN_Instance inst, *instp;
+ int i;
+
+ /* Leave at least half of the descriptors which can handled by select()
+ to other use */
+ if (sock_fd > FD_SETSIZE / 2) {
+ DEBUG_LOG("Rejected connection from %s (%s)",
+ UTI_IPSockAddrToString(addr), "too many descriptors");
+ return 0;
+ }
+
+ /* Find an unused server slot or one with an already stopped session */
+ for (i = 0, inst = NULL; i < ARR_GetSize(sessions); i++) {
+ instp = ARR_GetElement(sessions, i);
+ if (!*instp) {
+ /* NULL handler arg will be replaced with the session instance */
+ inst = NKSN_CreateInstance(1, NULL, handle_message, NULL);
+ *instp = inst;
+ break;
+ } else if (NKSN_IsStopped(*instp)) {
+ inst = *instp;
+ break;
+ }
+ }
+
+ if (!inst) {
+ DEBUG_LOG("Rejected connection from %s (%s)",
+ UTI_IPSockAddrToString(addr), "too many connections");
+ return 0;
+ }
+
+ assert(server_credentials);
+
+ if (!NKSN_StartSession(inst, sock_fd, UTI_IPSockAddrToString(addr),
+ server_credentials, SERVER_TIMEOUT))
+ return 0;
+
+ return 1;
+}
+
+/* ================================================== */
+
+static void
+update_key_siv(ServerKey *key, SIV_Algorithm algorithm)
+{
+ if (!key->siv || key->siv_algorithm != algorithm) {
+ if (key->siv)
+ SIV_DestroyInstance(key->siv);
+ key->siv_algorithm = algorithm;
+ key->siv = SIV_CreateInstance(algorithm);
+ key->nonce_length = MIN(SIV_GetMaxNonceLength(key->siv), MAX_COOKIE_NONCE_LENGTH);
+ }
+
+ if (!key->siv || !SIV_SetKey(key->siv, key->key, SIV_GetKeyLength(key->siv_algorithm)))
+ LOG_FATAL("Could not set SIV key");
+}
+
+/* ================================================== */
+
+static void
+handle_helper_request(int fd, int event, void *arg)
+{
+ SCK_Message *message;
+ HelperRequest *req;
+ IPSockAddr client_addr;
+ ServerKey *key;
+ int sock_fd;
+
+ /* Receive the helper request with the NTS-KE session socket.
+ With multiple helpers EAGAIN errors are expected here. */
+ message = SCK_ReceiveMessage(fd, SCK_FLAG_MSG_DESCRIPTOR);
+ if (!message)
+ return;
+
+ sock_fd = message->descriptor;
+ if (sock_fd < 0) {
+ /* Message with no descriptor is a shutdown command */
+ SCH_QuitProgram();
+ return;
+ }
+
+ if (!initialised) {
+ DEBUG_LOG("Uninitialised helper");
+ SCK_CloseSocket(sock_fd);
+ return;
+ }
+
+ if (message->length != sizeof (HelperRequest))
+ LOG_FATAL("Invalid helper request");
+
+ req = message->data;
+
+ /* Extract the current server key and client address from the request */
+ key = &server_keys[current_server_key];
+ key->id = ntohl(req->key_id);
+ assert(sizeof (key->key) == sizeof (req->key));
+ memcpy(key->key, req->key, sizeof (key->key));
+ UTI_IPNetworkToHost(&req->client_addr, &client_addr.ip_addr);
+ client_addr.port = ntohs(req->client_port);
+
+ update_key_siv(key, ntohl(req->siv_algorithm));
+
+ if (!handle_client(sock_fd, &client_addr)) {
+ SCK_CloseSocket(sock_fd);
+ return;
+ }
+
+ DEBUG_LOG("Accepted helper request fd=%d", sock_fd);
+}
+
+/* ================================================== */
+
+static void
+accept_connection(int listening_fd, int event, void *arg)
+{
+ SCK_Message message;
+ IPSockAddr addr;
+ int log_index, sock_fd;
+ struct timespec now;
+
+ sock_fd = SCK_AcceptConnection(listening_fd, &addr);
+ if (sock_fd < 0)
+ return;
+
+ if (!NCR_CheckAccessRestriction(&addr.ip_addr)) {
+ DEBUG_LOG("Rejected connection from %s (%s)",
+ UTI_IPSockAddrToString(&addr), "access denied");
+ SCK_CloseSocket(sock_fd);
+ return;
+ }
+
+ SCH_GetLastEventTime(&now, NULL, NULL);
+
+ log_index = CLG_LogServiceAccess(CLG_NTSKE, &addr.ip_addr, &now);
+ if (log_index >= 0 && CLG_LimitServiceRate(CLG_NTSKE, log_index)) {
+ DEBUG_LOG("Rejected connection from %s (%s)",
+ UTI_IPSockAddrToString(&addr), "rate limit");
+ SCK_CloseSocket(sock_fd);
+ return;
+ }
+
+ /* Pass the socket to a helper process if enabled. Otherwise, handle the
+ client in the main process. */
+ if (helper_sock_fd != INVALID_SOCK_FD) {
+ HelperRequest req;
+
+ memset(&req, 0, sizeof (req));
+
+ /* Include the current server key and client address in the request */
+ req.key_id = htonl(server_keys[current_server_key].id);
+ req.siv_algorithm = htonl(server_keys[current_server_key].siv_algorithm);
+ assert(sizeof (req.key) == sizeof (server_keys[current_server_key].key));
+ memcpy(req.key, server_keys[current_server_key].key, sizeof (req.key));
+ UTI_IPHostToNetwork(&addr.ip_addr, &req.client_addr);
+ req.client_port = htons(addr.port);
+
+ SCK_InitMessage(&message, SCK_ADDR_UNSPEC);
+ message.data = &req;
+ message.length = sizeof (req);
+ message.descriptor = sock_fd;
+
+ errno = 0;
+ if (!SCK_SendMessage(helper_sock_fd, &message, SCK_FLAG_MSG_DESCRIPTOR)) {
+ /* If sending failed with EPIPE, it means all helpers closed their end of
+ the socket (e.g. due to a fatal error) */
+ if (errno == EPIPE)
+ LOG_FATAL("NTS-KE helpers failed");
+ SCK_CloseSocket(sock_fd);
+ return;
+ }
+
+ SCK_CloseSocket(sock_fd);
+ } else {
+ if (!handle_client(sock_fd, &addr)) {
+ SCK_CloseSocket(sock_fd);
+ return;
+ }
+ }
+
+ DEBUG_LOG("Accepted connection from %s fd=%d", UTI_IPSockAddrToString(&addr), sock_fd);
+}
+
+/* ================================================== */
+
+static int
+open_socket(int family)
+{
+ IPSockAddr local_addr;
+ int backlog, sock_fd;
+ char *iface;
+
+ if (!SCK_IsIpFamilyEnabled(family))
+ return INVALID_SOCK_FD;
+
+ CNF_GetBindAddress(family, &local_addr.ip_addr);
+ local_addr.port = CNF_GetNtsServerPort();
+ iface = CNF_GetBindNtpInterface();
+
+ sock_fd = SCK_OpenTcpSocket(NULL, &local_addr, iface, 0);
+ if (sock_fd < 0) {
+ LOG(LOGS_ERR, "Could not open NTS-KE socket on %s", UTI_IPSockAddrToString(&local_addr));
+ return INVALID_SOCK_FD;
+ }
+
+ /* Set the maximum number of waiting connections on the socket to the maximum
+ number of concurrent sessions */
+ backlog = MAX(CNF_GetNtsServerProcesses(), 1) * CNF_GetNtsServerConnections();
+
+ if (!SCK_ListenOnSocket(sock_fd, backlog)) {
+ SCK_CloseSocket(sock_fd);
+ return INVALID_SOCK_FD;
+ }
+
+ SCH_AddFileHandler(sock_fd, SCH_FILE_INPUT, accept_connection, NULL);
+
+ return sock_fd;
+}
+
+/* ================================================== */
+
+static void
+helper_signal(int x)
+{
+ SCH_QuitProgram();
+}
+
+/* ================================================== */
+
+static int
+prepare_response(NKSN_Instance session, int error, int next_protocol, int aead_algorithm)
+{
+ NKE_Context context;
+ NKE_Cookie cookie;
+ char *ntp_server;
+ uint16_t datum;
+ int i;
+
+ DEBUG_LOG("NTS KE response: error=%d next=%d aead=%d", error, next_protocol, aead_algorithm);
+
+ NKSN_BeginMessage(session);
+
+ if (error >= 0) {
+ datum = htons(error);
+ if (!NKSN_AddRecord(session, 1, NKE_RECORD_ERROR, &datum, sizeof (datum)))
+ return 0;
+ } else if (next_protocol < 0) {
+ if (!NKSN_AddRecord(session, 1, NKE_RECORD_NEXT_PROTOCOL, NULL, 0))
+ return 0;
+ } else if (aead_algorithm < 0) {
+ datum = htons(next_protocol);
+ if (!NKSN_AddRecord(session, 1, NKE_RECORD_NEXT_PROTOCOL, &datum, sizeof (datum)))
+ return 0;
+ if (!NKSN_AddRecord(session, 1, NKE_RECORD_AEAD_ALGORITHM, NULL, 0))
+ return 0;
+ } else {
+ datum = htons(next_protocol);
+ if (!NKSN_AddRecord(session, 1, NKE_RECORD_NEXT_PROTOCOL, &datum, sizeof (datum)))
+ return 0;
+
+ datum = htons(aead_algorithm);
+ if (!NKSN_AddRecord(session, 1, NKE_RECORD_AEAD_ALGORITHM, &datum, sizeof (datum)))
+ return 0;
+
+ if (CNF_GetNTPPort() != NTP_PORT) {
+ datum = htons(CNF_GetNTPPort());
+ if (!NKSN_AddRecord(session, 1, NKE_RECORD_NTPV4_PORT_NEGOTIATION, &datum, sizeof (datum)))
+ return 0;
+ }
+
+ ntp_server = CNF_GetNtsNtpServer();
+ if (ntp_server) {
+ if (!NKSN_AddRecord(session, 1, NKE_RECORD_NTPV4_SERVER_NEGOTIATION,
+ ntp_server, strlen(ntp_server)))
+ return 0;
+ }
+
+ context.algorithm = aead_algorithm;
+
+ if (!NKSN_GetKeys(session, aead_algorithm, &context.c2s, &context.s2c))
+ return 0;
+
+ for (i = 0; i < NKE_MAX_COOKIES; i++) {
+ if (!NKS_GenerateCookie(&context, &cookie))
+ return 0;
+ if (!NKSN_AddRecord(session, 0, NKE_RECORD_COOKIE, cookie.cookie, cookie.length))
+ return 0;
+ }
+ }
+
+ if (!NKSN_EndMessage(session))
+ return 0;
+
+ return 1;
+}
+
+/* ================================================== */
+
+static int
+process_request(NKSN_Instance session)
+{
+ int next_protocol_records = 0, aead_algorithm_records = 0;
+ int next_protocol_values = 0, aead_algorithm_values = 0;
+ int next_protocol = -1, aead_algorithm = -1, error = -1;
+ int i, critical, type, length;
+ uint16_t data[NKE_MAX_RECORD_BODY_LENGTH / sizeof (uint16_t)];
+
+ assert(NKE_MAX_RECORD_BODY_LENGTH % sizeof (uint16_t) == 0);
+ assert(sizeof (uint16_t) == 2);
+
+ while (error < 0) {
+ if (!NKSN_GetRecord(session, &critical, &type, &length, &data, sizeof (data)))
+ break;
+
+ switch (type) {
+ case NKE_RECORD_NEXT_PROTOCOL:
+ if (!critical || length < 2 || length % 2 != 0) {
+ error = NKE_ERROR_BAD_REQUEST;
+ break;
+ }
+
+ next_protocol_records++;
+
+ for (i = 0; i < MIN(length, sizeof (data)) / 2; i++) {
+ next_protocol_values++;
+ if (ntohs(data[i]) == NKE_NEXT_PROTOCOL_NTPV4)
+ next_protocol = NKE_NEXT_PROTOCOL_NTPV4;
+ }
+ break;
+ case NKE_RECORD_AEAD_ALGORITHM:
+ if (length < 2 || length % 2 != 0) {
+ error = NKE_ERROR_BAD_REQUEST;
+ break;
+ }
+
+ aead_algorithm_records++;
+
+ for (i = 0; i < MIN(length, sizeof (data)) / 2; i++) {
+ aead_algorithm_values++;
+ /* Use the first supported algorithm */
+ if (aead_algorithm < 0 && SIV_GetKeyLength(ntohs(data[i])) > 0)
+ aead_algorithm = ntohs(data[i]);
+ }
+ break;
+ case NKE_RECORD_ERROR:
+ case NKE_RECORD_WARNING:
+ case NKE_RECORD_COOKIE:
+ error = NKE_ERROR_BAD_REQUEST;
+ break;
+ default:
+ if (critical)
+ error = NKE_ERROR_UNRECOGNIZED_CRITICAL_RECORD;
+ }
+ }
+
+ if (error < 0) {
+ if (next_protocol_records != 1 || next_protocol_values < 1 ||
+ (next_protocol == NKE_NEXT_PROTOCOL_NTPV4 &&
+ (aead_algorithm_records != 1 || aead_algorithm_values < 1)))
+ error = NKE_ERROR_BAD_REQUEST;
+ }
+
+ if (!prepare_response(session, error, next_protocol, aead_algorithm))
+ return 0;
+
+ return 1;
+}
+
+/* ================================================== */
+
+static int
+handle_message(void *arg)
+{
+ NKSN_Instance session = arg;
+
+ return process_request(session);
+}
+
+/* ================================================== */
+
+static void
+generate_key(int index)
+{
+ SIV_Algorithm algorithm;
+ ServerKey *key;
+ int key_length;
+
+ if (index < 0 || index >= MAX_SERVER_KEYS)
+ assert(0);
+
+ /* Prefer AES-128-GCM-SIV if available. Note that if older keys loaded
+ from ntsdumpdir use a different algorithm, responding to NTP requests
+ with cookies encrypted with those keys will not work if the new algorithm
+ produces longer cookies (i.e. response would be longer than request).
+ Switching from AES-SIV-CMAC-256 to AES-128-GCM-SIV is ok. */
+ algorithm = SIV_GetKeyLength(AEAD_AES_128_GCM_SIV) > 0 ?
+ AEAD_AES_128_GCM_SIV : AEAD_AES_SIV_CMAC_256;
+
+ key = &server_keys[index];
+
+ key_length = SIV_GetKeyLength(algorithm);
+ if (key_length > sizeof (key->key))
+ assert(0);
+
+ UTI_GetRandomBytesUrandom(key->key, key_length);
+ memset(key->key + key_length, 0, sizeof (key->key) - key_length);
+ UTI_GetRandomBytes(&key->id, sizeof (key->id));
+
+ /* Encode the index in the lowest bits of the ID */
+ key->id &= -1U << KEY_ID_INDEX_BITS;
+ key->id |= index;
+
+ update_key_siv(key, algorithm);
+
+ DEBUG_LOG("Generated key %08"PRIX32" (%d)", key->id, (int)key->siv_algorithm);
+
+ last_server_key_ts = SCH_GetLastEventMonoTime();
+}
+
+/* ================================================== */
+
+static void
+save_keys(void)
+{
+ char buf[SIV_MAX_KEY_LENGTH * 2 + 1], *dump_dir;
+ int i, index, key_length;
+ double last_key_age;
+ FILE *f;
+
+ /* Don't save the keys if rotation is disabled to enable an external
+ management of the keys (e.g. share them with another server) */
+ if (key_rotation_interval == 0)
+ return;
+
+ dump_dir = CNF_GetNtsDumpDir();
+ if (!dump_dir)
+ return;
+
+ f = UTI_OpenFile(dump_dir, DUMP_FILENAME, ".tmp", 'w', 0600);
+ if (!f)
+ return;
+
+ last_key_age = SCH_GetLastEventMonoTime() - last_server_key_ts;
+
+ if (fprintf(f, "%s%.1f\n", DUMP_IDENTIFIER, last_key_age) < 0)
+ goto error;
+
+ for (i = 0; i < MAX_SERVER_KEYS; i++) {
+ index = (current_server_key + i + 1 + FUTURE_KEYS) % MAX_SERVER_KEYS;
+ key_length = SIV_GetKeyLength(server_keys[index].siv_algorithm);
+
+ if (key_length > sizeof (server_keys[index].key) ||
+ !UTI_BytesToHex(server_keys[index].key, key_length, buf, sizeof (buf)) ||
+ fprintf(f, "%08"PRIX32" %s %d\n", server_keys[index].id, buf,
+ (int)server_keys[index].siv_algorithm) < 0)
+ goto error;
+ }
+
+ fclose(f);
+
+ /* Rename the temporary file, or remove it if that fails */
+ if (!UTI_RenameTempFile(dump_dir, DUMP_FILENAME, ".tmp", NULL)) {
+ if (!UTI_RemoveFile(dump_dir, DUMP_FILENAME, ".tmp"))
+ ;
+ }
+
+ return;
+
+error:
+ LOG(LOGS_ERR, "Could not %s %s", "save", "server NTS keys");
+ fclose(f);
+
+ if (!UTI_RemoveFile(dump_dir, DUMP_FILENAME, NULL))
+ ;
+}
+
+/* ================================================== */
+
+#define MAX_WORDS 3
+
+static int
+load_keys(void)
+{
+ int i, index, key_length, algorithm = 0, old_ver;
+ char *dump_dir, line[1024], *words[MAX_WORDS];
+ ServerKey new_keys[MAX_SERVER_KEYS];
+ double key_age;
+ FILE *f;
+
+ dump_dir = CNF_GetNtsDumpDir();
+ if (!dump_dir)
+ return 0;
+
+ f = UTI_OpenFile(dump_dir, DUMP_FILENAME, NULL, 'r', 0);
+ if (!f)
+ return 0;
+
+ if (!fgets(line, sizeof (line), f) ||
+ (strcmp(line, DUMP_IDENTIFIER) != 0 && strcmp(line, OLD_DUMP_IDENTIFIER) != 0))
+ goto error;
+
+ old_ver = strcmp(line, DUMP_IDENTIFIER) != 0;
+
+ if (!fgets(line, sizeof (line), f) ||
+ UTI_SplitString(line, words, MAX_WORDS) != (old_ver ? 2 : 1) ||
+ (old_ver && sscanf(words[0], "%d", &algorithm) != 1) ||
+ sscanf(words[old_ver ? 1 : 0], "%lf", &key_age) != 1)
+ goto error;
+
+ for (i = 0; i < MAX_SERVER_KEYS && fgets(line, sizeof (line), f); i++) {
+ if (UTI_SplitString(line, words, MAX_WORDS) != (old_ver ? 2 : 3) ||
+ sscanf(words[0], "%"PRIX32, &new_keys[i].id) != 1 ||
+ (!old_ver && sscanf(words[2], "%d", &algorithm) != 1))
+ goto error;
+
+ new_keys[i].siv_algorithm = algorithm;
+ key_length = SIV_GetKeyLength(algorithm);
+
+ if ((i > 0 && (new_keys[i].id - new_keys[i - 1].id) % MAX_SERVER_KEYS != 1) ||
+ key_length <= 0 ||
+ UTI_HexToBytes(words[1], new_keys[i].key, sizeof (new_keys[i].key)) != key_length)
+ goto error;
+ memset(new_keys[i].key + key_length, 0, sizeof (new_keys[i].key) - key_length);
+ }
+
+ if (i < MAX_SERVER_KEYS)
+ goto error;
+
+ for (i = 0; i < MAX_SERVER_KEYS; i++) {
+ index = new_keys[i].id % MAX_SERVER_KEYS;
+ server_keys[index].id = new_keys[i].id;
+ memcpy(server_keys[index].key, new_keys[i].key, sizeof (server_keys[index].key));
+
+ update_key_siv(&server_keys[index], new_keys[i].siv_algorithm);
+
+ DEBUG_LOG("Loaded key %08"PRIX32" (%d)",
+ server_keys[index].id, (int)server_keys[index].siv_algorithm);
+ }
+
+ current_server_key = (index + MAX_SERVER_KEYS - FUTURE_KEYS) % MAX_SERVER_KEYS;
+ last_server_key_ts = SCH_GetLastEventMonoTime() - MAX(key_age, 0.0);
+
+ fclose(f);
+
+ LOG(LOGS_INFO, "Loaded %s", "server NTS keys");
+ return 1;
+
+error:
+ LOG(LOGS_ERR, "Could not %s %s", "load", "server NTS keys");
+ fclose(f);
+
+ return 0;
+}
+
+/* ================================================== */
+
+static void
+key_timeout(void *arg)
+{
+ current_server_key = (current_server_key + 1) % MAX_SERVER_KEYS;
+ generate_key((current_server_key + FUTURE_KEYS) % MAX_SERVER_KEYS);
+ save_keys();
+
+ SCH_AddTimeoutByDelay(key_rotation_interval, key_timeout, NULL);
+}
+
+/* ================================================== */
+
+static void
+run_helper(uid_t uid, gid_t gid, int scfilter_level)
+{
+ LOG_Severity log_severity;
+
+ /* Finish minimal initialisation and run using the scheduler loop
+ similarly to the main process */
+
+ DEBUG_LOG("Helper started");
+
+ SCK_CloseReusableSockets();
+
+ /* Suppress a log message about disabled clock control */
+ log_severity = LOG_GetMinSeverity();
+ LOG_SetMinSeverity(LOGS_ERR);
+
+ SYS_Initialise(0);
+ LOG_SetMinSeverity(log_severity);
+
+ if (!geteuid() && (uid || gid))
+ SYS_DropRoot(uid, gid, SYS_NTSKE_HELPER);
+
+ NKS_Initialise();
+
+ UTI_SetQuitSignalsHandler(helper_signal, 1);
+ if (scfilter_level != 0)
+ SYS_EnableSystemCallFilter(scfilter_level, SYS_NTSKE_HELPER);
+
+ SCH_MainLoop();
+
+ DEBUG_LOG("Helper exiting");
+
+ NKS_Finalise();
+ SCK_Finalise();
+ SYS_Finalise();
+ SCH_Finalise();
+ LCL_Finalise();
+ PRV_Finalise();
+ CNF_Finalise();
+ LOG_Finalise();
+
+ UTI_ResetGetRandomFunctions();
+
+ exit(0);
+}
+
+/* ================================================== */
+
+void
+NKS_PreInitialise(uid_t uid, gid_t gid, int scfilter_level)
+{
+ int i, processes, sock_fd1, sock_fd2;
+ const char **certs, **keys;
+ char prefix[16];
+ pid_t pid;
+
+ helper_sock_fd = INVALID_SOCK_FD;
+ is_helper = 0;
+
+ if (CNF_GetNtsServerCertAndKeyFiles(&certs, &keys) <= 0)
+ return;
+
+ processes = CNF_GetNtsServerProcesses();
+ if (processes <= 0)
+ return;
+
+ /* Start helper processes to perform (computationally expensive) NTS-KE
+ sessions with clients on sockets forwarded from the main process */
+
+ sock_fd1 = SCK_OpenUnixSocketPair(0, &sock_fd2);
+ if (sock_fd1 < 0)
+ LOG_FATAL("Could not open socket pair");
+
+ for (i = 0; i < processes; i++) {
+ pid = fork();
+
+ if (pid < 0)
+ LOG_FATAL("fork() failed : %s", strerror(errno));
+
+ if (pid > 0)
+ continue;
+
+ is_helper = 1;
+
+ UTI_ResetGetRandomFunctions();
+
+ snprintf(prefix, sizeof (prefix), "nks#%d:", i + 1);
+ LOG_SetDebugPrefix(prefix);
+ LOG_CloseParentFd();
+
+ SCK_CloseSocket(sock_fd1);
+ SCH_AddFileHandler(sock_fd2, SCH_FILE_INPUT, handle_helper_request, NULL);
+
+ run_helper(uid, gid, scfilter_level);
+ }
+
+ SCK_CloseSocket(sock_fd2);
+ helper_sock_fd = sock_fd1;
+}
+
+/* ================================================== */
+
+void
+NKS_Initialise(void)
+{
+ const char **certs, **keys;
+ int i, n_certs_keys;
+ double key_delay;
+
+ server_sock_fd4 = INVALID_SOCK_FD;
+ server_sock_fd6 = INVALID_SOCK_FD;
+
+ n_certs_keys = CNF_GetNtsServerCertAndKeyFiles(&certs, &keys);
+ if (n_certs_keys <= 0)
+ return;
+
+ if (helper_sock_fd == INVALID_SOCK_FD) {
+ server_credentials = NKSN_CreateServerCertCredentials(certs, keys, n_certs_keys);
+ if (!server_credentials)
+ return;
+ } else {
+ server_credentials = NULL;
+ }
+
+ sessions = ARR_CreateInstance(sizeof (NKSN_Instance));
+ for (i = 0; i < CNF_GetNtsServerConnections(); i++)
+ *(NKSN_Instance *)ARR_GetNewElement(sessions) = NULL;
+
+ /* Generate random keys, even if they will be replaced by reloaded keys,
+ or unused (in the helper) */
+ for (i = 0; i < MAX_SERVER_KEYS; i++) {
+ server_keys[i].siv = NULL;
+ generate_key(i);
+ }
+
+ current_server_key = MAX_SERVER_KEYS - 1;
+
+ if (!is_helper) {
+ server_sock_fd4 = open_socket(IPADDR_INET4);
+ server_sock_fd6 = open_socket(IPADDR_INET6);
+
+ key_rotation_interval = MAX(CNF_GetNtsRotate(), 0);
+
+ /* Reload saved keys, or save the new keys */
+ if (!load_keys())
+ save_keys();
+
+ if (key_rotation_interval > 0) {
+ key_delay = key_rotation_interval - (SCH_GetLastEventMonoTime() - last_server_key_ts);
+ SCH_AddTimeoutByDelay(MAX(key_delay, 0.0), key_timeout, NULL);
+ }
+
+ /* Warn if keys are not saved, which can cause a flood of requests
+ after server restart */
+ if (!CNF_GetNtsDumpDir())
+ LOG(LOGS_WARN, "No ntsdumpdir to save server keys");
+ }
+
+ initialised = 1;
+}
+
+/* ================================================== */
+
+void
+NKS_Finalise(void)
+{
+ int i;
+
+ if (!initialised)
+ return;
+
+ if (helper_sock_fd != INVALID_SOCK_FD) {
+ /* Send the helpers a request to exit */
+ for (i = 0; i < CNF_GetNtsServerProcesses(); i++) {
+ if (!SCK_Send(helper_sock_fd, "", 1, 0))
+ ;
+ }
+ SCK_CloseSocket(helper_sock_fd);
+ }
+ if (server_sock_fd4 != INVALID_SOCK_FD)
+ SCK_CloseSocket(server_sock_fd4);
+ if (server_sock_fd6 != INVALID_SOCK_FD)
+ SCK_CloseSocket(server_sock_fd6);
+
+ if (!is_helper)
+ save_keys();
+
+ for (i = 0; i < MAX_SERVER_KEYS; i++)
+ SIV_DestroyInstance(server_keys[i].siv);
+
+ for (i = 0; i < ARR_GetSize(sessions); i++) {
+ NKSN_Instance session = *(NKSN_Instance *)ARR_GetElement(sessions, i);
+ if (session)
+ NKSN_DestroyInstance(session);
+ }
+ ARR_DestroyInstance(sessions);
+
+ if (server_credentials)
+ NKSN_DestroyCertCredentials(server_credentials);
+}
+
+/* ================================================== */
+
+void
+NKS_DumpKeys(void)
+{
+ save_keys();
+}
+
+/* ================================================== */
+
+void
+NKS_ReloadKeys(void)
+{
+ /* Don't load the keys if they are expected to be generated by this server
+ instance (i.e. they are already loaded) to not delay the next rotation */
+ if (key_rotation_interval > 0)
+ return;
+
+ load_keys();
+}
+
+/* ================================================== */
+
+/* A server cookie consists of key ID, nonce, and encrypted C2S+S2C keys */
+
+int
+NKS_GenerateCookie(NKE_Context *context, NKE_Cookie *cookie)
+{
+ unsigned char *nonce, plaintext[2 * NKE_MAX_KEY_LENGTH], *ciphertext;
+ int plaintext_length, tag_length;
+ ServerCookieHeader *header;
+ ServerKey *key;
+
+ if (!initialised) {
+ DEBUG_LOG("NTS server disabled");
+ return 0;
+ }
+
+ /* The AEAD ID is not encoded in the cookie. It is implied from the key
+ length (as long as only algorithms with different key lengths are
+ supported). */
+
+ if (context->c2s.length < 0 || context->c2s.length > NKE_MAX_KEY_LENGTH ||
+ context->s2c.length != context->c2s.length) {
+ DEBUG_LOG("Invalid key length");
+ return 0;
+ }
+
+ key = &server_keys[current_server_key];
+
+ header = (ServerCookieHeader *)cookie->cookie;
+
+ header->key_id = htonl(key->id);
+
+ nonce = cookie->cookie + sizeof (*header);
+ if (key->nonce_length > sizeof (cookie->cookie) - sizeof (*header))
+ assert(0);
+ UTI_GetRandomBytes(nonce, key->nonce_length);
+
+ plaintext_length = context->c2s.length + context->s2c.length;
+ assert(plaintext_length <= sizeof (plaintext));
+ memcpy(plaintext, context->c2s.key, context->c2s.length);
+ memcpy(plaintext + context->c2s.length, context->s2c.key, context->s2c.length);
+
+ tag_length = SIV_GetTagLength(key->siv);
+ cookie->length = sizeof (*header) + key->nonce_length + plaintext_length + tag_length;
+ assert(cookie->length <= sizeof (cookie->cookie));
+ ciphertext = cookie->cookie + sizeof (*header) + key->nonce_length;
+
+ if (!SIV_Encrypt(key->siv, nonce, key->nonce_length,
+ "", 0,
+ plaintext, plaintext_length,
+ ciphertext, plaintext_length + tag_length)) {
+ DEBUG_LOG("Could not encrypt cookie");
+ return 0;
+ }
+
+ return 1;
+}
+
+/* ================================================== */
+
+int
+NKS_DecodeCookie(NKE_Cookie *cookie, NKE_Context *context)
+{
+ unsigned char *nonce, plaintext[2 * NKE_MAX_KEY_LENGTH], *ciphertext;
+ int ciphertext_length, plaintext_length, tag_length;
+ ServerCookieHeader *header;
+ ServerKey *key;
+ uint32_t key_id;
+
+ if (!initialised) {
+ DEBUG_LOG("NTS server disabled");
+ return 0;
+ }
+
+ if (cookie->length <= (int)sizeof (*header)) {
+ DEBUG_LOG("Invalid cookie length");
+ return 0;
+ }
+
+ header = (ServerCookieHeader *)cookie->cookie;
+
+ key_id = ntohl(header->key_id);
+ key = &server_keys[key_id % MAX_SERVER_KEYS];
+ if (key_id != key->id) {
+ DEBUG_LOG("Unknown key %"PRIX32, key_id);
+ return 0;
+ }
+
+ tag_length = SIV_GetTagLength(key->siv);
+
+ if (cookie->length <= (int)sizeof (*header) + key->nonce_length + tag_length) {
+ DEBUG_LOG("Invalid cookie length");
+ return 0;
+ }
+
+ nonce = cookie->cookie + sizeof (*header);
+ ciphertext = cookie->cookie + sizeof (*header) + key->nonce_length;
+ ciphertext_length = cookie->length - sizeof (*header) - key->nonce_length;
+ plaintext_length = ciphertext_length - tag_length;
+
+ if (plaintext_length > sizeof (plaintext) || plaintext_length % 2 != 0) {
+ DEBUG_LOG("Invalid cookie length");
+ return 0;
+ }
+
+ if (!SIV_Decrypt(key->siv, nonce, key->nonce_length,
+ "", 0,
+ ciphertext, ciphertext_length,
+ plaintext, plaintext_length)) {
+ DEBUG_LOG("Could not decrypt cookie");
+ return 0;
+ }
+
+ /* Select a supported algorithm corresponding to the key length, avoiding
+ potentially slow SIV_GetKeyLength() */
+ switch (plaintext_length / 2) {
+ case 16:
+ context->algorithm = AEAD_AES_128_GCM_SIV;
+ break;
+ case 32:
+ context->algorithm = AEAD_AES_SIV_CMAC_256;
+ break;
+ default:
+ DEBUG_LOG("Unknown key length");
+ return 0;
+ }
+
+ context->c2s.length = plaintext_length / 2;
+ context->s2c.length = plaintext_length / 2;
+ assert(context->c2s.length <= sizeof (context->c2s.key));
+
+ memcpy(context->c2s.key, plaintext, context->c2s.length);
+ memcpy(context->s2c.key, plaintext + context->c2s.length, context->s2c.length);
+
+ return 1;
+}