If AES-128-GCM-SIV is available on the server, use it for encryption of cookies. This makes them shorter by 4 bytes due to shorter nonce and it might also improve the server performance. After server upgrade and restart with ntsdumpdir, the switch will happen on the second rotation of the server key. Clients should accept shorter cookies without restarting NTS-KE. The first response will have extra padding in the authenticator field to make the length symmetric.
1013 lines
27 KiB
C
1013 lines
27 KiB
C
/*
|
|
chronyd/chronyc - Programs for keeping computer clocks accurate.
|
|
|
|
**********************************************************************
|
|
* Copyright (C) Miroslav Lichvar 2020
|
|
*
|
|
* 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 "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);
|
|
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 %"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;
|
|
|
|
key_length = SIV_GetKeyLength(server_keys[0].siv_algorithm);
|
|
last_key_age = SCH_GetLastEventMonoTime() - last_server_key_ts;
|
|
|
|
if (fprintf(f, "%s%d %.1f\n", DUMP_IDENTIFIER, (int)server_keys[0].siv_algorithm,
|
|
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;
|
|
|
|
if (key_length > sizeof (server_keys[index].key) ||
|
|
server_keys[index].siv_algorithm != server_keys[0].siv_algorithm ||
|
|
!UTI_BytesToHex(server_keys[index].key, key_length, buf, sizeof (buf)) ||
|
|
fprintf(f, "%08"PRIX32" %s\n", server_keys[index].id, buf) < 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:
|
|
DEBUG_LOG("Could not %s server keys", "save");
|
|
fclose(f);
|
|
|
|
if (!UTI_RemoveFile(dump_dir, DUMP_FILENAME, NULL))
|
|
;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
#define MAX_WORDS 2
|
|
|
|
static int
|
|
load_keys(void)
|
|
{
|
|
char *dump_dir, line[1024], *words[MAX_WORDS];
|
|
unsigned char key[SIV_MAX_KEY_LENGTH];
|
|
int i, index, key_length, algorithm;
|
|
double key_age;
|
|
FILE *f;
|
|
uint32_t id;
|
|
|
|
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 ||
|
|
!fgets(line, sizeof (line), f) || UTI_SplitString(line, words, MAX_WORDS) != 2 ||
|
|
sscanf(words[0], "%d", &algorithm) != 1 || SIV_GetKeyLength(algorithm) <= 0 ||
|
|
sscanf(words[1], "%lf", &key_age) != 1)
|
|
goto error;
|
|
|
|
key_length = SIV_GetKeyLength(algorithm);
|
|
last_server_key_ts = SCH_GetLastEventMonoTime() - MAX(key_age, 0.0);
|
|
|
|
for (i = 0; i < MAX_SERVER_KEYS && fgets(line, sizeof (line), f); i++) {
|
|
if (UTI_SplitString(line, words, MAX_WORDS) != 2 ||
|
|
sscanf(words[0], "%"PRIX32, &id) != 1)
|
|
goto error;
|
|
|
|
if (UTI_HexToBytes(words[1], key, sizeof (key)) != key_length)
|
|
goto error;
|
|
|
|
index = id % MAX_SERVER_KEYS;
|
|
|
|
server_keys[index].id = id;
|
|
assert(sizeof (server_keys[index].key) == sizeof (key));
|
|
memcpy(server_keys[index].key, key, key_length);
|
|
|
|
update_key_siv(&server_keys[index], algorithm);
|
|
|
|
DEBUG_LOG("Loaded key %"PRIX32" (%d)", id, (int)algorithm);
|
|
|
|
current_server_key = (index + MAX_SERVER_KEYS - FUTURE_KEYS) % MAX_SERVER_KEYS;
|
|
}
|
|
|
|
fclose(f);
|
|
|
|
return 1;
|
|
|
|
error:
|
|
DEBUG_LOG("Could not %s server keys", "load");
|
|
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");
|
|
|
|
/* 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);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|