blob: ece1b4c01b21f208c54f37f458d4425511394085 [file] [log] [blame]
/*
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 SERVER_COOKIE_SIV AEAD_AES_SIV_CMAC_256
#define SERVER_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;
unsigned char nonce[SERVER_COOKIE_NONCE_LENGTH];
} ServerCookieHeader;
typedef struct {
uint32_t id;
unsigned char key[SIV_MAX_KEY_LENGTH];
SIV_Instance siv;
} ServerKey;
typedef struct {
uint32_t key_id;
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
handle_helper_request(int fd, int event, void *arg)
{
SCK_Message *message;
HelperRequest *req;
IPSockAddr client_addr;
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 */
server_keys[current_server_key].id = ntohl(req->key_id);
assert(sizeof (server_keys[current_server_key].key) == sizeof (req->key));
memcpy(server_keys[current_server_key].key, req->key,
sizeof (server_keys[current_server_key].key));
UTI_IPNetworkToHost(&req->client_addr, &client_addr.ip_addr);
client_addr.port = ntohs(req->client_port);
if (!SIV_SetKey(server_keys[current_server_key].siv, server_keys[current_server_key].key,
SIV_GetKeyLength(SERVER_COOKIE_SIV)))
LOG_FATAL("Could not set SIV key");
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);
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++;
if (ntohs(data[i]) == AEAD_AES_SIV_CMAC_256)
aead_algorithm = AEAD_AES_SIV_CMAC_256;
}
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)
{
int key_length;
if (index < 0 || index >= MAX_SERVER_KEYS)
assert(0);
key_length = SIV_GetKeyLength(SERVER_COOKIE_SIV);
if (key_length > sizeof (server_keys[index].key))
assert(0);
UTI_GetRandomBytesUrandom(server_keys[index].key, key_length);
if (!server_keys[index].siv ||
!SIV_SetKey(server_keys[index].siv, server_keys[index].key, key_length))
LOG_FATAL("Could not set SIV key");
UTI_GetRandomBytes(&server_keys[index].id, sizeof (server_keys[index].id));
/* Encode the index in the lowest bits of the ID */
server_keys[index].id &= -1U << KEY_ID_INDEX_BITS;
server_keys[index].id |= index;
DEBUG_LOG("Generated server key %"PRIX32, server_keys[index].id);
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_COOKIE_SIV);
last_key_age = SCH_GetLastEventMonoTime() - last_server_key_ts;
if (fprintf(f, "%s%d %.1f\n", DUMP_IDENTIFIER, SERVER_COOKIE_SIV, 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) ||
!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 || algorithm != SERVER_COOKIE_SIV ||
sscanf(words[1], "%lf", &key_age) != 1)
goto error;
key_length = SIV_GetKeyLength(SERVER_COOKIE_SIV);
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);
if (!SIV_SetKey(server_keys[index].siv, server_keys[index].key, key_length))
LOG_FATAL("Could not set SIV key");
DEBUG_LOG("Loaded key %"PRIX32, id);
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 = SIV_CreateInstance(SERVER_COOKIE_SIV);
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 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 algorithm is hardcoded for now */
if (context->algorithm != AEAD_AES_SIV_CMAC_256) {
DEBUG_LOG("Unexpected SIV algorithm");
return 0;
}
if (context->c2s.length < 0 || context->c2s.length > NKE_MAX_KEY_LENGTH ||
context->s2c.length < 0 || context->s2c.length > NKE_MAX_KEY_LENGTH) {
DEBUG_LOG("Invalid key length");
return 0;
}
key = &server_keys[current_server_key];
header = (ServerCookieHeader *)cookie->cookie;
header->key_id = htonl(key->id);
UTI_GetRandomBytes(header->nonce, sizeof (header->nonce));
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) + plaintext_length + tag_length;
assert(cookie->length <= sizeof (cookie->cookie));
ciphertext = cookie->cookie + sizeof (*header);
if (!SIV_Encrypt(key->siv, header->nonce, sizeof (header->nonce),
"", 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 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;
ciphertext = cookie->cookie + sizeof (*header);
ciphertext_length = cookie->length - sizeof (*header);
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 (tag_length >= ciphertext_length) {
DEBUG_LOG("Invalid cookie length");
return 0;
}
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, header->nonce, sizeof (header->nonce),
"", 0,
ciphertext, ciphertext_length,
plaintext, plaintext_length)) {
DEBUG_LOG("Could not decrypt cookie");
return 0;
}
context->algorithm = AEAD_AES_SIV_CMAC_256;
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;
}