When the NTS client and server negotiated use of AES-128-GCM-SIV keys, the keys exported from the TLS session and used for authentication and encryption of NTP messages do not comply to RFC8915. The exporter context value specified in the section 5.1 of RFC8915 function is incorrect. It is a hardcoded string which contains 15 (AES-SIV-CMAC-256) instead of 30 (AES-128-GCM-SIV). This causes chrony to not interoperate with NTS implementations that follow RFC8915 correctly. (At this time, there doesn't seem to be another implementation with AES-128-GCM-SIV support yet.) Replace the string with a proper construction of the exporter context from a specified AEAD ID and next protocol. Keep using the incorrect AEAD ID for AES-128-GCM-SIV to not break compatibility with existing chrony servers and clients. A new NTS-KE record will be added to negotiate the compliant exporter context. Reported-by: Martin Mayer <martin.mayer@m2-it-solutions.de>
465 lines
13 KiB
C
465 lines
13 KiB
C
/*
|
|
chronyd/chronyc - Programs for keeping computer clocks accurate.
|
|
|
|
**********************************************************************
|
|
* Copyright (C) Miroslav Lichvar 2020-2021
|
|
*
|
|
* 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 client
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include "sysincl.h"
|
|
|
|
#include "nts_ke_client.h"
|
|
|
|
#include "conf.h"
|
|
#include "logging.h"
|
|
#include "memory.h"
|
|
#include "nameserv_async.h"
|
|
#include "nts_ke_session.h"
|
|
#include "siv.h"
|
|
#include "socket.h"
|
|
#include "util.h"
|
|
|
|
#define CLIENT_TIMEOUT 16.0
|
|
|
|
struct NKC_Instance_Record {
|
|
char *name;
|
|
IPSockAddr address;
|
|
NKSN_Credentials credentials;
|
|
NKSN_Instance session;
|
|
int destroying;
|
|
int got_response;
|
|
int resolving_name;
|
|
|
|
NKE_Context context;
|
|
NKE_Cookie cookies[NKE_MAX_COOKIES];
|
|
int num_cookies;
|
|
char server_name[NKE_MAX_RECORD_BODY_LENGTH + 2];
|
|
IPSockAddr ntp_address;
|
|
};
|
|
|
|
/* ================================================== */
|
|
|
|
static NKSN_Credentials default_credentials = NULL;
|
|
static int default_credentials_refs = 0;
|
|
|
|
/* ================================================== */
|
|
|
|
static void
|
|
name_resolve_handler(DNS_Status status, int n_addrs, IPAddr *ip_addrs, void *arg)
|
|
{
|
|
NKC_Instance inst = arg;
|
|
int i;
|
|
|
|
inst->resolving_name = 0;
|
|
|
|
if (inst->destroying) {
|
|
Free(inst);
|
|
return;
|
|
}
|
|
|
|
if (status != DNS_Success || n_addrs < 1) {
|
|
LOG(LOGS_ERR, "Could not resolve NTP server %s from %s", inst->server_name, inst->name);
|
|
/* Force restart */
|
|
inst->got_response = 0;
|
|
return;
|
|
}
|
|
|
|
inst->ntp_address.ip_addr = ip_addrs[0];
|
|
|
|
/* Prefer an address in the same family as the NTS-KE server */
|
|
for (i = 0; i < n_addrs; i++) {
|
|
DEBUG_LOG("%s resolved to %s", inst->server_name, UTI_IPToString(&ip_addrs[i]));
|
|
if (ip_addrs[i].family == inst->address.ip_addr.family) {
|
|
inst->ntp_address.ip_addr = ip_addrs[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static int
|
|
prepare_request(NKC_Instance inst)
|
|
{
|
|
NKSN_Instance session = inst->session;
|
|
uint16_t data[2];
|
|
int length;
|
|
|
|
NKSN_BeginMessage(session);
|
|
|
|
data[0] = htons(NKE_NEXT_PROTOCOL_NTPV4);
|
|
if (!NKSN_AddRecord(session, 1, NKE_RECORD_NEXT_PROTOCOL, data, sizeof (data[0])))
|
|
return 0;
|
|
|
|
length = 0;
|
|
if (SIV_GetKeyLength(AEAD_AES_128_GCM_SIV) > 0)
|
|
data[length++] = htons(AEAD_AES_128_GCM_SIV);
|
|
if (SIV_GetKeyLength(AEAD_AES_SIV_CMAC_256) > 0)
|
|
data[length++] = htons(AEAD_AES_SIV_CMAC_256);
|
|
if (!NKSN_AddRecord(session, 1, NKE_RECORD_AEAD_ALGORITHM, data,
|
|
length * sizeof (data[0])))
|
|
return 0;
|
|
|
|
if (!NKSN_EndMessage(session))
|
|
return 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static int
|
|
process_response(NKC_Instance inst)
|
|
{
|
|
int next_protocol = -1, aead_algorithm = -1, error = 0;
|
|
int i, critical, type, length;
|
|
uint16_t data[NKE_MAX_RECORD_BODY_LENGTH / sizeof (uint16_t)];
|
|
|
|
assert(NKE_MAX_COOKIE_LENGTH <= NKE_MAX_RECORD_BODY_LENGTH);
|
|
assert(sizeof (data) % sizeof (uint16_t) == 0);
|
|
assert(sizeof (uint16_t) == 2);
|
|
|
|
inst->num_cookies = 0;
|
|
inst->ntp_address.ip_addr.family = IPADDR_UNSPEC;
|
|
inst->ntp_address.port = 0;
|
|
inst->server_name[0] = '\0';
|
|
|
|
while (!error) {
|
|
if (!NKSN_GetRecord(inst->session, &critical, &type, &length, &data, sizeof (data)))
|
|
break;
|
|
|
|
if (length > sizeof (data)) {
|
|
DEBUG_LOG("Record too long type=%d length=%d critical=%d", type, length, critical);
|
|
if (critical)
|
|
error = 1;
|
|
continue;
|
|
}
|
|
|
|
switch (type) {
|
|
case NKE_RECORD_NEXT_PROTOCOL:
|
|
if (!critical || length != 2 || ntohs(data[0]) != NKE_NEXT_PROTOCOL_NTPV4) {
|
|
DEBUG_LOG("Unexpected NTS-KE next protocol");
|
|
error = 1;
|
|
break;
|
|
}
|
|
next_protocol = NKE_NEXT_PROTOCOL_NTPV4;
|
|
break;
|
|
case NKE_RECORD_AEAD_ALGORITHM:
|
|
if (length != 2 || (ntohs(data[0]) != AEAD_AES_SIV_CMAC_256 &&
|
|
ntohs(data[0]) != AEAD_AES_128_GCM_SIV) ||
|
|
SIV_GetKeyLength(ntohs(data[0])) <= 0) {
|
|
DEBUG_LOG("Unexpected NTS-KE AEAD algorithm");
|
|
error = 1;
|
|
break;
|
|
}
|
|
aead_algorithm = ntohs(data[0]);
|
|
inst->context.algorithm = aead_algorithm;
|
|
break;
|
|
case NKE_RECORD_ERROR:
|
|
if (length == 2)
|
|
DEBUG_LOG("NTS-KE error %d", ntohs(data[0]));
|
|
error = 1;
|
|
break;
|
|
case NKE_RECORD_WARNING:
|
|
if (length == 2)
|
|
DEBUG_LOG("NTS-KE warning %d", ntohs(data[0]));
|
|
error = 1;
|
|
break;
|
|
case NKE_RECORD_COOKIE:
|
|
DEBUG_LOG("Got cookie length=%d", length);
|
|
|
|
if (length < 1 || length > NKE_MAX_COOKIE_LENGTH || length % 4 != 0 ||
|
|
inst->num_cookies >= NKE_MAX_COOKIES) {
|
|
DEBUG_LOG("Unexpected length/cookie");
|
|
break;
|
|
}
|
|
|
|
assert(NKE_MAX_COOKIE_LENGTH == sizeof (inst->cookies[inst->num_cookies].cookie));
|
|
assert(NKE_MAX_COOKIES == sizeof (inst->cookies) /
|
|
sizeof (inst->cookies[inst->num_cookies]));
|
|
inst->cookies[inst->num_cookies].length = length;
|
|
memcpy(inst->cookies[inst->num_cookies].cookie, data, length);
|
|
|
|
inst->num_cookies++;
|
|
break;
|
|
case NKE_RECORD_NTPV4_SERVER_NEGOTIATION:
|
|
if (length < 1 || length >= sizeof (inst->server_name)) {
|
|
DEBUG_LOG("Invalid server name");
|
|
error = 1;
|
|
break;
|
|
}
|
|
|
|
memcpy(inst->server_name, data, length);
|
|
inst->server_name[length] = '\0';
|
|
|
|
/* Make sure the name is printable and has no spaces */
|
|
for (i = 0; i < length && isgraph((unsigned char)inst->server_name[i]); i++)
|
|
;
|
|
if (i != length) {
|
|
DEBUG_LOG("Invalid server name");
|
|
error = 1;
|
|
break;
|
|
}
|
|
|
|
DEBUG_LOG("Negotiated server %s", inst->server_name);
|
|
break;
|
|
case NKE_RECORD_NTPV4_PORT_NEGOTIATION:
|
|
if (length != 2) {
|
|
DEBUG_LOG("Invalid port");
|
|
error = 1;
|
|
break;
|
|
}
|
|
inst->ntp_address.port = ntohs(data[0]);
|
|
DEBUG_LOG("Negotiated port %d", inst->ntp_address.port);
|
|
break;
|
|
default:
|
|
DEBUG_LOG("Unknown record type=%d length=%d critical=%d", type, length, critical);
|
|
if (critical)
|
|
error = 1;
|
|
}
|
|
}
|
|
|
|
DEBUG_LOG("NTS-KE response: error=%d next=%d aead=%d",
|
|
error, next_protocol, aead_algorithm);
|
|
|
|
if (error || inst->num_cookies == 0 ||
|
|
next_protocol != NKE_NEXT_PROTOCOL_NTPV4 ||
|
|
aead_algorithm < 0)
|
|
return 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
static int
|
|
handle_message(void *arg)
|
|
{
|
|
SIV_Algorithm exporter_algorithm;
|
|
NKC_Instance inst = arg;
|
|
|
|
if (!process_response(inst)) {
|
|
LOG(LOGS_ERR, "Received invalid NTS-KE response from %s", inst->name);
|
|
return 0;
|
|
}
|
|
|
|
exporter_algorithm = inst->context.algorithm;
|
|
|
|
/* With AES-128-GCM-SIV, set the algorithm ID in the RFC5705 key exporter
|
|
context incorrectly for compatibility with older chrony servers */
|
|
if (exporter_algorithm == AEAD_AES_128_GCM_SIV)
|
|
exporter_algorithm = AEAD_AES_SIV_CMAC_256;
|
|
|
|
if (!NKSN_GetKeys(inst->session, inst->context.algorithm, exporter_algorithm,
|
|
NKE_NEXT_PROTOCOL_NTPV4, &inst->context.c2s, &inst->context.s2c))
|
|
return 0;
|
|
|
|
if (inst->server_name[0] != '\0') {
|
|
if (inst->resolving_name)
|
|
return 0;
|
|
if (!UTI_StringToIP(inst->server_name, &inst->ntp_address.ip_addr)) {
|
|
int length = strlen(inst->server_name);
|
|
|
|
/* Add a trailing dot if not present to force the name to be
|
|
resolved as a fully qualified domain name */
|
|
if (length < 1 || length + 1 >= sizeof (inst->server_name))
|
|
return 0;
|
|
if (inst->server_name[length - 1] != '.') {
|
|
inst->server_name[length] = '.';
|
|
inst->server_name[length + 1] = '\0';
|
|
}
|
|
|
|
DNS_Name2IPAddressAsync(inst->server_name, name_resolve_handler, inst);
|
|
inst->resolving_name = 1;
|
|
}
|
|
}
|
|
|
|
inst->got_response = 1;
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
NKC_Instance
|
|
NKC_CreateInstance(IPSockAddr *address, const char *name, uint32_t cert_set)
|
|
{
|
|
const char **trusted_certs;
|
|
uint32_t *certs_ids;
|
|
NKC_Instance inst;
|
|
int n_certs;
|
|
|
|
inst = MallocNew(struct NKC_Instance_Record);
|
|
|
|
inst->address = *address;
|
|
inst->name = Strdup(name);
|
|
inst->session = NKSN_CreateInstance(0, inst->name, handle_message, inst);
|
|
inst->resolving_name = 0;
|
|
inst->destroying = 0;
|
|
inst->got_response = 0;
|
|
|
|
n_certs = CNF_GetNtsTrustedCertsPaths(&trusted_certs, &certs_ids);
|
|
|
|
/* Share the credentials among clients using the default set of trusted
|
|
certificates, which likely contains most certificates */
|
|
if (cert_set == 0) {
|
|
if (!default_credentials)
|
|
default_credentials = NKSN_CreateClientCertCredentials(trusted_certs, certs_ids,
|
|
n_certs, cert_set);
|
|
inst->credentials = default_credentials;
|
|
if (default_credentials)
|
|
default_credentials_refs++;
|
|
} else {
|
|
inst->credentials = NKSN_CreateClientCertCredentials(trusted_certs, certs_ids,
|
|
n_certs, cert_set);
|
|
}
|
|
|
|
return inst;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
void
|
|
NKC_DestroyInstance(NKC_Instance inst)
|
|
{
|
|
NKSN_DestroyInstance(inst->session);
|
|
|
|
Free(inst->name);
|
|
|
|
if (inst->credentials) {
|
|
if (inst->credentials == default_credentials) {
|
|
default_credentials_refs--;
|
|
if (default_credentials_refs <= 0) {
|
|
NKSN_DestroyCertCredentials(default_credentials);
|
|
default_credentials = NULL;
|
|
}
|
|
} else {
|
|
NKSN_DestroyCertCredentials(inst->credentials);
|
|
}
|
|
}
|
|
|
|
/* If the asynchronous resolver is running, let the handler free
|
|
the instance later */
|
|
if (inst->resolving_name) {
|
|
inst->destroying = 1;
|
|
return;
|
|
}
|
|
|
|
Free(inst);
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
int
|
|
NKC_Start(NKC_Instance inst)
|
|
{
|
|
IPSockAddr local_addr;
|
|
char label[512], *iface;
|
|
int sock_fd;
|
|
|
|
assert(!NKC_IsActive(inst));
|
|
|
|
inst->got_response = 0;
|
|
|
|
if (!inst->credentials) {
|
|
DEBUG_LOG("Missing client credentials");
|
|
return 0;
|
|
}
|
|
|
|
/* Don't try to connect if missing the algorithm which all servers
|
|
are required to support */
|
|
if (SIV_GetKeyLength(AEAD_AES_SIV_CMAC_256) <= 0) {
|
|
LOG(LOGS_ERR, "Missing AES-SIV-CMAC-256");
|
|
return 0;
|
|
}
|
|
|
|
/* Follow the bindacqaddress and bindacqdevice settings */
|
|
CNF_GetBindAcquisitionAddress(inst->address.ip_addr.family, &local_addr.ip_addr);
|
|
local_addr.port = 0;
|
|
iface = CNF_GetBindAcquisitionInterface();
|
|
|
|
/* Make a label containing both the address and name of the server */
|
|
if (snprintf(label, sizeof (label), "%s (%s)",
|
|
UTI_IPSockAddrToString(&inst->address), inst->name) >= sizeof (label))
|
|
;
|
|
|
|
sock_fd = SCK_OpenTcpSocket(&inst->address, &local_addr, iface, 0);
|
|
if (sock_fd < 0) {
|
|
LOG(LOGS_ERR, "Could not connect to %s", label);
|
|
return 0;
|
|
}
|
|
|
|
/* Start an NTS-KE session */
|
|
if (!NKSN_StartSession(inst->session, sock_fd, label, inst->credentials, CLIENT_TIMEOUT)) {
|
|
SCK_CloseSocket(sock_fd);
|
|
return 0;
|
|
}
|
|
|
|
/* Send a request */
|
|
if (!prepare_request(inst)) {
|
|
DEBUG_LOG("Could not prepare NTS-KE request");
|
|
NKSN_StopSession(inst->session);
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
int
|
|
NKC_IsActive(NKC_Instance inst)
|
|
{
|
|
return !NKSN_IsStopped(inst->session) || inst->resolving_name;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
int
|
|
NKC_GetNtsData(NKC_Instance inst, NKE_Context *context,
|
|
NKE_Cookie *cookies, int *num_cookies, int max_cookies,
|
|
IPSockAddr *ntp_address)
|
|
{
|
|
int i;
|
|
|
|
if (!inst->got_response || inst->resolving_name)
|
|
return 0;
|
|
|
|
*context = inst->context;
|
|
|
|
for (i = 0; i < inst->num_cookies && i < max_cookies; i++)
|
|
cookies[i] = inst->cookies[i];
|
|
*num_cookies = i;
|
|
|
|
*ntp_address = inst->ntp_address;
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* ================================================== */
|
|
|
|
int
|
|
NKC_GetRetryFactor(NKC_Instance inst)
|
|
{
|
|
return NKSN_GetRetryFactor(inst->session);
|
|
}
|