summaryrefslogtreecommitdiffstats
path: root/nts_ke_client.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--nts_ke_client.c457
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);
+}