diff options
Diffstat (limited to '')
-rw-r--r-- | nts_ke_client.c | 457 |
1 files changed, 457 insertions, 0 deletions
diff --git a/nts_ke_client.c b/nts_ke_client.c new file mode 100644 index 0000000..3891f71 --- /dev/null +++ b/nts_ke_client.c @@ -0,0 +1,457 @@ +/* + 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) +{ + NKC_Instance inst = arg; + + if (!process_response(inst)) { + LOG(LOGS_ERR, "Received invalid NTS-KE response from %s", inst->name); + return 0; + } + + if (!NKSN_GetKeys(inst->session, inst->context.algorithm, + &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); +} |